Skip to content

Commit dfb0928

Browse files
lidavidmcyb70289
authored andcommitted
ARROW-13096: [C++] Implement logarithm compute functions
Adds ln, log10, and log2. We could add a log1e and/or a logN if useful (probably not?) Has some code from/will conflict with #10544. Closes #10567 from lidavidm/arrow-13096 Authored-by: David Li <[email protected]> Signed-off-by: Yibo Cai <[email protected]>
1 parent bc86814 commit dfb0928

6 files changed

Lines changed: 363 additions & 8 deletions

File tree

‎cpp/src/arrow/compute/api_scalar.cc‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,10 @@ SCALAR_ARITHMETIC_UNARY(Asin, "asin", "asin_checked")
326326
SCALAR_ARITHMETIC_UNARY(Acos, "acos", "acos_checked")
327327
SCALAR_ARITHMETIC_UNARY(Tan, "tan", "tan_checked")
328328
SCALAR_EAGER_UNARY(Atan, "atan")
329+
SCALAR_ARITHMETIC_UNARY(Ln, "ln", "ln_checked")
330+
SCALAR_ARITHMETIC_UNARY(Log10, "log10", "log10_checked")
331+
SCALAR_ARITHMETIC_UNARY(Log2, "log2", "log2_checked")
332+
SCALAR_ARITHMETIC_UNARY(Log1p, "log1p", "log1p_checked")
329333

330334
#define SCALAR_ARITHMETIC_BINARY(NAME, REGISTRY_NAME, REGISTRY_CHECKED_NAME) \
331335
Result<Datum> NAME(const Datum& left, const Datum& right, ArithmeticOptions options, \

‎cpp/src/arrow/compute/api_scalar.h‎

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -246,8 +246,9 @@ class ARROW_EXPORT ProjectOptions : public FunctionOptions {
246246

247247
/// @}
248248

249-
/// \brief Get the absolute value of a value. Array values can be of arbitrary
250-
/// length. If argument is null the result will be null.
249+
/// \brief Get the absolute value of a value.
250+
///
251+
/// If argument is null the result will be null.
251252
///
252253
/// \param[in] arg the value transformed
253254
/// \param[in] options arithmetic options (overflow handling), optional
@@ -311,8 +312,9 @@ Result<Datum> Divide(const Datum& left, const Datum& right,
311312
ArithmeticOptions options = ArithmeticOptions(),
312313
ExecContext* ctx = NULLPTR);
313314

314-
/// \brief Negate a value. Array values can be of arbitrary length. If argument
315-
/// is null the result will be null.
315+
/// \brief Negate values.
316+
///
317+
/// If argument is null the result will be null.
316318
///
317319
/// \param[in] arg the value negated
318320
/// \param[in] options arithmetic options (overflow handling), optional
@@ -424,6 +426,55 @@ Result<Datum> Atan(const Datum& arg, ExecContext* ctx = NULLPTR);
424426
ARROW_EXPORT
425427
Result<Datum> Atan2(const Datum& y, const Datum& x, ExecContext* ctx = NULLPTR);
426428

429+
/// \brief Get the natural log of a value.
430+
///
431+
/// If argument is null the result will be null.
432+
///
433+
/// \param[in] arg The values to compute the logarithm for.
434+
/// \param[in] options arithmetic options (overflow handling), optional
435+
/// \param[in] ctx the function execution context, optional
436+
/// \return the elementwise natural log
437+
ARROW_EXPORT
438+
Result<Datum> Ln(const Datum& arg, ArithmeticOptions options = ArithmeticOptions(),
439+
ExecContext* ctx = NULLPTR);
440+
441+
/// \brief Get the log base 10 of a value.
442+
///
443+
/// If argument is null the result will be null.
444+
///
445+
/// \param[in] arg The values to compute the logarithm for.
446+
/// \param[in] options arithmetic options (overflow handling), optional
447+
/// \param[in] ctx the function execution context, optional
448+
/// \return the elementwise log base 10
449+
ARROW_EXPORT
450+
Result<Datum> Log10(const Datum& arg, ArithmeticOptions options = ArithmeticOptions(),
451+
ExecContext* ctx = NULLPTR);
452+
453+
/// \brief Get the log base 2 of a value.
454+
///
455+
/// If argument is null the result will be null.
456+
///
457+
/// \param[in] arg The values to compute the logarithm for.
458+
/// \param[in] options arithmetic options (overflow handling), optional
459+
/// \param[in] ctx the function execution context, optional
460+
/// \return the elementwise log base 2
461+
ARROW_EXPORT
462+
Result<Datum> Log2(const Datum& arg, ArithmeticOptions options = ArithmeticOptions(),
463+
ExecContext* ctx = NULLPTR);
464+
465+
/// \brief Get the natural log of (1 + value).
466+
///
467+
/// If argument is null the result will be null.
468+
/// This function may be more accurate than Log(1 + value) for values close to zero.
469+
///
470+
/// \param[in] arg The values to compute the logarithm for.
471+
/// \param[in] options arithmetic options (overflow handling), optional
472+
/// \param[in] ctx the function execution context, optional
473+
/// \return the elementwise natural log
474+
ARROW_EXPORT
475+
Result<Datum> Log1p(const Datum& arg, ArithmeticOptions options = ArithmeticOptions(),
476+
ExecContext* ctx = NULLPTR);
477+
427478
/// \brief Find the element-wise maximum of any number of arrays or scalars.
428479
/// Array values must be the same length.
429480
///

‎cpp/src/arrow/compute/kernels/scalar_arithmetic.cc‎

Lines changed: 200 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,11 @@ template <typename T>
5353
using is_signed_integer =
5454
std::integral_constant<bool, std::is_integral<T>::value && std::is_signed<T>::value>;
5555

56-
template <typename T>
57-
using enable_if_signed_integer = enable_if_t<is_signed_integer<T>::value, T>;
56+
template <typename T, typename R = T>
57+
using enable_if_signed_integer = enable_if_t<is_signed_integer<T>::value, R>;
5858

59-
template <typename T>
60-
using enable_if_unsigned_integer = enable_if_t<is_unsigned_integer<T>::value, T>;
59+
template <typename T, typename R = T>
60+
using enable_if_unsigned_integer = enable_if_t<is_unsigned_integer<T>::value, R>;
6161

6262
template <typename T, typename R = T>
6363
using enable_if_integer =
@@ -686,6 +686,118 @@ struct Atan2 {
686686
}
687687
};
688688

689+
struct LogNatural {
690+
template <typename T, typename Arg>
691+
static enable_if_floating_point<Arg, T> Call(KernelContext*, Arg arg, Status*) {
692+
static_assert(std::is_same<T, Arg>::value, "");
693+
if (arg == 0.0) {
694+
return -std::numeric_limits<T>::infinity();
695+
} else if (arg < 0.0) {
696+
return std::numeric_limits<T>::quiet_NaN();
697+
}
698+
return std::log(arg);
699+
}
700+
};
701+
702+
struct LogNaturalChecked {
703+
template <typename T, typename Arg>
704+
static enable_if_floating_point<Arg, T> Call(KernelContext*, Arg arg, Status* st) {
705+
static_assert(std::is_same<T, Arg>::value, "");
706+
if (arg == 0.0) {
707+
*st = Status::Invalid("logarithm of zero");
708+
return arg;
709+
} else if (arg < 0.0) {
710+
*st = Status::Invalid("logarithm of negative number");
711+
return arg;
712+
}
713+
return std::log(arg);
714+
}
715+
};
716+
717+
struct Log10 {
718+
template <typename T, typename Arg>
719+
static enable_if_floating_point<Arg, T> Call(KernelContext*, Arg arg, Status*) {
720+
static_assert(std::is_same<T, Arg>::value, "");
721+
if (arg == 0.0) {
722+
return -std::numeric_limits<T>::infinity();
723+
} else if (arg < 0.0) {
724+
return std::numeric_limits<T>::quiet_NaN();
725+
}
726+
return std::log10(arg);
727+
}
728+
};
729+
730+
struct Log10Checked {
731+
template <typename T, typename Arg>
732+
static enable_if_floating_point<Arg, T> Call(KernelContext*, Arg arg, Status* st) {
733+
static_assert(std::is_same<T, Arg>::value, "");
734+
if (arg == 0) {
735+
*st = Status::Invalid("logarithm of zero");
736+
return arg;
737+
} else if (arg < 0) {
738+
*st = Status::Invalid("logarithm of negative number");
739+
return arg;
740+
}
741+
return std::log10(arg);
742+
}
743+
};
744+
745+
struct Log2 {
746+
template <typename T, typename Arg>
747+
static enable_if_floating_point<Arg, T> Call(KernelContext*, Arg arg, Status*) {
748+
static_assert(std::is_same<T, Arg>::value, "");
749+
if (arg == 0.0) {
750+
return -std::numeric_limits<T>::infinity();
751+
} else if (arg < 0.0) {
752+
return std::numeric_limits<T>::quiet_NaN();
753+
}
754+
return std::log2(arg);
755+
}
756+
};
757+
758+
struct Log2Checked {
759+
template <typename T, typename Arg>
760+
static enable_if_floating_point<Arg, T> Call(KernelContext*, Arg arg, Status* st) {
761+
static_assert(std::is_same<T, Arg>::value, "");
762+
if (arg == 0.0) {
763+
*st = Status::Invalid("logarithm of zero");
764+
return arg;
765+
} else if (arg < 0.0) {
766+
*st = Status::Invalid("logarithm of negative number");
767+
return arg;
768+
}
769+
return std::log2(arg);
770+
}
771+
};
772+
773+
struct Log1p {
774+
template <typename T, typename Arg>
775+
static enable_if_floating_point<Arg, T> Call(KernelContext*, Arg arg, Status*) {
776+
static_assert(std::is_same<T, Arg>::value, "");
777+
if (arg == -1) {
778+
return -std::numeric_limits<T>::infinity();
779+
} else if (arg < -1) {
780+
return std::numeric_limits<T>::quiet_NaN();
781+
}
782+
return std::log1p(arg);
783+
}
784+
};
785+
786+
struct Log1pChecked {
787+
template <typename T, typename Arg>
788+
static enable_if_floating_point<Arg, T> Call(KernelContext*, Arg arg, Status* st) {
789+
static_assert(std::is_same<T, Arg>::value, "");
790+
if (arg == -1) {
791+
*st = Status::Invalid("logarithm of zero");
792+
return arg;
793+
} else if (arg < -1) {
794+
*st = Status::Invalid("logarithm of negative number");
795+
return arg;
796+
}
797+
return std::log1p(arg);
798+
}
799+
};
800+
689801
// Generate a kernel given an arithmetic functor
690802
template <template <typename... Args> class KernelGenerator, typename Op>
691803
ArrayKernelExec ArithmeticExecFromOp(detail::GetTypeId get_id) {
@@ -1295,6 +1407,60 @@ const FunctionDoc atan2_doc{
12951407
"Compute the inverse tangent using argument signs to determine the quadrant",
12961408
("Integer arguments return double values."),
12971409
{"y", "x"}};
1410+
1411+
const FunctionDoc ln_doc{
1412+
"Compute natural log of arguments element-wise",
1413+
("Non-positive values return -inf or NaN. Null values return null.\n"
1414+
"Use function \"ln_checked\" if you want non-positive values to raise an error."),
1415+
{"x"}};
1416+
1417+
const FunctionDoc ln_checked_doc{
1418+
"Compute natural log of arguments element-wise",
1419+
("Non-positive values return -inf or NaN. Null values return null.\n"
1420+
"Use function \"ln\" if you want non-positive values to return "
1421+
"-inf or NaN."),
1422+
{"x"}};
1423+
1424+
const FunctionDoc log10_doc{
1425+
"Compute log base 10 of arguments element-wise",
1426+
("Non-positive values return -inf or NaN. Null values return null.\n"
1427+
"Use function \"log10_checked\" if you want non-positive values to raise an error."),
1428+
{"x"}};
1429+
1430+
const FunctionDoc log10_checked_doc{
1431+
"Compute log base 10 of arguments element-wise",
1432+
("Non-positive values return -inf or NaN. Null values return null.\n"
1433+
"Use function \"log10\" if you want non-positive values to return "
1434+
"-inf or NaN."),
1435+
{"x"}};
1436+
1437+
const FunctionDoc log2_doc{
1438+
"Compute log base 2 of arguments element-wise",
1439+
("Non-positive values return -inf or NaN. Null values return null.\n"
1440+
"Use function \"log2_checked\" if you want non-positive values to raise an error."),
1441+
{"x"}};
1442+
1443+
const FunctionDoc log2_checked_doc{
1444+
"Compute log base 2 of arguments element-wise",
1445+
("Non-positive values return -inf or NaN. Null values return null.\n"
1446+
"Use function \"log2\" if you want non-positive values to return "
1447+
"-inf or NaN."),
1448+
{"x"}};
1449+
1450+
const FunctionDoc log1p_doc{
1451+
"Compute natural log of (1+x) element-wise",
1452+
("Values <= -1 return -inf or NaN. Null values return null.\n"
1453+
"This function may be more precise than log(1 + x) for x close to zero."
1454+
"Use function \"log1p_checked\" if you want non-positive values to raise an error."),
1455+
{"x"}};
1456+
1457+
const FunctionDoc log1p_checked_doc{
1458+
"Compute natural log of (1+x) element-wise",
1459+
("Values <= -1 return -inf or NaN. Null values return null.\n"
1460+
"This function may be more precise than log(1 + x) for x close to zero."
1461+
"Use function \"log1p\" if you want non-positive values to return "
1462+
"-inf or NaN."),
1463+
{"x"}};
12981464
} // namespace
12991465

13001466
void RegisterScalarArithmetic(FunctionRegistry* registry) {
@@ -1460,6 +1626,36 @@ void RegisterScalarArithmetic(FunctionRegistry* registry) {
14601626

14611627
auto atan2 = MakeArithmeticFunctionFloatingPoint<Atan2>("atan2", &atan2_doc);
14621628
DCHECK_OK(registry->AddFunction(std::move(atan2)));
1629+
1630+
// ----------------------------------------------------------------------
1631+
// Logarithms
1632+
auto ln = MakeUnaryArithmeticFunctionFloatingPoint<LogNatural>("ln", &ln_doc);
1633+
DCHECK_OK(registry->AddFunction(std::move(ln)));
1634+
1635+
auto ln_checked = MakeUnaryArithmeticFunctionFloatingPointNotNull<LogNaturalChecked>(
1636+
"ln_checked", &ln_checked_doc);
1637+
DCHECK_OK(registry->AddFunction(std::move(ln_checked)));
1638+
1639+
auto log10 = MakeUnaryArithmeticFunctionFloatingPoint<Log10>("log10", &log10_doc);
1640+
DCHECK_OK(registry->AddFunction(std::move(log10)));
1641+
1642+
auto log10_checked = MakeUnaryArithmeticFunctionFloatingPointNotNull<Log10Checked>(
1643+
"log10_checked", &log10_checked_doc);
1644+
DCHECK_OK(registry->AddFunction(std::move(log10_checked)));
1645+
1646+
auto log2 = MakeUnaryArithmeticFunctionFloatingPoint<Log2>("log2", &log2_doc);
1647+
DCHECK_OK(registry->AddFunction(std::move(log2)));
1648+
1649+
auto log2_checked = MakeUnaryArithmeticFunctionFloatingPointNotNull<Log2Checked>(
1650+
"log2_checked", &log2_checked_doc);
1651+
DCHECK_OK(registry->AddFunction(std::move(log2_checked)));
1652+
1653+
auto log1p = MakeUnaryArithmeticFunctionFloatingPoint<Log1p>("log1p", &log1p_doc);
1654+
DCHECK_OK(registry->AddFunction(std::move(log1p)));
1655+
1656+
auto log1p_checked = MakeUnaryArithmeticFunctionFloatingPointNotNull<Log1pChecked>(
1657+
"log1p_checked", &log1p_checked_doc);
1658+
DCHECK_OK(registry->AddFunction(std::move(log1p_checked)));
14631659
}
14641660

14651661
} // namespace internal

‎cpp/src/arrow/compute/kernels/scalar_arithmetic_test.cc‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1821,5 +1821,65 @@ TYPED_TEST(TestBinaryArithmeticFloating, TrigAtan2) {
18211821
-M_PI_2, 0, M_PI));
18221822
}
18231823

1824+
TYPED_TEST(TestUnaryArithmeticFloating, Log) {
1825+
using CType = typename TestFixture::CType;
1826+
auto ty = this->type_singleton();
1827+
this->SetNansEqual(true);
1828+
auto min_val = std::numeric_limits<CType>::min();
1829+
auto max_val = std::numeric_limits<CType>::max();
1830+
for (auto check_overflow : {false, true}) {
1831+
this->SetOverflowCheck(check_overflow);
1832+
this->AssertUnaryOp(Ln, "[1, 2.718281828459045, null, NaN, Inf]",
1833+
"[0, 1, null, NaN, Inf]");
1834+
// N.B. min() for float types is smallest normal number > 0
1835+
this->AssertUnaryOp(Ln, min_val, std::log(min_val));
1836+
this->AssertUnaryOp(Ln, max_val, std::log(max_val));
1837+
this->AssertUnaryOp(Log10, "[1, 10, null, NaN, Inf]", "[0, 1, null, NaN, Inf]");
1838+
this->AssertUnaryOp(Log10, min_val, std::log10(min_val));
1839+
this->AssertUnaryOp(Log10, max_val, std::log10(max_val));
1840+
this->AssertUnaryOp(Log2, "[1, 2, null, NaN, Inf]", "[0, 1, null, NaN, Inf]");
1841+
this->AssertUnaryOp(Log2, min_val, std::log2(min_val));
1842+
this->AssertUnaryOp(Log2, max_val, std::log2(max_val));
1843+
this->AssertUnaryOp(Log1p, "[0, 1.718281828459045, null, NaN, Inf]",
1844+
"[0, 1, null, NaN, Inf]");
1845+
this->AssertUnaryOp(Log1p, min_val, std::log1p(min_val));
1846+
this->AssertUnaryOp(Log1p, max_val, std::log1p(max_val));
1847+
}
1848+
this->SetOverflowCheck(false);
1849+
this->AssertUnaryOp(Ln, "[-Inf, -1, 0, Inf]", "[NaN, NaN, -Inf, Inf]");
1850+
this->AssertUnaryOp(Log10, "[-Inf, -1, 0, Inf]", "[NaN, NaN, -Inf, Inf]");
1851+
this->AssertUnaryOp(Log2, "[-Inf, -1, 0, Inf]", "[NaN, NaN, -Inf, Inf]");
1852+
this->AssertUnaryOp(Log1p, "[-Inf, -2, -1, Inf]", "[NaN, NaN, -Inf, Inf]");
1853+
this->SetOverflowCheck(true);
1854+
this->AssertUnaryOpRaises(Ln, "[0]", "logarithm of zero");
1855+
this->AssertUnaryOpRaises(Ln, "[-1]", "logarithm of negative number");
1856+
this->AssertUnaryOpRaises(Ln, "[-Inf]", "logarithm of negative number");
1857+
1858+
auto lowest_val = MakeScalar(std::numeric_limits<CType>::lowest());
1859+
// N.B. RapidJSON on some platforms raises "Number too big to be stored in double" so
1860+
// don't bounce through JSON
1861+
EXPECT_RAISES_WITH_MESSAGE_THAT(Invalid,
1862+
::testing::HasSubstr("logarithm of negative number"),
1863+
Ln(lowest_val, this->options_));
1864+
this->AssertUnaryOpRaises(Log10, "[0]", "logarithm of zero");
1865+
this->AssertUnaryOpRaises(Log10, "[-1]", "logarithm of negative number");
1866+
this->AssertUnaryOpRaises(Log10, "[-Inf]", "logarithm of negative number");
1867+
EXPECT_RAISES_WITH_MESSAGE_THAT(Invalid,
1868+
::testing::HasSubstr("logarithm of negative number"),
1869+
Log10(lowest_val, this->options_));
1870+
this->AssertUnaryOpRaises(Log2, "[0]", "logarithm of zero");
1871+
this->AssertUnaryOpRaises(Log2, "[-1]", "logarithm of negative number");
1872+
this->AssertUnaryOpRaises(Log2, "[-Inf]", "logarithm of negative number");
1873+
EXPECT_RAISES_WITH_MESSAGE_THAT(Invalid,
1874+
::testing::HasSubstr("logarithm of negative number"),
1875+
Log2(lowest_val, this->options_));
1876+
this->AssertUnaryOpRaises(Log1p, "[-1]", "logarithm of zero");
1877+
this->AssertUnaryOpRaises(Log1p, "[-2]", "logarithm of negative number");
1878+
this->AssertUnaryOpRaises(Log1p, "[-Inf]", "logarithm of negative number");
1879+
EXPECT_RAISES_WITH_MESSAGE_THAT(Invalid,
1880+
::testing::HasSubstr("logarithm of negative number"),
1881+
Log1p(lowest_val, this->options_));
1882+
}
1883+
18241884
} // namespace compute
18251885
} // namespace arrow

0 commit comments

Comments
 (0)