Skip to content

Commit 7d8a544

Browse files
authored
Support computing values for all rational powers (#213)
I thought we would need to wait for a `constexpr` version of `std::powl`, but I was wrong. Instead, we can get away with a `root` function. Note that there's no guarantee that `std::powl` would be better in all cases, even if we _could_ just use it. After all, its second argument is `1.0l / n`, which is a lossy representation of the Nth root. On the implementation side, we take advantage of the fact that this function is only ever meant to be called at compile time, which lets us prioritize accuracy over "speed" (because the runtime speed is always essentially infinite). We do a binary search between 1 and `x` after checking that `x > 1` (and `n > 1`). When we end up with two neighboring floating point values, we pick whichever gives the closest result when we run it through `checked_int_pow`. This algorithm is guaranteed to produce the best representable result for a "pure root". For other rational powers, it's technically possible that we could have some lossiness in the `checked_int_pow` computation, if we enter a floating point realm that is high enough such that not all integer values can be represented. That said, I tried a bunch of test cases to compare it to `powl`, and it's passed all the ones that I was able to come up with. Fixes #116.
1 parent a771f57 commit 7d8a544

File tree

3 files changed

+227
-18
lines changed

3 files changed

+227
-18
lines changed

au/magnitude.hh

Lines changed: 104 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ namespace detail {
270270
enum class MagRepresentationOutcome {
271271
OK,
272272
ERR_NON_INTEGER_IN_INTEGER_TYPE,
273-
ERR_RATIONAL_POWERS,
273+
ERR_INVALID_ROOT,
274274
ERR_CANNOT_FIT,
275275
};
276276

@@ -316,10 +316,101 @@ constexpr MagRepresentationOrError<T> checked_int_pow(T base, std::uintmax_t exp
316316
return result;
317317
}
318318

319-
template <typename T, std::intmax_t N, typename B>
319+
template <typename T>
320+
constexpr MagRepresentationOrError<T> root(T x, std::uintmax_t n) {
321+
// The "zeroth root" would be mathematically undefined.
322+
if (n == 0) {
323+
return {MagRepresentationOutcome::ERR_INVALID_ROOT};
324+
}
325+
326+
// The "first root" is trivial.
327+
if (n == 1) {
328+
return {MagRepresentationOutcome::OK, x};
329+
}
330+
331+
// We only support nontrivial roots of floating point types.
332+
if (!std::is_floating_point<T>::value) {
333+
return {MagRepresentationOutcome::ERR_NON_INTEGER_IN_INTEGER_TYPE};
334+
}
335+
336+
// Handle negative numbers: only odd roots are allowed.
337+
if (x < 0) {
338+
if (n % 2 == 0) {
339+
return {MagRepresentationOutcome::ERR_INVALID_ROOT};
340+
} else {
341+
const auto negative_result = root(-x, n);
342+
if (negative_result.outcome != MagRepresentationOutcome::OK) {
343+
return {negative_result.outcome};
344+
}
345+
return {MagRepresentationOutcome::OK, static_cast<T>(-negative_result.value)};
346+
}
347+
}
348+
349+
// Handle special cases of zero and one.
350+
if (x == 0 || x == 1) {
351+
return {MagRepresentationOutcome::OK, x};
352+
}
353+
354+
// Handle numbers bewtween 0 and 1.
355+
if (x < 1) {
356+
const auto inverse_result = root(T{1} / x, n);
357+
if (inverse_result.outcome != MagRepresentationOutcome::OK) {
358+
return {inverse_result.outcome};
359+
}
360+
return {MagRepresentationOutcome::OK, static_cast<T>(T{1} / inverse_result.value)};
361+
}
362+
363+
//
364+
// At this point, error conditions are finished, and we can proceed with the "core" algorithm.
365+
//
366+
367+
// Always use `long double` for intermediate computations. We don't ever expect people to be
368+
// calling this at runtime, so we want maximum accuracy.
369+
long double lo = 1.0;
370+
long double hi = x;
371+
372+
// Do a binary search to find the closest value such that `checked_int_pow` recovers the input.
373+
//
374+
// Because we know `n > 1`, and `x > 1`, and x^n is monotonically increasing, we know that
375+
// `checked_int_pow(lo, n) < x < checked_int_pow(hi, n)`. We will preserve this as an
376+
// invariant.
377+
while (lo < hi) {
378+
long double mid = lo + (hi - lo) / 2;
379+
380+
auto result = checked_int_pow(mid, n);
381+
382+
if (result.outcome != MagRepresentationOutcome::OK) {
383+
return {result.outcome};
384+
}
385+
386+
// Early return if we get lucky with an exact answer.
387+
if (result.value == x) {
388+
return {MagRepresentationOutcome::OK, static_cast<T>(mid)};
389+
}
390+
391+
// Check for stagnation.
392+
if (mid == lo || mid == hi) {
393+
break;
394+
}
395+
396+
// Preserve the invariant that `checked_int_pow(lo, n) < x < checked_int_pow(hi, n)`.
397+
if (result.value < x) {
398+
lo = mid;
399+
} else {
400+
hi = mid;
401+
}
402+
}
403+
404+
// Pick whichever one gets closer to the target.
405+
const auto lo_diff = x - checked_int_pow(lo, n).value;
406+
const auto hi_diff = checked_int_pow(hi, n).value - x;
407+
return {MagRepresentationOutcome::OK, static_cast<T>(lo_diff < hi_diff ? lo : hi)};
408+
}
409+
410+
template <typename T, std::intmax_t N, std::uintmax_t D, typename B>
320411
constexpr MagRepresentationOrError<Widen<T>> base_power_value(B base) {
321412
if (N < 0) {
322-
const auto inverse_result = base_power_value<T, -N>(base);
413+
const auto inverse_result = base_power_value<T, -N, D>(base);
323414
if (inverse_result.outcome != MagRepresentationOutcome::OK) {
324415
return inverse_result;
325416
}
@@ -329,7 +420,12 @@ constexpr MagRepresentationOrError<Widen<T>> base_power_value(B base) {
329420
};
330421
}
331422

332-
return checked_int_pow(static_cast<Widen<T>>(base), static_cast<std::uintmax_t>(N));
423+
const auto power_result =
424+
checked_int_pow(static_cast<Widen<T>>(base), static_cast<std::uintmax_t>(N));
425+
if (power_result.outcome != MagRepresentationOutcome::OK) {
426+
return {power_result.outcome};
427+
}
428+
return (D > 1) ? root(power_result.value, D) : power_result;
333429
}
334430

335431
template <typename T, std::size_t N>
@@ -393,15 +489,10 @@ constexpr MagRepresentationOrError<T> get_value_result(Magnitude<BPs...>) {
393489
return {MagRepresentationOutcome::ERR_NON_INTEGER_IN_INTEGER_TYPE};
394490
}
395491

396-
// Computing values for rational base powers is something we would _like_ to support, but we
397-
// need a `constexpr` implementation of `powl()` first.
398-
if (!all({(ExpT<BPs>::den == 1)...})) {
399-
return {MagRepresentationOutcome::ERR_RATIONAL_POWERS};
400-
}
401-
402492
// Force the expression to be evaluated in a constexpr context.
403493
constexpr auto widened_result =
404-
product({base_power_value<T, (ExpT<BPs>::num / ExpT<BPs>::den)>(BaseT<BPs>::value())...});
494+
product({base_power_value<T, ExpT<BPs>::num, static_cast<std::uintmax_t>(ExpT<BPs>::den)>(
495+
BaseT<BPs>::value())...});
405496

406497
if ((widened_result.outcome != MagRepresentationOutcome::OK) ||
407498
!safe_to_cast_to<T>(widened_result.value)) {
@@ -433,8 +524,8 @@ constexpr T get_value(Magnitude<BPs...> m) {
433524

434525
static_assert(result.outcome != MagRepresentationOutcome::ERR_NON_INTEGER_IN_INTEGER_TYPE,
435526
"Cannot represent non-integer in integral destination type");
436-
static_assert(result.outcome != MagRepresentationOutcome::ERR_RATIONAL_POWERS,
437-
"Computing values for rational powers not yet supported");
527+
static_assert(result.outcome != MagRepresentationOutcome::ERR_INVALID_ROOT,
528+
"Could not compute root for rational power of base");
438529
static_assert(result.outcome != MagRepresentationOutcome::ERR_CANNOT_FIT,
439530
"Value outside range of destination type");
440531

au/magnitude_test.cc

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
#include "gtest/gtest.h"
2121

2222
using ::testing::DoubleEq;
23+
using ::testing::Eq;
24+
using ::testing::FloatEq;
2325
using ::testing::StaticAssertTypeEq;
2426

2527
namespace au {
@@ -208,6 +210,11 @@ TEST(GetValue, ImpossibleRequestsArePreventedAtCompileTime) {
208210
// get_value<int>(sqrt_2);
209211
}
210212

213+
TEST(GetValue, HandlesRoots) {
214+
constexpr auto sqrt_2 = get_value<double>(root<2>(mag<2>()));
215+
EXPECT_DOUBLE_EQ(sqrt_2 * sqrt_2, 2.0);
216+
}
217+
211218
TEST(GetValue, WorksForEmptyPack) {
212219
constexpr auto one = Magnitude<>{};
213220
EXPECT_THAT(get_value<int>(one), SameTypeAndValue(1));
@@ -270,6 +277,15 @@ MATCHER(CannotFit, "") {
270277
return (arg.outcome == MagRepresentationOutcome::ERR_CANNOT_FIT) && (arg.value == 0);
271278
}
272279

280+
MATCHER(NonIntegerInIntegerType, "") {
281+
return (arg.outcome == MagRepresentationOutcome::ERR_NON_INTEGER_IN_INTEGER_TYPE) &&
282+
(arg.value == 0);
283+
}
284+
285+
MATCHER(InvalidRoot, "") {
286+
return (arg.outcome == MagRepresentationOutcome::ERR_INVALID_ROOT) && (arg.value == 0);
287+
}
288+
273289
template <typename T, typename ValueMatcher>
274290
auto FitsAndMatchesValue(ValueMatcher &&matcher) {
275291
return ::testing::AllOf(
@@ -298,6 +314,106 @@ TEST(CheckedIntPow, FindsAppropriateLimits) {
298314
EXPECT_THAT(checked_int_pow(10.0, 309), CannotFit());
299315
}
300316

317+
TEST(Root, ReturnsErrorForIntegralType) {
318+
EXPECT_THAT(root(4, 2), NonIntegerInIntegerType());
319+
EXPECT_THAT(root(uint8_t{125}, 3), NonIntegerInIntegerType());
320+
}
321+
322+
TEST(Root, ReturnsErrorForZerothRoot) {
323+
EXPECT_THAT(root(4.0, 0), InvalidRoot());
324+
EXPECT_THAT(root(125.0, 0), InvalidRoot());
325+
}
326+
327+
TEST(Root, NegativeRootsWorkForOddPowersOnly) {
328+
EXPECT_THAT(root(-4.0, 2), InvalidRoot());
329+
EXPECT_THAT(root(-125.0, 3), FitsAndProducesValue(-5.0));
330+
EXPECT_THAT(root(-10000.0, 4), InvalidRoot());
331+
}
332+
333+
TEST(Root, AnyRootOfOneIsOne) {
334+
for (const std::uintmax_t r : {1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u, 9u}) {
335+
EXPECT_THAT(root(1.0, r), FitsAndProducesValue(1.0));
336+
}
337+
}
338+
339+
TEST(Root, AnyRootOfZeroIsZero) {
340+
for (const std::uintmax_t r : {1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u, 9u}) {
341+
EXPECT_THAT(root(0.0, r), FitsAndProducesValue(0.0));
342+
}
343+
}
344+
345+
TEST(Root, OddRootOfNegativeOneIsItself) {
346+
EXPECT_THAT(root(-1.0, 1), FitsAndProducesValue(-1.0));
347+
EXPECT_THAT(root(-1.0, 2), InvalidRoot());
348+
EXPECT_THAT(root(-1.0, 3), FitsAndProducesValue(-1.0));
349+
EXPECT_THAT(root(-1.0, 4), InvalidRoot());
350+
EXPECT_THAT(root(-1.0, 5), FitsAndProducesValue(-1.0));
351+
}
352+
353+
TEST(Root, RecoversExactValueWherePossible) {
354+
{
355+
const auto sqrt_4f = root(4.0f, 2);
356+
EXPECT_THAT(sqrt_4f.outcome, Eq(MagRepresentationOutcome::OK));
357+
EXPECT_THAT(sqrt_4f.value, SameTypeAndValue(2.0f));
358+
}
359+
360+
{
361+
const auto cbrt_125L = root(125.0L, 3);
362+
EXPECT_THAT(cbrt_125L.outcome, Eq(MagRepresentationOutcome::OK));
363+
EXPECT_THAT(cbrt_125L.value, SameTypeAndValue(5.0L));
364+
}
365+
}
366+
367+
TEST(Root, HandlesArgumentsBetweenOneAndZero) {
368+
EXPECT_THAT(root(0.25, 2), FitsAndProducesValue(0.5));
369+
EXPECT_THAT(root(0.0001, 4), FitsAndMatchesValue<double>(DoubleEq(0.1)));
370+
}
371+
372+
TEST(Root, ResultIsVeryCloseToStdPowForPureRoots) {
373+
for (const double x : {55.5, 123.456, 789.012, 3456.789, 12345.6789, 5.67e25}) {
374+
for (const auto r : {2u, 3u, 4u, 5u, 6u, 7u, 8u, 9u}) {
375+
const auto double_result = root(x, r);
376+
EXPECT_THAT(double_result.outcome, Eq(MagRepresentationOutcome::OK));
377+
EXPECT_THAT(double_result.value, DoubleEq(static_cast<double>(std::pow(x, 1.0L / r))));
378+
379+
const auto float_result = root(static_cast<float>(x), r);
380+
EXPECT_THAT(float_result.outcome, Eq(MagRepresentationOutcome::OK));
381+
EXPECT_THAT(float_result.value, FloatEq(static_cast<float>(std::pow(x, 1.0L / r))));
382+
}
383+
}
384+
}
385+
386+
TEST(Root, ResultAtLeastAsGoodAsStdPowForRationalPowers) {
387+
struct RationalPower {
388+
std::uintmax_t num;
389+
std::uintmax_t den;
390+
};
391+
392+
auto result_via_root = [](double x, RationalPower power) {
393+
return static_cast<double>(
394+
root(checked_int_pow(static_cast<long double>(x), power.num).value, power.den).value);
395+
};
396+
397+
auto result_via_std_pow = [](double x, RationalPower power) {
398+
return static_cast<double>(
399+
std::pow(static_cast<long double>(x),
400+
static_cast<long double>(power.num) / static_cast<long double>(power.den)));
401+
};
402+
403+
auto round_trip_error = [](double x, RationalPower power, auto func) {
404+
const auto round_trip_result = func(func(x, power), {power.den, power.num});
405+
return std::abs(round_trip_result - x);
406+
};
407+
408+
for (const auto base : {2.0, 3.1415, 98.6, 1.2e-10, 5.5e15}) {
409+
for (const auto power : std::vector<RationalPower>{{5, 2}, {2, 3}, {7, 4}}) {
410+
const auto error_from_root = round_trip_error(base, power, result_via_root);
411+
const auto error_from_std_pow = round_trip_error(base, power, result_via_std_pow);
412+
EXPECT_LE(error_from_root, error_from_std_pow);
413+
}
414+
}
415+
}
416+
301417
TEST(GetValueResult, HandlesNumbersTooBigForUintmax) {
302418
EXPECT_THAT(get_value_result<std::uintmax_t>(pow<64>(mag<2>())), CannotFit());
303419
}

au/quantity_test.cc

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -263,18 +263,20 @@ TEST(Quantity, HandlesBaseDimensionsWithFractionalExponents) {
263263
}
264264

265265
TEST(Quantity, HandlesMagnitudesWithFractionalExponents) {
266-
using RootKiloFeet = decltype(root<2>(Kilo<Feet>{}));
267-
constexpr auto x = make_quantity<RootKiloFeet>(3);
266+
constexpr auto x = sqrt(kilo(feet))(3.0);
268267

269268
// We can retrieve the value in the same unit (regardless of the scale's fractional powers).
270-
EXPECT_EQ(x.in(RootKiloFeet{}), 3);
269+
EXPECT_EQ(x.in(sqrt(kilo(feet))), 3.0);
271270

272271
// We can retrieve the value in a *different* unit, which *also* has fractional powers, as long
273272
// as their *ratio* has no fractional powers.
274-
EXPECT_EQ(x.in(root<2>(Milli<Feet>{})), 3'000);
273+
EXPECT_EQ(x.in(sqrt(milli(feet))), 3'000.0);
274+
275+
// We can also retrieve the value in a different unit whose ratio *does* have fractional powers.
276+
EXPECT_NEAR(x.in(sqrt(feet)), 94.86833, 1e-5);
275277

276278
// Squaring the fractional base power gives us an exact non-fractional dimension and scale.
277-
EXPECT_EQ(x * x, kilo(feet)(9));
279+
EXPECT_EQ(x * x, kilo(feet)(9.0));
278280
}
279281

280282
// A custom "Quantity-equivalent" type, whose interop with Quantity we'll provide below.

0 commit comments

Comments
 (0)