Skip to content

Commit 794a4cf

Browse files
committed
Merge remote-tracking branch 'origin/pr/8' into hyperreal
2 parents bd294c5 + 230e245 commit 794a4cf

5 files changed

Lines changed: 222 additions & 5 deletions

File tree

benches/scalar_micro.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,18 @@ const SCALAR_MICRO_GROUPS: &[BenchGroupDoc] = &[
412412
name: "atanh_sqrt_two_error",
413413
description: "Rejects atanh(sqrt(2)) through exact structural domain checks.",
414414
},
415+
BenchDoc {
416+
name: "log2_power_of_two",
417+
description: "Folds log2(1024) to the exact rational 10 via the integer-log-detection shortcut.",
418+
},
419+
BenchDoc {
420+
name: "log2_rational_three",
421+
description: "Builds log2(3) as a lightweight Log2 symbolic certificate.",
422+
},
423+
BenchDoc {
424+
name: "log2_ln_quotient_fold",
425+
description: "Folds ln(5) / ln(2) into a Log2 certificate via the divide-recognize shortcut.",
426+
},
415427
],
416428
},
417429
BenchGroupDoc {
@@ -1063,6 +1075,33 @@ fn bench_exact_transcendental_special_forms(c: &mut Criterion) {
10631075
)
10641076
});
10651077

1078+
let log2_power = Real::new(Rational::new(1024));
1079+
let log2_three = Real::new(Rational::new(3));
1080+
let ln_five = Real::new(Rational::new(5)).ln().unwrap();
1081+
let ln_two_for_quotient = Real::new(Rational::new(2)).ln().unwrap();
1082+
1083+
group.bench_function("log2_power_of_two", |b| {
1084+
b.iter_batched(
1085+
|| log2_power.clone(),
1086+
|value| black_box(value.log2().unwrap()),
1087+
BatchSize::SmallInput,
1088+
)
1089+
});
1090+
group.bench_function("log2_rational_three", |b| {
1091+
b.iter_batched(
1092+
|| log2_three.clone(),
1093+
|value| black_box(value.log2().unwrap()),
1094+
BatchSize::SmallInput,
1095+
)
1096+
});
1097+
group.bench_function("log2_ln_quotient_fold", |b| {
1098+
b.iter_batched(
1099+
|| (ln_five.clone(), ln_two_for_quotient.clone()),
1100+
|(num, den)| black_box((num / den).unwrap()),
1101+
BatchSize::SmallInput,
1102+
)
1103+
});
1104+
10661105
group.finish();
10671106
}
10681107

benchmarks.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,9 @@ Construction-time shortcuts for exact rational multiples of pi and inverse compo
349349
| `exact_transcendental_special_forms/asinh_large` | not run | not run | Builds a large inverse hyperbolic sine without exact intermediate Reals. |
350350
| `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. |
351351
| `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. |
352+
| `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. |
353+
| `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. |
354+
| `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. |
352355

353356
### `symbolic_reductions`
354357

src/real/arithmetic.rs

Lines changed: 85 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ pub(crate) enum Class {
8181
// representation small while still preserving a lightweight symbolic form.
8282
LnProduct(Box<LnProductClass>), // Product of two logarithms, ordered by base
8383
Log10(Rational), // Rational > 1 and never a multiple of ten
84+
Log2(Rational), // Rational > 1 and never a power of two
8485
SinPi(Rational), // 0 < Rational < 1/2 also never 1/6 or 1/4 or 1/3
8586
TanPi(Rational), // 0 < Rational < 1/2 also never 1/6 or 1/4 or 1/3
8687
Irrational,
@@ -135,6 +136,7 @@ impl PartialEq for Class {
135136
left.left == right.left && left.right == right.right
136137
}
137138
(Log10(r), Log10(s)) => r == s,
139+
(Log2(r), Log2(s)) => r == s,
138140
(SinPi(r), SinPi(s)) => r == s,
139141
(TanPi(r), TanPi(s)) => r == s,
140142
(_, _) => false,
@@ -541,6 +543,9 @@ impl Class {
541543
Log10(base) => {
542544
Self::ln_computable(base).multiply(Self::ln_computable(&rationals::TEN).inverse())
543545
}
546+
Log2(base) => {
547+
Self::ln_computable(base).multiply(Self::ln_computable(&*rationals::TWO).inverse())
548+
}
544549
SinPi(rational) => {
545550
let argument =
546551
Computable::multiply(Computable::pi(), Computable::rational(rational.clone()));
@@ -1528,7 +1533,7 @@ impl Real {
15281533
Irrational => "scaled-computable",
15291534
Pi | PiPow(_) | PiInv | PiExp(_) | PiInvExp(_) | PiSqrt(_) | ConstProduct(_)
15301535
| ConstOffset(_) | ConstProductSqrt(_) | Sqrt(_) | Exp(_) | Ln(_) | LnAffine(_)
1531-
| LnProduct(_) | Log10(_) | SinPi(_) | TanPi(_) => "symbolic-nonzero-scale",
1536+
| LnProduct(_) | Log10(_) | Log2(_) | SinPi(_) | TanPi(_) => "symbolic-nonzero-scale",
15321537
}
15331538
);
15341539

@@ -1537,7 +1542,7 @@ impl Real {
15371542
One => Some(real_sign_from_num(rational_sign)),
15381543
Pi | PiPow(_) | PiInv | PiExp(_) | PiInvExp(_) | PiSqrt(_) | ConstProduct(_)
15391544
| ConstOffset(_) | ConstProductSqrt(_) | Sqrt(_) | Exp(_) | Ln(_) | LnAffine(_)
1540-
| LnProduct(_) | Log10(_) | SinPi(_) | TanPi(_) => {
1545+
| LnProduct(_) | Log10(_) | Log2(_) | SinPi(_) | TanPi(_) => {
15411546
// Exact symbolic classes are positive by construction, so the
15421547
// outer rational scale alone determines sign. Additive classes
15431548
// such as ConstOffset/LnAffine are admitted only when this
@@ -3295,6 +3300,61 @@ impl Real {
32953300
})
32963301
}
32973302

3303+
/// The base 2 logarithm of this Real or Problem::NotANumber if this Real is not positive.
3304+
pub fn log2(self) -> Result<Real, Problem> {
3305+
// Domain check uses structural sign first. Refinement-forced sign
3306+
// (`best_sign`) is reserved for the case where cheap inspection cannot
3307+
// decide; rejecting structurally known nonpositive inputs avoids
3308+
// ~2µs of computable work on the typical hot path.
3309+
match self.structural_facts().sign {
3310+
Some(RealSign::Positive) => {}
3311+
Some(RealSign::Zero | RealSign::Negative) => {
3312+
crate::trace_dispatch!("real", "log2", "domain-not-positive");
3313+
return Err(Problem::NotANumber);
3314+
}
3315+
None => {
3316+
if self.best_sign() != Sign::Plus {
3317+
crate::trace_dispatch!("real", "log2", "domain-not-positive");
3318+
return Err(Problem::NotANumber);
3319+
}
3320+
}
3321+
}
3322+
if let One = &self.class {
3323+
return Self::log2_rational(self.rational);
3324+
}
3325+
crate::trace_dispatch!("real", "log2", "ln-div-cached-ln2");
3326+
self.ln()? / constants::scaled_ln(2, 1).unwrap()
3327+
}
3328+
3329+
fn log2_rational(r: Rational) -> Result<Real, Problem> {
3330+
match r.cmp_one_structural() {
3331+
std::cmp::Ordering::Less => {
3332+
let inv = r.inverse()?;
3333+
return Ok(-Self::log2_rational(inv)?);
3334+
}
3335+
std::cmp::Ordering::Equal => return Ok(Self::zero()),
3336+
std::cmp::Ordering::Greater => {}
3337+
}
3338+
3339+
if let Some(n) = r.integer_magnitude()
3340+
&& let Some(log) = Self::integer_log(n, 2)
3341+
{
3342+
crate::trace_dispatch!("real", "log2", "rational-power-of-two");
3343+
return Ok(Self::new(Rational::new(log as i64)));
3344+
}
3345+
3346+
crate::trace_dispatch!("real", "log2", "rational-log2-special-form");
3347+
let computable =
3348+
Class::ln_computable(&r).multiply(Class::ln_computable(&*rationals::TWO).inverse());
3349+
Ok(Self {
3350+
rational: Rational::one(),
3351+
class: Log2(r),
3352+
computable: Some(computable),
3353+
signal: None,
3354+
primitive_approx_cache: Cell::new(PrimitiveApproxCache::Empty),
3355+
})
3356+
}
3357+
32983358
// Find Some(m) integral log with respect to this base or else None
32993359
// n should be positive (not zero) and base should be >= 2
33003360
fn integer_log(n: &BigUint, base: u32) -> Option<u64> {
@@ -4651,7 +4711,7 @@ fn structural_kind_for_class(class: &Class) -> StructuralKind {
46514711
Pi | PiPow(_) | PiInv => StructuralKind::PiLike,
46524712
Exp(_) | PiExp(_) | PiInvExp(_) => StructuralKind::ExpLike,
46534713
Sqrt(_) | PiSqrt(_) => StructuralKind::SqrtLike,
4654-
Ln(_) | LnAffine(_) | LnProduct(_) | Log10(_) => StructuralKind::LogLike,
4714+
Ln(_) | LnAffine(_) | LnProduct(_) | Log10(_) | Log2(_) => StructuralKind::LogLike,
46554715
SinPi(_) | TanPi(_) => StructuralKind::TrigExact,
46564716
ConstProduct(_) | ConstOffset(_) | ConstProductSqrt(_) => StructuralKind::ProductConstant,
46574717
Irrational => StructuralKind::ComputableOpaque,
@@ -4663,7 +4723,7 @@ fn symbolic_degree_for_class(class: &Class) -> ExpressionDegree {
46634723
Irrational => ExpressionDegree::Unknown,
46644724
One | Pi | PiPow(_) | PiInv | PiExp(_) | PiInvExp(_) | PiSqrt(_) | ConstProduct(_)
46654725
| ConstOffset(_) | ConstProductSqrt(_) | Sqrt(_) | Exp(_) | Ln(_) | LnAffine(_)
4666-
| LnProduct(_) | Log10(_) | SinPi(_) | TanPi(_) => ExpressionDegree::Constant,
4726+
| LnProduct(_) | Log10(_) | Log2(_) | SinPi(_) | TanPi(_) => ExpressionDegree::Constant,
46674727
}
46684728
}
46694729

@@ -4679,7 +4739,7 @@ fn symbolic_dependencies_for_class(class: &Class) -> SymbolicDependencyMask {
46794739
ConstProductSqrt(product) => pi_exp_dependency_mask(product.pi_power, &product.exp_power)
46804740
.union(SymbolicDependencyMask::SQRT),
46814741
Sqrt(_) => SymbolicDependencyMask::SQRT,
4682-
Ln(_) | LnAffine(_) | LnProduct(_) | Log10(_) => SymbolicDependencyMask::LOG,
4742+
Ln(_) | LnAffine(_) | LnProduct(_) | Log10(_) | Log2(_) => SymbolicDependencyMask::LOG,
46834743
SinPi(_) | TanPi(_) => SymbolicDependencyMask::TRIG.union(SymbolicDependencyMask::PI),
46844744
Irrational => SymbolicDependencyMask::OPAQUE,
46854745
}
@@ -4779,6 +4839,7 @@ impl fmt::Display for Real {
47794839
write!(f, " x ln({}) x ln({})", product.left, product.right)
47804840
}
47814841
Log10(n) => write!(f, " x log10({})", &n),
4842+
Log2(n) => write!(f, " x log2({})", &n),
47824843
Sqrt(n) => write!(f, " √({})", &n),
47834844
SinPi(n) => write!(f, " x sin({} x Pi)", &n),
47844845
TanPi(n) => write!(f, " x tan({} x Pi)", &n),
@@ -6033,6 +6094,25 @@ impl<T: AsRef<Real>> Div<T> for &Real {
60336094
..self.clone()
60346095
});
60356096
}
6097+
if s == *rationals::TWO {
6098+
// Same rationale as the log10 fold: keep two-log quotients
6099+
// anchored on a single Log2 certificate.
6100+
let Ln(r) = &self.class else {
6101+
unreachable!();
6102+
};
6103+
let rational = &self.rational / &other.rational;
6104+
let ln2 = constants::scaled_ln(2, 1).unwrap();
6105+
let computable = self
6106+
.computable_clone()
6107+
.multiply(ln2.computable_clone().inverse());
6108+
return Ok(Real {
6109+
rational,
6110+
class: Log2(r.clone()),
6111+
computable: Some(computable),
6112+
signal: self.signal.clone(),
6113+
primitive_approx_cache: Cell::new(PrimitiveApproxCache::Empty),
6114+
});
6115+
}
60366116
} else {
60376117
unreachable!();
60386118
}

src/real/tests.rs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1121,6 +1121,91 @@ mod tests {
11211121
assert!((actual - 14.508657738524219).abs() < 1e-12);
11221122
}
11231123

1124+
#[test]
1125+
fn log2_of_powers_of_two_is_exact_integer() {
1126+
for k in 0_i64..=20 {
1127+
let n = Real::new(Rational::new(1_i64 << k));
1128+
let answer = n.log2().unwrap();
1129+
assert_eq!(answer, Rational::new(k));
1130+
}
1131+
}
1132+
1133+
#[test]
1134+
fn log2_of_one_is_zero() {
1135+
assert_eq!(Real::one().log2().unwrap(), Real::zero());
1136+
}
1137+
1138+
#[test]
1139+
fn log2_of_one_half_is_negative_one() {
1140+
let half = Real::new(Rational::fraction(1, 2).unwrap());
1141+
assert_eq!(half.log2().unwrap(), Rational::new(-1));
1142+
}
1143+
1144+
#[test]
1145+
fn log2_of_inverse_power_of_two_is_negative_integer() {
1146+
for k in 1_i64..=12 {
1147+
let n = Real::new(Rational::fraction(1, 1_u64 << k).unwrap());
1148+
let answer = n.log2().unwrap();
1149+
assert_eq!(answer, Rational::new(-k));
1150+
}
1151+
}
1152+
1153+
#[test]
1154+
fn log2_of_rational_matches_f64() {
1155+
for &n in &[3_i64, 5, 7, 9, 11, 13, 17] {
1156+
let value: f64 = Real::new(Rational::new(n)).log2().unwrap().into();
1157+
let expected = (n as f64).log2();
1158+
assert!(
1159+
(value - expected).abs() < 1e-12,
1160+
"log2({n}) = {value}, expected {expected}"
1161+
);
1162+
}
1163+
}
1164+
1165+
#[test]
1166+
fn log2_of_negative_errors() {
1167+
let negative = Real::new(Rational::new(-3));
1168+
assert_eq!(negative.log2(), Err(Problem::NotANumber));
1169+
}
1170+
1171+
#[test]
1172+
fn log2_of_zero_errors() {
1173+
assert_eq!(Real::zero().log2(), Err(Problem::NotANumber));
1174+
}
1175+
1176+
#[test]
1177+
fn log2_matches_ln_div_ln2() {
1178+
let x = Real::new(Rational::new(7));
1179+
let direct = x.clone().log2().unwrap();
1180+
let via_quotient = (x.ln().unwrap() / Real::new(Rational::new(2)).ln().unwrap()).unwrap();
1181+
let difference: f64 = (direct - via_quotient).into();
1182+
assert!(difference.abs() < 1e-14);
1183+
}
1184+
1185+
#[test]
1186+
fn log2_of_sqrt_two_is_half() {
1187+
let sqrt_two = Real::from(2_i32).sqrt().unwrap();
1188+
let value: f64 = sqrt_two.log2().unwrap().into();
1189+
assert!((value - 0.5).abs() < 1e-12);
1190+
}
1191+
1192+
#[test]
1193+
fn log2_of_irrational_argument_matches_f64() {
1194+
let value = Real::from(2_i32) + Real::from(3_i32).sqrt().unwrap();
1195+
let actual: f64 = value.log2().unwrap().into();
1196+
let expected = (2.0_f64 + 3.0_f64.sqrt()).log2();
1197+
assert!((actual - expected).abs() < 1e-12);
1198+
}
1199+
1200+
#[test]
1201+
fn log2_ln_quotient_folds_to_log2_class() {
1202+
let numerator = Real::new(Rational::new(5)).ln().unwrap();
1203+
let denominator = Real::new(Rational::new(2)).ln().unwrap();
1204+
let quotient = (numerator / denominator).unwrap();
1205+
let expected = Real::new(Rational::new(5)).log2().unwrap();
1206+
assert_eq!(quotient, expected);
1207+
}
1208+
11241209
fn assert_close(value: Real, expected: f64, tolerance: f64) {
11251210
let actual: f64 = value.into();
11261211
let scale = expected.abs().max(1.0);

src/simple.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ enum Operator {
5151
Sqrt,
5252
Exp,
5353
Log10,
54+
Log2,
5455
Ln,
5556
Cos,
5657
Sin,
@@ -414,6 +415,14 @@ impl Simple {
414415
let value = operand.value(names)?.log10()?;
415416
Ok(value)
416417
}
418+
Log2 => {
419+
if self.operands.len() != 1 {
420+
return Err(Problem::ParseError);
421+
}
422+
let operand = self.operands.first().unwrap();
423+
let value = operand.value(names)?.log2()?;
424+
Ok(value)
425+
}
417426
Ln => {
418427
if self.operands.len() != 1 {
419428
return Err(Problem::ParseError);
@@ -525,6 +534,7 @@ impl Simple {
525534
use Operator::*;
526535
match Self::consume_operator_token(chars).as_str() {
527536
"log10" | "log" => Ok(Log10),
537+
"log2" | "lg" => Ok(Log2),
528538
"ln" | "l" => Ok(Ln),
529539
"exp" | "e" => Ok(Exp),
530540
"sqrt" | "s" => Ok(Sqrt),

0 commit comments

Comments
 (0)