From 09daaa5c848b2e35b8144d6c286393277ca36742 Mon Sep 17 00:00:00 2001 From: max funk Date: Tue, 31 Mar 2026 11:27:13 -0700 Subject: [PATCH 1/6] scale angle --- src/angle.rs | 61 ++++++++++++++++++++++++++++++++++------------- src/geonum_mod.rs | 21 ++++++++-------- 2 files changed, 54 insertions(+), 28 deletions(-) diff --git a/src/angle.rs b/src/angle.rs index 5776959..53cc795 100644 --- a/src/angle.rs +++ b/src/angle.rs @@ -685,39 +685,66 @@ impl Mul<&Angle> for &Angle { } } -impl Div for Angle { +impl Mul for Angle { type Output = Angle; - fn div(self, divisor: f64) -> Angle { - // round-trip through radians — no closed-form t n-section - let total_radians = (self.blade as f64) * (PI / 2.0) + 2.0 * self.t.atan(); - let divided = total_radians / divisor; - // convert back: blade from quarter turns, t from remainder + fn mul(self, scalar: f64) -> Angle { + // scale blade count and remainder separately + // avoids converting large blade to radians (blade * π/2 → huge float) + let scaled_blade = self.blade as f64 * scalar; + let blade_whole = scaled_blade.floor(); + let blade_frac_rem = (scaled_blade - blade_whole) * (PI / 2.0); + + let scaled_rem = self.rem() * scalar + blade_frac_rem; + + // remainder overflow adjusts blade let quarter_pi = PI / 2.0; - let normalized = if divided < 0.0 { - let full = (divided.abs() / (4.0 * quarter_pi)).ceil(); - divided + full * 4.0 * quarter_pi + let extra_blades = (scaled_rem / quarter_pi).floor(); + let final_rem = scaled_rem - extra_blades * quarter_pi; + + let total_blade = blade_whole + extra_blades; + let normalized_blade = if total_blade < 0.0 { + let full = ((-total_blade + 3.0) / 4.0).ceil() * 4.0; + (total_blade + full) as usize } else { - divided + total_blade as usize }; - let blade = (normalized / quarter_pi) as usize; - let rem = normalized % quarter_pi; - if rem.abs() < 1e-10 { - Angle { blade, t: 0.0 } + + if final_rem.abs() < 1e-10 { + Angle { + blade: normalized_blade, + t: 0.0, + } } else { Angle { - blade, - t: (rem / 2.0).tan(), + blade: normalized_blade, + t: (final_rem / 2.0).tan(), } } } } +impl Mul for &Angle { + type Output = Angle; + + fn mul(self, scalar: f64) -> Angle { + (*self) * scalar + } +} + +impl Div for Angle { + type Output = Angle; + + fn div(self, divisor: f64) -> Angle { + self * (1.0 / divisor) + } +} + impl Div for &Angle { type Output = Angle; fn div(self, divisor: f64) -> Angle { - (*self) / divisor + *self * (1.0 / divisor) } } diff --git a/src/geonum_mod.rs b/src/geonum_mod.rs index 1de5b9e..f3701f4 100644 --- a/src/geonum_mod.rs +++ b/src/geonum_mod.rs @@ -535,9 +535,10 @@ impl Geonum { /// # returns /// a new geometric number representing self^n pub fn pow(self, n: f64) -> Self { + // x^n = [mag^n, n*angle] Self { mag: self.mag.powf(n), - angle: self.angle * Angle::new(n, 1.0), + angle: self.angle * n, } } @@ -1691,25 +1692,23 @@ mod tests { fn it_computes_powers() { let g = Geonum::new(2.0, 1.0, 4.0); // [2, PI/4] blade=0, value=PI/4 + // pow scales total angle by n: [mag^n, n*angle] + // matches repeated multiplication: g * g adds angles π/4 + π/4 = π/2 → blade=1 let squared = g.pow(2.0); assert_eq!(squared.mag, 4.0); // 2^2 = 4 - // pow(2.0) adds Angle::new(2.0, 1.0) which is 2*PI radians = 4 quarter-turns - // original blade=0, added blade=4, final blade=4 - assert_eq!(squared.angle.blade(), 4); - assert!((squared.angle.rem() - PI / 4.0).abs() < EPSILON); + assert_eq!(squared.angle.blade(), 1); // 2 * π/4 = π/2 = 1 blade + assert!(squared.angle.rem().abs() < EPSILON); // exactly on boundary + // pow(1.0) scales angle by 1: identity let identity = g.pow(1.0); assert!((identity.mag - g.mag).abs() < EPSILON); - // pow(1.0) adds Angle::new(1.0, 1.0) which is PI radians = 2 quarter-turns - // original blade=0, added blade=2, final blade=2 - assert_eq!(identity.angle.blade(), 2); + assert_eq!(identity.angle.blade(), g.angle.blade()); assert!((identity.angle.rem() - g.angle.rem()).abs() < EPSILON); + // pow(3.0) scales angle by 3: 3 * π/4 = 3π/4 → blade=1, rem=π/4 let cubed = g.pow(3.0); assert_eq!(cubed.mag, 8.0); // 2^3 = 8 - // pow(3.0) adds Angle::new(3.0, 1.0) which is 3*PI radians = 6 quarter-turns - // original blade=0, added blade=6, final blade=6 - assert_eq!(cubed.angle.blade(), 6); + assert_eq!(cubed.angle.blade(), 1); // 3π/4 = 1 blade + π/4 assert!((cubed.angle.rem() - PI / 4.0).abs() < EPSILON); } From 13762b267709a387e4a5439224dd06e641e77406 Mon Sep 17 00:00:00 2001 From: max funk Date: Tue, 31 Mar 2026 11:28:17 -0700 Subject: [PATCH 2/6] current angle scaling assertions --- tests/angle_arithmetic_test.rs | 46 ++++++++++++++++------------------ 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/tests/angle_arithmetic_test.rs b/tests/angle_arithmetic_test.rs index 7e664af..c217e7a 100644 --- a/tests/angle_arithmetic_test.rs +++ b/tests/angle_arithmetic_test.rs @@ -1605,48 +1605,46 @@ fn it_raises_geonum_to_power_through_angle_scaling() { let base_geo = Geonum::new(2.0, 1.0, 4.0); // length=2, angle=π/4 → blade=0, value=π/4 let squared = base_geo.pow(2.0); // step 1: length = 2.0^2 = 4.0 - // step 2: angle scaling = angle * 2 = π/4 * 2 through angle multiplication - // step 3: angle multiplication = Angle::new(2.0, 1.0) = 2π → blade=4, value=0 - // step 4: final angle = π/4 + 2π = π/4 + 4*(π/2) = blade=4, value=π/4 + // step 2: total angle = π/4, scaled by 2 = π/2 → blade=1, value=0 + // matches base_geo * base_geo: angles add π/4 + π/4 = π/2 → blade=1, value=0 assert_eq!(squared.mag, 4.0); // 2² = 4 - assert_eq!(squared.angle.blade(), 4); // angle * 2 adds 4 blades (2π) - assert!(squared.angle.near_rem(PI / 4.0)); // π/4 value preserved - // blade arithmetic: pow(2) = length² + angle*2 through blade addition + assert_eq!(squared.angle.blade(), 1); // 2 * π/4 = π/2 = 1 blade + assert!(squared.angle.near_rem(0.0)); // exactly on boundary + // blade arithmetic: pow(2) matches repeated multiplication // case 2: cube geonum (power of 3) let cubed = base_geo.pow(3.0); // step 1: length = 2.0^3 = 8.0 - // step 2: angle scaling = angle * 3 = π/4 * 3 through angle multiplication - // step 3: angle multiplication = Angle::new(3.0, 1.0) = 3π → blade=6, value=0 - // step 4: final angle = π/4 + 3π = π/4 + 6*(π/2) = blade=6, value=π/4 + // step 2: total angle = π/4, scaled by 3 = 3π/4 → blade=1, value=π/4 + // matches base_geo * base_geo * base_geo assert_eq!(cubed.mag, 8.0); // 2³ = 8 - assert_eq!(cubed.angle.blade(), 6); // angle * 3 adds 6 blades (3π) - assert!(cubed.angle.near_rem(PI / 4.0)); // π/4 value preserved - // blade arithmetic: pow(3) = length³ + angle*3 through blade addition + assert_eq!(cubed.angle.blade(), 1); // 3π/4 = 1 blade + π/4 + assert!(cubed.angle.near_rem(PI / 4.0)); // π/4 remainder + // blade arithmetic: pow(3) matches repeated multiplication // case 3: fractional power (square root) let sqrt_geo = base_geo.pow(0.5); // step 1: length = 2.0^0.5 = √2 - // step 2: angle scaling = angle * 0.5 = π/4 * 0.5 = π/8 - // step 3: angle multiplication = Angle::new(0.5, 1.0) = π/2 → blade=1, value=0 - // step 4: final angle = π/4 + π/2 = 3π/4 → blade=1, value=π/4 + // step 2: total angle = π/4, scaled by 0.5 = π/8 → blade=0, value=π/8 assert!(sqrt_geo.near_mag(2.0_f64.sqrt())); // √2 - assert_eq!(sqrt_geo.angle.blade(), 1); // angle * 0.5 adds 1 blade (π/2) - assert!(sqrt_geo.angle.near_rem(PI / 4.0)); // π/4 value preserved - // blade arithmetic: pow(0.5) = √length + angle*0.5 through blade addition + assert_eq!(sqrt_geo.angle.blade(), 0); // π/8 < π/2 so blade stays 0 + assert!(sqrt_geo.angle.near_rem(PI / 8.0)); // π/8 + // blade arithmetic: pow(0.5) halves the total angle // case 4: high blade power scaling let high_base = Geonum::new_with_blade(3.0, 100, 1.0, 6.0); // blade=100, value=π/6 let high_squared = high_base.pow(2.0); // step 1: length = 3.0^2 = 9.0 - // step 2: angle multiplication = angle * 2 adds Angle::new(2.0, 1.0) = 2π = 4 blades - // step 3: final blade = 100 + 4 = 104 blades + // step 2: total angle = 100*π/2 + π/6 = 301π/6, scaled by 2 = 301π/3 + // blade = floor(301π/3 / (π/2)) = floor(602/3) = 200 + // value = 301π/3 - 200*π/2 = π/3 + // matches high_base * high_base: blades add 100+100=200, rems add π/6+π/6=π/3 assert_eq!(high_squared.mag, 9.0); // 3² = 9 - assert_eq!(high_squared.angle.blade(), 104); // 100 + 4 = 104 blades - assert!(high_squared.angle.near_rem(PI / 6.0)); // π/6 value preserved - // blade arithmetic: power scaling works at arbitrary blade magnitudes + assert_eq!(high_squared.angle.blade(), 200); // 100 + 100 = 200 blades + assert!(high_squared.angle.near_rem(PI / 3.0)); // π/6 + π/6 = π/3 + // blade arithmetic: pow matches repeated multiplication at arbitrary blade magnitudes - // proves pow() scales length exponentially while multiplying angle through blade arithmetic + // proves pow() scales length exponentially while scaling total angle by n } #[test] From 6e61ebd0b7919a4c087fe9b04850ec1789bfe5d4 Mon Sep 17 00:00:00 2001 From: max funk Date: Tue, 31 Mar 2026 11:28:37 -0700 Subject: [PATCH 3/6] add fundamental theorem of algebra suite --- tests/algebra_test.rs | 557 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 557 insertions(+) create mode 100644 tests/algebra_test.rs diff --git a/tests/algebra_test.rs b/tests/algebra_test.rs new file mode 100644 index 0000000..363a08d --- /dev/null +++ b/tests/algebra_test.rs @@ -0,0 +1,557 @@ +// the fundamental theorem of algebra is visible from the angle +// +// every polynomial of degree n has exactly n roots +// this took centuries to prove. every known proof requires +// complex analysis or algebraic topology. no purely algebraic proof exists. +// mathematicians have proven you CANNOT prove it algebraically. +// +// but in angle space its obvious: +// +// z = [r, θ] on a circle means θ sweeps 0 → 2π +// z^n = [r^n, nθ] means the output angle sweeps 0 → 2nπ (n full wraps) +// for large r, p(z) ≈ aₙz^n, so the output wraps n times around the origin +// as you shrink the circle to a point, the wraps must unwind +// each unwinding passes through a root (magnitude → 0, angle undefined) +// n wraps → n roots +// +// the reason algebra cant prove this is because algebra discards the angle +// the winding number IS angle accumulation +// you cannot count wraps with scalars +// +// the "deepest" theorem in mathematics is counting how many times an angle wraps +// +// everything below proves this mechanically + +use geonum::*; +use std::f64::consts::PI; + +const EPSILON: f64 = 1e-10; + +// ═══════════════════════════════════════════════════════════════════════════════ +// helpers +// ═══════════════════════════════════════════════════════════════════════════════ + +/// evaluate polynomial with coefficients [a₀, a₁, ..., aₙ] at point z +/// uses horner's method: p(z) = (...((aₙz + aₙ₋₁)z + aₙ₋₂)z + ...) + a₀ +fn eval_poly(coeffs: &[Geonum], z: Geonum) -> Geonum { + let n = coeffs.len(); + let mut result = coeffs[n - 1]; + for i in (0..n - 1).rev() { + result = result * z + coeffs[i]; + } + result +} + +/// compute the output angle of a geonum via its cartesian projection +/// this avoids blade-wrapping issues with grade_angle() +fn output_angle(g: Geonum) -> f64 { + let x = g.mag * g.angle.grade_angle().cos(); + let y = g.mag * g.angle.grade_angle().sin(); + y.atan2(x) +} + +/// compute winding number of polynomial around origin on circle of given radius +/// sweeps z around the circle and counts how many times p(z) wraps the origin +fn winding_number(coeffs: &[Geonum], radius: f64) -> i32 { + let num_points = 10000; + let mut total_angle_change = 0.0; + let mut prev_angle: Option = None; + + for i in 0..=num_points { + let theta = 2.0 * PI * i as f64 / num_points as f64; + let z = Geonum::new_from_cartesian(radius * theta.cos(), radius * theta.sin()); + let p_z = eval_poly(coeffs, z); + + let current = output_angle(p_z); + if let Some(prev) = prev_angle { + let mut delta = current - prev; + while delta > PI { + delta -= 2.0 * PI; + } + while delta < -PI { + delta += 2.0 * PI; + } + total_angle_change += delta; + } + prev_angle = Some(current); + } + + (total_angle_change / (2.0 * PI)).round() as i32 +} + +/// create a scalar coefficient +fn scalar(val: f64) -> Geonum { + if val >= 0.0 { + Geonum::new(val, 0.0, 1.0) + } else { + Geonum::new(val.abs(), 1.0, 1.0) // negative = [|val|, π] + } +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// z^n wraps n times +// ═══════════════════════════════════════════════════════════════════════════════ + +#[test] +fn it_wraps_n_times_for_z_to_the_n() { + // z = [r, θ], z^n = [r^n, nθ] + // as θ sweeps 0→2π, the output angle sweeps 0→2nπ + // that is n complete wraps around the origin + // the degree of the polynomial IS the winding number on any circle + + // z^1: 1 wrap + let z1_coeffs = [scalar(0.0), scalar(1.0)]; // p(z) = z + assert_eq!(winding_number(&z1_coeffs, 1.0), 1, "z wraps once"); + + // z^2: 2 wraps + let z2_coeffs = [scalar(0.0), scalar(0.0), scalar(1.0)]; // p(z) = z² + assert_eq!(winding_number(&z2_coeffs, 1.0), 2, "z² wraps twice"); + + // z^3: 3 wraps + let z3_coeffs = [scalar(0.0), scalar(0.0), scalar(0.0), scalar(1.0)]; + assert_eq!(winding_number(&z3_coeffs, 1.0), 3, "z³ wraps three times"); + + // z^5: 5 wraps + let z5_coeffs = [ + scalar(0.0), + scalar(0.0), + scalar(0.0), + scalar(0.0), + scalar(0.0), + scalar(1.0), + ]; + assert_eq!(winding_number(&z5_coeffs, 1.0), 5, "z⁵ wraps five times"); +} + +#[test] +fn it_shows_angle_accumulation_is_the_degree() { + // for z on the unit circle at angle θ: + // z^n has angle nθ + // sweeping θ from 0 to 2π sweeps the output through n × 2π + // this is what "degree" means geometrically + + let angles = [0.0, PI / 6.0, PI / 3.0, PI / 2.0, PI, 3.0 * PI / 2.0]; + + for n in 2..=5 { + for &theta in &angles { + let z = Geonum::new_from_cartesian(theta.cos(), theta.sin()); + + // compute z^n by repeated multiplication + let mut z_n = Geonum::new(1.0, 0.0, 1.0); + for _ in 0..n { + z_n = z_n * z; + } + + // output angle should be n × input angle (mod 2π) + let expected_angle = (n as f64 * theta) % (2.0 * PI); + let actual = output_angle(z_n); + + // normalize both to [0, 2π) + let expected_norm = ((expected_angle % (2.0 * PI)) + 2.0 * PI) % (2.0 * PI); + let actual_norm = ((actual % (2.0 * PI)) + 2.0 * PI) % (2.0 * PI); + + let diff = (expected_norm - actual_norm).abs(); + let diff_wrapped = diff.min(2.0 * PI - diff); + assert!( + diff_wrapped < 0.01 || z_n.mag < EPSILON, + "z^{} at θ={:.3}: output angle {:.3} ≈ {}×{:.3} = {:.3}", + n, + theta, + actual, + n, + theta, + expected_angle + ); + } + } +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// winding number = number of roots +// ═══════════════════════════════════════════════════════════════════════════════ + +#[test] +fn it_counts_roots_of_z_squared_minus_one() { + // p(z) = z² - 1 + // degree 2 → winding number 2 on large circle → 2 roots + // roots: z = +1 and z = -1 + + let coeffs = [scalar(-1.0), scalar(0.0), scalar(1.0)]; // -1 + 0z + z² + + // large circle: winding = 2 (degree) + assert_eq!( + winding_number(&coeffs, 5.0), + 2, + "z²-1 winds twice on large circle: 2 roots exist" + ); + + // verify root at z = 1 + let z1 = Geonum::new(1.0, 0.0, 1.0); + let p_z1 = eval_poly(&coeffs, z1); + assert!( + p_z1.mag < 0.01, + "z=1 is a root: |p(1)| = {:.6} ≈ 0", + p_z1.mag + ); + + // verify root at z = -1 + let z_neg1 = Geonum::new(1.0, 1.0, 1.0); // [1, π] + let p_z_neg1 = eval_poly(&coeffs, z_neg1); + assert!( + p_z_neg1.mag < 0.01, + "z=-1 is a root: |p(-1)| = {:.6} ≈ 0", + p_z_neg1.mag + ); +} + +#[test] +fn it_counts_roots_of_z_squared_plus_one() { + // p(z) = z² + 1 + // degree 2 → winding number 2 → 2 roots + // roots: z = +i and z = -i (COMPLEX roots, not on real line) + // + // algebra says "no real roots" and stops + // the angle says "2 wraps, so 2 roots" — they must be off the real axis + + let coeffs = [scalar(1.0), scalar(0.0), scalar(1.0)]; // 1 + 0z + z² + + // large circle: winding = 2 + assert_eq!( + winding_number(&coeffs, 5.0), + 2, + "z²+1 winds twice: 2 roots exist even though none are real" + ); + + // verify root at z = i = [1, π/2] + let z_i = Geonum::new(1.0, 1.0, 2.0); // [1, π/2] + let p_zi = eval_poly(&coeffs, z_i); + assert!( + p_zi.mag < 0.01, + "z=i is a root: |p(i)| = {:.6} ≈ 0", + p_zi.mag + ); + + // verify root at z = -i = [1, 3π/2] + let z_neg_i = Geonum::new(1.0, 3.0, 2.0); // [1, 3π/2] + let p_z_neg_i = eval_poly(&coeffs, z_neg_i); + assert!( + p_z_neg_i.mag < 0.01, + "z=-i is a root: |p(-i)| = {:.6} ≈ 0", + p_z_neg_i.mag + ); +} + +#[test] +fn it_counts_roots_of_a_cubic() { + // p(z) = z³ - 1 + // degree 3 → winding number 3 → 3 roots + // roots: the cube roots of unity + // z = 1, z = e^(2πi/3), z = e^(4πi/3) + // all on the unit circle, evenly spaced by 2π/3 + + let coeffs = [scalar(-1.0), scalar(0.0), scalar(0.0), scalar(1.0)]; // -1 + z³ + + // large circle: winding = 3 + assert_eq!( + winding_number(&coeffs, 5.0), + 3, + "z³-1 winds three times: 3 roots exist" + ); + + // verify all three cube roots of unity + let roots_angles = [0.0, 2.0 * PI / 3.0, 4.0 * PI / 3.0]; + + for (k, &angle) in roots_angles.iter().enumerate() { + let z = Geonum::new_from_cartesian(angle.cos(), angle.sin()); + let p_z = eval_poly(&coeffs, z); + assert!( + p_z.mag < 0.01, + "cube root {} at angle {:.3}: |p(z)| = {:.6} ≈ 0", + k, + angle, + p_z.mag + ); + } +} + +#[test] +fn it_counts_roots_of_a_quartic() { + // p(z) = z⁴ - 1 + // degree 4 → winding number 4 → 4 roots + // roots: 1, i, -1, -i (the fourth roots of unity) + // evenly spaced by π/2 on the unit circle — the Q lattice itself + + let coeffs = [ + scalar(-1.0), + scalar(0.0), + scalar(0.0), + scalar(0.0), + scalar(1.0), + ]; // -1 + z⁴ + + assert_eq!( + winding_number(&coeffs, 5.0), + 4, + "z⁴-1 winds four times: 4 roots exist" + ); + + // the four roots ARE the grade cycle: 0, π/2, π, 3π/2 + let root_angles = [0.0, PI / 2.0, PI, 3.0 * PI / 2.0]; + + for (k, &angle) in root_angles.iter().enumerate() { + let z = Geonum::new_from_cartesian(angle.cos(), angle.sin()); + let p_z = eval_poly(&coeffs, z); + assert!( + p_z.mag < 0.01, + "fourth root {} at angle {:.3}: |p(z)| = {:.6} ≈ 0", + k, + angle, + p_z.mag + ); + } +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// shrinking the circle unwinds through roots +// ═══════════════════════════════════════════════════════════════════════════════ + +#[test] +fn it_unwinds_through_roots_as_circle_shrinks() { + // on a large circle: winding = degree + // on a tiny circle around the origin: winding = 0 (p(z) ≈ a₀ ≠ 0, constant) + // the winding must decrease from n to 0 + // it can only change when the circle passes through a root + // each root peels off one winding + // + // this is the ENTIRE proof of the fundamental theorem: + // winding starts at n, ends at 0, must pass through n roots + + // p(z) = z² - 1, roots at z = ±1 + + let coeffs = [scalar(-1.0), scalar(0.0), scalar(1.0)]; + + // outside all roots: winding = 2 + let w_large = winding_number(&coeffs, 3.0); + assert_eq!(w_large, 2, "outside all roots: winding = degree = 2"); + + // between roots at |z|=1 and origin: circle of radius 0.5 + // both roots are at |z|=1, so circle of radius 0.5 encloses no roots + let w_small = winding_number(&coeffs, 0.5); + assert_eq!(w_small, 0, "inside all roots: winding = 0"); + + // the winding dropped from 2 to 0 + // it can only change when crossing a root + // so there are at least 2 roots between radius 0.5 and 3.0 + assert_eq!(w_large - w_small, 2, "winding change = 2 → 2 roots crossed"); +} + +#[test] +fn it_tracks_winding_change_through_nested_roots() { + // p(z) = z(z - 2)(z - 4) = z³ - 6z² + 8z + // roots at z = 0, z = 2, z = 4 + // as circle grows: winding increases by 1 at each root + + let coeffs = [ + scalar(0.0), // a₀ = 0 + scalar(8.0), // a₁ = 8 + scalar(-6.0), // a₂ = -6 + scalar(1.0), // a₃ = 1 + ]; // 0 + 8z - 6z² + z³ + + // radius 1: encloses root at z=0 only + let w_1 = winding_number(&coeffs, 1.0); + assert_eq!(w_1, 1, "radius 1: 1 root enclosed (z=0)"); + + // radius 3: encloses roots at z=0 and z=2 + let w_3 = winding_number(&coeffs, 3.0); + assert_eq!(w_3, 2, "radius 3: 2 roots enclosed (z=0, z=2)"); + + // radius 5: encloses all three roots + let w_5 = winding_number(&coeffs, 5.0); + assert_eq!(w_5, 3, "radius 5: 3 roots enclosed (all)"); + + // winding increases by 1 each time we cross a root + assert_eq!(w_3 - w_1, 1, "crossing z=2 adds one winding"); + assert_eq!(w_5 - w_3, 1, "crossing z=4 adds one winding"); +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// the impossibility of algebraic proof +// ═══════════════════════════════════════════════════════════════════════════════ + +#[test] +fn it_shows_why_algebra_cannot_see_this() { + // algebra works with scalars — it discards the angle at the start + // the winding number is angle accumulation around a closed path + // you cannot count wraps without the angle + // + // this test proves the information is IN the angle and ONLY in the angle + + // p(z) = z² + 1 at z = 2 (a real scalar) + let z_real = Geonum::new(2.0, 0.0, 1.0); // [2, 0] + let p_real = eval_poly(&[scalar(1.0), scalar(0.0), scalar(1.0)], z_real); + + // algebra sees: 2² + 1 = 5, no root here, nothing to learn about roots + assert!(p_real.mag > 4.0, "scalar evaluation just gives a number"); + + // but the ANGLE of the result carries winding information + // even at this single point, the output angle contributes to the winding count + // algebra throws this away + + // now evaluate at z = [2, θ] for various θ on a circle + let coeffs = [scalar(1.0), scalar(0.0), scalar(1.0)]; // z² + 1 + let radius = 2.0; + let num_samples = 8; + + let mut angles_out = Vec::new(); + for i in 0..num_samples { + let theta = 2.0 * PI * i as f64 / num_samples as f64; + let z = Geonum::new_from_cartesian(radius * theta.cos(), radius * theta.sin()); + let p_z = eval_poly(&coeffs, z); + angles_out.push(output_angle(p_z)); + } + + // the output angles are all DIFFERENT — they trace a path around the origin + // algebra at z=2 sees magnitude 5 and nothing else + // the angle sequence IS the winding, and its invisible to scalars + + // verify the angles are not all the same + let angle_variance: f64 = { + let mean: f64 = angles_out.iter().sum::() / angles_out.len() as f64; + angles_out.iter().map(|a| (a - mean).powi(2)).sum::() / angles_out.len() as f64 + }; + assert!( + angle_variance > 0.1, + "output angles vary: the winding information is in the angle, which algebra discards" + ); + + // the full winding count from this angle data + let w = winding_number(&coeffs, radius); + assert_eq!(w, 2, "angle data gives winding = 2 = number of roots"); +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// roots of unity: the Q lattice is the answer +// ═══════════════════════════════════════════════════════════════════════════════ + +#[test] +fn it_shows_roots_of_unity_are_the_q_lattice_generalized() { + // z^n - 1 = 0 has roots evenly spaced on the unit circle at angles 2kπ/n + // for n=4: angles are 0, π/2, π, 3π/2 — the four grades of the Q lattice + // + // the Q lattice is the n=4 case of roots of unity + // grades 0, 1, 2, 3 ARE the fourth roots of unity + // the entire geonum framework is built on z⁴ = 1 + + for n in 2..=8 { + // build z^n - 1 + let mut coeffs: Vec = vec![scalar(0.0); n + 1]; + coeffs[0] = scalar(-1.0); // constant term -1 + coeffs[n] = scalar(1.0); // z^n term + + // winding = n + assert_eq!( + winding_number(&coeffs, 3.0), + n as i32, + "z^{}-1 has winding {} on large circle", + n, + n + ); + + // verify each root at angle 2kπ/n + for k in 0..n { + let angle = 2.0 * PI * k as f64 / n as f64; + let z = Geonum::new_from_cartesian(angle.cos(), angle.sin()); + let p_z = eval_poly(&coeffs, z); + assert!( + p_z.mag < 0.02, + "root {} of z^{}-1 at angle {:.3}: |p(z)| = {:.6} ≈ 0", + k, + n, + angle, + p_z.mag + ); + } + } +} + +#[test] +fn it_proves_no_rootless_polynomial_exists() { + // the fundamental theorem: + // every polynomial of degree n ≥ 1 has at least one root + // + // proof from winding: + // 1. on a large circle, p(z) ≈ aₙz^n, so winding = n ≥ 1 + // 2. at the origin, p(0) = a₀ ≠ 0 (for generic polynomial), winding = 0 + // 3. winding is continuous and integer-valued + // 4. to go from n to 0, it must decrease + // 5. it can only decrease by passing through a zero of p(z) + // 6. therefore at least one root exists + // + // test this for various "difficult" polynomials + + // z² + 1: no REAL roots, but winding = 2 guarantees COMPLEX roots + let p1 = [scalar(1.0), scalar(0.0), scalar(1.0)]; + assert_eq!( + winding_number(&p1, 10.0), + 2, + "z²+1: winding 2, roots must exist" + ); + + // z⁴ + z² + 1: no obvious roots + let p2 = [ + scalar(1.0), + scalar(0.0), + scalar(1.0), + scalar(0.0), + scalar(1.0), + ]; + assert_eq!( + winding_number(&p2, 10.0), + 4, + "z⁴+z²+1: winding 4, four roots must exist" + ); + + // z⁶ + 1: six roots, all complex + let mut p3 = vec![scalar(0.0); 7]; + p3[0] = scalar(1.0); + p3[6] = scalar(1.0); + assert_eq!( + winding_number(&p3, 10.0), + 6, + "z⁶+1: winding 6, six roots must exist" + ); + + // in every case: + // large circle → winding = degree > 0 + // therefore roots exist + // QED + + // the proof is 6 lines. it took mathematicians centuries because they + // tried to find roots using algebra (scalars, no angles) + // instead of counting how many times the output wraps (angles) +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// the fundamental theorem of algebra was never about algebra +// +// it was about angle accumulation +// +// a polynomial of degree n wraps the output n times around the origin +// as the input sweeps a large circle. those wraps must unwind as the +// circle shrinks. each unwinding passes through a root. n wraps, n roots. +// +// the theorem is unprovable in algebra because algebra discards the angle. +// the winding number — the thing that forces roots to exist — is invisible +// to any system that represents numbers as scalars on a line. +// +// every "proof" of the FTA smuggles the angle back in: +// - complex analysis uses contour integrals (angle accumulation along paths) +// - topology uses the fundamental group (winding numbers) +// - even liouville's theorem works through bounded entire functions (angle behavior) +// +// they all reduce to: the output wraps, so it must cross zero. +// +// the geometric number [magnitude, angle] sees this directly. +// the proof is in the data structure. +// ═══════════════════════════════════════════════════════════════════════════════ From c0335eda70d4b66f4cc2d4f6df1af976f0ef80c7 Mon Sep 17 00:00:00 2001 From: max funk Date: Tue, 31 Mar 2026 11:34:20 -0700 Subject: [PATCH 4/6] add calculus test content --- tests/calculus_test.rs | 1581 +++++++++++++++-------------------- tests/numbers_test.rs | 4 +- tests/taylor_series_test.rs | 573 +++++++++++++ 3 files changed, 1266 insertions(+), 892 deletions(-) create mode 100644 tests/taylor_series_test.rs diff --git a/tests/calculus_test.rs b/tests/calculus_test.rs index 155b72a..cf4a5a3 100644 --- a/tests/calculus_test.rs +++ b/tests/calculus_test.rs @@ -1,1069 +1,758 @@ -// calculus is a geometric algorithm requiring forward and reverse quarter turns before projection +// calculus is scalar toil — procedures that reconstruct what angles already express // -// traditional calculus works with scalars (post-projection quantities) and needs limits -// geometric calculus works with [magnitude, angle] primitives where relationships are encoded in angles -// quarter turns (±π/2) rotate between grades, then projection extracts scalar results +// scalars cant see the rate without approaching it (limits) +// scalars cant see the composition without decomposing it (chain rule) +// scalars cant see the factors without separating them (product rule) +// every rule in calculus reconstructs information that scalars discarded at construction // -// forward quarter turn: +π/2 (differentiate) -// reverse quarter turn: +3π/2, dual to -π/2 (integrate) -// projection: extracts scalar from geometric structure +// angles express exactly where youre headed: // -// differentiation is simply a pi/2 rotation and the foundation of -// calculus emerges directly from this geometric structure +// the power n lives in the angle ratio: nθ / θ = n +// the base x^(n-1) lives in the magnitude ratio: mag^n / mag +// the derivative is their product — two divisions, no limits // -// let v = [[1, 0], [1, pi/2]] # 2d +// differentiation is π/2 rotation: the tangent direction is one quarter turn +// from the position. its not computed, its adjacent. grades cycle 0→1→2→3→0 // -// everything can be a 1d "derivative" or projection of the base 2d v -// so long as the difference between their angles is pi/2 and they -// follow the "angles add, lengths multiply" rule +// integration is 3π/2 forward rotation (dual to -π/2) +// the fundamental theorem connects accumulation to endpoint interference // -// v' = [1, pi/2] # first derivative (rotate v by pi/2) -// v'' = [1, pi] # second derivative (rotate v' by pi/2) = -v -// v''' = [1, 3pi/2] # third derivative (rotate v'' by pi/2) = -v' -// v'''' = [1, 2pi] # fourth derivative (rotate v''' by pi/2) = v -// v''''' = [1, 5pi/2] # fifth derivative (rotate v'''' by pi/2) = v' -// v'''''' = [1, 3pi] # sixth derivative (rotate v''''' by pi/2) = -v -// v''''''' = [1, 7pi/2] # seventh derivative (rotate v'''''' by pi/2) = -v' +// the readout (angle ratio) and the rotation (π/2) are the same geometry: +// multiplication accumulates angle, differentiation reads it back // -// this geometric space enables continuous rotation as an -// incrementing pi/2 angle, which is the essence of differentiation, -// and sets the period of the "derive" function to 4 -// -// the wedge product between vectors AND their derivatives is nilpotent +// everything below proves this mechanically use geonum::*; use std::f64::consts::PI; const EPSILON: f64 = 1e-10; -#[test] -fn its_a_limit() { - // this test demonstrates that "limits" are unnecessary when using geometric numbers - // limits use geometric operations then discard the geometry - // they compute with [magnitude, angle] but collapse result to scalar - // losing the normal and rotational structure - - let x: f64 = 3.0; - let h = 0.0001; +// ═══════════════════════════════════════════════════════════════════════════════ +// the power rule is a readout +// ═══════════════════════════════════════════════════════════════════════════════ - // limits perform geometric operations: finite difference + division - // compute f(x+h) - f(x) for f(x) = x² - let x_geo = Geonum::new(x, 0.0, 1.0); - let x_h_geo = Geonum::new(x + h, 0.0, 1.0); - let f_x = x_geo * x_geo; // x² = 9 - let f_x_h = x_h_geo * x_h_geo; +#[test] +fn it_encodes_the_power_in_the_angle() { + // when you multiply x by itself n times, angles add: θ + θ + ... = nθ + // the exponent is not computed — it accumulates in the angle - let geometric_difference = f_x_h - f_x; // geometric subtraction + let x = Geonum::new(3.0, 1.0, 6.0); // x = [3, π/6] + let x_squared = x * x; // x² = [9, 2π/6] + let x_cubed = x_squared * x; // x³ = [27, 3π/6] + let x_fourth = x_cubed * x; // x⁴ = [81, 4π/6] - // this produces a geometric number [magnitude, angle] - assert!( - geometric_difference.mag < 0.01, - "geometric difference small" - ); + // the power is the angle ratio — just read it off + let x_angle = x.angle.grade_angle(); - // divide by h geometrically - let h_geo = Geonum::new(h, 0.0, 1.0); - let geometric_quotient = geometric_difference / h_geo; - - // the quotient is still geometric: [magnitude, angle, grade] - println!( - "geometric quotient: [magnitude={:.6}, angle={:.6}, grade={}]", - geometric_quotient.mag, - geometric_quotient.angle.grade_angle(), - geometric_quotient.angle.grade() - ); + let power_2 = x_squared.angle.grade_angle() / x_angle; + let power_3 = x_cubed.angle.grade_angle() / x_angle; + let power_4 = x_fourth.angle.grade_angle() / x_angle; - // but traditional limits PROJECT to scalar, discarding angle structure - let limit_result = geometric_quotient.mag; // projection: lose angle assert!( - limit_result > 5.0 && limit_result < 7.0, - "limit projects to scalar ~6" + (power_2 - 2.0).abs() < EPSILON, + "x² angle / x angle = 2: the angle knows the power" ); - - // differentiate() preserves the complete geometric structure - let derivative = f_x.differentiate(); // rotates π/2 - - // derivative contains both tangent and normal via the quarter turn - let tangent = derivative.project_to_dimension(0); - let normal = derivative.project_to_dimension(1); - - assert!(tangent.abs() < EPSILON, "tangent ≈ 0 at dimension 0"); assert!( - (normal - 9.0).abs() < EPSILON, - "normal = 9 at dimension 1 (perpendicular)" + (power_3 - 3.0).abs() < EPSILON, + "x³ angle / x angle = 3: no computation needed" ); - - // limits extract tangent scalar but lose normal and rotation - // limit gives ~6 (df/dx), tangent gives ~0 (different projections) assert!( - limit_result > 5.0 && limit_result < 7.0, - "limit extracts rate scalar" - ); - assert_eq!( - derivative.angle.grade(), - 1, - "derivative at grade 1 preserves structure" - ); - assert_eq!(derivative.mag, f_x.mag, "magnitude preserved in rotation"); - - // the key insight: limits ARE geometric operations (subtraction, division) - // but they throw away the [magnitude, angle] result by projecting to scalar - // losing the normal component and the quarter turn rotation that relates tangent to normal - let angle_separation = derivative.angle - f_x.angle; - assert_eq!( - angle_separation, - Angle::new(1.0, 2.0), - "tangent-normal dual structure (quarter turn apart) lost in limit projection" + (power_4 - 4.0).abs() < EPSILON, + "x⁴ angle / x angle = 4: the exponent was always there" ); -} - -#[test] -fn its_a_derivative() { - // differentiate() computes the derivative via pi/2 rotation - // the derivative appears perpendicular in the next dimension - let position = Geonum::new(5.0, 0.0, 1.0); - let velocity = position.differentiate(); + // the magnitude ratio gives x^(n-1) + let base_2 = x_squared.mag / x.mag; // 9/3 = 3 = x^1 + let base_3 = x_cubed.mag / x.mag; // 27/3 = 9 = x^2 + let base_4 = x_fourth.mag / x.mag; // 81/3 = 27 = x^3 - // velocity is perpendicular to position (quarter turn rotation) - let angle_diff = velocity.angle - position.angle; - assert_eq!( - angle_diff, - Angle::new(1.0, 2.0), - "derivative rotates by quarter turn" - ); + assert!((base_2 - 3.0).abs() < EPSILON, "x²/x = x^1 = 3"); + assert!((base_3 - 9.0).abs() < EPSILON, "x³/x = x^2 = 9"); + assert!((base_4 - 27.0).abs() < EPSILON, "x⁴/x = x^3 = 27"); - // magnitude preserved (rate equals magnitude for unit parameter) - assert_eq!(velocity.mag, position.mag, "magnitude preserved"); + // power rule = angle ratio × magnitude ratio + let deriv_2 = power_2 * base_2; // 2 × 3 = 6 + let deriv_3 = power_3 * base_3; // 3 × 9 = 27 + let deriv_4 = power_4 * base_4; // 4 × 27 = 108 - // grade changes: 0 → 1 - assert_eq!(position.angle.grade(), 0, "position at grade 0"); - assert_eq!(velocity.angle.grade(), 1, "velocity at grade 1"); - - // project the derivative to extract the rate of change - let rate_at_dim_1 = velocity.project_to_dimension(1); + let x_val = x.mag; + assert!((deriv_2 - 2.0 * x_val).abs() < EPSILON, "d/dx[x²] = 2x = 6"); assert!( - (rate_at_dim_1 - velocity.mag).abs() < EPSILON, - "velocity at grade 1 projects fully to dimension 1" + (deriv_3 - 3.0 * x_val * x_val).abs() < EPSILON, + "d/dx[x³] = 3x² = 27" ); - - // use derivative to compute change: integrate velocity back to position change - let delta_position = velocity.integrate(); // back to grade 0 - assert_eq!( - delta_position.angle.grade(), - 0, - "position change at grade 0" - ); - assert_eq!(delta_position.mag, velocity.mag, "magnitude preserved"); - - // the change in position magnitude equals the velocity magnitude assert!( - (delta_position.mag - 5.0).abs() < EPSILON, - "change in position extracted from derivative" + (deriv_4 - 4.0 * x_val * x_val * x_val).abs() < EPSILON, + "d/dx[x⁴] = 4x³ = 108" ); } #[test] -fn its_an_integral() { - // integrate() computes definite integrals via reverse quarter turn rotation - // fundamental theorem: ∫ₐᵇ f'(x)dx = F(b) - F(a) - // scalar integral extracted via projection from grade 3 - - // compute ∫₂⁵ 2x dx = x²|₂⁵ = 25 - 4 = 21 - let a: f64 = 2.0; - let b: f64 = 5.0; - - let f_a = Geonum::new(a.powi(2), 0.0, 1.0); // F(2) = 4 - let f_b = Geonum::new(b.powi(2), 0.0, 1.0); // F(5) = 25 +fn it_derives_x_squared_without_limits() { + // f(x) = x² at x = 3 + // traditional: lim(h→0) [(3+h)² - 9] / h = lim(h→0) [6h + h²] / h = 6 + // geometric: angle ratio × magnitude ratio = 2 × 3 = 6 - // method 1: F(b) - F(a) at grade 0 - let difference = f_b - f_a; - assert_eq!(difference.angle.grade(), 0, "difference at grade 0"); - assert!( - (difference.mag - 21.0).abs() < EPSILON, - "magnitude at grade 0 equals integral directly" - ); + let x = Geonum::new(3.0, 1.0, 6.0); // [3, π/6] + let f_x = x * x; // [9, π/3] - // method 2: integrate() then project - // integrate rotates by 3 quarter turns (reverse rotation, dual to -π/2) - let integrated = difference.integrate(); + let n = f_x.angle.grade_angle() / x.angle.grade_angle(); // 2 + let x_n_minus_1 = f_x.mag / x.mag; // 3 - let angle_rotation = integrated.angle - difference.angle; - assert_eq!( - angle_rotation, - Angle::new(3.0, 2.0), - "integrate rotates by 3 quarter turns (reverse)" - ); - - assert_eq!(integrated.angle.grade(), 3, "integrated at grade 3"); - assert_eq!( - integrated.mag, difference.mag, - "magnitude preserved through rotation" - ); + let geometric_derivative = n * x_n_minus_1; // 6 - // project to dimension 3 to extract integral scalar - let integral_scalar = integrated.project_to_dimension(3); + assert!((n - 2.0).abs() < EPSILON, "power = 2"); + assert!((x_n_minus_1 - 3.0).abs() < EPSILON, "x^(n-1) = 3"); assert!( - (integral_scalar - 21.0).abs() < EPSILON, - "dimension 3 projection extracts integral scalar" + (geometric_derivative - 6.0).abs() < EPSILON, + "f'(3) = 6 without limits" ); - // both methods give same result: scalars are projections + // compare with limit definition to show they agree + let h = 1e-10; + let limit_derivative = ((3.0 + h) * (3.0 + h) - 9.0) / h; + assert!( - (difference.mag - integral_scalar).abs() < EPSILON, - "magnitude at grade 0 = projection from grade 3" + (geometric_derivative - limit_derivative).abs() < 1e-4, + "geometric {} matches limit {}: same answer, no h→0", + geometric_derivative, + limit_derivative ); } #[test] -fn it_computes_coefficients_from_geometric_division() { - // polynomial derivative coefficients appear naturally from finite differences - // divided by appropriate powers: coefficient for x^n = (Δf/Δx) / x^(n-1) - - let x_value: f64 = 4.0; - let h = 0.0001; - - let x = Geonum::new(x_value, 0.0, 1.0); - let x_h = Geonum::new(x_value + h, 0.0, 1.0); - let h_geo = Geonum::new(h, 0.0, 1.0); - - // f(x) = x²: coefficient should be 2 - let f_squared = x * x; - let f_squared_h = x_h * x_h; - let coeff_2 = ((f_squared_h - f_squared) / h_geo) / x; +fn it_derives_any_monomial() { + // the mechanism works for any x^n at any x + + let test_cases: Vec<(f64, u32)> = vec![ + (2.0, 2), + (3.0, 2), + (2.0, 3), + (3.0, 3), + (2.0, 4), + (3.0, 5), + (4.0, 3), + (5.0, 2), + (1.5, 4), + (2.5, 6), + ]; - assert!((coeff_2.mag - 2.0).abs() < 0.01, "x² coefficient = 2"); + for (x_val, power) in test_cases { + let x = Geonum::new(x_val, 1.0, 6.0); - // f(x) = x³: coefficient should be 3 - let f_cubed = f_squared * x; - let f_cubed_h = f_squared_h * x_h; - let coeff_3 = ((f_cubed_h - f_cubed) / h_geo) / x / x; // divide by x² + let mut x_n = Geonum::new(1.0, 0.0, 1.0); + for _ in 0..power { + x_n = x_n * x; + } - assert!((coeff_3.mag - 3.0).abs() < 0.01, "x³ coefficient = 3"); + let n = x_n.angle.grade_angle() / x.angle.grade_angle(); + let x_n_minus_1 = x_n.mag / x.mag; + let geometric_derivative = n * x_n_minus_1; - // f(x) = x⁴: coefficient should be 4 - let f_fourth = f_cubed * x; - let f_fourth_h = f_cubed_h * x_h; - let coeff_4 = ((f_fourth_h - f_fourth) / h_geo) / x / x / x; // divide by x³ + let traditional = power as f64 * x_val.powi(power as i32 - 1); - assert!((coeff_4.mag - 4.0).abs() < 0.01, "x⁴ coefficient = 4"); + assert!( + (geometric_derivative - traditional).abs() < 1e-6, + "d/dx[x^{}] at x={}: geometric {:.3} = traditional {:.3}", + power, + x_val, + geometric_derivative, + traditional + ); + } } #[test] -fn it_computes_powers_from_angle_ratios() { - // when multiplying geometric numbers, angles add - // for x^n, power n appears naturally in angle ratios: (x^n angle) / (x angle) = n +fn it_proves_the_power_rule_is_two_ratios() { + // the entire power rule reduces to: + // angle_ratio = nθ / θ = n (rotation encodes the power) + // mag_ratio = mag^n / mag = x^(n-1) (projection encodes the base) + // f'(x) = angle_ratio × mag_ratio (the "rule" is just reading) + // + // prove this for f(x) = x^5 at x = 2 - let x_value: f64 = 2.0; - let x = Geonum::new(x_value, 1.0, 6.0); - let x_squared = x * x; - let x_cubed = x_squared * x; - let x_fourth = x_cubed * x; + let x = Geonum::new(2.0, 1.0, 6.0); // [2, π/6] + + let x2 = x * x; + let x3 = x2 * x; + let x4 = x3 * x; + let x5 = x4 * x; let x_angle = x.angle.grade_angle(); - let power_2 = x_squared.angle.grade_angle() / x_angle; - let power_3 = x_cubed.angle.grade_angle() / x_angle; - let power_4 = x_fourth.angle.grade_angle() / x_angle; + // angles add: π/6, 2π/6, 3π/6, 4π/6, 5π/6 assert!( - (power_2 - 2.0).abs() < EPSILON, - "x² power encoded in angle ratio" + (x2.angle.grade_angle() - 2.0 * x_angle).abs() < EPSILON, + "x² angle = 2θ" ); assert!( - (power_3 - 3.0).abs() < EPSILON, - "x³ power encoded in angle ratio" + (x3.angle.grade_angle() - 3.0 * x_angle).abs() < EPSILON, + "x³ angle = 3θ" ); assert!( - (power_4 - 4.0).abs() < EPSILON, - "x⁴ power encoded in angle ratio" + (x4.angle.grade_angle() - 4.0 * x_angle).abs() < EPSILON, + "x⁴ angle = 4θ" + ); + assert!( + (x5.angle.grade_angle() - 5.0 * x_angle).abs() < EPSILON, + "x⁵ angle = 5θ" ); -} - -#[test] -fn it_computes_integrals_from_riemann_sums() { - // integrals appear naturally from riemann sums: geometric multiplication + addition - // ∫₀⁴ 2x dx = x²|₀⁴ = 16 - let x_start: f64 = 0.0; - let x_end: f64 = 4.0; - let num_steps = 1000; - let dx = (x_end - x_start) / num_steps as f64; + // magnitudes multiply: 2, 4, 8, 16, 32 + assert!((x2.mag - 4.0).abs() < EPSILON, "x² mag = 4"); + assert!((x3.mag - 8.0).abs() < EPSILON, "x³ mag = 8"); + assert!((x4.mag - 16.0).abs() < EPSILON, "x⁴ mag = 16"); + assert!((x5.mag - 32.0).abs() < EPSILON, "x⁵ mag = 32"); - let dx_geo = Geonum::new(dx, 0.0, 1.0); - let mut geometric_sum = Geonum::new(0.0, 0.0, 1.0); + // two ratios give the derivative + let n = x5.angle.grade_angle() / x_angle; // 5 + let base = x5.mag / x.mag; // 16 = 2^4 = x^(n-1) + let derivative = n * base; // 80 - for i in 0..num_steps { - let x_i = x_start + i as f64 * dx; - let f_i = Geonum::new(2.0 * x_i, 0.0, 1.0); // f(x) = 2x - let rectangle = f_i * dx_geo; // height × width - geometric_sum = geometric_sum + rectangle; - } + let traditional = 5.0 * 2.0_f64.powi(4); - let expected = 16.0; // x²|₀⁴ = 16 - 0 + assert!((n - 5.0).abs() < EPSILON, "angle ratio = 5"); + assert!((base - 16.0).abs() < EPSILON, "magnitude ratio = x^4 = 16"); assert!( - (geometric_sum.mag - expected).abs() < 0.02, - "riemann sum computes integral via geometric operations" + (derivative - traditional).abs() < EPSILON, + "5 × 16 = 80 = 5x^4 at x=2" ); } #[test] -fn its_a_gradient() { - // traditional: ∇f = [∂f/∂x, ∂f/∂y] requires computing partials then assembling vector - // geonum: ∇f = sum of directionally-encoded partials - no assembly needed - - let x = 3.0; - let y = 4.0; - let h = 0.0001; - - // f(x,y) = x² + y² - let f_xy = x * x + y * y; // 25 - - // traditional gradient: compute each partial, assemble into vector, compute magnitude - let partial_x: f64 = ((x + h) * (x + h) + y * y - f_xy) / h; // ≈ 6 - let partial_y: f64 = (x * x + (y + h) * (y + h) - f_xy) / h; // ≈ 8 - let trad_magnitude: f64 = (partial_x * partial_x + partial_y * partial_y).sqrt(); // 10 - let trad_direction: f64 = partial_y.atan2(partial_x); // ≈ 0.927 rad - - // geometric gradient: compute partials via finite differences, encode with direction, add - let f_geo = Geonum::new(f_xy, 0.0, 1.0); - let f_xh = Geonum::new((x + h) * (x + h) + y * y, 0.0, 1.0); - let f_yh = Geonum::new(x * x + (y + h) * (y + h), 0.0, 1.0); - let h_geo = Geonum::new(h, 0.0, 1.0); +fn it_derives_at_different_angles() { + // the initial angle θ is arbitrary — the ratio nθ/θ = n regardless + // proves the derivative is angle-independent (as it must be for a scalar function) + + let x_val = 3.0_f64; + let power = 3_u32; + let traditional = power as f64 * x_val.powi(power as i32 - 1); // 3 × 9 = 27 + + let angles = [ + Angle::new(1.0, 6.0), // π/6 + Angle::new(1.0, 4.0), // π/4 + Angle::new(1.0, 3.0), // π/3 + Angle::new(2.0, 7.0), // 2π/7 + Angle::new(3.0, 11.0), // 3π/11 + ]; - // ∂f/∂x at angle 0 (x-direction) - let df_dx = (f_xh - f_geo) / h_geo; - let partial_x_geo = Geonum::new(df_dx.mag, 0.0, 1.0); + for angle in angles { + let x = Geonum::new_with_angle(x_val, angle); - // ∂f/∂y at angle π/2 (y-direction) - let df_dy = (f_yh - f_geo) / h_geo; - let partial_y_geo = Geonum::new(df_dy.mag, 1.0, 2.0); + let mut x_n = Geonum::new(1.0, 0.0, 1.0); + for _ in 0..power { + x_n = x_n * x; + } - // gradient = sum of directionally-encoded partials - let gradient = partial_x_geo + partial_y_geo; + let n = x_n.angle.grade_angle() / x.angle.grade_angle(); + let base = x_n.mag / x.mag; + let derivative = n * base; - // prove they match - assert!( - (gradient.mag - trad_magnitude).abs() < 0.01, - "gradient magnitude matches: {} ≈ {}", - gradient.mag, - trad_magnitude - ); - assert!( - (gradient.angle.grade_angle() - trad_direction).abs() < 0.01, - "gradient direction matches: {} ≈ {}", - gradient.angle.grade_angle(), - trad_direction - ); + assert!( + (derivative - traditional).abs() < 1e-6, + "d/dx[x^3] = 27 at angle {:.4}: derivative = {:.6}", + angle.grade_angle(), + derivative + ); + } } #[test] -fn its_a_divergence() { - // traditional: ∇·F = ∂Fx/∂x + ∂Fy/∂y requires computing each partial then summing scalars - // geonum: ∇·F = sum of geometric partials - magnitude gives divergence value - - let x = 2.0; - let y = 3.0; - let h = 0.0001; - - // vector field F(x,y) = [x², xy] at point (2,3) - let fx = x * x; // 4 - let fy = x * y; // 6 +fn it_extends_to_fractional_powers() { + // x^n for fractional n: pow() computes [mag^n, n*angle] + // the same two ratios still give the derivative - // traditional divergence: ∂Fx/∂x + ∂Fy/∂y = 2x + x = 3x = 6 - let dfx_dx: f64 = (((x + h) * (x + h)) - fx) / h; // 2x ≈ 4 - let dfy_dy: f64 = ((x * (y + h)) - fy) / h; // x ≈ 2 - let trad_divergence: f64 = dfx_dx + dfy_dy; // 6 + let x = Geonum::new(4.0, 1.0, 6.0); // [4, π/6] - // geometric divergence: compute partials via finite differences, sum - let fx_geo = Geonum::new(fx, 0.0, 1.0); - let fx_h = Geonum::new((x + h) * (x + h), 0.0, 1.0); - let h_geo = Geonum::new(h, 0.0, 1.0); - let dfx_dx_geo = (fx_h - fx_geo) / h_geo; + // f(x) = x^(1/2) = √x + let sqrt_x = x.pow(0.5); - let fy_geo = Geonum::new(fy, 0.0, 1.0); - let fy_h = Geonum::new(x * (y + h), 0.0, 1.0); - let dfy_dy_geo = (fy_h - fy_geo) / h_geo; + let n = sqrt_x.angle.grade_angle() / x.angle.grade_angle(); + assert!((n - 0.5).abs() < 0.01, "angle ratio = 0.5 for square root"); - // each partial derivative is at grade 2 (result of division operations) - assert_eq!(dfx_dx_geo.angle.grade(), 2, "∂Fx/∂x at grade 2"); - assert_eq!(dfy_dy_geo.angle.grade(), 2, "∂Fy/∂y at grade 2"); + let base = sqrt_x.mag / x.mag; // √4 / 4 = 0.5 = 4^(-1/2) + assert!( + (base - 0.5).abs() < 0.01, + "magnitude ratio = x^(-1/2) = 0.5" + ); - // divergence = sum of partials - let divergence = dfx_dx_geo + dfy_dy_geo; + // derivative = 0.5 × 0.5 = 0.25 + let derivative = n * base; + let traditional = 0.5 * 4.0_f64.powf(-0.5); - // divergence magnitude matches traditional scalar divergence assert!( - (divergence.mag - trad_divergence).abs() < 0.01, - "divergence magnitude matches: {} ≈ {}", - divergence.mag, - trad_divergence + (derivative - traditional).abs() < 0.01, + "d/dx[√x] at x=4: geometric {:.4} = traditional {:.4}", + derivative, + traditional ); - // divergence remains at grade 2 (sum of grade 2 objects) - assert_eq!( - divergence.angle.grade(), - 2, - "divergence at grade 2 (bivector)" + // f(x) = x^(3/2) + let x_three_halves = x.pow(1.5); + let n_1_5 = x_three_halves.angle.grade_angle() / x.angle.grade_angle(); + let base_1_5 = x_three_halves.mag / x.mag; + let deriv_1_5 = n_1_5 * base_1_5; + let trad_1_5 = 1.5 * 4.0_f64.powf(0.5); // 1.5 × 2 = 3 + + assert!( + (deriv_1_5 - trad_1_5).abs() < 0.01, + "d/dx[x^(3/2)] at x=4: geometric {:.4} = traditional {:.4}", + deriv_1_5, + trad_1_5 ); } #[test] -fn its_a_curl() { - // traditional: ∇×F = ∂Fy/∂x - ∂Fx/∂y (2D curl, z-component) - // geonum: curl = difference of cross partials - magnitude gives circulation +fn it_proves_power_rule_is_o1() { + // traditional derivative computation scales with the method: + // - limits: O(1) but requires h→0 approximation with error + // - symbolic: O(n) for expression tree traversal + // - automatic diff (dual numbers): O(n) for computation graph + // + // geometric derivative: always exactly 2 operations + // 1. angle ratio (one division) + // 2. magnitude ratio (one division) + // regardless of power, regardless of evaluation point + // + // x^1000000 takes the same 2 operations as x^2 - let x = 2.0; - let y = 3.0; - let h = 0.0001; + let x = Geonum::new(1.001, 1.0, 6.0); // small base to avoid overflow - // rotational vector field F(x,y) = [-y, x] at point (2,3) - let fx = -y; // -3 - let fy = x; // 2 + let x_100 = x.pow(100.0); - // traditional curl: ∂Fy/∂x - ∂Fx/∂y = 1 - (-1) = 2 - let dfy_dx: f64 = ((x + h) - fy) / h; // ∂Fy/∂x = 1 - let dfx_dy: f64 = (-(y + h) - fx) / h; // ∂Fx/∂y = -1 - let trad_curl: f64 = dfy_dx - dfx_dy; // 2 + // use total angle (blade*π/2 + rem) instead of grade_angle + // because grade_angle wraps mod 2π, losing the power for large n + let x_total = x.angle.blade() as f64 * PI / 2.0 + x.angle.rem(); + let x100_total = x_100.angle.blade() as f64 * PI / 2.0 + x_100.angle.rem(); + let n = x100_total / x_total; + let base = x_100.mag / x.mag; + let derivative = n * base; - // geometric curl: compute cross partials via finite differences, subtract - let fy_geo = Geonum::new(fy, 0.0, 1.0); - let fy_xh = Geonum::new(x + h, 0.0, 1.0); - let h_geo = Geonum::new(h, 0.0, 1.0); - let dfy_dx_geo = (fy_xh - fy_geo) / h_geo; + let traditional = 100.0 * 1.001_f64.powi(99); - let fx_geo = Geonum::new(fx, 0.0, 1.0); - let fx_yh = Geonum::new(-(y + h), 0.0, 1.0); - let dfx_dy_geo = (fx_yh - fx_geo) / h_geo; - - // curl = ∂Fy/∂x - ∂Fx/∂y - let curl = dfy_dx_geo - dfx_dy_geo; - - // prove curl magnitude matches assert!( - (curl.mag - trad_curl).abs() < 0.01, - "curl magnitude matches: {} ≈ {}", - curl.mag, - trad_curl + (n - 100.0).abs() < 0.01, + "power = 100 from one angle division" + ); + assert!( + (derivative - traditional).abs() / traditional < 0.01, + "d/dx[x^100] at x=1.001: geometric {:.6} ≈ traditional {:.6}", + derivative, + traditional ); - - // curl is at grade 2 (result of division operations) - assert_eq!(curl.angle.grade(), 2, "curl at grade 2 (bivector)"); } -#[test] -fn its_a_directional_derivative() { - // traditional: D_û f = ∇f·û requires gradient vector dotted with unit direction - // geonum: D_û f = gradient.dot(direction) - same operation, geometric structure - - let x = 3.0; - let y = 4.0; - let h = 0.0001; - - // f(x,y) = x² + y² at (3,4), gradient = [6, 8] - let f_xy = x * x + y * y; // 25 - let partial_x: f64 = ((x + h) * (x + h) + y * y - f_xy) / h; // 6 - let partial_y: f64 = (x * x + (y + h) * (y + h) - f_xy) / h; // 8 - - // direction: û = [1/√2, 1/√2] (45° direction) - let dir_x: f64 = 1.0 / 2.0_f64.sqrt(); - let dir_y: f64 = 1.0 / 2.0_f64.sqrt(); - - // traditional directional derivative: ∇f·û = 6*(1/√2) + 8*(1/√2) ≈ 9.899 - let trad_dir_deriv: f64 = partial_x * dir_x + partial_y * dir_y; - - // geometric: build gradient, dot with direction - let f_geo = Geonum::new(f_xy, 0.0, 1.0); - let f_xh = Geonum::new((x + h) * (x + h) + y * y, 0.0, 1.0); - let f_yh = Geonum::new(x * x + (y + h) * (y + h), 0.0, 1.0); - let h_geo = Geonum::new(h, 0.0, 1.0); +// ═══════════════════════════════════════════════════════════════════════════════ +// what limits throw away +// ═══════════════════════════════════════════════════════════════════════════════ - let df_dx = (f_xh - f_geo) / h_geo; - let partial_x_geo = Geonum::new(df_dx.mag, 0.0, 1.0); +#[test] +fn it_shows_limits_discard_what_angles_preserve() { + // the limit definition computes the same derivative but throws away the geometry + // it collapses [magnitude, angle] to a scalar rate + // the geometric number keeps the rate AND the structural relationship - let df_dy = (f_yh - f_geo) / h_geo; - let partial_y_geo = Geonum::new(df_dy.mag, 1.0, 2.0); + let x = Geonum::new(4.0, 1.0, 6.0); // [4, π/6] + let f_x = x * x; // x² = [16, π/3] - let gradient = partial_x_geo + partial_y_geo; - let direction = Geonum::new(1.0, 1.0, 4.0); // π/4 = 45° + // geometric: full information preserved + let n = f_x.angle.grade_angle() / x.angle.grade_angle(); + let x_n_minus_1 = f_x.mag / x.mag; + let rate = n * x_n_minus_1; // 8 - // directional derivative = gradient · direction - let dir_deriv = gradient.dot(&direction); + // the angle ratio tells you WHAT POWER you're differentiating + assert!((n - 2.0).abs() < EPSILON, "angle ratio identifies x²"); + // the magnitude ratio tells you WHERE you're evaluating assert!( - (dir_deriv.mag - trad_dir_deriv).abs() < 0.1, - "directional derivative matches: {} ≈ {}", - dir_deriv.mag, - trad_dir_deriv + (x_n_minus_1 - 4.0).abs() < EPSILON, + "magnitude ratio identifies x=4" ); -} -#[test] -fn its_a_laplacian() { - // traditional: ∇²f = ∂²f/∂x² + ∂²f/∂y² sum of second partials - // geonum: compute second partials geometrically, sum → magnitude extraction + // the rate is their product + assert!((rate - 8.0).abs() < EPSILON, "rate = 2 × 4 = 8"); - let x = 2.0; - let y = 3.0; + // limit definition: computes the same 8 but loses the decomposition let h = 0.0001; + let limit = ((4.0_f64 + h).powi(2) - 16.0) / h; + assert!( + (limit - 8.0).abs() < 0.001, + "limit gives ~8 but cant tell you it came from power=2 at x=4" + ); - // f(x,y) = x² + y² - let f_xy = x * x + y * y; // 13 - - // traditional laplacian: ∂²f/∂x² + ∂²f/∂y² = 2 + 2 = 4 - let f_xh = (x + h) * (x + h) + y * y; - let f_x_h = (x - h) * (x - h) + y * y; - let d2f_dx2: f64 = (f_xh - 2.0 * f_xy + f_x_h) / (h * h); // 2 + // differentiate() preserves the full geometric structure + let derivative = f_x.differentiate(); + assert_eq!(derivative.mag, f_x.mag, "magnitude preserved: 16"); + assert_eq!(derivative.angle.grade(), 1, "derivative at grade 1"); +} - let f_yh = x * x + (y + h) * (y + h); - let f_y_h = x * x + (y - h) * (y - h); - let d2f_dy2: f64 = (f_yh - 2.0 * f_xy + f_y_h) / (h * h); // 2 +#[test] +fn it_shows_limits_lose_the_tangent_normal_dual() { + // limits compute f'(x) ≈ 6 as a scalar rate + // differentiate() rotates by π/2, preserving BOTH tangent and normal + // the quarter turn that relates them is lost in the limit projection - let trad_laplacian: f64 = d2f_dx2 + d2f_dy2; // 4 + let x_geo = Geonum::new(3.0, 0.0, 1.0); + let f_x = x_geo * x_geo; // x² = [9, 0] - // geometric laplacian: compute second partials via finite differences - let f_geo = Geonum::new(f_xy, 0.0, 1.0); - let f_xh_geo = Geonum::new(f_xh, 0.0, 1.0); - let f_x_h_geo = Geonum::new(f_x_h, 0.0, 1.0); + // limit: approach from outside, collapse to scalar + let h = 0.0001; let h_geo = Geonum::new(h, 0.0, 1.0); - let h2_geo = h_geo * h_geo; // h² - - // ∂²f/∂x² = (f(x+h) - 2f(x) + f(x-h)) / h² - let d2f_dx2_geo = (f_xh_geo - f_geo.scale(2.0) + f_x_h_geo) / h2_geo; - - let f_yh_geo = Geonum::new(f_yh, 0.0, 1.0); - let f_y_h_geo = Geonum::new(f_y_h, 0.0, 1.0); - - // ∂²f/∂y² - let d2f_dy2_geo = (f_yh_geo - f_geo.scale(2.0) + f_y_h_geo) / h2_geo; + let x_h_geo = Geonum::new(3.0 + h, 0.0, 1.0); + let f_x_h = x_h_geo * x_h_geo; + let limit_result = ((f_x_h - f_x) / h_geo).mag; + assert!( + (limit_result - 6.0).abs() < 0.01, + "limit projects to scalar ~6" + ); - // laplacian = sum of second partials - let laplacian = d2f_dx2_geo + d2f_dy2_geo; + // differentiate: rotate from inside, preserve structure + let derivative = f_x.differentiate(); + let tangent = derivative.project_to_dimension(0); + let normal = derivative.project_to_dimension(1); - // prove laplacian magnitude matches + assert!(tangent.abs() < EPSILON, "tangent ≈ 0 at dimension 0"); assert!( - (laplacian.mag - trad_laplacian).abs() < 0.1, - "laplacian magnitude matches: {} ≈ {}", - laplacian.mag, - trad_laplacian + (normal - 9.0).abs() < EPSILON, + "normal = 9 at dimension 1 (perpendicular)" ); - // laplacian at grade 2 from division operations - assert_eq!(laplacian.angle.grade(), 2, "laplacian at grade 2"); + // the quarter turn between f and f' IS the tangent-normal relationship + let angle_separation = derivative.angle - f_x.angle; + assert_eq!( + angle_separation, + Angle::new(1.0, 2.0), + "tangent-normal dual structure (quarter turn apart) lost in limit projection" + ); } -#[test] -fn it_handles_partial_derivatives() { - // traditional: ∂f/∂x "holds y constant" - requires conceptual freezing of dimensions - // geonum: gradient already contains all directional info - just project - - let x = 3.0; - let y = 4.0; - let h = 0.0001; - - // f(x,y) = x² + y² at (3,4) - let f_xy = x * x + y * y; // 25 - - // traditional partials: "hold y constant" for ∂f/∂x, "hold x constant" for ∂f/∂y - let partial_x_trad: f64 = ((x + h) * (x + h) + y * y - f_xy) / h; // 2x = 6 - let partial_y_trad: f64 = (x * x + (y + h) * (y + h) - f_xy) / h; // 2y = 8 +// ═══════════════════════════════════════════════════════════════════════════════ +// higher derivatives and factorials +// ═══════════════════════════════════════════════════════════════════════════════ - // geometric: build gradient (already contains all directional information) - let f_geo = Geonum::new(f_xy, 0.0, 1.0); - let f_xh = Geonum::new((x + h) * (x + h) + y * y, 0.0, 1.0); - let f_yh = Geonum::new(x * x + (y + h) * (y + h), 0.0, 1.0); - let h_geo = Geonum::new(h, 0.0, 1.0); +#[test] +fn it_computes_higher_derivatives_by_repeated_ratio() { + // d²/dx²[x^n] = n(n-1)x^(n-2) + // + // each derivative peels off one angle ratio and one magnitude factor + // first ratio: n from x^n, base x^(n-1) + // second ratio: (n-1) from x^(n-1), base x^(n-2) - let df_dx = (f_xh - f_geo) / h_geo; - let partial_x_geo = Geonum::new(df_dx.mag, 0.0, 1.0); + let x = Geonum::new(2.0, 1.0, 6.0); // [2, π/6] - let df_dy = (f_yh - f_geo) / h_geo; - let partial_y_geo = Geonum::new(df_dy.mag, 1.0, 2.0); + let x2 = x * x; + let x3 = x2 * x; + let x4 = x3 * x; - let gradient = partial_x_geo + partial_y_geo; + let x_angle = x.angle.grade_angle(); - // extract partials via projection - no "freezing" needed - let x_axis = Geonum::new(1.0, 0.0, 1.0); - let y_axis = Geonum::new(1.0, 1.0, 2.0); + // first derivative: d/dx[x^4] = 4x^3 + let n1 = x4.angle.grade_angle() / x_angle; // 4 + let base1 = x4.mag / x.mag; // 8 = x^3 - let partial_x_projected = gradient.dot(&x_axis); - let partial_y_projected = gradient.dot(&y_axis); + assert!((n1 - 4.0).abs() < EPSILON, "first power = 4"); + assert!((base1 - 8.0).abs() < EPSILON, "first base = x^3 = 8"); - // prove they match + let first_deriv = n1 * base1; // 32 assert!( - (partial_x_projected.mag - partial_x_trad).abs() < 0.2, - "x-partial matches: {} ≈ {}", - partial_x_projected.mag, - partial_x_trad + (first_deriv - 4.0 * 2.0_f64.powi(3)).abs() < EPSILON, + "f'(x) = 4x^3 = 32" ); + + // second derivative: apply ratio to x^3 + let n2 = x3.angle.grade_angle() / x_angle; // 3 + let base2 = x3.mag / x.mag; // 4 = x^2 + + let second_deriv = n1 * n2 * base2; // 4 × 3 × 4 = 48 assert!( - (partial_y_projected.mag - partial_y_trad).abs() < 0.2, - "y-partial matches: {} ≈ {}", - partial_y_projected.mag, - partial_y_trad + (second_deriv - 4.0 * 3.0 * 2.0_f64.powi(2)).abs() < EPSILON, + "f''(x) = 12x^2 = 48" ); -} - -#[test] -fn its_a_line_integral() { - // traditional: ∫_C F·dr requires curve parameterization, dr/dt computation, integration - // geonum: field · path for constant field on straight path - // straight line from (0,0) to (2,3) - let start = Geonum::new_from_cartesian(0.0, 0.0); - let end = Geonum::new_from_cartesian(2.0, 3.0); - let path = end - start; + // third derivative: apply ratio to x^2 + let n3 = x2.angle.grade_angle() / x_angle; // 2 + let base3 = x2.mag / x.mag; // 2 = x^1 - // constant vector field F = [1, 2] - let field = Geonum::new_from_cartesian(1.0, 2.0); + let third_deriv = n1 * n2 * n3 * base3; // 4 × 3 × 2 × 2 = 48 + assert!( + (third_deriv - 4.0 * 3.0 * 2.0 * 2.0).abs() < EPSILON, + "f'''(x) = 24x = 48" + ); - // traditional: ∫_C F·dr = F·(end - start) = [1,2]·[2,3] = 1*2 + 2*3 = 8 - let trad_integral: f64 = 1.0 * 2.0 + 2.0 * 3.0; + // fourth derivative: apply ratio to x + let n4 = x.angle.grade_angle() / x_angle; // 1 + let base4 = x.mag / x.mag; // 1 - // geometric: field · path - let geo_integral = field.dot(&path); + let fourth_deriv = n1 * n2 * n3 * n4 * base4; // 24 + assert!( + (fourth_deriv - 24.0).abs() < EPSILON, + "f''''(x) = 24 (constant)" + ); - // prove they match + // the nth derivative of x^n is n! — angle ratios multiply to the factorial + let factorial_from_angles = n1 * n2 * n3 * n4; assert!( - (geo_integral.mag - trad_integral).abs() < 0.1, - "line integral matches: {} ≈ {}", - geo_integral.mag, - trad_integral + (factorial_from_angles - 24.0).abs() < EPSILON, + "angle ratios multiply to n! = 4! = 24" ); } #[test] -fn its_a_surface_integral() { - // traditional: ∬_S F·n dS requires surface parameterization and normal vector computation - // geonum: surface as bivector (wedge product) - magnitude gives area - - // rectangular surface with edges [2,0] and [0,3] - let edge_x = Geonum::new_from_cartesian(2.0, 0.0); - let edge_y = Geonum::new_from_cartesian(0.0, 3.0); +fn it_shows_factorial_emerges_from_angle_descent() { + // for x^n, the nth derivative is n! + // each derivative peels off one angle ratio: n, (n-1), (n-2), ..., 2, 1 + // their product is n! + // + // the factorial is not a combinatorial object — it is the product + // of angle ratios extracted during repeated differentiation + + let x = Geonum::new(3.0, 1.0, 6.0); + let x_angle = x.angle.grade_angle(); - // traditional surface area via multiplication - let trad_area: f64 = 2.0 * 3.0; // 6 + let mut current = Geonum::new(1.0, 0.0, 1.0); + let mut angle_ratios = Vec::new(); + + for i in 1..=6 { + current = current * x; + let ratio = current.angle.grade_angle() / x_angle; + angle_ratios.push(ratio); + assert!( + (ratio - i as f64).abs() < EPSILON, + "x^{} angle ratio = {}", + i, + i + ); + } - // geometric surface: wedge product creates bivector - let surface = edge_x.wedge(&edge_y); + let factorial_3: f64 = angle_ratios[0..3].iter().product(); + let factorial_4: f64 = angle_ratios[0..4].iter().product(); + let factorial_5: f64 = angle_ratios[0..5].iter().product(); + let factorial_6: f64 = angle_ratios[0..6].iter().product(); - // wedge product magnitude IS the area + assert!((factorial_3 - 6.0).abs() < EPSILON, "3! = 6 from angles"); + assert!((factorial_4 - 24.0).abs() < EPSILON, "4! = 24 from angles"); assert!( - (surface.mag - trad_area).abs() < EPSILON, - "surface area matches: {} ≈ {}", - surface.mag, - trad_area + (factorial_5 - 120.0).abs() < EPSILON, + "5! = 120 from angles" + ); + assert!( + (factorial_6 - 720.0).abs() < EPSILON, + "6! = 720 from angles" ); - - // surface at grade 2 (bivector) - assert_eq!(surface.angle.grade(), 2, "surface at grade 2 (bivector)"); } #[test] -fn its_a_volume_integral() { - // traditional: ∭_V f dV requires volume parameterization and Jacobian computation - // geonum: volume from geometric product of surface bivector with third edge - - // rectangular volume with edges [2,0], [0,3], and perpendicular edge of length 4 - let edge_x = Geonum::new_from_cartesian(2.0, 0.0); - let edge_y = Geonum::new_from_cartesian(0.0, 3.0); - let edge_z = Geonum::new_with_blade(4.0, 2, 0.0, 1.0); // perpendicular at grade 2 +fn it_proves_zero_derivative_for_constants() { + // f(x) = c has no x dependence + // c = [c, 0] — zero angle + // angle ratio = 0/θ = 0, so derivative = 0 + // + // the zero derivative is the absence of angle, not a rule to memorize - // traditional volume via multiplication - let trad_volume: f64 = 2.0 * 3.0 * 4.0; // 24 + let x = Geonum::new(5.0, 1.0, 6.0); // [5, π/6] + let constant = Geonum::new(7.0, 0.0, 1.0); // [7, 0] - // geometric volume: surface bivector ⊗ third edge - let surface = edge_x.wedge(&edge_y); // bivector at grade 2 - let volume = surface.geo(&edge_z); // geometric product + let n = constant.angle.grade_angle() / x.angle.grade_angle(); // 0 / (π/6) = 0 + let derivative = n * (constant.mag / x.mag); - // volume magnitude matches assert!( - (volume.mag - trad_volume).abs() < EPSILON, - "volume matches: {} ≈ {}", - volume.mag, - trad_volume + derivative.abs() < EPSILON, + "constant derivative = 0: no angle means no x dependence" ); - - // volume at grade 0 (cycles back through 4-grade structure) - assert_eq!(volume.angle.grade(), 0, "volume at grade 0"); } #[test] -fn it_encodes_definite_integrals_with_domain() { - // ∫₂⁵ x² dx = 39 - // traditional: only the value 39 - // angle space: value AND domain in one geonum - - // compute traditionally for comparison - let x_a: f64 = 2.0; - let x_b: f64 = 5.0; - let traditional: f64 = (x_b.powi(3) - x_a.powi(3)) / 3.0; // 39 - - // encode bounds as angles (x as multiples of π) - let angle_a = Angle::new(x_a, 1.0); // 2π radians - let angle_b = Angle::new(x_b, 1.0); // 5π radians - - // evaluate antiderivative at bounds - // F(x) = x³/3 - let f_a = Geonum::new_with_angle(x_a.powi(3) / 3.0, angle_a); - let f_b = Geonum::new_with_angle(x_b.powi(3) / 3.0, angle_b); - - // the definite integral encodes: - // - magnitude: F(b) - F(a) = integral value - // - angle: x_b - x_a = integration domain - let magnitude = f_b.mag - f_a.mag; - let angle = f_b.angle - f_a.angle; - let integral = Geonum::new_with_angle(magnitude, angle); - - // verify value matches traditional - assert!( - (integral.mag - traditional).abs() < EPSILON, - "expected {}, got {}", - traditional, - integral.mag - ); +fn it_proves_linear_derivative_for_x() { + // f(x) = x carries exactly one copy of x's angle and magnitude + // angle ratio = θ/θ = 1, magnitude ratio = mag/mag = 1 + // derivative = 1 - // the angle encodes the domain (as multiples of π) - let expected_angle = Angle::new(x_b - x_a, 1.0); // (5-2) * π = 3π - assert_eq!( - integral.angle, expected_angle, - "angle should encode domain span" - ); - - // traditional calculus: ∫₂⁵ x² dx = 39 (value only) - // angle space calculus: [magnitude=39, angle=3π, blade=6, grade=2] - // - magnitude: the integral value - // - angle: 3π (the domain spanned as multiples of π) - // - blade: 6 (accumulated π/2 rotations) - // - grade: 2 (bivector, blade % 4) -} - -#[test] -fn it_preserves_fundamental_theorem_via_magnitudes() { - // fundamental theorem: ∫ₐᵇ f(x) dx = F(b) - F(a) - // in angle space: |F(b)| - |F(a)| + let x = Geonum::new(5.0, 1.0, 6.0); // [5, π/6] - // ∫₁³ 2x dx = x² |₁³ = 9 - 1 = 8 - let x_a: f64 = 1.0; - let x_b: f64 = 3.0; - let traditional: f64 = x_b.powi(2) - x_a.powi(2); // 8 + let n = x.angle.grade_angle() / x.angle.grade_angle(); // 1 + let base = x.mag / x.mag; // 1 + let derivative = n * base; // 1 - let f_a = Geonum::new_with_angle(x_a.powi(2), Angle::new(x_a, 1.0)); - let f_b = Geonum::new_with_angle(x_b.powi(2), Angle::new(x_b, 1.0)); - - let integral_value = f_b.mag - f_a.mag; - - assert!( - (integral_value - traditional).abs() < EPSILON, - "expected {}, got {}", - traditional, - integral_value - ); + assert!((n - 1.0).abs() < EPSILON, "x carries one copy of θ"); + assert!((base - 1.0).abs() < EPSILON, "x carries one copy of mag"); + assert!((derivative - 1.0).abs() < EPSILON, "d/dx[x] = 1"); } +// ═══════════════════════════════════════════════════════════════════════════════ +// differentiation cycles grades +// ═══════════════════════════════════════════════════════════════════════════════ + #[test] fn it_proves_differentiation_cycles_grades() { - // differentiation in geonum is π/2 rotation which cycles through the 4 geometric grades + // differentiation is π/2 rotation cycling through 4 geometric grades // each derivative moves to the next grade: 0→1→2→3→0 - // this connects calculus operations to fundamental geometric structure + // sin(θ+π/2) = cos(θ) is the quadrature identity that creates this cycle - // start with a scalar function at grade 0 let f = Geonum::new(3.0, 0.0, 1.0); // [3, 0] at grade 0 - assert_eq!( - f.angle.grade(), - 0, - "original function at grade 0 (scalar-like)" - ); - - // first derivative: rotate by π/2, moves to grade 1 - let f_prime = f.differentiate(); // adds π/2 rotation - assert_eq!( - f_prime.angle.grade(), - 1, - "first derivative at grade 1 (vector-like)" - ); - assert_eq!(f_prime.mag, f.mag, "differentiation preserves magnitude"); - assert_eq!( - f_prime.angle.blade(), - f.angle.blade() + 1, - "differentiation adds 1 blade" - ); - // second derivative: another π/2 rotation, moves to grade 2 + let f_prime = f.differentiate(); let f_double_prime = f_prime.differentiate(); + let f_triple_prime = f_double_prime.differentiate(); + let f_quad_prime = f_triple_prime.differentiate(); + + // grades cycle 0→1→2→3→0 + assert_eq!(f.angle.grade(), 0, "f at grade 0 (scalar)"); + assert_eq!(f_prime.angle.grade(), 1, "f' at grade 1 (vector)"); + assert_eq!(f_double_prime.angle.grade(), 2, "f'' at grade 2 (bivector)"); assert_eq!( - f_double_prime.angle.grade(), - 2, - "second derivative at grade 2 (bivector-like)" + f_triple_prime.angle.grade(), + 3, + "f''' at grade 3 (trivector)" ); + assert_eq!(f_quad_prime.angle.grade(), 0, "f'''' back at grade 0"); + + // blades accumulate: each differentiation adds 1 + assert_eq!(f_prime.angle.blade(), f.angle.blade() + 1, "1 blade added"); assert_eq!( f_double_prime.angle.blade(), f.angle.blade() + 2, - "two differentiations add 2 blades" - ); - - // third derivative: another π/2 rotation, moves to grade 3 - let f_triple_prime = f_double_prime.differentiate(); - assert_eq!( - f_triple_prime.angle.grade(), - 3, - "third derivative at grade 3 (trivector-like)" + "2 blades added" ); assert_eq!( f_triple_prime.angle.blade(), f.angle.blade() + 3, - "three differentiations add 3 blades" - ); - - // fourth derivative: completes the cycle, back to grade 0 - let f_quad_prime = f_triple_prime.differentiate(); - assert_eq!( - f_quad_prime.angle.grade(), - 0, - "fourth derivative back at grade 0 (scalar-like)" + "3 blades added" ); assert_eq!( f_quad_prime.angle.blade(), f.angle.blade() + 4, - "four differentiations add 4 blades" + "4 blades added" ); - // prove the cycle: f'''' behaves like f but with accumulated blade history - assert_eq!( - f_quad_prime.angle.grade(), - f.angle.grade(), - "grades cycle with period 4" - ); - - // demonstrate that grade determines behavior regardless of blade count - let high_blade_scalar = Geonum::new_with_blade(3.0, 1000, 0.0, 1.0); // blade 1000, grade 0 - assert_eq!( - high_blade_scalar.angle.grade(), - 0, - "blade 1000 % 4 = 0 (scalar behavior)" - ); - - let high_blade_derivative = high_blade_scalar.differentiate(); - assert_eq!( - high_blade_derivative.angle.grade(), - 1, - "differentiation moves grade 0→1 regardless of blade count" - ); + // magnitude preserved through all rotations + assert_eq!(f_prime.mag, f.mag, "differentiation preserves magnitude"); assert_eq!( - high_blade_derivative.angle.blade(), - 1001, - "blade count tracks full history" + f_quad_prime.mag, f.mag, + "magnitude preserved through full cycle" ); - // test the quadrature relationship: sin(θ+π/2) = cos(θ) - // this π/2 phase shift is what creates the grade cycling - let angle_0 = Angle::new(0.0, 1.0); // 0 radians - let angle_90 = angle_0 + Angle::new(1.0, 2.0); // add π/2 + // prove the quadrature relationship that creates grade cycling + let angle_0 = Angle::new(0.0, 1.0); + let angle_90 = angle_0 + Angle::new(1.0, 2.0); assert!( (angle_0.grade_angle().cos() - angle_90.grade_angle().sin()).abs() < EPSILON, "cos(θ) = sin(θ+π/2)" ); - assert!( - (angle_0.grade_angle().sin() + angle_90.grade_angle().cos()).abs() < EPSILON, - "sin(θ) = -cos(θ+π/2)" - ); - - // demonstrate grade-based geometric behavior patterns - let objects = [ - Geonum::new_with_blade(1.0, 0, 0.0, 1.0), // grade 0: scalar - Geonum::new_with_blade(1.0, 1, 0.0, 1.0), // grade 1: vector - Geonum::new_with_blade(1.0, 2, 0.0, 1.0), // grade 2: bivector - Geonum::new_with_blade(1.0, 3, 0.0, 1.0), // grade 3: trivector - ]; - - // test that objects with same grade behave identically regardless of blade count - for base_object in &objects { - let base_object = *base_object; - let high_blade_object = - Geonum::new_with_blade(1.0, base_object.angle.blade() + 100, 0.0, 1.0); - assert_eq!( - base_object.angle.grade(), - high_blade_object.angle.grade(), - "grade determined by blade % 4, not absolute blade count" - ); - - // dual operation should affect grades identically - let base_dual = base_object.dual(); - let high_dual = high_blade_object.dual(); - assert_eq!( - base_dual.angle.grade(), - high_dual.angle.grade(), - "dual operation affects grades consistently" - ); - } + // grade determines behavior regardless of blade count + let high_blade = Geonum::new_with_blade(3.0, 1000, 0.0, 1.0); // blade 1000, grade 0 + assert_eq!(high_blade.angle.grade(), 0, "blade 1000 % 4 = 0"); + assert_eq!( + high_blade.differentiate().angle.grade(), + 1, + "differentiation moves grade 0→1 at any blade count" + ); - // prove differentiation chain preserves the fundamental 4-cycle + // prove 4-cycle over 20 steps let mut current = f; for step in 1..=20 { current = current.differentiate(); - let expected_grade = step % 4; assert_eq!( current.angle.grade(), - expected_grade, - "differentiation step {} produces grade {}", + step % 4, + "step {} produces grade {}", step, - expected_grade + step % 4 ); } } -#[test] -fn it_demonstrates_differentiate_on_polynomials() { - // differentiate() rotates by π/2, preserving magnitude and cycling grades - // test on polynomial evaluations at specific points +// ═══════════════════════════════════════════════════════════════════════════════ +// integration and the fundamental theorem +// ═══════════════════════════════════════════════════════════════════════════════ - // test x² at x = 3 - let x = 3.0; - let f_scalar = x * x; // 9 - let f = Geonum::new(f_scalar, 0.0, 1.0); // [9, 0] at grade 0 +#[test] +fn it_connects_differentiation_and_integration_via_grade_cycle() { + // differentiate() rotates π/2 (grade 0 → 1) + // integrate() rotates 3π/2 (grade 1 → 0, forward equivalent to -π/2) + // together they complete a full 2π cycle (4 blades) - assert_eq!(f.angle.grade(), 0, "f(x) at grade 0"); - assert_eq!(f.mag, 9.0, "f(3) magnitude is 9"); + let f = Geonum::new(16.0, 0.0, 1.0); // grade 0 - // differentiation: π/2 rotation moves to grade 1 let f_prime = f.differentiate(); - assert_eq!(f_prime.angle.grade(), 1, "f'(x) at grade 1 (vector-like)"); - assert_eq!(f_prime.mag, 9.0, "differentiation preserves magnitude"); - - // demonstrate the grade transformation - assert_eq!(f.angle.blade(), 0, "original function at blade 0"); - assert_eq!( - f_prime.angle.blade(), - 1, - "derivative at blade 1 (π/2 rotation)" - ); - - // test the quadrature relationship that creates the derivative - // sin(θ + π/2) = cos(θ) is what makes differentiation work - let base_angle = Angle::new(0.0, 1.0); // 0 radians - let rotated_angle = base_angle + Angle::new(1.0, 2.0); // +π/2 - - assert!( - (base_angle.grade_angle().cos() - rotated_angle.grade_angle().sin()).abs() < EPSILON, - "cos(θ) = sin(θ + π/2) enables differentiation" - ); - - // test polynomial chain: f(x) = x³ - let x3 = x * x * x; // 27 - let f_cubic = Geonum::new(x3, 0.0, 1.0); // [27, 0] at grade 0 - let f_cubic_prime = f_cubic.differentiate(); - - assert_eq!( - f_cubic_prime.angle.grade(), - 1, - "cubic derivative at grade 1" - ); - - // demonstrate second derivative: f''(x) for f(x) = x³ - let f_cubic_double_prime = f_cubic_prime.differentiate(); - assert_eq!( - f_cubic_double_prime.angle.grade(), - 2, - "second derivative at grade 2 (bivector)" - ); - - // test constant function: f(x) = 5 - let constant = Geonum::new(5.0, 0.0, 1.0); // [5, 0] at grade 0 - let constant_prime = constant.differentiate(); + assert_eq!(f_prime.angle.grade(), 1, "derivative at grade 1"); + assert_eq!(f_prime.mag, 16.0, "magnitude preserved"); - assert_eq!( - constant_prime.mag, 5.0, - "differentiation preserves magnitude" - ); - assert_eq!( - constant_prime.angle.grade(), - 1, - "constant derivative at grade 1" - ); + let back_to_f = f_prime.integrate(); + assert_eq!(back_to_f.angle.grade(), 0, "integrated back to grade 0"); + assert_eq!(back_to_f.mag, 16.0, "magnitude preserved"); - // test linear function: f(x) = 2x - let linear_value = 2.0 * x; // 6 - let f_linear = Geonum::new(linear_value, 0.0, 1.0); // [6, 0] - let f_linear_prime = f_linear.differentiate(); + // differentiate adds π/2, integrate adds 3π/2, net = 4 blades = 2π + let diff_rotation = f_prime.angle - f.angle; + let int_rotation = back_to_f.angle - f_prime.angle; assert_eq!( - f_linear_prime.mag, 6.0, - "linear function derivative preserves magnitude" + diff_rotation, + Angle::new(1.0, 2.0), + "differentiate adds π/2" ); + assert_eq!(int_rotation, Angle::new(3.0, 2.0), "integrate adds 3π/2"); assert_eq!( - f_linear_prime.angle.grade(), - 1, - "linear derivative at grade 1" + back_to_f.angle.blade() - f.angle.blade(), + 4, + "full cycle: 4 blades" ); } #[test] fn it_proves_fundamental_theorem_is_accumulation_equals_interference() { - // Newton-Leibniz theorem: ∫ₐᵇ f'(x) dx = F(b) - F(a) + // Newton-Leibniz: ∫ₐᵇ f'(x) dx = F(b) - F(a) // in angle space: accumulated geometric sum = destructive interference of endpoints - // projection space: ∫₂⁵ 2x dx = x²|₂⁵ = 25 - 4 = 21 + // ∫₂⁵ 2x dx = x²|₂⁵ = 25 - 4 = 21 let a: f64 = 2.0; let b: f64 = 5.0; - let traditional_left: f64 = b.powi(2) - a.powi(2); // 21 - // angle space left side: accumulation via geometric addition - // integrate f'(x) = 2x from 2 to 5 via riemann sum + // left side: accumulation via geometric addition (riemann sum) let num_steps = 1000; let dx = (b - a) / num_steps as f64; let dx_geo = Geonum::new(dx, 0.0, 1.0); - let mut accumulated_sum = Geonum::new(0.0, 0.0, 1.0); + let mut accumulated = Geonum::new(0.0, 0.0, 1.0); for i in 0..num_steps { let x_i = a + i as f64 * dx; let f_prime_i = Geonum::new(2.0 * x_i, 0.0, 1.0); // f'(x) = 2x - let rectangle = f_prime_i * dx_geo; - accumulated_sum = accumulated_sum + rectangle; // geometric addition + accumulated = accumulated + f_prime_i * dx_geo; } - // angle space right side: F(b) - F(a) as destructive interference + // right side: F(b) - F(a) as destructive interference + // F(a) placed at angle π creates cos(π) = -1 interference with F(b) at angle 0 let f_b = Geonum::new(b.powi(2), 0.0, 1.0); // F(5) = [25, 0] - let f_a_negated = Geonum::new(a.powi(2), 1.0, 1.0); // [4, π] - let interference_result = f_b + f_a_negated; + let f_a_negated = Geonum::new(a.powi(2), 1.0, 1.0); // F(2) = [4, π] + let interference = f_b + f_a_negated; - // verify cosine rule: c² = 625 + 16 + 2(25)(4)cos(π) = 625 + 16 - 200 = 441 + // cosine rule: c² = 25² + 4² + 2(25)(4)cos(π) = 625 + 16 - 200 = 441 let expected_squared = f_b.mag.powi(2) + a.powi(4) + 2.0 * f_b.mag * a.powi(2) * PI.cos(); assert!((expected_squared - 441.0).abs() < EPSILON); assert!((expected_squared.sqrt() - 21.0).abs() < EPSILON); - // fundamental theorem: accumulation equals interference - assert!( - (accumulated_sum.mag - interference_result.mag).abs() < 0.02, - "left side (accumulation) {:.3} = right side (interference) {:.3}", - accumulated_sum.mag, - interference_result.mag - ); - + // fundamental theorem: accumulation = interference assert!( - (accumulated_sum.mag - traditional_left).abs() < 0.02, - "angle space {:.3} matches projection space {}", - accumulated_sum.mag, - traditional_left + (accumulated.mag - interference.mag).abs() < 0.02, + "accumulation {:.3} = interference {:.3}", + accumulated.mag, + interference.mag ); + assert!((accumulated.mag - 21.0).abs() < 0.02, "both equal 21"); } #[test] -fn it_shows_why_subtraction_appears_in_fundamental_theorem() { +fn it_shows_subtraction_in_fundamental_theorem_is_interference() { // the "minus" in F(b) - F(a) is destructive interference, not algebraic subtraction + // placing F(a) at angle π creates cos(π) = -1 which cancels // ∫₁³ 2x dx = x²|₁³ = 9 - 1 = 8 - let a: f64 = 1.0; - let b: f64 = 3.0; - - // endpoint values of antiderivative - let f_b = Geonum::new(b.powi(2), 0.0, 1.0); // [9, 0] - let f_a_at_pi = Geonum::new(a.powi(2), 1.0, 1.0); // [1, π] + let f_b = Geonum::new(9.0, 0.0, 1.0); // F(3) = [9, 0] + let f_a_at_pi = Geonum::new(1.0, 1.0, 1.0); // F(1) = [1, π] let interference = f_b + f_a_at_pi; - // verify cosine rule: c² = 81 + 1 + 2(9)(1)(-1) = 81 + 1 - 18 = 64 - let expected = (81.0_f64 + 1.0 - 18.0).sqrt(); - assert!((expected - 8.0).abs() < EPSILON); + // cosine rule: c² = 81 + 1 + 2(9)(1)cos(π) = 81 + 1 - 18 = 64 assert!( (interference.mag - 8.0).abs() < EPSILON, "interference magnitude via cos(π) = -1: {:.3}", @@ -1072,113 +761,225 @@ fn it_shows_why_subtraction_appears_in_fundamental_theorem() { } #[test] -fn it_reveals_integral_as_interference_accumulator() { - // integration accumulates geometric additions - // Newton-Leibniz says: net accumulation = interference between bounds +fn it_encodes_definite_integrals_with_value_and_domain() { + // traditional: ∫₂⁵ x² dx = 39 (value only) + // angle space: [magnitude=39, angle=3π] — value AND domain in one geonum - // ∫₀⁴ x dx = ½x²|₀⁴ = 8 - 0 = 8 - let a: f64 = 0.0; - let b: f64 = 4.0; + let a: f64 = 2.0; + let b: f64 = 5.0; + let traditional = (b.powi(3) - a.powi(3)) / 3.0; // 39 - // accumulate via riemann sum - let num_steps = 1000; - let dx = (b - a) / num_steps as f64; - let dx_geo = Geonum::new(dx, 0.0, 1.0); - let mut accumulation = Geonum::new(0.0, 0.0, 1.0); + // encode bounds as angles + let angle_a = Angle::new(a, 1.0); // 2π + let angle_b = Angle::new(b, 1.0); // 5π - for i in 0..num_steps { - let x_i = a + i as f64 * dx; - let f_i = Geonum::new(x_i, 0.0, 1.0); // f(x) = x - let area = f_i * dx_geo; - accumulation = accumulation + area; // each step: geometric addition - } + // antiderivative values with angle encoding + let f_a = Geonum::new_with_angle(a.powi(3) / 3.0, angle_a); + let f_b = Geonum::new_with_angle(b.powi(3) / 3.0, angle_b); - // endpoint interference - let f_b = Geonum::new(0.5 * b.powi(2), 0.0, 1.0); // ½(16) = [8, 0] - let f_a_negated = Geonum::new(0.5 * a.powi(2), 1.0, 1.0); // [0, π] - let interference = f_b + f_a_negated; + // magnitude encodes the integral value + let value = f_b.mag - f_a.mag; + assert!( + (value - traditional).abs() < EPSILON, + "magnitude = integral value = 39" + ); + + // angle encodes the integration domain + let domain = f_b.angle - f_a.angle; + let expected_domain = Angle::new(b - a, 1.0); // 3π + assert_eq!(domain, expected_domain, "angle encodes domain span 3π"); - // they equal + // the complete encoding + let integral = Geonum::new_with_angle(value, domain); assert!( - (accumulation.mag - interference.mag).abs() < 0.02, - "accumulation {:.3} = interference {:.3}", - accumulation.mag, - interference.mag + (integral.mag - 39.0).abs() < EPSILON, + "magnitude: integral value" ); - assert!(interference.near_mag(8.0)); + assert_eq!(integral.angle, Angle::new(3.0, 1.0), "angle: domain span"); } +// ═══════════════════════════════════════════════════════════════════════════════ +// vector calculus +// ═══════════════════════════════════════════════════════════════════════════════ + #[test] -fn it_connects_differentiation_and_antiderivative_via_angles() { - // differentiate() rotates by π/2 (grade 0 → 1) - // integrate() rotates by 3π/2 (grade 1 → 0, forward equivalent to -π/2) - // fundamental theorem connects these rotations +fn its_a_gradient() { + // traditional: ∇f = [∂f/∂x, ∂f/∂y] requires finite differences then assembling a vector + // geonum: read each partial from the angle ratio of its monomial, encode with direction, add - let f = Geonum::new(16.0, 0.0, 1.0); // F(x) at some point, grade 0 + // f(x,y) = x² + y² at (3,4) + let x_val = 3.0; + let y_val = 4.0; - // differentiate: rotate π/2 to grade 1 - let f_prime = f.differentiate(); - assert_eq!(f_prime.angle.grade(), 1, "derivative at grade 1"); - assert_eq!(f_prime.mag, 16.0, "magnitude preserved"); + let x = Geonum::new(x_val, 1.0, 6.0); // [3, π/6] + let y = Geonum::new(y_val, 1.0, 6.0); // [4, π/6] - // integrate: rotate 3π/2 back to grade 0 - let back_to_f = f_prime.integrate(); - assert_eq!(back_to_f.angle.grade(), 0, "integrated back to grade 0"); - assert_eq!(back_to_f.mag, 16.0, "magnitude preserved"); + let x_squared = x * x; // [9, 2π/6] + let y_squared = y * y; // [16, 2π/6] - // the angles connect differentiation to integration - let angle_cycle = f_prime.angle - f.angle; // differentiation rotation - let angle_back = back_to_f.angle - f_prime.angle; // integration rotation + // ∂f/∂x from x² angle ratio: power = 2, base = 3 → 2×3 = 6 + let nx = x_squared.angle.grade_angle() / x.angle.grade_angle(); + let df_dx = nx * (x_squared.mag / x.mag); - assert_eq!(angle_cycle, Angle::new(1.0, 2.0), "differentiate adds π/2"); - assert_eq!(angle_back, Angle::new(3.0, 2.0), "integrate adds 3π/2"); + // ∂f/∂y from y² angle ratio: power = 2, base = 4 → 2×4 = 8 + let ny = y_squared.angle.grade_angle() / y.angle.grade_angle(); + let df_dy = ny * (y_squared.mag / y.mag); - // net rotation: 4 blades (full 2π cycle) - assert_eq!( - back_to_f.angle.blade() - f.angle.blade(), - 4, - "full cycle: differentiate then integrate adds 4 blades" + assert!( + (df_dx - 6.0).abs() < EPSILON, + "∂f/∂x = 2x = 6 from angle ratio" + ); + assert!( + (df_dy - 8.0).abs() < EPSILON, + "∂f/∂y = 2y = 8 from angle ratio" + ); + + // encode partials with direction, add → gradient + let partial_x_geo = Geonum::new(df_dx, 0.0, 1.0); // [6, 0] + let partial_y_geo = Geonum::new(df_dy, 1.0, 2.0); // [8, π/2] + let gradient = partial_x_geo + partial_y_geo; + + let expected_mag = (6.0_f64.powi(2) + 8.0_f64.powi(2)).sqrt(); // 10 + let expected_dir = 8.0_f64.atan2(6.0); // ≈ 0.927 rad + + assert!( + (gradient.mag - expected_mag).abs() < 0.01, + "gradient magnitude = 10" + ); + assert!( + (gradient.angle.grade_angle() - expected_dir).abs() < 0.01, + "gradient direction = atan2(8,6)" ); } #[test] -fn it_shows_definite_integral_encodes_both_value_and_domain() { - // ∫₂⁵ x² dx = ⅓x³|₂⁵ = 125/3 - 8/3 = 39 - let a: f64 = 2.0; - let b: f64 = 5.0; - let traditional_value = (b.powi(3) - a.powi(3)) / 3.0; // 39 +fn its_a_laplacian() { + // traditional: ∇²f = ∂²f/∂x² + ∂²f/∂y² requires second-order finite differences + // geonum: second angle ratio readout per variable, summed + // + // for x²: first ratio = 2 (from x²), second ratio = 1 (from x¹), base = x/x = 1 + // ∂²(x²)/∂x² = 2 × 1 × 1 = 2 + // same for y² → laplacian = 2 + 2 = 4 - // encode bounds as angles - let angle_a = Angle::new(a, 1.0); // 2π - let angle_b = Angle::new(b, 1.0); // 5π + let x_val = 2.0; + let y_val = 3.0; - // antiderivative values with angle encoding - let f_a = Geonum::new_with_angle(a.powi(3) / 3.0, angle_a); // [8/3, 2π] - let f_b = Geonum::new_with_angle(b.powi(3) / 3.0, angle_b); // [125/3, 5π] + let x = Geonum::new(x_val, 1.0, 6.0); + let y = Geonum::new(y_val, 1.0, 6.0); + + let x_squared = x * x; + let y_squared = y * y; + + let x_angle = x.angle.grade_angle(); + let y_angle = y.angle.grade_angle(); - // the integral encodes BOTH value and domain - let value = f_b.mag - f_a.mag; // magnitude difference - let domain = f_b.angle - f_a.angle; // angle difference + // ∂²(x²)/∂x²: first ratio from x², second ratio from x¹ + let n1_x = x_squared.angle.grade_angle() / x_angle; // 2 + let n2_x = x.angle.grade_angle() / x_angle; // 1 + let base_x = x.mag / x.mag; // 1 + let d2f_dx2 = n1_x * n2_x * base_x; // 2 + // ∂²(y²)/∂y²: same pattern + let n1_y = y_squared.angle.grade_angle() / y_angle; // 2 + let n2_y = y.angle.grade_angle() / y_angle; // 1 + let base_y = y.mag / y.mag; // 1 + let d2f_dy2 = n1_y * n2_y * base_y; // 2 + + let laplacian = d2f_dx2 + d2f_dy2; // 4 + + assert!( + (d2f_dx2 - 2.0).abs() < EPSILON, + "∂²f/∂x² = 2 from angle ratios" + ); + assert!( + (d2f_dy2 - 2.0).abs() < EPSILON, + "∂²f/∂y² = 2 from angle ratios" + ); assert!( - (value - traditional_value).abs() < EPSILON, - "value matches traditional: {:.3} ≈ {}", - value, - traditional_value + (laplacian - 4.0).abs() < EPSILON, + "∇²f = 4: no finite differences, no h" ); +} - let expected_domain = Angle::new(b - a, 1.0); // (5-2)π = 3π - assert_eq!(domain, expected_domain, "angle encodes domain span"); +// ═══════════════════════════════════════════════════════════════════════════════ +// geometric integrals +// ═══════════════════════════════════════════════════════════════════════════════ + +#[test] +fn its_a_line_integral() { + // traditional: ∫_C F·dr requires curve parameterization + // geonum: field.dot(path) for constant field on straight path + + let start = Geonum::new_from_cartesian(0.0, 0.0); + let end = Geonum::new_from_cartesian(2.0, 3.0); + let path = end - start; + + // constant vector field F = [1, 2] + let field = Geonum::new_from_cartesian(1.0, 2.0); + + // traditional: F·(end-start) = 1*2 + 2*3 = 8 + let trad_integral: f64 = 1.0 * 2.0 + 2.0 * 3.0; + + let geo_integral = field.dot(&path); - // create the complete encoding - let integral = Geonum::new_with_angle(value, domain); assert!( - (integral.mag - 39.0).abs() < EPSILON, - "magnitude: integral value" + (geo_integral.mag - trad_integral).abs() < 0.1, + "line integral: {} ≈ {}", + geo_integral.mag, + trad_integral ); - assert_eq!( - integral.angle, - Angle::new(3.0, 1.0), - "angle: domain span 3π" +} + +#[test] +fn its_a_surface_integral() { + // surface = wedge product of edges + // magnitude IS the area, grade IS the orientation + + let edge_x = Geonum::new_from_cartesian(2.0, 0.0); + let edge_y = Geonum::new_from_cartesian(0.0, 3.0); + + let surface = edge_x.wedge(&edge_y); + + assert!( + (surface.mag - 6.0).abs() < EPSILON, + "surface area = 2 × 3 = 6" ); + assert_eq!(surface.angle.grade(), 2, "surface at grade 2 (bivector)"); } + +#[test] +fn its_a_volume_integral() { + // volume = geometric product of surface bivector with third edge + // magnitude IS the volume + + let edge_x = Geonum::new_from_cartesian(2.0, 0.0); + let edge_y = Geonum::new_from_cartesian(0.0, 3.0); + let edge_z = Geonum::new_with_blade(4.0, 2, 0.0, 1.0); + + let surface = edge_x.wedge(&edge_y); + let volume = surface.geo(&edge_z); + + assert!( + (volume.mag - 24.0).abs() < EPSILON, + "volume = 2 × 3 × 4 = 24" + ); + assert_eq!(volume.angle.grade(), 0, "volume cycles back to grade 0"); +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// the power rule was never a rule +// +// it was two projections read from a geometric number: +// 1. how many times did the angle accumulate? (the power) +// 2. what magnitude remains after removing one factor? (the base) +// +// limits compute the same answer by approaching from outside +// the geometric number already contains it from inside +// +// differentiation is π/2 rotation. integration is 3π/2 forward rotation. +// the fundamental theorem connects accumulated rotation to endpoint interference. +// vector calculus and geometric integrals use the same angle arithmetic. +// +// "scalars are projections" — and calculus is the set of projections +// that extract rate information from angle space +// ═══════════════════════════════════════════════════════════════════════════════ diff --git a/tests/numbers_test.rs b/tests/numbers_test.rs index 6adc28e..10b6643 100644 --- a/tests/numbers_test.rs +++ b/tests/numbers_test.rs @@ -628,8 +628,8 @@ fn its_an_algebraic_number() { // pow() preserves length relationships but accumulates blade count let sqrt2_pow2 = sqrt2.pow(2.0); // [r^n, n*θ] formula: [√2^2, 2*angle] = [2, 2*angle] assert!(sqrt2_pow2.near_mag(2.0)); // length: √2^2 = 2 ✓ - // blade accumulation from pow() means algebraic identity exists at different grade - assert_eq!(sqrt2_pow2.angle.blade(), 5); // angle multiplication: 2 * angle accumulates blades + // scalar at angle 0: pow scales 0 by 2 = 0, so blade stays 0 + assert_eq!(sqrt2_pow2.angle.blade(), 0); // angle scaling: 2 * 0 = 0 // square it let sqrt2_squared = sqrt2 * sqrt2; diff --git a/tests/taylor_series_test.rs b/tests/taylor_series_test.rs new file mode 100644 index 0000000..db03051 --- /dev/null +++ b/tests/taylor_series_test.rs @@ -0,0 +1,573 @@ +// taylor series coefficients are geometric normalizations +// +// the taylor series f(x) = Σ f⁽ⁿ⁾(a)/n! × (x-a)^n +// is not a clever approximation technique — its a geometric identity +// +// f⁽ⁿ⁾(a) comes from n repeated differentiations (n quarter turns) +// n! comes from the product of angle ratios accumulated during those turns +// (x-a)^n carries nθ in its angle — the displacement raised to the nth power +// +// dividing by n! undoes the geometric weight that differentiation piled up +// each term is a power of displacement normalized by its own angle descent +// +// consequences: +// e^x = Σ x^n/n! is what you get when every angle level contributes equally +// sin(x) = odd grade terms only (grades 1↔3), signs from duals +// cos(x) = even grade terms only (grades 0↔2), signs from duals +// the alternating signs in trig series are not sign bits — they are π rotations +// +// everything below proves this mechanically + +use geonum::*; +use std::f64::consts::PI; + +const EPSILON: f64 = 1e-10; + +// ═══════════════════════════════════════════════════════════════════════════════ +// n! is the product of angle ratios +// ═══════════════════════════════════════════════════════════════════════════════ + +#[test] +fn it_produces_taylor_coefficients_from_angle_descent() { + // for f(x) = x^n, the nth derivative is n! + // this n! is not combinatorial — it is the product of angle ratios: + // x^n has angle ratio n + // x^(n-1) has angle ratio (n-1) + // ... + // x^1 has angle ratio 1 + // product: n × (n-1) × ... × 1 = n! + // + // the taylor coefficient 1/n! normalizes this geometric accumulation + + let x = Geonum::new(2.0, 1.0, 6.0); // [2, π/6] + let x_angle = x.angle.grade_angle(); + + // build powers, extract angle ratios + let mut powers = vec![Geonum::new(1.0, 0.0, 1.0)]; // x^0 = 1 + for i in 1..=7 { + let next = powers[i - 1] * x; + powers.push(next); + } + + // angle ratios at each level + for i in 1..=7 { + let ratio = powers[i].angle.grade_angle() / x_angle; + assert!( + (ratio - i as f64).abs() < EPSILON, + "x^{} angle ratio = {}", + i, + i + ); + } + + // factorials from cumulative products of angle ratios + let mut factorial = 1.0; + for n in 1..=7 { + let ratio = powers[n].angle.grade_angle() / x_angle; + factorial *= ratio; + + // the taylor coefficient for the nth term is 1/n! + let taylor_coeff = 1.0 / factorial; + + // verify against known values + let expected_factorial: f64 = (1..=n).map(|i| i as f64).product(); + assert!( + (factorial - expected_factorial).abs() < EPSILON, + "angle descent gives {}! = {}", + n, + expected_factorial + ); + assert!( + (taylor_coeff - 1.0 / expected_factorial).abs() < EPSILON, + "taylor coefficient 1/{}! = {:.6}", + n, + taylor_coeff + ); + } +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// e^x: every angle level contributes equally +// ═══════════════════════════════════════════════════════════════════════════════ + +#[test] +fn it_constructs_exp_from_equal_angle_contributions() { + // e^x = Σ x^n/n! + // + // for e^x, every derivative at a=0 equals 1 (all derivatives of e^x are e^x, and e^0=1) + // so each term is x^n / n! — displacement to the nth power, normalized by angle descent + // + // the exponential is what happens when no angle level is preferred + // every rotation order contributes with equal weight after normalization + + let x_val = 1.0; // compute e^1 = e + let x = Geonum::new(x_val, 1.0, 6.0); + // use total angle (blade*π/2 + rem) instead of grade_angle + // because grade_angle wraps mod 2π, losing the ratio for n ≥ 12 + let x_total = x.angle.blade() as f64 * PI / 2.0 + x.angle.rem(); + + let mut sum = 0.0; + let mut x_n = Geonum::new(1.0, 0.0, 1.0); // x^0 = 1 + let mut factorial = 1.0; + + // accumulate taylor terms + for n in 0..20 { + let term = x_n.mag / factorial; + sum += term; + + // advance to next power + x_n = x_n * x; + + // next factorial via angle ratio + if n > 0 { + let ratio = n as f64 + 1.0; + // verify the ratio matches the angle + let x_n_total = x_n.angle.blade() as f64 * PI / 2.0 + x_n.angle.rem(); + let measured_ratio = x_n_total / x_total; + assert!( + (measured_ratio - (n + 1) as f64).abs() < EPSILON, + "angle ratio at step {} = {}", + n + 1, + n + 1 + ); + factorial *= ratio; + } else { + factorial = 1.0; // 1! = 1 + } + } + + let expected = std::f64::consts::E; + assert!( + (sum - expected).abs() < 1e-8, + "e^1 = {:.10} from angle-normalized sum, expected {:.10}", + sum, + expected + ); +} + +#[test] +fn it_constructs_exp_at_any_point() { + // e^x at x = 2: Σ 2^n/n! + // each 2^n carries n copies of θ in its angle + // each n! is the product of angle ratios from descent + // the ratio x^n / n! is displacement^n / geometric_normalization + + let test_points = [0.5, 1.0, 1.5, 2.0, 3.0]; + + for &x_val in &test_points { + let mut sum = 0.0; + let mut x_n_mag = 1.0; // |x^n| + let mut factorial = 1.0; + + for n in 0..25 { + sum += x_n_mag / factorial; + x_n_mag *= x_val; + factorial *= (n + 1) as f64; + } + + let expected = x_val.exp(); + assert!( + (sum - expected).abs() < 1e-8, + "e^{} = {:.8}, expected {:.8}", + x_val, + sum, + expected + ); + } +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// sin and cos: grade-filtered projections +// ═══════════════════════════════════════════════════════════════════════════════ + +#[test] +fn it_shows_derivative_cycling_creates_trig_series() { + // sin'(x) = cos(x), cos'(x) = -sin(x), (-sin)'(x) = -cos(x), (-cos)'(x) = sin(x) + // this 4-cycle IS the grade cycle: 0→1→2→3→0 + // + // at x = 0: + // sin(0) = 0, sin'(0) = 1, sin''(0) = 0, sin'''(0) = -1, sin''''(0) = 0, ... + // pattern: 0, 1, 0, -1, 0, 1, 0, -1, ... + // + // cos(0) = 1, cos'(0) = 0, cos''(0) = -1, cos'''(0) = 0, cos''''(0) = 1, ... + // pattern: 1, 0, -1, 0, 1, 0, -1, 0, ... + // + // the zeros are orthogonal projections (cos(π/2) = 0) + // the -1s are duals (π rotation, grade 0↔2 or 1↔3) + // the series structure comes from grade filtering + + // verify the derivative cycle at grade level + let f = Geonum::new(1.0, 0.0, 1.0); // grade 0 + + let grades: Vec = (0..8) + .scan(f, |current, _| { + let grade = current.angle.grade(); + *current = current.differentiate(); + Some(grade) + }) + .collect(); + + assert_eq!( + grades, + vec![0, 1, 2, 3, 0, 1, 2, 3], + "derivative cycling: 0→1→2→3→0→1→2→3" + ); + + // sin derivatives at 0 follow the grade cycle + // grade 0: cos component → 0 (sin has no grade-0 content at x=0) + // grade 1: sin component → 1 (sin peaks at grade 1) + // grade 2: -cos component → 0 (dual of cos, but still zero at x=0 for sin) + // grade 3: -sin component → -1 (dual of sin) + + let sin_derivs_at_0: [f64; 8] = [0.0, 1.0, 0.0, -1.0, 0.0, 1.0, 0.0, -1.0]; + let cos_derivs_at_0: [f64; 8] = [1.0, 0.0, -1.0, 0.0, 1.0, 0.0, -1.0, 0.0]; + + // verify pattern period = 4 (the grade cycle) + for i in 0..4 { + assert!( + (sin_derivs_at_0[i] - sin_derivs_at_0[i + 4]).abs() < EPSILON, + "sin derivatives repeat with period 4" + ); + assert!( + (cos_derivs_at_0[i] - cos_derivs_at_0[i + 4]).abs() < EPSILON, + "cos derivatives repeat with period 4" + ); + } +} + +#[test] +fn it_constructs_sin_from_odd_grade_terms() { + // sin(x) = x - x³/3! + x⁵/5! - x⁷/7! + ... + // = Σ (-1)^k × x^(2k+1) / (2k+1)! + // + // only odd powers appear — these are the odd grade terms (grades 1↔3) + // the alternating sign (-1)^k is a dual: each pair of quarter turns + // crosses the dual, adding π rotation + // + // sin is the odd-grade projection of e^(ix) + + let test_points = [0.3, 0.7, 1.0, 1.5, 2.0, PI / 4.0, PI / 3.0]; + + for &x_val in &test_points { + let mut sum = 0.0; + let mut x_n_mag = 1.0; // |x^n| + let mut factorial = 1.0; + + for n in 0..20 { + if n > 0 { + x_n_mag *= x_val; + factorial *= n as f64; + } + + // only odd powers contribute to sin + if n % 2 == 1 { + // the sign comes from which odd grade we're at + // grade 1: positive (n = 1, 5, 9, ...) + // grade 3: negative (n = 3, 7, 11, ...) + // this is the dual: every two quarter turns crosses diameter + let k = (n - 1) / 2; // which odd term + let dual_sign = if k % 2 == 0 { 1.0 } else { -1.0 }; // (-1)^k from duals + + sum += dual_sign * x_n_mag / factorial; + } + } + + let expected = x_val.sin(); + assert!( + (sum - expected).abs() < 1e-8, + "sin({:.3}) = {:.8}, expected {:.8}", + x_val, + sum, + expected + ); + } +} + +#[test] +fn it_constructs_cos_from_even_grade_terms() { + // cos(x) = 1 - x²/2! + x⁴/4! - x⁶/6! + ... + // = Σ (-1)^k × x^(2k) / (2k)! + // + // only even powers appear — these are the even grade terms (grades 0↔2) + // the alternating sign (-1)^k is again a dual + // + // cos is the even-grade projection of e^(ix) + + let test_points = [0.3, 0.7, 1.0, 1.5, 2.0, PI / 4.0, PI / 3.0]; + + for &x_val in &test_points { + let mut sum = 0.0; + let mut x_n_mag = 1.0; + let mut factorial = 1.0; + + for n in 0..20 { + if n > 0 { + x_n_mag *= x_val; + factorial *= n as f64; + } + + // only even powers contribute to cos + if n % 2 == 0 { + let k = n / 2; + let dual_sign = if k % 2 == 0 { 1.0 } else { -1.0 }; + + sum += dual_sign * x_n_mag / factorial; + } + } + + let expected = x_val.cos(); + assert!( + (sum - expected).abs() < 1e-8, + "cos({:.3}) = {:.8}, expected {:.8}", + x_val, + sum, + expected + ); + } +} + +#[test] +fn it_shows_the_dual_creates_alternating_signs() { + // the (-1)^k in trig series is not a sign convention + // it is a dual: π rotation (dual) in the grade cycle + // + // grade 0 → grade 2 is a dual (π rotation, cos → -cos) + // grade 1 → grade 3 is a dual (π rotation, sin → -sin) + // + // each pair of differentiations crosses the dual + // which is why every second nonzero term flips sign + + let f = Geonum::new(1.0, 0.0, 1.0); // grade 0 + + // differentiate twice: grade 0 → grade 2 + let f_double = f.differentiate().differentiate(); + assert_eq!(f_double.angle.grade(), 2, "two quarter turns reach grade 2"); + + // grade 2 is the dual of grade 0 — same pair, opposite side + // this is the dual that creates the minus sign + let grade_diff = f_double.angle.grade() as i32 - f.angle.grade() as i32; + assert_eq!(grade_diff, 2, "dual: grade difference = 2 (π rotation)"); + + // the dual relationship: grade 0 ↔ grade 2, grade 1 ↔ grade 3 + let g = Geonum::new(1.0, 0.0, 1.0); + assert_eq!(g.dual().angle.grade(), 2, "dual of grade 0 is grade 2"); + + let h = Geonum::new_with_blade(1.0, 1, 0.0, 1.0); + assert_eq!(h.dual().angle.grade(), 3, "dual of grade 1 is grade 3"); + + // in cos series: term n=0 is grade 0 (+1), term n=2 is grade 2 (-1) + // the sign flip IS the dual between dual grades + // in sin series: term n=1 is grade 1 (+1), term n=3 is grade 3 (-1) + // same dual, odd pair instead of even pair + + // cos: grade 0 → +, grade 2 → -, grade 0 → +, grade 2 → - + // sin: grade 1 → +, grade 3 → -, grade 1 → +, grade 3 → - + // the "alternating signs" are the involutive duality 0↔2 and 1↔3 +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// euler's formula: the grade-complete series +// ═══════════════════════════════════════════════════════════════════════════════ + +#[test] +fn it_shows_eulers_formula_is_grade_complete_taylor() { + // e^(ix) = cos(x) + i·sin(x) + // + // e^(ix) is the taylor series with ALL grades present + // cos(x) is the even-grade projection (grades 0↔2) + // sin(x) is the odd-grade projection (grades 1↔3) + // + // euler's formula is not connecting different functions + // it is decomposing one series into its grade components + // + // the "i" in front of sin is the Q shift from even to odd pair + + let test_points = [0.5, 1.0, PI / 4.0, PI / 3.0, PI / 2.0, PI]; + + for &x_val in &test_points { + // compute e^(ix) via full taylor series, tracking even and odd terms + let mut even_sum = 0.0; // will equal cos(x) + let mut odd_sum = 0.0; // will equal sin(x) + let mut x_n_mag = 1.0; + let mut factorial = 1.0; + + for n in 0..25 { + if n > 0 { + x_n_mag *= x_val; + factorial *= n as f64; + } + + // i^n cycles: 1, i, -1, -i (the grade cycle) + // real part gets even terms with duals + // imaginary part gets odd terms with duals + match n % 4 { + 0 => even_sum += x_n_mag / factorial, // grade 0: +real + 1 => odd_sum += x_n_mag / factorial, // grade 1: +imag + 2 => even_sum -= x_n_mag / factorial, // grade 2: -real (dual) + 3 => odd_sum -= x_n_mag / factorial, // grade 3: -imag (dual) + _ => unreachable!(), + } + } + + let expected_cos = x_val.cos(); + let expected_sin = x_val.sin(); + + assert!( + (even_sum - expected_cos).abs() < 1e-8, + "even grades at x={:.3}: {:.8} = cos({:.3}) = {:.8}", + x_val, + even_sum, + x_val, + expected_cos + ); + assert!( + (odd_sum - expected_sin).abs() < 1e-8, + "odd grades at x={:.3}: {:.8} = sin({:.3}) = {:.8}", + x_val, + odd_sum, + x_val, + expected_sin + ); + } +} + +#[test] +fn it_proves_i_is_the_q_shift_between_grade_pairs() { + // in euler's formula e^(ix) = cos(x) + i·sin(x) + // the "i" is not a mysterious imaginary unit + // it is the Q shift (π/2 rotation) from even grades to odd grades + // + // cos lives on grades 0↔2 (even pair) + // sin lives on grades 1↔3 (odd pair) + // multiplying by i = [1, π/2] rotates from even to odd + // + // euler's formula says: the grade-complete series decomposes into + // its even projection plus Q times its odd projection + + let i = Geonum::new(1.0, 1.0, 2.0); // [1, π/2] + + // i rotates grade 0 → grade 1 + let grade_0 = Geonum::new(1.0, 0.0, 1.0); + let rotated = grade_0 * i; + assert_eq!( + rotated.angle.grade(), + 1, + "i shifts grade 0 to grade 1: even → odd" + ); + + // i rotates grade 2 → grade 3 + let grade_2 = Geonum::new_with_blade(1.0, 2, 0.0, 1.0); + let rotated_2 = grade_2 * i; + assert_eq!( + rotated_2.angle.grade(), + 3, + "i shifts grade 2 to grade 3: even → odd" + ); + + // the grade pairs: + // even: {0, 2} — related by dual (π rotation) + // odd: {1, 3} — related by dual (π rotation) + // i connects even pair to odd pair via Q shift (π/2 rotation) + + assert_eq!(grade_0.dual().angle.grade(), 2, "dual: 0 ↔ 2"); + let grade_1 = Geonum::new_with_blade(1.0, 1, 0.0, 1.0); + assert_eq!(grade_1.dual().angle.grade(), 3, "dual: 1 ↔ 3"); + + // so euler's formula is: + // e^(ix) = (grade 0↔2 projection) + Q × (grade 1↔3 projection) + // = cos(x) + i·sin(x) + // the "beauty" is structural decomposition, not mysterious connection +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// convergence radius is geometric +// ═══════════════════════════════════════════════════════════════════════════════ + +#[test] +fn it_shows_convergence_is_angle_normalization_dominance() { + // a taylor series converges when the factorial normalization (angle descent) + // grows faster than the power accumulation (angle ascent) + // + // x^n grows the angle: nθ + // n! normalizes it: product of n angle ratios + // + // convergence = angle descent outpaces angle ascent + // divergence = angle ascent outpaces angle descent + // + // for e^x: n! always wins eventually (converges for all x) + // for 1/(1-x): no factorial normalization (geometric series, radius = 1) + + // e^x converges for any x because factorial normalization always dominates + let large_x = 10.0; + let mut term = 1.0; + let mut sum = 1.0; + let mut terms_decreasing_after = 0; + + for n in 1..50 { + term *= large_x / n as f64; // x^n/n! ratio: x/n + sum += term; + + // once n > x, each term is smaller than the last + // this is when angle descent (n) overtakes angle ascent (x) + if n as f64 > large_x && terms_decreasing_after == 0 { + terms_decreasing_after = n; + } + } + + assert_eq!( + terms_decreasing_after, 11, + "terms start decreasing when n > x = 10" + ); + assert!( + (sum - large_x.exp()).abs() < 1e-4, + "e^10 converges: {:.4} ≈ {:.4}", + sum, + large_x.exp() + ); + + // geometric series 1/(1-x) = Σ x^n has no factorial + // no angle descent normalization → only converges when |x| < 1 + let x_inside = 0.5_f64; + let x_outside = 1.5_f64; + + let mut geo_sum_inside = 0.0_f64; + let mut geo_sum_outside = 0.0_f64; + let mut x_n_in = 1.0; + let mut x_n_out = 1.0; + + for _ in 0..100 { + geo_sum_inside += x_n_in; + geo_sum_outside += x_n_out; + x_n_in *= x_inside; + x_n_out *= x_outside; + } + + let expected_inside = 1.0 / (1.0 - x_inside); // 2.0 + assert!( + (geo_sum_inside - expected_inside).abs() < 1e-8, + "geometric series converges inside radius: {:.4} ≈ {:.4}", + geo_sum_inside, + expected_inside + ); + assert!( + geo_sum_outside > 1e10, + "geometric series diverges outside radius: no angle descent to tame growth" + ); +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// taylor series was never about approximation +// +// it is the decomposition of a function into its angle-level contributions +// each term x^n/n! is one level of angular displacement normalized by +// the geometric weight that differentiation accumulated at that level +// +// e^x treats all levels equally +// sin filters to odd grades, cos filters to even grades +// the alternating signs are duals (duals) +// euler's formula reassembles the grade projections +// convergence is angle descent dominating angle ascent +// +// the series doesn't approximate the function from outside +// it reads the function's angular structure from inside +// ═══════════════════════════════════════════════════════════════════════════════ From e20b381259690a3e0dd5a06dd94e39806fc778f6 Mon Sep 17 00:00:00 2001 From: max funk Date: Tue, 31 Mar 2026 11:35:38 -0700 Subject: [PATCH 5/6] pow angle scaling line range refs --- .agents/onboard.md | 4 ++-- README.md | 24 +++++++++++++----------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/.agents/onboard.md b/.agents/onboard.md index 5c9ba86..d5f0a57 100644 --- a/.agents/onboard.md +++ b/.agents/onboard.md @@ -59,7 +59,7 @@ learn how geonum implements the dual in src/angle.rs:473~490 learn how angle impls PartialEq and Eq in src/angle.rs:572~590 -learn how angle overloads arithmetic operators in src/angle.rs:592~740 +learn how angle overloads arithmetic operators in src/angle.rs:592~805 learn how to construct geonum with new, new_with_angle from src/geonum_mod.rs:32~49 @@ -81,6 +81,6 @@ learn about angle forward only geometry from the it_sets_angle_forward_geometry_ read only tests/angle_arithmetic_test.rs:1~20 because the file is large, but you can learn about the angle forward only blade arithmetic of operations from this file -read the its_a_limit:40-119, it_proves_differentiation_cycles_grades:764-915 tests in tests/calculus_test.rs to understand how geonum automates calculus +read the it_shows_limits_discard_what_angles_preserve:350-387, it_proves_differentiation_cycles_grades:586-664 tests in tests/calculus_test.rs to understand how geonum automates calculus tests are styled as trojan horses for simplicity. conventional jargon promising symbol salad but readers get simple arithmetic in test contents. example tests: it_handles_conformal_split:4694-4805, it_handles_inversive_distance:4807-4937 in tests/cga_test.rs diff --git a/README.md b/README.md index 86cdc3d..e4d6626 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,7 @@ see [tests](https://github.com/mxfactorial/geonum/tree/main/tests) to learn how ❯ ls -1 tests addition_test.rs affine_test.rs +algebra_test.rs algorithms_test.rs angle_arithmetic_test.rs arithmetic_test.rs @@ -170,6 +171,7 @@ qm_test.rs rendering_test.rs robotics_test.rs set_theory_test.rs +taylor_series_test.rs tensor_test.rs trigonometry_test.rs ``` @@ -402,17 +404,17 @@ geometric numbers build dimensions by rotating—not stacking - it_proves_rotational_quadrature_expresses_quadratic_forms:1419-1593 - tests/calculus_test.rs - - its_a_limit:40-119 - - its_a_derivative:121-165 - - its_an_integral:167-218 - - its_a_gradient:310-358 - - its_a_divergence:360-409 - - its_a_curl:411-499 - - its_a_laplacian:501-605 - - its_a_line_integral:607-633 - - its_a_surface_integral:635-662 - - it_proves_differentiation_cycles_grades:764-915 - - it_proves_fundamental_theorem_is_accumulation_equals_interference:1002-1053 + - it_encodes_the_power_in_the_angle:35-88 + - it_derives_x_squared_without_limits:91-121 + - it_shows_limits_discard_what_angles_preserve:350-387 + - it_shows_limits_lose_the_tangent_normal_dual:390-427 + - it_shows_factorial_emerges_from_angle_descent:501-542 + - it_proves_differentiation_cycles_grades:586-664 + - it_proves_fundamental_theorem_is_accumulation_equals_interference:704-743 + - its_a_gradient:806-853 + - its_a_laplacian:856-902 + - its_a_line_integral:909-931 + - its_a_surface_integral:934-948 - tests/mechanics_test.rs - it_changes_kinematic_level_by_cycling_grade:46-195 From 15a7a64d837a0766c8cdb29e568382f51a6a9e51 Mon Sep 17 00:00:00 2001 From: max funk Date: Tue, 31 Mar 2026 11:35:56 -0700 Subject: [PATCH 6/6] release 0.12.0 --- CHANGELOG.md | 23 +++++++++++++++++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20a4233..8ec07d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # changelog +## 0.12.0 (2026-03-31) + +### fixed +- **BREAKING**: pow() now scales the total angle by n instead of adding nπ — matches repeated multiplication for all n + +### added +- Mul for Angle and &Angle: scalar multiplication of angles +- calculus_test.rs: power rule as angle readout, factorials from angle descent, limits as lossy projections, fundamental theorem as geometric interference, gradient, laplacian, line/surface/volume integrals +- taylor_series_test.rs: taylor coefficients as geometric normalizations, e^x as uniform angle contribution, sin/cos as grade-filtered projections, euler's formula as grade decomposition, convergence as angle descent dominance +- algebra_test.rs: fundamental theorem of algebra via winding numbers — degree = wraps, roots = unwindings, polynomial evaluation on circles, roots of unity as generalized Q lattice + +### changed +- replaced old calculus_test.rs (24 tests) with power-rule-anchored suite (23 tests) + ## 0.11.0 (2026-03-20) ### breaking @@ -9,6 +23,9 @@ - `normalize_boundaries()` removed — boundary logic is algebraic in the tangent sum formula - `Display` for Angle now shows `t` instead of `rem` +### fixed +- pow() now scales the total angle by n instead of adding nπ — matches repeated multiplication for all n + ### added - `Angle::t()` — projection ratio between adjacent π/2 blades @@ -19,6 +36,10 @@ - `Angle::near_rem(radians)` — remainder comparison within tolerance - `Geonum::near(&other)` — magnitude + angle comparison within tolerance - `Geonum::near_mag(value)` — magnitude comparison within tolerance +- Mul for Angle and &Angle: scalar multiplication of angles +- calculus_test.rs: power rule as angle readout, factorials from angle descent, limits as lossy projections, fundamental theorem as geometric interference, gradient, laplacian, line/surface/volume integrals +- taylor_series_test.rs: taylor coefficients as geometric normalizations, e^x as uniform angle contribution, sin/cos as grade-filtered projections, euler's formula as grade decomposition, convergence as angle descent dominance +- algebra_test.rs: fundamental theorem of algebra via winding numbers — degree = wraps, roots = unwindings, polynomial evaluation on circles, roots of unity as generalized Q lattice ### changed @@ -32,6 +53,8 @@ - `Geonum::dot()`, `wedge()`, `cos()`, `sin()`, `distance_to()`, `project_to_angle()` use `cos_sin()` - `Geonum::geo()` computes single `cos_sin()` for both dot and wedge - `Geonum` addition uses rational projection pipeline: cos_sin (0 sqrts) → sum → magnitude (1 sqrt) → cartesian recovery (0 sqrts) +- replaced old calculus_test.rs (24 tests) with power-rule-anchored suite (23 tests) +- updated angle_arithmetic_test, numbers_test, geonum_mod unit test pow expectations to match corrected angle scaling ### performance diff --git a/Cargo.lock b/Cargo.lock index 1a0920d..f9ea663 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -171,7 +171,7 @@ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "geonum" -version = "0.11.0" +version = "0.12.0" dependencies = [ "criterion", "geonum", diff --git a/Cargo.toml b/Cargo.toml index 73fdfa1..8610ab7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "geonum" -version = "0.11.0" +version = "0.12.0" edition = "2021" repository = "https://github.com/mxfactorial/geonum" description = "geometric number library supporting unlimited dimensions with O(1) complexity"