Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GH-44903: [C++] Add the Expm1(exponent) scalar arithmetic function #44904

Merged
merged 4 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cpp/src/arrow/compute/api_scalar.cc
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,7 @@ SCALAR_ARITHMETIC_UNARY(Sin, "sin", "sin_checked")
SCALAR_ARITHMETIC_UNARY(Tan, "tan", "tan_checked")
SCALAR_EAGER_UNARY(Atan, "atan")
SCALAR_EAGER_UNARY(Exp, "exp")
SCALAR_EAGER_UNARY(Expm1, "expm1")
SCALAR_EAGER_UNARY(Sign, "sign")

Result<Datum> Round(const Datum& arg, RoundOptions options, ExecContext* ctx) {
Expand Down
12 changes: 12 additions & 0 deletions cpp/src/arrow/compute/api_scalar.h
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,18 @@ Result<Datum> Power(const Datum& left, const Datum& right,
ARROW_EXPORT
Result<Datum> Exp(const Datum& arg, ExecContext* ctx = NULLPTR);

/// \brief More accurately calculates `exp(arg) - 1` for values close to zero.
felipecrv marked this conversation as resolved.
Show resolved Hide resolved
/// If the exponent value is null the result will be null.
///
/// This function is more accurate than calculating `exp(value) - 1` directly for values
/// close to zero.
///
/// \param[in] arg the exponent
/// \param[in] ctx the function execution context, optional
/// \return the element-wise Euler's number raised to the power of exponent minus 1
ARROW_EXPORT
Result<Datum> Expm1(const Datum& arg, ExecContext* ctx = NULLPTR);

/// \brief Left shift the left array by the right array. Array values must be the
/// same length. If either operand is null, the result will be null.
///
Expand Down
8 changes: 8 additions & 0 deletions cpp/src/arrow/compute/kernels/base_arithmetic_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,14 @@ struct Exp {
}
};

struct Expm1 {
template <typename T, typename Arg>
static T Call(KernelContext*, Arg exp, Status*) {
static_assert(std::is_same<T, Arg>::value);
return std::expm1(exp);
}
};

struct Power {
ARROW_NOINLINE
static uint64_t IntegerPower(uint64_t base, uint64_t exp) {
Expand Down
10 changes: 10 additions & 0 deletions cpp/src/arrow/compute/kernels/scalar_arithmetic.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1087,6 +1087,12 @@ const FunctionDoc exp_doc{
("If exponent is null the result will be null."),
{"exponent"}};

const FunctionDoc expm1_doc{
"Compute Euler's number raised to the power of specified exponent, "
"then decrement 1, element-wise",
("If exponent is null the result will be null."),
{"exponent"}};
Comment on lines +1090 to +1094
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we mention that "this is more accurate than directly computing exp(value) - 1" in the function doc?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I considered it, but I think it's better to keep this succinct. expm1 is very googlable and explaining why it's more accurate in a short phrase isn't easy.


const FunctionDoc pow_checked_doc{
"Raise arguments to power element-wise",
("An error is returned when integer to negative integer power is encountered,\n"
Expand Down Expand Up @@ -1614,6 +1620,10 @@ void RegisterScalarArithmetic(FunctionRegistry* registry) {
auto exp = MakeUnaryArithmeticFunctionFloatingPoint<Exp>("exp", exp_doc);
DCHECK_OK(registry->AddFunction(std::move(exp)));

// ----------------------------------------------------------------------
auto expm1 = MakeUnaryArithmeticFunctionFloatingPoint<Expm1>("expm1", expm1_doc);
DCHECK_OK(registry->AddFunction(std::move(expm1)));

// ----------------------------------------------------------------------
auto sqrt = MakeUnaryArithmeticFunctionFloatingPoint<SquareRoot>("sqrt", sqrt_doc);
DCHECK_OK(registry->AddFunction(std::move(sqrt)));
Expand Down
91 changes: 91 additions & 0 deletions cpp/src/arrow/compute/kernels/scalar_arithmetic_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1563,6 +1563,97 @@ TEST_F(TestUnaryArithmeticDecimal, Exp) {
}
}

TYPED_TEST(TestUnaryArithmeticUnsigned, Expm1) {
auto expm1 = [](const Datum& arg, ArithmeticOptions, ExecContext* ctx) {
return Expm1(arg, ctx);
};
// Empty arrays
this->AssertUnaryOp(expm1, "[]", ArrayFromJSON(float64(), "[]"));
// Array with nulls
this->AssertUnaryOp(expm1, "[null]", ArrayFromJSON(float64(), "[null]"));
this->AssertUnaryOp(expm1, this->MakeNullScalar(), arrow::MakeNullScalar(float64()));
this->AssertUnaryOp(
expm1, "[null, 1, 10]",
ArrayFromJSON(float64(), "[null, 1.718281828459045, 22025.465794806718]"));
this->AssertUnaryOp(expm1, this->MakeScalar(1),
arrow::MakeScalar<double>(1.718281828459045F));
felipecrv marked this conversation as resolved.
Show resolved Hide resolved
}

TYPED_TEST(TestUnaryArithmeticSigned, Expm1) {
auto expm1 = [](const Datum& arg, ArithmeticOptions, ExecContext* ctx) {
return Expm1(arg, ctx);
};
// Empty arrays
this->AssertUnaryOp(expm1, "[]", ArrayFromJSON(float64(), "[]"));
// Array with nulls
this->AssertUnaryOp(expm1, "[null]", ArrayFromJSON(float64(), "[null]"));
this->AssertUnaryOp(expm1, this->MakeNullScalar(), arrow::MakeNullScalar(float64()));
this->AssertUnaryOp(expm1, "[-10, -1, 0, null, 1, 10]",
ArrayFromJSON(float64(),
"[-0.9999546000702375, -0.6321205588285577, 0.0, "
"null, 1.718281828459045, 22025.465794806718]"));
this->AssertUnaryOp(expm1, this->MakeScalar(1),
arrow::MakeScalar<double>(1.718281828459045F));
}

TYPED_TEST(TestUnaryArithmeticFloating, Expm1) {
using CType = typename TestFixture::CType;

auto min = std::numeric_limits<CType>::lowest();
auto max = std::numeric_limits<CType>::max();

auto expm1 = [](const Datum& arg, ArithmeticOptions, ExecContext* ctx) {
return Expm1(arg, ctx);
};
// Empty arrays
this->AssertUnaryOp(expm1, "[]", "[]");
// Array with nulls
this->AssertUnaryOp(expm1, "[null]", "[null]");
this->AssertUnaryOp(expm1, this->MakeNullScalar(), this->MakeNullScalar());
this->AssertUnaryOp(expm1, "[-1.0, 0.0, null, 10.0]",
"[-0.6321205588285577, 0.0, null, 22025.465794806718]");
// Ordinary arrays (positive, negative, fractional, and zero inputs)
this->AssertUnaryOp(
expm1, "[-10.0, 0.0, 0.5, 1.0]",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: since the main point of expm1 is to be more precise when the input is close to zero, shouldn't we focus on this use case here? Though of course this is all dependent on the stdlib's implementation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added 0.0 but you're right about the need for other tiny numbers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

"[-0.9999546000702375, 0.0, 0.6487212707001282, 1.718281828459045]");
this->AssertUnaryOp(expm1, 1.3F, 2.6692964926535487F);
this->AssertUnaryOp(expm1, this->MakeScalar(1.3F),
this->MakeScalar(2.6692964926535487F));
// Arrays with infinites
this->AssertUnaryOp(expm1, "[-Inf, Inf]", "[-1, Inf]");
// Arrays with NaNs
this->SetNansEqual(true);
this->AssertUnaryOp(expm1, "[NaN]", "[NaN]");
// Min/max
this->AssertUnaryOp(expm1, min, -1.0);
this->AssertUnaryOp(expm1, max, std::numeric_limits<CType>::infinity());
}

TEST_F(TestUnaryArithmeticDecimal, Expm1) {
auto max128 = Decimal128::GetMaxValue(38);
auto max256 = Decimal256::GetMaxValue(76);
const auto func = "expm1";
for (const auto& ty : PositiveScaleTypes()) {
CheckScalar(func, {ArrayFromJSON(ty, R"([])")}, ArrayFromJSON(float64(), "[]"));
CheckScalar(
func, {ArrayFromJSON(ty, R"(["-1.00", "0.00", "10.00", null])")},
ArrayFromJSON(float64(), "[-0.6321205588285577, 0.0, 22025.465794806718, null]"));
}
CheckScalar(func, {std::make_shared<Decimal128Scalar>(max128, decimal128(38, 0))},
ScalarFromJSON(float64(), "Inf"));
CheckScalar(func, {std::make_shared<Decimal128Scalar>(-max128, decimal128(38, 0))},
ScalarFromJSON(float64(), "-1.0"));
CheckScalar(func, {std::make_shared<Decimal256Scalar>(max256, decimal256(76, 0))},
ScalarFromJSON(float64(), "Inf"));
CheckScalar(func, {std::make_shared<Decimal256Scalar>(-max256, decimal256(76, 0))},
ScalarFromJSON(float64(), "-1.0"));
for (const auto& ty : NegativeScaleTypes()) {
CheckScalar(func, {ArrayFromJSON(ty, R"([])")}, ArrayFromJSON(float64(), "[]"));
CheckScalar(func, {DecimalArrayFromJSON(ty, R"(["12E2", "0", "-42E2", null])")},
ArrayFromJSON(float64(), "[Inf, 0.0, -1.0, null]"));
}
}

TEST_F(TestUnaryArithmeticDecimal, Log) {
std::vector<std::string> unchecked = {"ln", "log2", "log10", "log1p"};
std::vector<std::string> checked = {"ln_checked", "log2_checked", "log10_checked",
Expand Down
2 changes: 2 additions & 0 deletions docs/source/cpp/compute.rst
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,8 @@ Mixed time resolution temporal inputs will be cast to finest input resolution.
+------------------+--------+-------------------------+---------------------------+-------+
| exp | Unary | Numeric | Float32/Float64 | |
+------------------+--------+-------------------------+---------------------------+-------+
| expm1 | Unary | Numeric | Float32/Float64 | |
+------------------+--------+-------------------------+---------------------------+-------+
| multiply | Binary | Numeric/Temporal | Numeric/Temporal | \(1) |
+------------------+--------+-------------------------+---------------------------+-------+
| multiply_checked | Binary | Numeric/Temporal | Numeric/Temporal | \(1) |
Expand Down
Loading