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
39 changes: 39 additions & 0 deletions benches/scalar_micro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,18 @@ const SCALAR_MICRO_GROUPS: &[BenchGroupDoc] = &[
name: "atanh_sqrt_two_error",
description: "Rejects atanh(sqrt(2)) through exact structural domain checks.",
},
BenchDoc {
name: "log2_power_of_two",
description: "Folds log2(1024) to the exact rational 10 via the integer-log-detection shortcut.",
},
BenchDoc {
name: "log2_rational_three",
description: "Builds log2(3) as a lightweight Log2 symbolic certificate.",
},
BenchDoc {
name: "log2_ln_quotient_fold",
description: "Folds ln(5) / ln(2) into a Log2 certificate via the divide-recognize shortcut.",
},
],
},
BenchGroupDoc {
Expand Down Expand Up @@ -1063,6 +1075,33 @@ fn bench_exact_transcendental_special_forms(c: &mut Criterion) {
)
});

let log2_power = Real::new(Rational::new(1024));
let log2_three = Real::new(Rational::new(3));
let ln_five = Real::new(Rational::new(5)).ln().unwrap();
let ln_two_for_quotient = Real::new(Rational::new(2)).ln().unwrap();

group.bench_function("log2_power_of_two", |b| {
b.iter_batched(
|| log2_power.clone(),
|value| black_box(value.log2().unwrap()),
BatchSize::SmallInput,
)
});
group.bench_function("log2_rational_three", |b| {
b.iter_batched(
|| log2_three.clone(),
|value| black_box(value.log2().unwrap()),
BatchSize::SmallInput,
)
});
group.bench_function("log2_ln_quotient_fold", |b| {
b.iter_batched(
|| (ln_five.clone(), ln_two_for_quotient.clone()),
|(num, den)| black_box((num / den).unwrap()),
BatchSize::SmallInput,
)
});

group.finish();
}

Expand Down
3 changes: 3 additions & 0 deletions benchmarks.md
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,9 @@ Construction-time shortcuts for exact rational multiples of pi and inverse compo
| `exact_transcendental_special_forms/asinh_large` | not run | not run | Builds a large inverse hyperbolic sine without exact intermediate Reals. |
| `exact_transcendental_special_forms/atanh_sqrt_half` | 192.18 ns | 189.98 ns - 194.71 ns | Builds atanh(sqrt(2)/2) after exact structural domain checks. |
| `exact_transcendental_special_forms/atanh_sqrt_two_error` | 199.45 ns | 121.09 ns - 355.20 ns | Rejects atanh(sqrt(2)) through exact structural domain checks. |
| `exact_transcendental_special_forms/log2_power_of_two` | 173.46 ns | 171.28 ns - 175.58 ns | Folds log2(1024) to the exact rational 10 via the integer-log-detection shortcut. |
| `exact_transcendental_special_forms/log2_rational_three` | 289.47 ns | 284.87 ns - 294.52 ns | Builds log2(3) as a lightweight Log2 symbolic certificate. |
| `exact_transcendental_special_forms/log2_ln_quotient_fold` | 1.283 us | 1.228 us - 1.371 us | Folds ln(5) / ln(2) into a Log2 certificate via the divide-recognize shortcut. |

### `symbolic_reductions`

Expand Down
90 changes: 85 additions & 5 deletions src/real/arithmetic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ pub(crate) enum Class {
// representation small while still preserving a lightweight symbolic form.
LnProduct(Box<LnProductClass>), // Product of two logarithms, ordered by base
Log10(Rational), // Rational > 1 and never a multiple of ten
Log2(Rational), // Rational > 1 and never a power of two
SinPi(Rational), // 0 < Rational < 1/2 also never 1/6 or 1/4 or 1/3
TanPi(Rational), // 0 < Rational < 1/2 also never 1/6 or 1/4 or 1/3
Irrational,
Expand Down Expand Up @@ -135,6 +136,7 @@ impl PartialEq for Class {
left.left == right.left && left.right == right.right
}
(Log10(r), Log10(s)) => r == s,
(Log2(r), Log2(s)) => r == s,
(SinPi(r), SinPi(s)) => r == s,
(TanPi(r), TanPi(s)) => r == s,
(_, _) => false,
Expand Down Expand Up @@ -541,6 +543,9 @@ impl Class {
Log10(base) => {
Self::ln_computable(base).multiply(Self::ln_computable(&*rationals::TEN).inverse())
}
Log2(base) => {
Self::ln_computable(base).multiply(Self::ln_computable(&*rationals::TWO).inverse())
}
SinPi(rational) => {
let argument =
Computable::multiply(Computable::pi(), Computable::rational(rational.clone()));
Expand Down Expand Up @@ -1496,7 +1501,7 @@ impl Real {
Irrational => "scaled-computable",
Pi | PiPow(_) | PiInv | PiExp(_) | PiInvExp(_) | PiSqrt(_) | ConstProduct(_)
| ConstOffset(_) | ConstProductSqrt(_) | Sqrt(_) | Exp(_) | Ln(_) | LnAffine(_)
| LnProduct(_) | Log10(_) | SinPi(_) | TanPi(_) => "symbolic-nonzero-scale",
| LnProduct(_) | Log10(_) | Log2(_) | SinPi(_) | TanPi(_) => "symbolic-nonzero-scale",
}
);

Expand All @@ -1505,7 +1510,7 @@ impl Real {
One => Some(real_sign_from_num(rational_sign)),
Pi | PiPow(_) | PiInv | PiExp(_) | PiInvExp(_) | PiSqrt(_) | ConstProduct(_)
| ConstOffset(_) | ConstProductSqrt(_) | Sqrt(_) | Exp(_) | Ln(_) | LnAffine(_)
| LnProduct(_) | Log10(_) | SinPi(_) | TanPi(_) => {
| LnProduct(_) | Log10(_) | Log2(_) | SinPi(_) | TanPi(_) => {
// Exact symbolic classes are positive by construction, so the
// outer rational scale alone determines sign. Additive classes
// such as ConstOffset/LnAffine are admitted only when this
Expand Down Expand Up @@ -3263,6 +3268,61 @@ impl Real {
})
}

/// The base 2 logarithm of this Real or Problem::NotANumber if this Real is not positive.
pub fn log2(self) -> Result<Real, Problem> {
// Domain check uses structural sign first. Refinement-forced sign
// (`best_sign`) is reserved for the case where cheap inspection cannot
// decide; rejecting structurally known nonpositive inputs avoids
// ~2µs of computable work on the typical hot path.
match self.structural_facts().sign {
Some(RealSign::Positive) => {}
Some(RealSign::Zero | RealSign::Negative) => {
crate::trace_dispatch!("real", "log2", "domain-not-positive");
return Err(Problem::NotANumber);
}
None => {
if self.best_sign() != Sign::Plus {
crate::trace_dispatch!("real", "log2", "domain-not-positive");
return Err(Problem::NotANumber);
}
}
}
if let One = &self.class {
return Self::log2_rational(self.rational);
}
crate::trace_dispatch!("real", "log2", "ln-div-cached-ln2");
self.ln()? / constants::scaled_ln(2, 1).unwrap()
}

fn log2_rational(r: Rational) -> Result<Real, Problem> {
match r.cmp_one_structural() {
std::cmp::Ordering::Less => {
let inv = r.inverse()?;
return Ok(-Self::log2_rational(inv)?);
}
std::cmp::Ordering::Equal => return Ok(Self::zero()),
std::cmp::Ordering::Greater => {}
}

if let Some(n) = r.integer_magnitude()
&& let Some(log) = Self::integer_log(n, 2)
{
crate::trace_dispatch!("real", "log2", "rational-power-of-two");
return Ok(Self::new(Rational::new(log as i64)));
}

crate::trace_dispatch!("real", "log2", "rational-log2-special-form");
let computable =
Class::ln_computable(&r).multiply(Class::ln_computable(&*rationals::TWO).inverse());
Ok(Self {
rational: Rational::one(),
class: Log2(r),
computable: Some(computable),
signal: None,
primitive_approx_cache: Cell::new(PrimitiveApproxCache::Empty),
})
}

// Find Some(m) integral log with respect to this base or else None
// n should be positive (not zero) and base should be >= 2
fn integer_log(n: &BigUint, base: u32) -> Option<u64> {
Expand Down Expand Up @@ -4619,7 +4679,7 @@ fn structural_kind_for_class(class: &Class) -> StructuralKind {
Pi | PiPow(_) | PiInv => StructuralKind::PiLike,
Exp(_) | PiExp(_) | PiInvExp(_) => StructuralKind::ExpLike,
Sqrt(_) | PiSqrt(_) => StructuralKind::SqrtLike,
Ln(_) | LnAffine(_) | LnProduct(_) | Log10(_) => StructuralKind::LogLike,
Ln(_) | LnAffine(_) | LnProduct(_) | Log10(_) | Log2(_) => StructuralKind::LogLike,
SinPi(_) | TanPi(_) => StructuralKind::TrigExact,
ConstProduct(_) | ConstOffset(_) | ConstProductSqrt(_) => StructuralKind::ProductConstant,
Irrational => StructuralKind::ComputableOpaque,
Expand All @@ -4631,7 +4691,7 @@ fn symbolic_degree_for_class(class: &Class) -> ExpressionDegree {
Irrational => ExpressionDegree::Unknown,
One | Pi | PiPow(_) | PiInv | PiExp(_) | PiInvExp(_) | PiSqrt(_) | ConstProduct(_)
| ConstOffset(_) | ConstProductSqrt(_) | Sqrt(_) | Exp(_) | Ln(_) | LnAffine(_)
| LnProduct(_) | Log10(_) | SinPi(_) | TanPi(_) => ExpressionDegree::Constant,
| LnProduct(_) | Log10(_) | Log2(_) | SinPi(_) | TanPi(_) => ExpressionDegree::Constant,
}
}

Expand All @@ -4647,7 +4707,7 @@ fn symbolic_dependencies_for_class(class: &Class) -> SymbolicDependencyMask {
ConstProductSqrt(product) => pi_exp_dependency_mask(product.pi_power, &product.exp_power)
.union(SymbolicDependencyMask::SQRT),
Sqrt(_) => SymbolicDependencyMask::SQRT,
Ln(_) | LnAffine(_) | LnProduct(_) | Log10(_) => SymbolicDependencyMask::LOG,
Ln(_) | LnAffine(_) | LnProduct(_) | Log10(_) | Log2(_) => SymbolicDependencyMask::LOG,
SinPi(_) | TanPi(_) => SymbolicDependencyMask::TRIG.union(SymbolicDependencyMask::PI),
Irrational => SymbolicDependencyMask::OPAQUE,
}
Expand Down Expand Up @@ -4747,6 +4807,7 @@ impl fmt::Display for Real {
write!(f, " x ln({}) x ln({})", product.left, product.right)
}
Log10(n) => write!(f, " x log10({})", &n),
Log2(n) => write!(f, " x log2({})", &n),
Sqrt(n) => write!(f, " √({})", &n),
SinPi(n) => write!(f, " x sin({} x Pi)", &n),
TanPi(n) => write!(f, " x tan({} x Pi)", &n),
Expand Down Expand Up @@ -5884,6 +5945,25 @@ impl<T: AsRef<Real>> Div<T> for &Real {
..self.clone()
});
}
if s == *rationals::TWO {
// Same rationale as the log10 fold: keep two-log quotients
// anchored on a single Log2 certificate.
let Ln(r) = &self.class else {
unreachable!();
};
let rational = &self.rational / &other.rational;
let ln2 = constants::scaled_ln(2, 1).unwrap();
let computable = self
.computable_clone()
.multiply(ln2.computable_clone().inverse());
return Ok(Real {
rational,
class: Log2(r.clone()),
computable: Some(computable),
signal: self.signal.clone(),
primitive_approx_cache: Cell::new(PrimitiveApproxCache::Empty),
});
Comment thread
TimTheBig marked this conversation as resolved.
}
} else {
unreachable!();
}
Expand Down
85 changes: 85 additions & 0 deletions src/real/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1120,6 +1120,91 @@ mod tests {
assert!((actual - 14.508657738524219).abs() < 1e-12);
}

#[test]
fn log2_of_powers_of_two_is_exact_integer() {
for k in 0_i64..=20 {
let n = Real::new(Rational::new(1_i64 << k));
let answer = n.log2().unwrap();
assert_eq!(answer, Rational::new(k));
}
}

#[test]
fn log2_of_one_is_zero() {
assert_eq!(Real::one().log2().unwrap(), Real::zero());
}

#[test]
fn log2_of_one_half_is_negative_one() {
let half = Real::new(Rational::fraction(1, 2).unwrap());
assert_eq!(half.log2().unwrap(), Rational::new(-1));
}

#[test]
fn log2_of_inverse_power_of_two_is_negative_integer() {
for k in 1_i64..=12 {
let n = Real::new(Rational::fraction(1, 1_u64 << k).unwrap());
let answer = n.log2().unwrap();
assert_eq!(answer, Rational::new(-k));
}
}

#[test]
fn log2_of_rational_matches_f64() {
for &n in &[3_i64, 5, 7, 9, 11, 13, 17] {
let value: f64 = Real::new(Rational::new(n)).log2().unwrap().into();
let expected = (n as f64).log2();
assert!(
(value - expected).abs() < 1e-12,
"log2({n}) = {value}, expected {expected}"
);
}
}

#[test]
fn log2_of_negative_errors() {
let negative = Real::new(Rational::new(-3));
assert_eq!(negative.log2(), Err(Problem::NotANumber));
}

#[test]
fn log2_of_zero_errors() {
assert_eq!(Real::zero().log2(), Err(Problem::NotANumber));
}

#[test]
fn log2_matches_ln_div_ln2() {
let x = Real::new(Rational::new(7));
let direct = x.clone().log2().unwrap();
let via_quotient = (x.ln().unwrap() / Real::new(Rational::new(2)).ln().unwrap()).unwrap();
let difference: f64 = (direct - via_quotient).into();
assert!(difference.abs() < 1e-14);
}

#[test]
fn log2_of_sqrt_two_is_half() {
let sqrt_two = Real::from(2_i32).sqrt().unwrap();
let value: f64 = sqrt_two.log2().unwrap().into();
assert!((value - 0.5).abs() < 1e-12);
}

#[test]
fn log2_of_irrational_argument_matches_f64() {
let value = Real::from(2_i32) + Real::from(3_i32).sqrt().unwrap();
let actual: f64 = value.log2().unwrap().into();
let expected = (2.0_f64 + 3.0_f64.sqrt()).log2();
assert!((actual - expected).abs() < 1e-12);
}

#[test]
fn log2_ln_quotient_folds_to_log2_class() {
let numerator = Real::new(Rational::new(5)).ln().unwrap();
let denominator = Real::new(Rational::new(2)).ln().unwrap();
let quotient = (numerator / denominator).unwrap();
let expected = Real::new(Rational::new(5)).log2().unwrap();
assert_eq!(quotient, expected);
}

fn assert_close(value: Real, expected: f64, tolerance: f64) {
let actual: f64 = value.into();
let scale = expected.abs().max(1.0);
Expand Down
10 changes: 10 additions & 0 deletions src/simple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ enum Operator {
Sqrt,
Exp,
Log10,
Log2,
Ln,
Cos,
Sin,
Expand Down Expand Up @@ -414,6 +415,14 @@ impl Simple {
let value = operand.value(names)?.log10()?;
Ok(value)
}
Log2 => {
if self.operands.len() != 1 {
return Err(Problem::ParseError);
}
let operand = self.operands.first().unwrap();
let value = operand.value(names)?.log2()?;
Ok(value)
}
Ln => {
if self.operands.len() != 1 {
return Err(Problem::ParseError);
Expand Down Expand Up @@ -525,6 +534,7 @@ impl Simple {
use Operator::*;
match Self::consume_operator_token(chars).as_str() {
"log10" | "log" => Ok(Log10),
"log2" | "lg" => Ok(Log2),
"ln" | "l" => Ok(Ln),
"exp" | "e" => Ok(Exp),
"sqrt" | "s" => Ok(Sqrt),
Expand Down
Loading