Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
7 changes: 3 additions & 4 deletions math/fixed_point/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,12 @@ single `i128`-style input or output.
`log2`, `ln`, and `log10` are computed from a shared `log2` kernel; `ln` and
`log10` apply a base-conversion factor on top.

- **UD30x9** aborts on `x < 1` (the result would be negative).
Rounds down.
- **UD30x9** aborts on `x < 1` (the result would be negative). Rounds down.
- **SD29x9** aborts on `x <= 0`. Rounds toward zero, matching `mul_trunc`,
`div_trunc`, and `pow` in the same module.

Round-down compounding can put the result 1 ulp from the mathematical answer
at irrational identity points (e.g. `log10(10·SCALE) == SCALE - 1`).
`log10` is exact on integer powers of ten (including sub-unit `10^-k` on
`SD29x9`): `log10(10^k) == k * SCALE`.

```move
use openzeppelin_fp_math::{sd29x9, ud30x9_convert};
Expand Down
21 changes: 18 additions & 3 deletions math/fixed_point/sources/sd29x9/sd29x9_base.move
Original file line number Diff line number Diff line change
Expand Up @@ -228,20 +228,35 @@ public fun ln(x: SD29x9): SD29x9 {

/// Computes the base-10 logarithm of an `SD29x9` value.
///
/// Derived from `log2` via `log10(x) = log2(x) * log10(2)`. Rounded toward
/// zero; see `log2` for full rounding semantics on signed results.
/// Exact when `x` is an integer power of ten, including sub-unit powers
/// `10^-k` (`k` in `1..=9`). Otherwise derived from `log2` via
/// `log10(x) = log2(x) * log10(2)` and rounded toward zero; see `log2`
/// for full rounding semantics on signed results.
///
/// #### Parameters
/// - `x`: Input value.
///
/// #### Returns
/// - `log10(x)`, rounded toward zero.
/// - `log10(x)`, rounded toward zero (exact at integer powers of ten).
///
/// #### Aborts
/// - `ELogUndefined` if `x` is zero or negative.
public fun log10(x: SD29x9): SD29x9 {
let Components { neg, mag } = decompose(x.unwrap());
assert!(!neg && mag > 0, ELogUndefined);
// Applied on the decomposed magnitude before the sign branch so sub-unit
// `10^-k` inputs (raw magnitudes below `SCALE`) also resolve exactly.
if (u256::is_power_of_ten(mag)) {
// Subtract `9 = log10(SCALE)` to strip the embedded scale; `j < 9`
// means a sub-unit input (`10^-k`), producing a negative result.
let j = u256::log10(mag, rounding::down());
let (is_neg, result_abs) = if (j >= 9) {
(false, ((j - 9) as u256) * common::scale_u256!())
} else {
(true, ((9 - j) as u256) * common::scale_u256!())
};
return wrap_components(Components { neg: is_neg, mag: result_abs })
};
let (log_neg, log_mag_internal) = common::raw_log2(mag as u128);
let result_mag = common::apply_log2_factor(log_mag_internal, common::log10_2_e18!());
wrap_components(Components { neg: log_neg, mag: result_mag as u256 })
Expand Down
16 changes: 11 additions & 5 deletions math/fixed_point/sources/ud30x9/ud30x9_base.move
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use openzeppelin_fp_math::common;
use openzeppelin_fp_math::sd29x9::{Self, SD29x9};
use openzeppelin_fp_math::ud30x9::{UD30x9, wrap, zero, one};
use openzeppelin_math::rounding;
use openzeppelin_math::u128;
use openzeppelin_math::u256;

// === Errors ===
Expand Down Expand Up @@ -279,16 +280,16 @@ public fun ln(x: UD30x9): UD30x9 {

/// Computes the base-10 logarithm of a `UD30x9` value.
///
/// Derived from `log2` via the identity `log10(x) = log2(x) * log10(2)`. Both
/// factors round toward zero, so the result may sit up to one ulp below the
/// true value. In particular, `log10(10) == one() - 1 ulp` under pure
/// round-down arithmetic.
/// Exact when `x` is an integer power of ten (`10^k`, `k >= 0`). Otherwise
/// derived from `log2` via `log10(x) = log2(x) * log10(2)`; both factors
/// floor, so the result may sit up to one ulp below the true value.
///
/// #### Parameters
/// - `x`: Input value.
///
/// #### Returns
/// - `log10(x)`, rounded down to the nearest representable `UD30x9` value.
/// - `log10(x)`, rounded down to the nearest representable `UD30x9` value
/// (exact at integer powers of ten).
///
/// #### Aborts
/// - `ELogUndefined` if `x` is zero.
Expand All @@ -298,6 +299,11 @@ public fun log10(x: UD30x9): UD30x9 {
let raw = x.unwrap();
assert!(raw > 0, ELogUndefined);
assert!(raw >= common::scale!(), ELogResultUnrepresentable);
if (u128::is_power_of_ten(raw)) {
// Subtract `9 = log10(SCALE)` to strip the embedded scale.
let j = u128::log10(raw, rounding::down());
return wrap(((j - 9) as u128) * common::scale!())
};
// The `raw >= scale` precondition guarantees `raw_log2` returns a
// non-negative sign, so the discarded sign flag is provably `false`.
let (_, mag) = common::raw_log2(raw);
Expand Down
27 changes: 7 additions & 20 deletions math/fixed_point/tests/sd29x9_tests/log10_tests.move
Original file line number Diff line number Diff line change
Expand Up @@ -15,37 +15,25 @@ fun log10_of_one_is_zero() {
assert_eq!(sd29x9::one().log10(), sd29x9::zero());
}

// === Powers of ten (within 1 ulp at UD30x9 scale) ===
// === Powers of ten (exact) ===

#[test]
fun log10_of_positive_powers_of_ten_pins_values() {
// log10(1) = 0 exactly.
fun log10_of_positive_powers_of_ten_is_exact() {
assert_eq!(pos(SCALE).log10(), pos(0));
// For k >= 1, log10(10^k) = k exactly, but flooring the floored-constant
// product at user scale lands the result 1 ulp below k * SCALE.
let mut k: u8 = 1;
while (k <= 11) {
let x_raw = std::u128::pow(10, k) * SCALE;
let expected = pos((k as u128) * SCALE - 1);
assert_eq!(pos(x_raw).log10(), expected);
assert_eq!(pos(x_raw).log10(), pos((k as u128) * SCALE));
k = k + 1;
};
}

#[test]
fun log10_of_negative_powers_of_ten_pins_values() {
// log10(10^-k) = -k exactly. The negative-branch kernel produces a high-
// biased magnitude; combined with the floored `log10(2)` constant, the
// user-scale floor lands at `k * SCALE` for small `k` and one ulp below
// for larger `k`. Empirically:
// k = 1..=4: magnitude = k * SCALE
// k = 5..=9: magnitude = k * SCALE - 1
fun log10_of_negative_powers_of_ten_is_exact() {
let mut k: u8 = 1;
while (k <= 9) {
let x_raw = SCALE / std::u128::pow(10, k);
let mag = (k as u128) * SCALE;
let expected = neg(if (k <= 4) mag else mag - 1);
assert_eq!(pos(x_raw).log10(), expected);
assert_eq!(pos(x_raw).log10(), neg((k as u128) * SCALE));
k = k + 1;
};
}
Expand Down Expand Up @@ -79,7 +67,6 @@ fun log10_of_min_value_aborts() {

#[test]
fun log10_of_pos_1_matches_reference() {
// pos(1) represents 10^-9. log10(10^-9) = -9 exactly; flooring at user
// scale lands the magnitude 1 ulp below 9 * SCALE.
assert_eq!(pos(1).log10(), neg(9 * SCALE - 1));
// pos(1) represents 10^-9.
assert_eq!(pos(1).log10(), neg(9 * SCALE));
}
10 changes: 3 additions & 7 deletions math/fixed_point/tests/ud30x9_tests/log10_tests.move
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,15 @@ fun log10_of_one_is_zero() {
assert_eq!(ud30x9::one().log10(), ud30x9::zero());
}

// === Powers of 10 (algorithm floors at user scale, so k >= 1 lands 1 ulp below) ===
// === Powers of 10 (exact) ===

#[test]
fun log10_of_powers_of_ten_pins_values() {
// log10(1) = 0 exactly.
fun log10_of_powers_of_ten_is_exact() {
assert_eq!(fixed(SCALE).log10(), fixed(0));
// For k >= 1, log10(10^k) = k exactly, but flooring the floored-constant
// product at user scale lands the result 1 ulp below k * SCALE.
let mut k: u8 = 1;
while (k <= 11) {
let x_raw = std::u128::pow(10, k) * SCALE;
let expected = (k as u128) * SCALE - 1;
assert_eq!(fixed(x_raw).log10(), fixed(expected));
assert_eq!(fixed(x_raw).log10(), fixed((k as u128) * SCALE));
k = k + 1;
};
}
Expand Down
Loading