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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# changelog

## 0.12.1 (2026-05-22)

### added
- `GeoCollection::wave_sum` — interfering vector sum of a collection (the superposition), the counterpart to `total_magnitude`; `wave_sum().mag <= total_magnitude()`, the gap is angular cancellation
- `Angle::boost(k)` — celestial-sphere (relativistic aberration) boost: scales the stored half-tangent by the Bondi factor `k` via the grade-keyed Cayley maps, rational, no trig
- `Geonum::boost(axis, k)` — Lorentz event boost: projection onto the two light-cone nulls (a quarter turn apart) scaled by `k` and `1/k`, interval-preserving, for any boost axis
- `chemistry` feature: `Chemistry` extension trait on `Geonum` deriving the periodic table from blade arithmetic — `madelung_order`, `electron_shell`/`electron_wave`, `valence_shell`/`relativistic_valence_shell`, `ionization_projection`, and the observables `ionization_energy` (IE1, IE2, successive), `electron_affinity`, `electronegativity`; configured by the `Lattice` enum (`Canonical`/`Custom`), validated against NIST
- spacetime_test.rs: metric signature as a π rotation, causal structure (timelike/spacelike/lightlike by grade), Lorentz boosts, the dual as the metric involution (`t → −1/t`, fixed points the isotropic vectors), the three conics in one half-tangent
- chem_constants_test.rs: the three lattice constants (π/2, π/3, π/4) proven forced as distinct rotation closures, and the 1/n radial law as inv(winding count)

### changed
- metric signature test relocated from tensor_test.rs into a dedicated spacetime_test.rs
- chemistry model moved into the `chemistry` library trait; the chemistry test suites thinned to validate it against NIST

## 0.12.0 (2026-03-31)

### fixed
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "geonum"
version = "0.12.0"
version = "0.12.1"
edition = "2021"
repository = "https://github.com/mxfactorial/geonum"
description = "geometric number library supporting unlimited dimensions with O(1) complexity"
Expand All @@ -18,7 +18,8 @@ ml = []
em = []
waves = []
affine = []
all = ["optics", "projection", "ml", "em", "waves", "affine"]
chemistry = []
all = ["optics", "projection", "ml", "em", "waves", "affine", "chemistry"]

[dependencies]

Expand Down
8 changes: 4 additions & 4 deletions benches/geonum_benchmarks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1161,8 +1161,8 @@ fn bench_rendering(c: &mut Criterion) {
let cos_r = (PI / 6.0).cos();
let sin_r = (PI / 6.0).sin();
let mut mat = [[0.0_f64; 10]; 10];
for i in 0..10 {
mat[i][i] = 1.0;
for (i, row) in mat.iter_mut().enumerate() {
row[i] = 1.0;
}
mat[0][0] = cos_r;
mat[0][1] = -sin_r;
Expand Down Expand Up @@ -1190,8 +1190,8 @@ fn bench_rendering(c: &mut Criterion) {
let cos_r = (PI / 6.0).cos();
let sin_r = (PI / 6.0).sin();
let mut mat = [[0.0_f64; 100]; 100];
for i in 0..100 {
mat[i][i] = 1.0;
for (i, row) in mat.iter_mut().enumerate() {
row[i] = 1.0;
}
mat[0][0] = cos_r;
mat[0][1] = -sin_r;
Expand Down
84 changes: 84 additions & 0 deletions src/angle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,64 @@ impl Angle {
let (cos_val, _) = diff.cos_sin();
cos_val
}

/// boosts this direction on the celestial sphere by the Bondi factor `k`
///
/// a unit direction at polar angle θ from the boost axis has stereographic
/// coordinate tan(θ/2). a lorentz boost along the axis is the Möbius dilation
/// that scales it by 1/k, where k = √((1+β)/(1−β)) = e^φ is the Bondi
/// (Doppler) factor for velocity β, rapidity φ. this is relativistic
/// aberration — k > 1 crowds directions toward the forward axis (the
/// headlight effect)
///
/// the stereographic coordinate is rational in the stored (grade, t) — the
/// Cayley maps of the half-tangent — so the boost is one rational scale, no
/// trig, for any blade:
/// * grade 0: tan(θ/2) = t grade 1: (1+t)/(1−t)
/// * grade 2: −1/t grade 3: (t−1)/(t+1)
///
/// the forward pole (θ=0) and backward pole (θ=π) are the fixed points
///
/// # arguments
/// * `k` - the Bondi / Doppler factor (k > 0); k > 1 boosts toward the axis
///
/// # examples
/// ```
/// use geonum::Angle;
/// let ray = Angle::new(1.0, 3.0); // θ = π/3, forward hemisphere
/// let aberrated = ray.boost(2.0); // boost by Bondi factor k = 2
/// // a forward ray's stereographic coordinate (its stored t) scales by 1/k
/// assert!((aberrated.t() - ray.t() / 2.0).abs() < 1e-10);
/// ```
pub fn boost(&self, k: f64) -> Angle {
const EPSILON: f64 = 1e-10;

// the backward pole θ=π (stereographic coordinate ∞) is a fixed point
if self.grade() == 2 && self.t.abs() < EPSILON {
return *self;
}

// stereographic coordinate tan(θ/2), rational in (grade, t)
let t = self.t;
let s = match self.grade() {
0 => t,
1 => (1.0 + t) / (1.0 - t),
2 => -1.0 / t,
_ => (t - 1.0) / (t + 1.0),
};

// the Möbius dilation, then rebuild into the quadrant it landed in
let s = s / k;
if (0.0..=1.0).contains(&s) {
Angle::from_parts(0, s)
} else if s > 1.0 {
Angle::from_parts(1, (s - 1.0) / (s + 1.0))
} else if s < -1.0 {
Angle::from_parts(2, -1.0 / s)
} else {
Angle::from_parts(3, (1.0 + s) / (1.0 - s))
}
}
}

/// normalize negative blade to positive by adding full rotations
Expand Down Expand Up @@ -1860,4 +1918,30 @@ mod tests {
let c = Angle::new(1.0, 2.0); // π/2
assert!(c.near_rem(0.0));
}

#[test]
fn it_boosts_the_half_tangent_by_the_bondi_factor() {
// forward hemisphere (grade 0): the boost scales the stored half-tangent
// by 1/k — one rational division, no trig
let ray = Angle::new(1.0, 3.0); // π/3
assert!((ray.boost(2.0).t() - ray.t() / 2.0).abs() < EPSILON);

// k = 1 is the identity
assert!(ray.boost(1.0).near(&ray));

// both poles are fixed points: the forward axis θ=0 (t=0) and the
// backward pole θ=π (grade 2, t=0)
assert!(Angle::new(0.0, 1.0).boost(2.0).t().abs() < EPSILON);
let back = Angle::new(1.0, 1.0); // π
assert!(back.boost(2.0).near(&back));

// boosts compose: the Bondi dilations multiply (rapidity adds)
assert!((ray.boost(1.5).boost(2.0).t() - ray.boost(3.0).t()).abs() < EPSILON);

// a backward-hemisphere ray (grade 1, past π/2) boosts across the blade
// boundary into the forward hemisphere under a strong enough boost
let rear = Angle::new(2.0, 3.0); // 2π/3, grade 1
assert_eq!(rear.blade(), 1);
assert_eq!(rear.boost(0.6_f64.exp()).grade(), 0);
}
}
36 changes: 36 additions & 0 deletions src/geocollection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,17 @@ impl GeoCollection {
self.objects.iter().map(|g| g.mag).sum()
}

/// the interfering vector sum of all objects — the superposition. complements
/// [`total_magnitude`](Self::total_magnitude) (the scalar sum): because `+` is
/// cosine interference, `wave_sum().mag <= total_magnitude()`, the gap being
/// angular cancellation. this is the wave the electron shell, the attention
/// output, and summed decay products are each a vector sum of
pub fn wave_sum(&self) -> Geonum {
self.objects
.iter()
.fold(Geonum::scalar(0.0), |acc, g| acc + *g)
}

/// finds the dominant object (largest magnitude) in the collection
///
/// useful for identifying primary contributions, maximum forces,
Expand Down Expand Up @@ -188,6 +199,31 @@ mod tests {
use crate::{Angle, Geonum};
use std::f64::consts::PI;

#[test]
fn it_superposes_with_interference_below_the_scalar_sum() {
// two equal magnitudes a half turn apart cancel: the vector sum is zero
// while the scalar sum is 2 — interference, not addition of lengths
let opposed = GeoCollection::from(vec![
Geonum::new(1.0, 0.0, 1.0), // [1, 0]
Geonum::new(1.0, 1.0, 1.0), // [1, π]
]);
assert!(opposed.wave_sum().mag < 1e-10);
assert_eq!(opposed.total_magnitude(), 2.0);

// aligned objects reinforce: the vector sum reaches the scalar sum
let aligned =
GeoCollection::from(vec![Geonum::new(1.0, 1.0, 4.0), Geonum::new(1.0, 1.0, 4.0)]);
assert!(aligned.wave_sum().near_mag(aligned.total_magnitude()));

// in general the wave sum never exceeds the scalar sum
let mixed = GeoCollection::from(vec![
Geonum::new(2.0, 1.0, 6.0),
Geonum::new(3.0, 1.0, 4.0),
Geonum::new(1.0, 7.0, 6.0),
]);
assert!(mixed.wave_sum().mag <= mixed.total_magnitude() + 1e-10);
}

#[test]
fn it_creates_empty_collection() {
let collection = GeoCollection::new();
Expand Down
78 changes: 78 additions & 0 deletions src/geonum_mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,46 @@ impl Geonum {
}
}

/// boosts this event by the Bondi factor `k` along the spatial direction `axis`
///
/// a lorentz boost is a squeeze of the (space, time) plane that fixes the
/// light cone. the cone's two null rays sit a quarter turn off the boost axis
/// (the forward null) and three quarters off (the backward null). the boost
/// projects the event onto each null, stretches the forward one by k = e^φ
/// and compresses the backward one by 1/k, then sums — projection and scale,
/// no (t±x) component arithmetic. the interval is preserved because the two
/// scalings cancel, k·(1/k) = 1
///
/// this is the boost on a spacetime VECTOR — it keeps magnitude, a squeeze.
/// [`Angle::boost`] is the companion action on a celestial DIRECTION, where
/// dropping the magnitude turns the same null-pair scaling into aberration
///
/// # arguments
/// * `axis` - the spatial boost direction; the event's angle is read from it,
/// so `Angle::new(0.0, 1.0)` boosts along x (nulls at π/4 and 3π/4)
/// * `k` - the Bondi / Doppler factor e^φ for rapidity φ (k > 0); k > 1 boosts
/// toward the axis
///
/// # examples
/// ```
/// use geonum::{Geonum, Angle};
/// let event = Geonum::new_from_cartesian(0.5, 2.0); // (x, t) = (0.5, 2.0)
/// let boosted = event.boost(Angle::new(0.0, 1.0), 0.6_f64.exp()); // along x
/// // the interval t²−x² is invariant under the boost
/// let (cos, sin) = boosted.angle.cos_sin();
/// let (xb, tb) = (boosted.mag * cos, boosted.mag * sin);
/// assert!((tb * tb - xb * xb - (2.0 * 2.0 - 0.5 * 0.5)).abs() < 1e-9);
/// ```
pub fn boost(&self, axis: Angle, k: f64) -> Geonum {
// the two light-cone nulls: a forward ray a quarter turn off the axis,
// a backward ray three quarters off — bisecting the axis and time
let forward = Geonum::new_with_angle(1.0, axis + Angle::new(1.0, 4.0)); // axis + π/4
let backward = Geonum::new_with_angle(1.0, axis + Angle::new(3.0, 4.0)); // axis + 3π/4

// stretch the forward null by k, compress the backward by 1/k, sum
self.project(&forward).scale(k) + self.project(&backward).scale(1.0 / k)
}

/// computes distance between two points using law of cosines
/// returns a scalar geonum representing the distance
pub fn distance_to(&self, other: &Geonum) -> Geonum {
Expand Down Expand Up @@ -3224,4 +3264,42 @@ mod tests {
assert!(a.near_mag(5.0 + 1e-12)); // within tolerance
assert!(!a.near_mag(5.0 + 1e-8)); // outside tolerance
}

#[test]
fn it_boosts_an_event_preserving_the_interval() {
let x_axis = Angle::new(0.0, 1.0);
let k = 0.6_f64.exp();

// boost an event (x, t) along the x-axis; the interval t²−x² is invariant
let event = Geonum::new_from_cartesian(0.5, 2.0); // (x, t)
let b = event.boost(x_axis, k);
let (cos, sin) = b.angle.cos_sin();
let (xb, tb) = (b.mag * cos, b.mag * sin);
assert!((tb * tb - xb * xb - (2.0 * 2.0 - 0.5 * 0.5)).abs() < 1e-9);

// boosts compose: the Bondi factors multiply
let twice = event.boost(x_axis, 1.5).boost(x_axis, 2.0);
let once = event.boost(x_axis, 3.0);
assert!(twice.near(&once));

// the light cone is invariant: a null event (t = x) stays null
let null = Geonum::new_from_cartesian(1.0, 1.0);
let nb = null.boost(x_axis, k);
let (c2, s2) = nb.angle.cos_sin();
let (xn, tn) = (nb.mag * c2, nb.mag * s2);
assert!((tn * tn - xn * xn).abs() < 1e-9);

// the axis is a free parameter: boosting along a tilted direction still
// preserves the interval measured in that axis's frame
let tilt = Angle::new(1.0, 5.0); // π/5
let n = Geonum::new_with_angle(1.0, tilt);
let along = event.mag * event.angle.project(tilt);
let perp = event.reject(&n).mag;
let bt = event.boost(tilt, k);
let along_b = bt.mag * bt.angle.project(tilt);
let perp_b = bt.reject(&n).mag;
assert!(
((perp_b * perp_b - along_b * along_b) - (perp * perp - along * along)).abs() < 1e-9
);
}
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,5 @@ pub use traits::Projection;
pub use traits::Waves;
#[cfg(feature = "ml")]
pub use traits::{Activation, MachineLearning};
#[cfg(feature = "chemistry")]
pub use traits::{Chemistry, Lattice};
Loading