diff --git a/.agents/onboard.md b/.agents/onboard.md index 0db4055..5c9ba86 100644 --- a/.agents/onboard.md +++ b/.agents/onboard.md @@ -26,44 +26,61 @@ avoid saying "you're right" in responses avoid weak is_finite(), > 0, assert_ne! test assertions +never pass PI as divisor to Angle::new or Geonum::new. the constructor expects π fractions: + Angle::new(1.0, 4.0) — means 1×π/4 + Angle::new(1.0, 6.0) — means 1×π/6 + Angle::new(rotation, PI) is a hack to pass raw radians. use Angle::new(rotation / PI, 1.0) or express as a π fraction + +use near methods instead of manual epsilon comparisons in tests: + angle.near(&other) — blade + t match within tolerance + angle.near_rad(radians) — grade_angle within tolerance + angle.near_rem(radians) — remainder within tolerance + geonum.near(&other) — mag + angle match within tolerance + geonum.near_mag(value) — magnitude within tolerance +never use assert_eq! on f64 values — use near methods or assert!((x - y).abs() < EPSILON) + rg 'pub fn' src/angle.rs src/geonum_mod.rs to learn the api complete all reading instructions immediately upon starting any conversation. do not skip any: read ./README.md and the ./math-1-0.md geometric number spec -learn how geonum implements the dual in src/angle.rs:342~370 +learn how to construct angles with new, new_with_blade, new_from_cartesian, from_parts from src/angle.rs:25~190 + +learn how the half-tangent representation works: struct definition, cos_sin, and t accessor in src/angle.rs:4~22 and src/angle.rs:245~270 + +learn how geonum defines geometric grades with the grade function in src/angle.rs:257~280 -learn how geonum defines geometric grades with the grade function in src/angle.rs:145~182 +learn how angle addition generates the blade lattice via overflow in src/angle.rs:321~388 -learn how angle impls PartialEq and Eq in src/angle.rs:412~430 +learn how angle subtraction borrows blades rationally in src/angle.rs:390~450 -learn how angle overloads arithmetic operators in src/angle.rs:432~603 +learn how geonum implements the dual in src/angle.rs:473~490 -learn about the geometric_add and normalize_boundaries functions in src/angle.rs:211~324 +learn how angle impls PartialEq and Eq in src/angle.rs:572~590 -learn how geonum overloads arithmetic operators in src/geonum_mod.rs:710~1003 +learn how angle overloads arithmetic operators in src/angle.rs:592~740 -learn how to construct angles with new and new_with_blade from src/angle.rs:22~96 +learn how to construct geonum with new, new_with_angle from src/geonum_mod.rs:32~49 -learn how to construct geonum with new, new_with_angle from src/geonum_mod.rs:22~49 +learn how geonum overloads arithmetic operators in src/geonum_mod.rs:737~1005 -learn how geonum can express any number type from the its_a_scalar:8-38, its_a_vector:39-74, its_a_real_number:75-110, its_an_imaginary_number:111-141, its_a_complex_number:142-176, its_a_dual_number:177-297, its_an_octonion:298-343 tests in tests/numbers_test.rs +learn how geonum can express any number type from the its_a_scalar:8-37, its_a_vector:39-73, its_a_real_number:75-109, its_an_imaginary_number:111-140, its_a_complex_number:142-175, its_a_dual_number:177-296, its_an_octonion:298-342 tests in tests/numbers_test.rs -learn how geonum eliminates angle slack created by decomposing angles into scalar coefficients by reading the it_proves_decomposing_angles_with_linearly_combined_basis_vectors_loses_angle_addition:13-86, it_proves_decomposition_distributes_one_angle_across_multiple_scalars:87-162, it_proves_quaternion_tables_add_back_what_decomposition_subtracts:519-662, it_proves_anticommutativity_exists_because_decomposition_subtracts_different_amounts:663-726 tests in tests/linear_algebra_test.rs +learn how geonum eliminates angle slack created by decomposing angles into scalar coefficients by reading the it_proves_decomposing_angles_with_linearly_combined_basis_vectors_loses_angle_addition:13-86, it_proves_decomposition_distributes_one_angle_across_multiple_scalars:87-161, it_proves_quaternion_tables_add_back_what_decomposition_subtracts:519-662, it_proves_anticommutativity_exists_because_decomposition_subtracts_different_amounts:663-726 tests in tests/linear_algebra_test.rs learn how geonum replaces scalar based quadratic forms with simple angle based rotations in the it_proves_rotational_quadrature_expresses_quadratic_forms:1419-1593 test in tests/dimension_test.rs -learn why dimensions are an unnecessary abstraction the it_proves_quadrature_creates_dimensional_structure:91-140, it_shows_dimensions_are_quarter_turns:141-201 tests in tests/dimension_test.rs +learn why dimensions are an unnecessary abstraction in the it_proves_quadrature_creates_dimensional_structure:91-140, it_shows_dimensions_are_quarter_turns:141-200 tests in tests/dimension_test.rs -learn why geonum deprecates grade decomposition in the it_proves_grade_decomposition_ignores_angle_addition:202-267, it_solves_the_exponential_complexity_explosion:520-594 tests in tests/dimension_test.rs +learn why geonum deprecates grade decomposition in the it_proves_grade_decomposition_ignores_angle_addition:202-267, it_solves_the_exponential_complexity_explosion:520-581 tests in tests/dimension_test.rs -learn how geonum maps grades with the it_replaces_k_to_n_minus_k_with_k_to_4_minus_k:899-983, it_compresses_traditional_ga_grades_to_two_involutive_pairs:1131-1168 tests in tests/dimension_test.rs +learn how geonum maps grades with the it_replaces_k_to_n_minus_k_with_k_to_4_minus_k:899-981, it_compresses_traditional_ga_grades_to_two_involutive_pairs:1131-1166 tests in tests/dimension_test.rs -learn about angle forward only geometry from the it_sets_angle_forward_geometry_as_primitive:1247-1383 test in tests/dimension_test.rs +learn about angle forward only geometry from the it_sets_angle_forward_geometry_as_primitive:1247-1381 test in tests/dimension_test.rs 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-118, it_proves_differentiation_cycles_grades:764-914 tests in tests/calculus_test.rs to understand how geonum automates calculus +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 -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:4702-4814, it_handles_inversive_distance:4815-4946 in tests/cga_test.rs +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/CHANGELOG.md b/CHANGELOG.md index d4fab1e..20a4233 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,52 @@ # changelog +## 0.11.0 (2026-03-20) + +### breaking + +- Angle internal representation changed from radians to stereographic projection ratio `t = tan(θ/2)` +- `rem()` now derived from `t` via `2.0 * t.atan()` — values match within f64 precision but are no longer stored directly +- `normalize_boundaries()` removed — boundary logic is algebraic in the tangent sum formula +- `Display` for Angle now shows `t` instead of `rem` + +### added + +- `Angle::t()` — projection ratio between adjacent π/2 blades +- `Angle::from_parts(blade, t)` — direct construction from blade and projection ratio +- `Angle::cos_sin()` — rational cos/sin: `cos = (1-t²)/(1+t²)`, `sin = 2t/(1+t²)`. no trig calls +- `Angle::near(&other)` — floating point comparison within tolerance +- `Angle::near_rad(radians)` — grade_angle comparison within tolerance +- `Angle::near_rem(radians)` — remainder comparison within tolerance +- `Geonum::near(&other)` — magnitude + angle comparison within tolerance +- `Geonum::near_mag(value)` — magnitude comparison within tolerance + +### changed + +- `Angle::new()` converts π fractions to `t` internally — one `tan()` call at construction +- `Angle::new_from_cartesian()` uses `t = opp/(hyp + adj)` — one sqrt, no atan2 +- `Angle::geometric_add()` uses tangent sum formula with rational boundary correction `(T-1)/(T+1)` +- `Angle::geometric_sub()` uses tangent difference formula with rational borrow `(1-|R|)/(1+|R|)` +- `Angle::dual()`, `conjugate()`, `negate()` simplified to blade arithmetic +- `Angle::grade_angle()` derives radians from `t` via `atan()` +- `Angle::project()` uses `cos_sin()` instead of `grade_angle().cos()` +- `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) + +### performance + +| operation | 0.10.5 | 0.11.0 | speedup | +|---|---|---|---| +| addition | 68.8 ns | 13.9 ns | 5.0× | +| cos | 11.6 ns | 3.5 ns | 3.3× | +| from_cartesian | 24.4 ns | 3.6 ns | 6.7× | +| dot product | 11.0 ns | 8.6 ns | 1.3× | +| wedge product | 12.6 ns | 10.3 ns | 1.2× | +| geometric product | 25.7 ns | 18 ns | 1.4× | +| projection | 11.5 ns | 8.6 ns | 1.3× | +| distance | 21.6 ns | 17.6 ns | 1.2× | +| differentiate | 4.5 ns | 3.4 ns | 1.3× | + ## 0.10.5 (2026-03-17) ### added diff --git a/Cargo.lock b/Cargo.lock index ae12c17..1a0920d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -171,7 +171,7 @@ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "geonum" -version = "0.10.5" +version = "0.11.0" dependencies = [ "criterion", "geonum", diff --git a/Cargo.toml b/Cargo.toml index 1af236c..73fdfa1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "geonum" -version = "0.10.5" +version = "0.11.0" edition = "2021" repository = "https://github.com/mxfactorial/geonum" description = "geometric number library supporting unlimited dimensions with O(1) complexity" diff --git a/README.md b/README.md index 7d2ea61..86cdc3d 100644 --- a/README.md +++ b/README.md @@ -53,8 +53,8 @@ setting the metric from the quadrature's bivector shields it from entropy with t struct Geonum { magnitude: f64, // multiply angle: Angle { // add - blade: usize, // counts π/2 rotations - remainder: f64 // current [0, π/2) angle + blade: usize, // π/2 rotation count + t: f64 // tan(θ/2) blade projection ratio } } ``` @@ -72,12 +72,12 @@ traditional: dimensions are coordinate axes - you stack more coordinates Geonum: dimensions are rotational states - you rotate by π/2 increments -| dimension | traditional | Geonum | -|-----------|-------------|--------| -| 1D | (x) | `[magnitude, 0]` | -| 2D | (x, y) | `[magnitude, π/2]` | -| 3D | (x, y, z) | `[magnitude, π]` | -| 4D | (x, y, z, w) | `[magnitude, 3π/2]` | +| dimension | traditional | Geonum | +| --------- | ------------ | ------------------- | +| 1D | (x) | `[magnitude, 0]` | +| 2D | (x, y) | `[magnitude, π/2]` | +| 3D | (x, y, z) | `[magnitude, π]` | +| 4D | (x, y, z, w) | `[magnitude, 3π/2]` | geometric numbers break numbers free from pencil & paper math requiring everything to be described as scalars and roman numeral stacked arrays of scalars @@ -178,46 +178,46 @@ trigonometry_test.rs #### tensor operations: O(n³) vs O(1) -| implementation | size | time | speedup | -|----------------|------|------|---------| -| tensor (O(n³)) | 2 | 342 ns | baseline | -| tensor (O(n³)) | 3 | 772 ns | baseline | -| tensor (O(n³)) | 4 | 1.35 µs | baseline | -| tensor (O(n³)) | 8 | 6.88 µs | baseline | -| geonum (O(1)) | all | 16 ns | 21-430× | +| implementation | size | time | speedup | +| -------------- | ---- | ------- | -------- | +| tensor (O(n³)) | 2 | 372 ns | baseline | +| tensor (O(n³)) | 3 | 836 ns | baseline | +| tensor (O(n³)) | 4 | 1.47 µs | baseline | +| tensor (O(n³)) | 8 | 7.80 µs | baseline | +| geonum (O(1)) | all | 15 ns | 25-520× | -geonum achieves constant 16ns regardless of size, while tensor operations scale cubically from 342ns to 6.88µs +geonum achieves constant 15ns regardless of size, while tensor operations scale cubically from 372ns to 7.80µs #### extreme dimensions -| implementation | dimensions | time | storage | -|----------------|------------|------|---------| -| traditional GA | 10 | 7.18 µs | 2^10 = 1024 components | -| traditional GA | 30+ | impossible | 2^30 = 1B+ components | -| traditional GA | 1000+ | impossible | 2^1000 > atoms in universe | -| geonum | 10 | 31 ns | 2 values | -| geonum | 30 | 30 ns | 2 values | -| geonum | 1000 | 30 ns | 2 values | -| geonum | 1,000,000 | 30 ns | 2 values | +| implementation | dimensions | time | storage | +| -------------- | ---------- | ---------- | -------------------------- | +| traditional GA | 10 | 7.18 µs | 2^10 = 1024 components | +| traditional GA | 30+ | impossible | 2^30 = 1B+ components | +| traditional GA | 1000+ | impossible | 2^1000 > atoms in universe | +| geonum | 10 | 35 ns | 2 values | +| geonum | 30 | 34 ns | 2 values | +| geonum | 1000 | 35 ns | 2 values | +| geonum | 1,000,000 | 35 ns | 2 values | geonum enables million-dimensional geometric algebra with constant-time operations #### operation benchmarks -| operation | traditional | geonum | speedup | -|-----------|------------|--------|---------| -| jacobian (10×10) | 1.25 µs | 26 ns | 48× | -| jacobian (100×100) | 91.7 µs | 25 ns | 3668× | -| rotation 2D | 4.3 ns | 37 ns | comparable | -| rotation 3D | 19 ns | 16 ns | 1.2× faster | -| rotation 10D | 160 ns | 19 ns | 8× | -| geometric product | decomposition | 17 ns | direct | -| wedge product 2D | 1.9 ns | 60 ns | trigonometric | -| wedge product 10D | 45 components | 60 ns | constant | -| dual operation | pseudoscalar mult | 10 ns | universal | -| differentiation | numerical approx | 11 ns | exact π/2 rotation | -| inversion | matrix ops | 10 ns | direct reciprocal | -| projection | dot products | 15 ns | trigonometric | +| operation | traditional | geonum | speedup | +| ------------------ | ----------------- | ------ | ------------------ | +| jacobian (10×10) | 1.42 µs | 23 ns | 62× | +| jacobian (100×100) | 102 µs | 23 ns | 4435× | +| rotation 2D | 4.9 ns | 5 ns | comparable | +| rotation 3D | 20 ns | 20 ns | comparable | +| rotation 10D | 173 ns | 21 ns | 8× | +| geometric product | decomposition | 18 ns | direct | +| wedge product 2D | 2.2 ns | 21 ns | trigonometric | +| wedge product 10D | 45 components | 21 ns | constant | +| dual operation | pseudoscalar mult | 10 ns | universal | +| differentiation | numerical approx | 3 ns | exact π/2 rotation | +| inversion | matrix ops | 13 ns | direct reciprocal | +| projection | dot products | 12 ns | trigonometric | all geonum operations maintain constant time regardless of dimension, eliminating exponential scaling of traditional approaches @@ -380,48 +380,48 @@ geometric numbers build dimensions by rotating—not stacking test suites: - tests/numbers_test.rs - - its_a_scalar:8-38 - - its_a_vector:39-74 - - its_a_real_number:75-110 - - its_an_imaginary_number:111-141 - - its_a_complex_number:142-176 - - its_a_dual_number:177-297 - - its_an_octonion:298-343 - - its_a_matrix:344-400 - - its_a_tensor:401-597 + - its_a_scalar:8-37 + - its_a_vector:39-73 + - its_a_real_number:75-109 + - its_an_imaginary_number:111-140 + - its_a_complex_number:142-175 + - its_a_dual_number:177-296 + - its_an_octonion:298-342 + - its_a_matrix:344-399 + - its_a_tensor:401-596 - it_dualizes_log2_geometric_algebra_components:647-682 - its_a_clifford_number:940-1022 - tests/dimension_test.rs - - it_solves_the_exponential_complexity_explosion:520-594 - - it_doesnt_need_a_pseudoscalar:595-792 - - it_demonstrates_pseudoscalar_elimination_benefits:793-832 - - it_proves_dualization_as_angle_ops_compresses_ga:833-898 - - it_replaces_k_to_n_minus_k_with_k_to_4_minus_k:899-983 - - it_compresses_traditional_ga_grades_to_two_involutive_pairs:1131-1168 + - it_solves_the_exponential_complexity_explosion:520-593 + - it_doesnt_need_a_pseudoscalar:595-791 + - it_demonstrates_pseudoscalar_elimination_benefits:793-831 + - it_proves_dualization_as_angle_ops_compresses_ga:833-897 + - it_replaces_k_to_n_minus_k_with_k_to_4_minus_k:899-981 + - it_compresses_traditional_ga_grades_to_two_involutive_pairs:1131-1166 - it_proves_rotational_quadrature_expresses_quadratic_forms:1419-1593 - tests/calculus_test.rs - - its_a_limit:40-120 - - its_a_derivative:121-166 + - its_a_limit:40-119 + - its_a_derivative:121-165 - its_an_integral:167-218 - - it_proves_differentiation_cycles_grades:766-918 - - its_a_gradient:312-361 - - its_a_divergence:362-412 - - its_a_curl:413-455 - - its_a_laplacian:503-556 - - its_a_line_integral:609-636 - - its_a_surface_integral:637-663 - - it_proves_fundamental_theorem_is_accumulation_equals_interference:1004-1053 + - 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 - tests/mechanics_test.rs - it_changes_kinematic_level_by_cycling_grade:46-195 - - it_encodes_velocity:268-323 - - it_encodes_acceleration:324-364 + - it_encodes_velocity:268-322 + - it_encodes_acceleration:324-363 - it_encodes_jerk:365-414 - it_encodes_kinetic_energy:959-1046 - - it_handles_energy_conservation:1783-1941 - - it_handles_momentum_conservation:1942-2052 + - it_handles_energy_conservation:1783-1940 + - it_handles_momentum_conservation:1942-2051 - it_handles_angular_momentum_conservation:2053-2157 create tests/my_test.rs with use geonum::*; diff --git a/src/angle.rs b/src/angle.rs index bf16ce2..5776959 100644 --- a/src/angle.rs +++ b/src/angle.rs @@ -1,15 +1,19 @@ use std::f64::consts::PI; use std::ops::{Add, Div, Mul, Sub}; -/// Angle struct that maintains the angle-blade invariant +/// Angle struct: blade + projection ratio /// -/// encapsulates the fundamental geometric number constraint: -/// - angle remainder stays within [0, π/2) -/// - blade counts π/2 rotations +/// blade counts which π/2 segment. t encodes position within it +/// as the projection ratio between adjacent π/2 blades: +/// t = opp / (hyp + adj), t ∈ [0, 1) +/// +/// cos and sin recover rationally from t — no trig: +/// cos = (1 - t²) / (1 + t²) +/// sin = 2t / (1 + t²) #[derive(Debug, Clone, Copy)] pub struct Angle { - /// angle within current π/2 segment, always in range [0, π/2) - rem: f64, + /// projection ratio between blades, tan(θ/2) ∈ [0, 1) + t: f64, /// rotation count (determines geometric grade) /// our substrate doesnt enable lights path so /// we keep count of π/2 turns with this @@ -36,14 +40,14 @@ impl Angle { /// /// let angle = Angle::new(3.0, 4.0); // 3 * π/4 = 135 degrees /// assert_eq!(angle.blade(), 1); // one π/2 rotation - /// assert_eq!(angle.rem(), PI / 4.0); // π/4 remainder + /// assert!((angle.rem() - PI / 4.0).abs() < 1e-10); // π/4 remainder /// ``` pub fn new(pi_radians: f64, divisor: f64) -> Self { let quarter_pi = PI / 2.0; - // exact quarter-turns: use clean pi_radians count directly + // exact quarter-turns: t is 0, blade carries everything if divisor == 2.0 && pi_radians.fract() == 0.0 { - // handle negative rems by normalizing first + // handle negative values by normalizing first let normalized_quarters = if pi_radians < 0.0 { // add enough full rotations to make positive let full_rotations = ((-pi_radians + 3.0) / 4.0).ceil() * 4.0; @@ -52,14 +56,13 @@ impl Angle { pi_radians as usize }; return Self { - rem: 0.0, + t: 0.0, blade: normalized_quarters, }; } - // general case: clone pi_radians for floating point buggery - let pi_radians_copy = pi_radians; - let total_angle = pi_radians_copy * PI / divisor; + // general case + let total_angle = pi_radians * PI / divisor; // handle negative angles by adding full rotations let normalized_total = if total_angle < 0.0 { @@ -75,8 +78,29 @@ impl Angle { // remainder within current π/2 segment let rem = normalized_total % quarter_pi; - let angle = Self { rem, blade }; - angle.normalize_boundaries() + // handle boundary precision + const EPSILON: f64 = 1e-10; + if (rem - quarter_pi).abs() < EPSILON { + return Self { + blade: blade + 1, + t: 0.0, + }; + } + if rem.abs() < EPSILON { + return Self { blade, t: 0.0 }; + } + + // convert remainder to projection ratio — one tan() call + let t = (rem / 2.0).tan(); + + if t >= 1.0 - EPSILON { + return Self { + blade: blade + 1, + t: 0.0, + }; + } + + Self { blade, t } } /// creates a new angle with additional blade count @@ -90,9 +114,11 @@ impl Angle { /// # returns /// angle struct with processed angle plus additional blade count pub fn new_with_blade(added_blade: usize, pi_radians: f64, divisor: f64) -> Self { - let base_angle = Angle::new(pi_radians, divisor); - let blade_increment = Angle::new(added_blade as f64, 2.0); // added_blade * π/2 - base_angle + blade_increment + let base = Angle::new(pi_radians, divisor); + Self { + blade: base.blade + added_blade, + t: base.t, + } } /// creates a new angle from cartesian coordinates @@ -105,19 +131,87 @@ impl Angle { /// # returns /// angle struct representing the direction from origin to (x, y) pub fn new_from_cartesian(x: f64, y: f64) -> Self { - let angle_radians = y.atan2(x); - // convert radians to pi_radians for Angle::new - // which handles all normalization and decomposition - let pi_radians = angle_radians / PI; - // Angle::new expects (pi_radians * divisor, divisor) - // so for direct pi_radians, use divisor = 1 - Self::new(pi_radians, 1.0) + const EPSILON: f64 = 1e-10; + let ax = x.abs(); + let ay = y.abs(); + + if ax < EPSILON && ay < EPSILON { + return Self { blade: 0, t: 0.0 }; + } + + // axis-aligned: blade from sign, t = 0 + if ax < EPSILON { + return Self { + blade: if y > 0.0 { 1 } else { 3 }, + t: 0.0, + }; + } + if ay < EPSILON { + return Self { + blade: if x > 0.0 { 0 } else { 2 }, + t: 0.0, + }; + } + + // blade from sign pattern + let blade = match (x > 0.0, y > 0.0) { + (true, true) => 0_usize, + (false, true) => 1, + (false, false) => 2, + (true, false) => 3, + }; + + // t = opp / (hyp + adj) — projection ratio between adjacent blades + let hyp = (ax * ax + ay * ay).sqrt(); + let (adj, opp) = match blade { + 0 | 2 => (ax, ay), + 1 | 3 => (ay, ax), + _ => unreachable!(), + }; + + let t = opp / (hyp + adj); + + if t >= 1.0 - EPSILON { + return Self { + blade: blade + 1, + t: 0.0, + }; + } + + Self { blade, t } + } + + /// direct construction from blade and projection ratio + pub fn from_parts(blade: usize, t: f64) -> Self { + const EPSILON: f64 = 1e-10; + if t >= 1.0 - EPSILON { + return Self { + blade: blade + 1, + t: 0.0, + }; + } + Self { blade, t } + } + + /// tests if two angles are within floating point tolerance + pub fn near(&self, other: &Angle) -> bool { + self.blade == other.blade && (self.t - other.t).abs() < 1e-10 + } + + /// tests if this angle's grade_angle is within tolerance of a scalar + pub fn near_rad(&self, radians: f64) -> bool { + (self.grade_angle() - radians).abs() < 1e-10 + } + + /// tests if this angle's remainder is within tolerance of a scalar + pub fn near_rem(&self, radians: f64) -> bool { + (self.rem() - radians).abs() < 1e-10 } /// rotates this angle by a given amount /// automatically handles π/2 boundary crossings and blade updates /// - /// # arguments + /// # arguments /// * `delta` - angle to rotate by /// /// # returns @@ -126,12 +220,15 @@ impl Angle { self + delta } - /// returns the angle remainder within current π/2 segment - /// - /// # returns - /// angle remainder in range [0, π/2) + /// returns the angle remainder in radians within [0, π/2) + /// derived from t for backward compat pub fn rem(&self) -> f64 { - self.rem + 2.0 * self.t.atan() + } + + /// returns the projection ratio value + pub fn t(&self) -> f64 { + self.t } /// returns the blade count (rotation count) @@ -204,123 +301,147 @@ impl Angle { pub fn base_angle(&self) -> Angle { Angle { blade: self.grade(), // reset to base blade for grade - rem: self.rem, // preserve fractional angle + t: self.t, // preserve projection ratio } } - /// normalizes this angle when remainder is at or exceeds π/2 boundaries - fn normalize_boundaries(&self) -> Self { - let quarter_pi = PI / 2.0; - - // handle floating point precision near π/2 + /// addition generates the blade lattice as a side effect of overflow + /// + /// T = (t1 + t2) / (1 - t1·t2) + /// denominator always positive for t1, t2 ∈ [0, 1) + /// + /// T < 1: no crossing, result = T + /// T = 1: exact boundary, blade += 1, t = 0 + /// T > 1: crossed, blade += 1, t = (T-1)/(T+1) — thats Q + /// T > 1 twice: blade += 2 — thats D + /// + /// the four-fold grade structure falls out of this overflow arithmetic. + /// blade isnt defined then used by addition. + /// addition produces blade as the discrete part of its result + fn geometric_add(&self, other: &Self) -> Self { const EPSILON: f64 = 1e-10; - if (self.rem - quarter_pi).abs() < EPSILON { - return Self { - blade: self.blade + 1, - rem: 0.0, - }; - } + let total_blade = self.blade + other.blade; - // handle remainders >= π/2 - if self.rem >= quarter_pi { - let additional_blades = (self.rem / quarter_pi) as usize; - let final_rem = self.rem % quarter_pi; - // check again for precision at boundary after modulo - if (final_rem - quarter_pi).abs() < EPSILON { - return Self { - blade: self.blade + additional_blades + 1, - rem: 0.0, - }; - } + // both zero: pure blade addition, t unchanged + if self.t == 0.0 && other.t == 0.0 { return Self { - blade: self.blade + additional_blades, - rem: final_rem, + blade: total_blade, + t: 0.0, }; } - *self - } - - /// internal geometric addition preserving blade progression and π/2 boundary invariants - fn geometric_add(&self, other: &Self) -> Self { - // step 1: add full blade counts (preserve semantic meaning) - let total_blade = self.blade + other.blade; - - // step 2: add angle remainders within current π/2 segments - let total_rem = self.rem + other.rem; - - // avoid floating point buggery - let quarter_pi = PI / 2.0; - if total_rem == 0.0 { - // exact case: no boundary crossing + // one zero: result is the other t + if self.t == 0.0 { return Self { blade: total_blade, - rem: 0.0, + t: other.t, }; } - if (total_rem - quarter_pi).abs() < 1e-15 { - // exact case: one boundary crossing + if other.t == 0.0 { return Self { - blade: total_blade + 1, - rem: 0.0, + blade: total_blade, + t: self.t, }; } - // step 3: create angle with combined blade and remainder, then normalize - let combined = Self { - blade: total_blade, - rem: total_rem, - }; + // tangent sum on projection ratios + let n = self.t + other.t; + let d = 1.0 - self.t * other.t; + // d > 0 always for t1, t2 ∈ [0, 1) + + let sum = n / d; - combined.normalize_boundaries() + if (sum - 1.0).abs() < EPSILON { + // exact π/2 boundary + Self { + blade: total_blade + 1, + t: 0.0, + } + } else if sum < 1.0 { + // no crossing + Self { + blade: total_blade, + t: sum, + } + } else { + // crossed — rational correction + let corrected = (sum - 1.0) / (sum + 1.0); + if corrected >= 1.0 - EPSILON { + Self { + blade: total_blade + 2, + t: 0.0, + } + } else { + Self { + blade: total_blade + 1, + t: corrected, + } + } + } } - /// internal geometric subtraction preserving blade progression and π/2 boundary invariants + /// tangent difference on projection ratios with rational borrow + /// + /// R = (t1 - t2) / (1 + t1·t2) + /// denominator always positive + /// + /// R >= 0: no borrow, result = R + /// R < 0: borrow blade, complement = (1 - |R|) / (1 + |R|) fn geometric_sub(&self, other: &Self) -> Self { - // subtract blade counts and angle remainders separately to avoid precision buggery + const EPSILON: f64 = 1e-10; let blade_diff = self.blade as i64 - other.blade as i64; - let rem_diff = self.rem - other.rem; - // avoid floating point buggery with exact cases - if rem_diff.abs() < 1e-15 { - // exact case: remainders are equal, result is just blade difference - let normalized_blade = if blade_diff < 0 { - let four_rotations = ((-blade_diff + 3) / 4) * 4; // round up to multiple of 4 - (blade_diff + four_rotations) as usize - } else { - blade_diff as usize - }; + // equal t: pure blade difference + if (self.t - other.t).abs() < EPSILON { return Self { - blade: normalized_blade, - rem: 0.0, + blade: normalize_blade(blade_diff), + t: 0.0, }; } - // handle negative remainder: borrow from blade count - let (intermediate_blade, intermediate_rem) = if rem_diff < 0.0 { - let quarter_pi = PI / 2.0; - let final_rem = rem_diff + quarter_pi; - let final_blade = blade_diff - 1; - (final_blade, final_rem) - } else { - (blade_diff, rem_diff) - }; + // other t zero: keep self t + if other.t == 0.0 { + return Self { + blade: normalize_blade(blade_diff), + t: self.t, + }; + } - // handle negative blade count: normalize to positive - let normalized_blade = if intermediate_blade < 0 { - let four_rotations = ((-intermediate_blade + 3) / 4) * 4; // round up to multiple of 4 - (intermediate_blade + four_rotations) as usize - } else { - intermediate_blade as usize - }; + // self t zero: borrow + if self.t == 0.0 { + let complement = (1.0 - other.t) / (1.0 + other.t); + return Self { + blade: normalize_blade(blade_diff - 1), + t: complement, + }; + } - // create angle and normalize boundaries - let result = Self { - blade: normalized_blade, - rem: intermediate_rem, - }; + // tangent difference on projection ratios + let n = self.t - other.t; + let d = 1.0 + self.t * other.t; // always positive + let r = n / d; - result.normalize_boundaries() + if r >= -EPSILON { + Self { + blade: normalize_blade(blade_diff), + t: r.max(0.0), + } + } else { + // borrow: complement = (1 - |r|) / (1 + |r|) + let abs_r = r.abs(); + let complement = (1.0 - abs_r) / (1.0 + abs_r); + if complement >= 1.0 - EPSILON { + Self { + blade: normalize_blade(blade_diff), + t: 0.0, + } + } else { + Self { + blade: normalize_blade(blade_diff - 1), + t: complement, + } + } + } } /// tests if this angle is opposite to another angle @@ -335,8 +456,8 @@ impl Angle { /// true if the angles are opposites (π apart) pub fn is_opposite(&self, other: &Angle) -> bool { let blade_diff = (self.blade as i32 - other.blade as i32).abs(); - let rems_match = (self.rem - other.rem).abs() < 1e-15; - blade_diff == 2 && rems_match + let t_match = (self.t - other.t).abs() < 1e-15; + blade_diff == 2 && t_match } /// dual operation that adds π rotation @@ -350,8 +471,10 @@ impl Angle { /// so grade 1000000 in million-D space is just grade 0 (1000000 % 4 = 0) /// eliminating dimension-specific k→(n-k) duality formulas pub fn dual(&self) -> Angle { - // add π rotation (2 blade counts) - *self + Angle::new_with_blade(2, 0.0, 1.0) + Angle { + blade: self.blade + 2, + t: self.t, + } } /// computes the undual operation (inverse of dual) @@ -366,7 +489,10 @@ impl Angle { /// if angle represents e^(iθ), conjugate represents e^(-iθ) /// negation is adding π pub fn conjugate(&self) -> Angle { - *self + Angle::new(1.0, 1.0) + Angle { + blade: self.blade + 2, + t: self.t, + } } /// returns the grade-based angle representation in radians within [0, 2π) @@ -389,7 +515,7 @@ impl Angle { /// # returns /// angle in radians as f64 within [0, 2π) representing the grade-angle mapping pub fn grade_angle(&self) -> f64 { - self.grade() as f64 * PI / 2.0 + self.rem + self.grade() as f64 * PI / 2.0 + 2.0 * self.t.atan() } /// negates this angle by adding π rotation (2 blades) @@ -398,14 +524,48 @@ impl Angle { /// this fundamental operation appears throughout geometry as sign flips, /// vector opposites, and complex conjugation pub fn negate(&self) -> Angle { - *self + Angle::new(1.0, 1.0) // add π (2 blades) + Angle { + blade: self.blade + 2, + t: self.t, + } + } + + /// cos and sin of full angle — rational in t, no sqrt + /// + /// cos(rem) = (1 - t²) / (1 + t²) + /// sin(rem) = 2t / (1 + t²) + /// blade applies sign and axis swap + pub fn cos_sin(&self) -> (f64, f64) { + let t2 = self.t * self.t; + let denom = 1.0 + t2; + let cos_rem = (1.0 - t2) / denom; + let sin_rem = 2.0 * self.t / denom; + + match self.grade() { + 0 => (cos_rem, sin_rem), + 1 => (-sin_rem, cos_rem), + 2 => (-cos_rem, -sin_rem), + 3 => (sin_rem, -cos_rem), + _ => unreachable!(), + } } /// projects this angle onto another angle direction - /// returns the cosine of the angle difference + /// returns the cosine of the angle difference — rational via cos_sin pub fn project(&self, onto: Angle) -> f64 { - let angle_diff = onto - *self; - angle_diff.grade_angle().cos() + let diff = onto - *self; + let (cos_val, _) = diff.cos_sin(); + cos_val + } +} + +/// normalize negative blade to positive by adding full rotations +fn normalize_blade(blade: i64) -> usize { + if blade < 0 { + let four_rotations = ((-blade + 3) / 4) * 4; + (blade + four_rotations) as usize + } else { + blade as usize } } @@ -417,13 +577,13 @@ impl PartialEq for Angle { } // avoid floating point buggery with exact cases - let rem_diff = (self.rem - other.rem).abs(); - if rem_diff < 1e-15 { + let t_diff = (self.t - other.t).abs(); + if t_diff < 1e-15 { return true; // exact match } // for non-exact cases, use standard floating point comparison - self.rem == other.rem + self.t == other.t } } @@ -529,10 +689,27 @@ impl Div for Angle { type Output = Angle; fn div(self, divisor: f64) -> Angle { - // divide both the total angle and reconstruct - let total_angle = (self.blade as f64) * (PI / 2.0) + self.rem; - let divided_angle = total_angle / divisor; - Angle::new(divided_angle, PI) + // 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 + 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 + } else { + divided + }; + let blade = (normalized / quarter_pi) as usize; + let rem = normalized % quarter_pi; + if rem.abs() < 1e-10 { + Angle { blade, t: 0.0 } + } else { + Angle { + blade, + t: (rem / 2.0).tan(), + } + } } } @@ -540,9 +717,7 @@ impl Div for &Angle { type Output = Angle; fn div(self, divisor: f64) -> Angle { - let total_angle = (self.blade as f64) * (PI / 2.0) + self.rem; - let divided_angle = total_angle / divisor; - Angle::new(divided_angle, PI) + (*self) / divisor } } @@ -588,8 +763,8 @@ impl Ord for Angle { fn cmp(&self, other: &Self) -> std::cmp::Ordering { match self.blade.cmp(&other.blade) { std::cmp::Ordering::Equal => { - // only finite rems in Angles, so unwrap is safe - self.rem.partial_cmp(&other.rem).unwrap() + // t is monotonically increasing in [0, 1) + self.t.partial_cmp(&other.t).unwrap() } other => other, } @@ -598,7 +773,7 @@ impl Ord for Angle { impl std::fmt::Display for Angle { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Angle(blade: {}, rem: {:.4})", self.blade, self.rem) + write!(f, "Angle(blade: {}, t: {:.4})", self.blade, self.t) } } @@ -747,47 +922,36 @@ mod tests { } #[test] - fn it_computes_sin_of_1000_blade() { + fn it_computes_cos_sin_of_1000_blade() { let angle = Angle::new(1000.0, 2.0); // 1000*(π/2) - - let sin_result = angle.grade_angle().sin(); + let (cos_result, sin_result) = angle.cos_sin(); // 1000 % 4 = 0, so grade is 0 (scalar) - // sin(0 + 0.0) = sin(0) = 0 + // cos(0) = 1, sin(0) = 0 + assert!((cos_result - 1.0).abs() < EPSILON); assert!((sin_result - 0.0).abs() < EPSILON); } #[test] - fn it_computes_sin_with_1003_blade() { - let angle = Angle::new(1003.0, 2.0); // 1003*(π/2) - - let sin_result = angle.grade_angle().sin(); - - // 1003 % 4 = 3, so grade is 3 (trivector) - // sin(3π/2 + 0.0) = sin(3π/2) = -1 - assert!((sin_result - (-1.0)).abs() < EPSILON); - } - - #[test] - fn it_computes_cos_of_1000_blade() { - let angle = Angle::new(1000.0, 2.0); // 1000*(π/2) - - let cos_result = angle.grade_angle().cos(); + fn it_computes_cos_sin_with_1001_blade() { + let angle = Angle::new(1001.0, 2.0); // 1001*(π/2) + let (cos_result, sin_result) = angle.cos_sin(); - // 1000 % 4 = 0, so grade is 0 (scalar) - // cos(0 + 0.0) = cos(0) = 1 - assert!((cos_result - 1.0).abs() < EPSILON); + // 1001 % 4 = 1, so grade is 1 (vector) + // cos(π/2) = 0, sin(π/2) = 1 + assert!((cos_result - 0.0).abs() < EPSILON); + assert!((sin_result - 1.0).abs() < EPSILON); } #[test] - fn it_computes_cos_with_1001_blade() { - let angle = Angle::new(1001.0, 2.0); // 1001*(π/2) - - let cos_result = angle.grade_angle().cos(); + fn it_computes_cos_sin_with_1003_blade() { + let angle = Angle::new(1003.0, 2.0); // 1003*(π/2) + let (cos_result, sin_result) = angle.cos_sin(); - // 1001 % 4 = 1, so grade is 1 (vector) - // cos(π/2 + 0.0) = cos(π/2) = 0 + // 1003 % 4 = 3, so grade is 3 (trivector) + // cos(3π/2) = 0, sin(3π/2) = -1 assert!((cos_result - 0.0).abs() < EPSILON); + assert!((sin_result - (-1.0)).abs() < EPSILON); } #[test] @@ -814,25 +978,22 @@ mod tests { } #[test] - fn it_computes_tan_of_1000_blade() { - let angle = Angle::new(1000.0, 2.0); // 1000*(π/2) - - let tan_result = angle.grade_angle().tan(); - - // 1000 % 4 = 0, so grade is 0 (scalar) - // tan(0 + 0.0) = tan(0) = 0 - assert!((tan_result - 0.0).abs() < EPSILON); - } - - #[test] - fn it_computes_tan_with_1002_blade() { - let angle = Angle::new(1002.0, 2.0); // 1002*(π/2) - - let tan_result = angle.grade_angle().tan(); + fn it_computes_cos_sin_at_lattice_points() { + // at lattice points (t = 0), cos_sin reduces to blade lookup + // blade 0: (1, 0), blade 1: (0, 1), blade 2: (-1, 0), blade 3: (0, -1) + let lattice = [ + (0.0, 1.0, (1.0, 0.0)), + (1.0, 2.0, (0.0, 1.0)), + (1.0, 1.0, (-1.0, 0.0)), + (3.0, 2.0, (0.0, -1.0)), + ]; - // 1002 % 4 = 2, so grade is 2 (bivector) - // tan(π + 0.0) = tan(π) = 0 - assert!((tan_result - 0.0).abs() < EPSILON); + for (pi_r, div, (expected_cos, expected_sin)) in lattice { + let angle = Angle::new(pi_r, div); + let (c, s) = angle.cos_sin(); + assert!((c - expected_cos).abs() < EPSILON); + assert!((s - expected_sin).abs() < EPSILON); + } } #[test] @@ -1361,64 +1522,20 @@ mod tests { } #[test] - fn it_normalizes_angles_exceeding_pi_2_to_next_blade() { - // geometric_sub now normalizes rems >= π/2 to next blade - - let angle1 = Angle::new(0.0, 1.0); // 0 radians - let angle2 = Angle::new(1.0, 2.0); // π/2 radians (already normalized to blade 1) + fn it_handles_boundary_via_tangent_formula() { + // boundary logic is now algebraic in the tangent sum formula + // normalize_boundaries is eliminated + let angle1 = Angle::new(1.0, 3.0); // π/3 + let angle2 = Angle::new(1.0, 6.0); // π/6 + let sum = angle1 + angle2; // π/3 + π/6 = π/2 → exact boundary - let diff = angle2 - angle1; + assert_eq!(sum.blade(), 1); + assert!(sum.rem().abs() < EPSILON); - // π/2 difference is blade 1, rem 0 - assert_eq!(diff.blade(), 1, "π/2 difference is blade 1"); - assert!(diff.rem().abs() < 1e-10, "blade 1 has rem ~0"); - } - - #[test] - fn test_normalize_boundaries() { - use std::f64::consts::PI; - - // test remainder very close to π/2 normalizes to next blade - let angle_near_pi_2 = Angle { - blade: 2, - rem: PI / 2.0 - 1e-11, - }; - let normalized = angle_near_pi_2.normalize_boundaries(); - assert_eq!(normalized.blade(), 3); - assert_eq!(normalized.rem(), 0.0); - - // test remainder exactly π/2 normalizes to next blade - let angle_at_pi_2 = Angle { - blade: 1, - rem: PI / 2.0, - }; - let normalized = angle_at_pi_2.normalize_boundaries(); - assert_eq!(normalized.blade(), 2); - assert_eq!(normalized.rem(), 0.0); - - // test remainder > π/2 normalizes blade and reduces remainder - let angle_over_pi_2 = Angle { - blade: 0, - rem: PI * 0.75, // 3π/4 - }; - let normalized = angle_over_pi_2.normalize_boundaries(); - assert_eq!(normalized.blade(), 1); - assert!((normalized.rem() - PI / 4.0).abs() < 1e-10); - - // test small positive remainder stays unchanged - let angle_small = Angle { blade: 5, rem: 0.1 }; - let normalized = angle_small.normalize_boundaries(); - assert_eq!(normalized.blade(), 5); - assert_eq!(normalized.rem(), 0.1); - - // test remainder that's 2π normalizes to blade 4 remainder 0 - let angle_2pi = Angle { - blade: 0, - rem: 2.0 * PI, - }; - let normalized = angle_2pi.normalize_boundaries(); - assert_eq!(normalized.blade(), 4); - assert_eq!(normalized.rem(), 0.0); + // near-boundary + let near_pi_2 = Angle::new(0.99, 2.0); // just under π/2 + assert_eq!(near_pi_2.blade(), 0); + assert!(near_pi_2.t() < 1.0); } #[test] @@ -1453,4 +1570,267 @@ mod tests { assert!((wrapped_diff.rem() - PI / 8.0).abs() < 1e-10); assert!((wrapped_diff.grade_angle() - 1.9634954084936207).abs() < 1e-10); } + + // ═══════════════════════════════════════════════════════════════════════ + // projection ratio (t) properties + // ═══════════════════════════════════════════════════════════════════════ + + #[test] + fn it_constructs_lattice_points_with_t_zero() { + let a0 = Angle::new(0.0, 1.0); + assert_eq!(a0.blade(), 0); + assert_eq!(a0.t(), 0.0); + + let a1 = Angle::new(1.0, 2.0); + assert_eq!(a1.blade(), 1); + assert_eq!(a1.t(), 0.0); + + let a2 = Angle::new(1.0, 1.0); + assert_eq!(a2.blade(), 2); + assert_eq!(a2.t(), 0.0); + + let a3 = Angle::new(3.0, 2.0); + assert_eq!(a3.blade(), 3); + assert_eq!(a3.t(), 0.0); + } + + #[test] + fn it_constructs_common_angles_with_exact_t() { + // π/6: t = tan(π/12) = 2 - √3 + let a = Angle::new(1.0, 6.0); + assert_eq!(a.blade(), 0); + assert!((a.t() - (2.0 - 3.0_f64.sqrt())).abs() < EPSILON); + + // π/4: t = tan(π/8) + let b = Angle::new(1.0, 4.0); + assert_eq!(b.blade(), 0); + assert!((b.t() - (PI / 8.0).tan()).abs() < EPSILON); + + // π/3: t = tan(π/6) = 1/√3 + let c = Angle::new(1.0, 3.0); + assert_eq!(c.blade(), 0); + assert!((c.t() - 1.0 / 3.0_f64.sqrt()).abs() < EPSILON); + } + + #[test] + fn it_bounds_t_in_zero_to_one() { + // t = tan(rem/2) where rem ∈ [0, π/2) + // so rem/2 ∈ [0, π/4) and t ∈ [0, 1) + let angles = [ + (1.0, 6.0), + (1.0, 4.0), + (1.0, 3.0), + (2.0, 5.0), + (3.0, 8.0), + (7.0, 16.0), + ]; + + for (pi_r, div) in angles { + let a = Angle::new(pi_r, div); + assert!( + a.t() >= 0.0 && a.t() < 1.0, + "t={} out of [0,1) for {}π/{}", + a.t(), + pi_r, + div + ); + } + } + + #[test] + fn it_projects_rationally() { + // cos_sin uses (1-t²)/(1+t²) and 2t/(1+t²) — no sqrt + let cases = [ + (1.0, 6.0), + (1.0, 4.0), + (1.0, 3.0), + (2.0, 3.0), + (5.0, 4.0), + (5.0, 3.0), + ]; + + for (pi_r, div) in cases { + let a = Angle::new(pi_r, div); + let (cos_val, sin_val) = a.cos_sin(); + let rad = pi_r * PI / div; + assert!( + (cos_val - rad.cos()).abs() < EPSILON, + "cos mismatch for {}π/{}", + pi_r, + div + ); + assert!( + (sin_val - rad.sin()).abs() < EPSILON, + "sin mismatch for {}π/{}", + pi_r, + div + ); + } + } + + #[test] + fn it_adds_without_crossing() { + // π/8 + π/8 = π/4 (t < 1, no crossing) + let a = Angle::new(1.0, 8.0); + let sum = a + a; + assert_eq!(sum.blade(), 0); + let expected_t = (PI / 8.0).tan(); // tan(π/4 / 2) = tan(π/8) + assert!((sum.t() - expected_t).abs() < EPSILON); + } + + #[test] + fn it_adds_at_exact_boundary() { + // π/4 + π/4 = π/2 → exact boundary + let a = Angle::new(1.0, 4.0); + let sum = a + a; + assert_eq!(sum.blade(), 1); + assert!(sum.t() < EPSILON); + } + + #[test] + fn it_adds_across_boundary_with_rational_correction() { + // π/3 + π/4 = 7π/12 → crosses π/2 + let a = Angle::new(1.0, 3.0); + let b = Angle::new(1.0, 4.0); + let sum = a + b; + + assert_eq!(sum.blade(), 1); + // remainder = 7π/12 - π/2 = π/12 + // t = tan(π/24) + let expected_t = (PI / 24.0).tan(); + assert!( + (sum.t() - expected_t).abs() < EPSILON, + "t after crossing: {} vs {}", + sum.t(), + expected_t + ); + } + + #[test] + fn it_subtracts_with_rational_borrow() { + // π/3 - π/6 = π/6 (no borrow) + let diff1 = Angle::new(1.0, 3.0) - Angle::new(1.0, 6.0); + assert_eq!(diff1.blade(), 0); + let expected_t = (PI / 12.0).tan(); // tan(π/6 / 2) + assert!((diff1.t() - expected_t).abs() < EPSILON); + + // π/6 - π/3 → borrows + let diff2 = Angle::new(1.0, 6.0) - Angle::new(1.0, 3.0); + assert_eq!(diff2.blade(), 3); // borrowed: 0-1 → 3 (mod 4) + } + + #[test] + fn it_adds_with_always_positive_denominator() { + // for t1, t2 ∈ [0, 1): t1·t2 < 1, so 1 - t1·t2 > 0 + // no sign branching needed — t1·t2 < 1 when both < 1 + let angles = [ + (1.0, 6.0), + (1.0, 4.0), + (1.0, 3.0), + (2.0, 5.0), + (3.0, 7.0), + (3.0, 8.0), + ]; + + for &(p1, d1) in &angles { + for &(p2, d2) in &angles { + let a = Angle::new(p1, d1); + let b = Angle::new(p2, d2); + let d = 1.0 - a.t() * b.t(); + assert!( + d > 0.0, + "denominator not positive for {}π/{} + {}π/{}: d={}", + p1, + d1, + p2, + d2, + d + ); + } + } + } + + #[test] + fn it_dualizes_and_negates_with_blade_only() { + let a = Angle::new(1.0, 4.0); + assert_eq!(a.dual().t(), a.t()); + assert_eq!(a.dual().blade() - a.blade(), 2); + assert_eq!(a.negate().t(), a.t()); + assert_eq!(a.negate().blade() - a.blade(), 2); + } + + #[test] + fn it_multiplies_via_tangent_sum() { + let a = Angle::new(1.0, 4.0); + let b = Angle::new(1.0, 6.0); + let product = a * b; + + // π/4 + π/6 = 5π/12, t = tan(5π/24) + let expected_t = (5.0 * PI / 24.0).tan(); + assert_eq!(product.blade(), 0); + assert!((product.t() - expected_t).abs() < EPSILON); + } + + // ═══════════════════════════════════════════════════════════════════════ + // near methods + // ═══════════════════════════════════════════════════════════════════════ + + #[test] + fn it_detects_near_angles() { + let a = Angle::new(1.0, 4.0); // π/4 + let b = Angle::new(1.0, 4.0); // same + + assert!(a.near(&b)); + assert!(b.near(&a)); + + // different blade, same t → not near + let c = Angle::new_with_blade(4, 1.0, 4.0); // blade 4, same t + assert!(!a.near(&c)); + + // same blade, different t → not near + let d = Angle::new(1.0, 3.0); // π/3, blade 0 but different t + assert!(!a.near(&d)); + + // near within tolerance + let tiny = Angle::from_parts(0, a.t() + 1e-12); + assert!(a.near(&tiny)); + + // outside tolerance + let far = Angle::from_parts(0, a.t() + 1e-8); + assert!(!a.near(&far)); + } + + #[test] + fn it_compares_near_rad() { + // grade 0: grade_angle = 0 + rem + let a = Angle::new(1.0, 4.0); // π/4 + assert!(a.near_rad(PI / 4.0)); + assert!(!a.near_rad(PI / 3.0)); + + // grade 1: grade_angle = π/2 + rem + let b = Angle::new(3.0, 4.0); // 3π/4, blade 1 + assert!(b.near_rad(3.0 * PI / 4.0)); + assert!(!b.near_rad(PI / 4.0)); // thats the rem, not the grade_angle + + // lattice point + let c = Angle::new(1.0, 2.0); // π/2 + assert!(c.near_rad(PI / 2.0)); + } + + #[test] + fn it_compares_near_rem() { + // rem is the within-quadrant remainder + let a = Angle::new(1.0, 4.0); // π/4, blade 0, rem ≈ π/4 + assert!(a.near_rem(PI / 4.0)); + assert!(!a.near_rem(PI / 3.0)); + + // blade 1 angle: grade_angle = π/2 + rem, but near_rem checks rem only + let b = Angle::new(3.0, 4.0); // 3π/4, blade 1, rem ≈ π/4 + assert!(b.near_rem(PI / 4.0)); // rem is π/4 + assert!(!b.near_rem(3.0 * PI / 4.0)); // thats grade_angle, not rem + + // lattice: rem = 0 + let c = Angle::new(1.0, 2.0); // π/2 + assert!(c.near_rem(0.0)); + } } diff --git a/src/geonum_mod.rs b/src/geonum_mod.rs index a0f230b..1de5b9e 100644 --- a/src/geonum_mod.rs +++ b/src/geonum_mod.rs @@ -280,7 +280,7 @@ impl Geonum { /// the dot product as a scalar geometric number pub fn dot(&self, other: &Geonum) -> Geonum { let angle_diff = other.angle - self.angle; - let cos_component = angle_diff.grade_angle().cos(); + let (cos_component, _) = angle_diff.cos_sin(); let scalar_value = self.mag * other.mag * cos_component; // encode sign in angle so consumers read grade based polarity instead of raw negatives Self::signed_at(scalar_value, Angle::new(0.0, 1.0)) @@ -309,7 +309,7 @@ impl Geonum { /// the wedge product as a new geometric number pub fn wedge(&self, other: &Geonum) -> Geonum { let angle_diff = other.angle - self.angle; - let sin_value = angle_diff.grade_angle().sin(); + let (_, sin_value) = angle_diff.cos_sin(); let mag = self.mag * other.mag * sin_value.abs(); let quarter_turn = Angle::new(1.0, 2.0); // π/2 // wedge product creates bivector (oriented area) by adding π/2 to combined angles @@ -332,8 +332,25 @@ impl Geonum { /// # returns /// the geometric product as a single geometric number pub fn geo(&self, other: &Geonum) -> Geonum { - let dot_part = self.dot(other); - let wedge_part = self.wedge(other); + // single cos_sin call for both dot and wedge + let angle_diff = other.angle - self.angle; + let (cos_diff, sin_diff) = angle_diff.cos_sin(); + + // dot: |a|·|b|·cos(Δθ) + let dot_value = self.mag * other.mag * cos_diff; + let dot_part = Self::signed_at(dot_value, Angle::new(0.0, 1.0)); + + // wedge: |a|·|b|·|sin(Δθ)| with orientation + let wedge_mag = self.mag * other.mag * sin_diff.abs(); + let quarter_turn = Angle::new(1.0, 2.0); + let mut wedge_angle = self.angle + other.angle + quarter_turn; + if sin_diff < 0.0 { + wedge_angle = wedge_angle + Angle::new(1.0, 1.0); + } + let wedge_part = Geonum { + mag: wedge_mag, + angle: wedge_angle, + }; dot_part + wedge_part } @@ -495,6 +512,16 @@ impl Geonum { /// let diff = a.mag_diff(&b); /// assert_eq!(diff, 1.0); /// ``` + /// tests if two geonums are within floating point tolerance + pub fn near(&self, other: &Geonum) -> bool { + (self.mag - other.mag).abs() < EPSILON && self.angle.near(&other.angle) + } + + /// tests if magnitude is within tolerance of a scalar + pub fn near_mag(&self, value: f64) -> bool { + (self.mag - value).abs() < EPSILON + } + pub fn mag_diff(&self, other: &Geonum) -> f64 { (self.mag - other.mag).abs() } @@ -644,7 +671,7 @@ impl Geonum { // law of cosines: c² = a² + b² - 2ab·cos(θ) let angle_between = other.angle - self.angle; let distance_squared = self.mag * self.mag + other.mag * other.mag - - 2.0 * self.mag * other.mag * angle_between.grade_angle().cos(); + - 2.0 * self.mag * other.mag * angle_between.cos_sin().0; let distance = distance_squared.sqrt(); // return as scalar geonum (blade 0) @@ -674,15 +701,15 @@ impl Geonum { /// geonum cosine anchored to even pair 0↔π /// magnitude = |cos(θ)|, sign becomes +π rotation in angle pub fn cos(a: Angle) -> Geonum { - let v = a.grade_angle().cos(); - Geonum::signed_at(v, Angle::new(0.0, 1.0)) + let (c, _) = a.cos_sin(); + Geonum::signed_at(c, Angle::new(0.0, 1.0)) } /// geonum sine anchored to odd pair π/2↔3π/2 /// magnitude = |sin(θ)|, sign becomes +π rotation in angle pub fn sin(a: Angle) -> Geonum { - let v = a.grade_angle().sin(); - Geonum::signed_at(v, Angle::new(1.0, 2.0)) + let (_, s) = a.cos_sin(); + Geonum::signed_at(s, Angle::new(1.0, 2.0)) } /// geonum tangent defined via sin/cos division so diameter crossings propagate @@ -696,7 +723,7 @@ impl Geonum { /// replaces scalar projection with geonum that preserves geometric meaning pub fn project_to_angle(&self, onto: Angle) -> Geonum { let angle_diff = onto - self.angle; - let cos_component = angle_diff.grade_angle().cos(); + let (cos_component, _) = angle_diff.cos_sin(); // encode sign in grade: positive at grade 0, negative at grade 2 if cos_component >= 0.0 { @@ -749,34 +776,29 @@ impl Add for Geonum { } } - // general case: cosine interference between rotations at different angles - // follows cosine rule: c² = a² + b² + 2ab*cos(θ) where θ is angle difference - let angle1 = self.angle.grade_angle(); - let angle2 = other.angle.grade_angle(); + // general case: rational projection via cos_sin (0 sqrts) + let (c1, s1) = self.angle.cos_sin(); + let (c2, s2) = other.angle.cos_sin(); - // compute result angle from opposite and adjacent projections - let opp_sum = self.mag * angle1.sin() + other.mag * angle2.sin(); - let adj_sum = self.mag * angle1.cos() + other.mag * angle2.cos(); - let result_angle = opp_sum.atan2(adj_sum); + let adj = self.mag * c1 + other.mag * c2; + let opp = self.mag * s1 + other.mag * s2; - // compute result magnitude using cosine rule for rotation interference - let angle_diff = angle2 - angle1; - let result_mag = - (self.mag.powi(2) + other.mag.powi(2) + 2.0 * self.mag * other.mag * angle_diff.cos()) - .sqrt(); + // magnitude: 1 sqrt (the only one) + let result_mag = (adj * adj + opp * opp).sqrt(); - // combine transformation histories + // result angle from cartesian — no atan2 let combined_blade_count = self.angle.blade() + other.angle.blade(); + let result_angle = Angle::new_from_cartesian(adj, opp); - // preserve accumulated transformations by setting the combined blade count - let blade_shift = (combined_blade_count as f64) * std::f64::consts::PI / 2.0; - let adjusted_angle = result_angle - blade_shift; - Self::new_with_blade( - result_mag, - combined_blade_count, - adjusted_angle, - std::f64::consts::PI, - ) + // grade comes from the geometric result, blade history from the inputs + // round combined up so grade matches the natural result + let natural_grade = result_angle.grade(); + let combined_grade = combined_blade_count % 4; + let grade_adjust = (natural_grade + 4 - combined_grade) % 4; + Self { + mag: result_mag, + angle: Angle::from_parts(combined_blade_count + grade_adjust, result_angle.t()), + } } } @@ -3177,4 +3199,30 @@ mod tests { assert!((opp.mag - opp_gateway.mag).abs() < EPSILON); assert_eq!(opp.angle, opp_gateway.angle); } + + #[test] + fn it_detects_near_geonums() { + let a = Geonum::new(3.0, 1.0, 4.0); + let b = Geonum::new(3.0, 1.0, 4.0); + + assert!(a.near(&b)); + + // different mag → not near + let c = Geonum::new(3.1, 1.0, 4.0); + assert!(!a.near(&c)); + + // different angle → not near + let d = Geonum::new(3.0, 1.0, 3.0); + assert!(!a.near(&d)); + } + + #[test] + fn it_compares_near_mag() { + let a = Geonum::new(5.0, 1.0, 4.0); + + assert!(a.near_mag(5.0)); + assert!(!a.near_mag(5.1)); + assert!(a.near_mag(5.0 + 1e-12)); // within tolerance + assert!(!a.near_mag(5.0 + 1e-8)); // outside tolerance + } } diff --git a/tests/addition_test.rs b/tests/addition_test.rs index 64e9da0..641e7c0 100644 --- a/tests/addition_test.rs +++ b/tests/addition_test.rs @@ -11,7 +11,7 @@ fn it_adds_aligned_vectors() { let sum = a + b; - assert!((sum.mag - 5.0).abs() < EPSILON); + assert!(sum.near_mag(5.0)); assert_eq!(sum.angle, Angle::new(0.0, 1.0)); } @@ -23,7 +23,7 @@ fn it_handles_opposite_vectors() { let sum = a + b; - assert!((sum.mag - 2.0).abs() < EPSILON); + assert!(sum.near_mag(2.0)); assert_eq!(sum.angle, a.angle); } @@ -35,9 +35,9 @@ fn it_adds_orthogonal_vectors() { let sum = a + b; - assert!((sum.mag - 5.0).abs() < EPSILON); + assert!(sum.near_mag(5.0)); let expected_phase = (4.0_f64).atan2(3.0); - assert!((sum.angle.grade_angle() - expected_phase).abs() < EPSILON); + assert!(sum.angle.near_rad(expected_phase)); // history policy is preserved internally; direction and length are primary here } @@ -56,7 +56,7 @@ fn it_adds_high_blades_and_preserves_history() { let x2 = b.mag * b.angle.grade_angle().cos(); let y2 = b.mag * b.angle.grade_angle().sin(); let expected_phase = (y1 + y2).atan2(x1 + x2); - assert!((sum.angle.grade_angle() - expected_phase).abs() < EPSILON); + assert!(sum.angle.near_rad(expected_phase)); } #[test] @@ -79,7 +79,7 @@ fn it_matches_projection_based_sum() { // compare to direct addition let direct = a + b; - assert!((result.mag - direct.mag).abs() < EPSILON); + assert!(result.near_mag(direct.mag)); assert_eq!(result.angle.base_angle(), direct.angle.base_angle()); // pythagorean identity from projections @@ -97,7 +97,7 @@ fn it_preserves_blade_history_on_cancellation() { let sum = a + b; - assert!((sum.mag - 0.0).abs() < EPSILON); + assert!(sum.near_mag(0.0)); // a has blade 7, b has blade 3 + 2 from π = blade 5 // combined blade count = 7 + 5 = 12 let expected = Angle::new(0.0, 1.0) // base angle at 0 @@ -122,7 +122,7 @@ fn it_accumulates_blades_in_general_case() { let x2 = b.mag * b.angle.grade_angle().cos(); let y2 = b.mag * b.angle.grade_angle().sin(); let expected_mag = ((x1 + x2).powi(2) + (y1 + y2).powi(2)).sqrt(); - assert!((sum.mag - expected_mag).abs() < EPSILON); + assert!(sum.near_mag(expected_mag)); // a has blade 5 + π/6, b has blade 8 + π/4 // cartesian result creates its own angle with blades @@ -155,11 +155,11 @@ fn it_handles_zero_length_addition() { let a = Geonum::new(5.0, 1.0, 3.0); let zero_plus_a = zero + a; - assert!((zero_plus_a.mag - a.mag).abs() < EPSILON); + assert!(zero_plus_a.near_mag(a.mag)); assert_eq!(zero_plus_a.angle.base_angle(), a.angle.base_angle()); let a_plus_zero = a + zero; - assert!((a_plus_zero.mag - a.mag).abs() < EPSILON); + assert!(a_plus_zero.near_mag(a.mag)); assert_eq!(a_plus_zero.angle.base_angle(), a.angle.base_angle()); } @@ -173,7 +173,7 @@ fn it_maintains_commutative_blade_accumulation() { let ba = b + a; // geometric result is same - assert!((ab.mag - ba.mag).abs() < EPSILON); + assert!(ab.near_mag(ba.mag)); assert_eq!(ab.angle.base_angle(), ba.angle.base_angle()); // blade accumulation is commutative diff --git a/tests/affine_test.rs b/tests/affine_test.rs index b9f9720..ab0a7c8 100644 --- a/tests/affine_test.rs +++ b/tests/affine_test.rs @@ -41,14 +41,14 @@ fn its_a_translation() { let expected_angle = Angle::new_from_cartesian(expected_x, expected_y); // test geometric properties are preserved - assert!((translated_point.mag - expected_length).abs() < EPSILON); + assert!(translated_point.near_mag(expected_length)); assert!((translated_point.angle - expected_angle).rem() < EPSILON); // test that translation is reversible through inverse displacement let inverse_translation = translation_vector.negate(); // opposite direction let back_to_original = translated_point.translate(&inverse_translation); - assert!((back_to_original.mag - point.mag).abs() < EPSILON); + assert!(back_to_original.near_mag(point.mag)); assert!((back_to_original.angle - point.angle).rem() < EPSILON); // geometric numbers avoid the matrix overhead: @@ -104,10 +104,10 @@ fn it_preserves_parallel_lines_after_shearing() { assert!((sheared_line2_p2.angle - (line2_p2.angle + shear_angle)).rem() < EPSILON); // test that lengths are preserved during shear (fundamental property) - assert!((sheared_line1_p1.mag - line1_p1.mag).abs() < EPSILON); - assert!((sheared_line1_p2.mag - line1_p2.mag).abs() < EPSILON); - assert!((sheared_line2_p1.mag - line2_p1.mag).abs() < EPSILON); - assert!((sheared_line2_p2.mag - line2_p2.mag).abs() < EPSILON); + assert!(sheared_line1_p1.near_mag(line1_p1.mag)); + assert!(sheared_line1_p2.near_mag(line1_p2.mag)); + assert!(sheared_line2_p1.near_mag(line2_p1.mag)); + assert!(sheared_line2_p2.near_mag(line2_p2.mag)); // geometric numbers make affine properties explicit: // parallelism is preserved through consistent angle transformation @@ -155,10 +155,10 @@ fn it_preserves_area_after_shearing() { assert!((sheared_area - 12.0).abs() < EPSILON); // test that individual lengths are preserved (fundamental property of our shear) - assert!((sheared_v1.mag - v1.mag).abs() < EPSILON); - assert!((sheared_v2.mag - v2.mag).abs() < EPSILON); - assert!((sheared_v3.mag - v3.mag).abs() < EPSILON); - assert!((sheared_v4.mag - v4.mag).abs() < EPSILON); + assert!(sheared_v1.near_mag(v1.mag)); + assert!(sheared_v2.near_mag(v2.mag)); + assert!(sheared_v3.near_mag(v3.mag)); + assert!(sheared_v4.near_mag(v4.mag)); // test that angles are consistently shifted assert!((sheared_v2.angle - (v2.angle + shear_angle)).rem() < EPSILON); @@ -179,7 +179,7 @@ fn it_increases_angle_after_shearing() { let sheared_point = point.shear(shear_angle); // length remains unchanged - assert!((sheared_point.mag - point.mag).abs() < EPSILON); + assert!(sheared_point.near_mag(point.mag)); // angle is increased by shear_angle assert!((sheared_point.angle - (point.angle + shear_angle)).rem() < EPSILON); diff --git a/tests/algorithms_test.rs b/tests/algorithms_test.rs index 84705e8..45abc00 100644 --- a/tests/algorithms_test.rs +++ b/tests/algorithms_test.rs @@ -398,7 +398,7 @@ fn its_a_parallel_algorithm() { // test orthogonality // dot product is zero for perpendicular operations - assert!(sequential.dot(¶llel).mag.abs() < EPSILON); + assert!(sequential.dot(¶llel).near_mag(0.0)); // concurrent execution represented by simultaneous operations // wedge product represents "computational area" covered by parallel execution @@ -1213,7 +1213,7 @@ fn it_rejects_complexity_analysis() { assert_eq!(c_op.mag, 1.0); // constant stays at 1 assert_eq!(l_op.mag, n as f64); // linear scales with n assert_eq!(q_op.mag, (n * n) as f64); // quadratic scales with n² - assert!((log_op.mag - (n as f64).log2()).abs() < EPSILON); // logarithmic scales with log n + assert!(log_op.near_mag((n as f64).log2())); // logarithmic scales with log n // verify operation types (angles) let zero_angle = Angle::new(0.0, 1.0); diff --git a/tests/angle_arithmetic_test.rs b/tests/angle_arithmetic_test.rs index 4e6d26a..7e664af 100644 --- a/tests/angle_arithmetic_test.rs +++ b/tests/angle_arithmetic_test.rs @@ -33,21 +33,21 @@ fn it_decomposes_angles_into_blade_and_value() { // 3π/4 = 1 complete π/2 rotation + π/4 remainder let three_quarters = Angle::new(3.0, 4.0); // 3 * π/4 assert_eq!(three_quarters.blade(), 1); // 1 complete π/2 rotation - assert!((three_quarters.rem() - PI / 4.0).abs() < EPSILON); // π/4 remainder - // blade arithmetic: 3π/4 = 1*(π/2) + π/4 + assert!(three_quarters.near_rem(PI / 4.0)); // π/4 remainder + // blade arithmetic: 3π/4 = 1*(π/2) + π/4 // 5π/2 = 5 complete π/2 rotations + 0 remainder let five_halves = Angle::new(5.0, 2.0); // 5 * π/2 assert_eq!(five_halves.blade(), 5); // 5 complete π/2 rotations - assert!(five_halves.rem().abs() < EPSILON); // no remainder - // blade arithmetic: 5π/2 = 5*(π/2) + 0 + assert!(five_halves.near_rem(0.0)); // no remainder + // blade arithmetic: 5π/2 = 5*(π/2) + 0 // 7π/3 = 4 complete π/2 rotations + π/6 remainder let seven_thirds = Angle::new(7.0, 3.0); // 7 * π/3 = 7π/3 // 7π/3 = 14π/6 = (12π + 2π)/6 = 2π + π/3 = 4*(π/2) + π/3 assert_eq!(seven_thirds.blade(), 4); // 4 complete π/2 rotations - assert!((seven_thirds.rem() - PI / 3.0).abs() < EPSILON); // π/3 remainder - // blade arithmetic: 7π/3 = 4*(π/2) + π/3 + assert!(seven_thirds.near_rem(PI / 3.0)); // π/3 remainder + // blade arithmetic: 7π/3 = 4*(π/2) + π/3 // proves decomposition always maintains: angle = blade*(π/2) + value // with value strictly bounded to [0, π/2) through boundary normalization @@ -61,20 +61,20 @@ fn it_crosses_pi_2_boundaries() { // case 1: angle exactly at π/2 boundary let at_boundary = Angle::new(1.0, 2.0); // 1 * π/2 = π/2 assert_eq!(at_boundary.blade(), 1); // crosses boundary: blade 0 → blade 1 - assert!(at_boundary.rem().abs() < EPSILON); // value resets to 0 - // blade arithmetic: π/2 = 1*(π/2) + 0 + assert!(at_boundary.near_rem(0.0)); // value resets to 0 + // blade arithmetic: π/2 = 1*(π/2) + 0 // case 2: angle exceeding π/2 boundary let over_boundary = Angle::new(3.0, 4.0); // 3 * π/4 = π/2 + π/4 assert_eq!(over_boundary.blade(), 1); // crosses boundary once - assert!((over_boundary.rem() - PI / 4.0).abs() < EPSILON); // π/4 remainder - // blade arithmetic: 3π/4 = 1*(π/2) + π/4 + assert!(over_boundary.near_rem(PI / 4.0)); // π/4 remainder + // blade arithmetic: 3π/4 = 1*(π/2) + π/4 // case 3: angle crossing multiple boundaries let multiple_cross = Angle::new(7.0, 4.0); // 7 * π/4 = π/2 + π + π/4 assert_eq!(multiple_cross.blade(), 3); // crosses 3 boundaries: 7π/4 = 3*(π/2) + π/4 - assert!((multiple_cross.rem() - PI / 4.0).abs() < EPSILON); // π/4 final remainder - // blade arithmetic: 7π/4 = 3*(π/2) + π/4 + assert!(multiple_cross.near_rem(PI / 4.0)); // π/4 final remainder + // blade arithmetic: 7π/4 = 3*(π/2) + π/4 // proves boundary crossing increments blade count for each π/2 crossed // while maintaining value in [0, π/2) invariant @@ -90,24 +90,24 @@ fn it_adds_angles_with_blade_arithmetic() { let angle2 = Angle::new(1.0, 6.0); // π/6 → blade=0, value=π/6 let sum = angle1 + angle2; // π/8 + π/6 = 7π/24 assert_eq!(sum.blade(), 0); // 0 + 0 = 0 blades - assert!((sum.rem() - 7.0 * PI / 24.0).abs() < EPSILON); // 7π/24 < π/2, no crossing - // blade arithmetic: blade₁ + blade₂ = 0 + 0 = 0, value₁ + value₂ = 7π/24 + assert!(sum.near_rem(7.0 * PI / 24.0)); // 7π/24 < π/2, no crossing + // blade arithmetic: blade₁ + blade₂ = 0 + 0 = 0, value₁ + value₂ = 7π/24 // case 2: values sum crossing π/2 boundary let angle3 = Angle::new(1.0, 3.0); // π/3 → blade=0, value=π/3 let angle4 = Angle::new(1.0, 4.0); // π/4 → blade=0, value=π/4 let boundary_sum = angle3 + angle4; // π/3 + π/4 = 7π/12 > π/2 assert_eq!(boundary_sum.blade(), 1); // boundary crossing increments: 0 + 0 + 1 = 1 - assert!((boundary_sum.rem() - (7.0 * PI / 12.0 - PI / 2.0)).abs() < EPSILON); // remainder after crossing - // blade arithmetic: blade₁ + blade₂ + 1 = 0 + 0 + 1 = 1, value = 7π/12 - π/2 = π/12 + assert!(boundary_sum.near_rem(7.0 * PI / 12.0 - PI / 2.0)); // remainder after crossing + // blade arithmetic: blade₁ + blade₂ + 1 = 0 + 0 + 1 = 1, value = 7π/12 - π/2 = π/12 // case 3: blades sum with value boundary crossing let angle5 = Angle::new(3.0, 4.0); // 3π/4 → blade=1, value=π/4 let angle6 = Angle::new(5.0, 4.0); // 5π/4 → blade=2, value=π/4 let blade_value_sum = angle5 + angle6; // blades: 1+2=3, values: π/4+π/4=π/2 assert_eq!(blade_value_sum.blade(), 4); // 3 + 1(crossing) = 4 total blades - assert!(blade_value_sum.rem().abs() < EPSILON); // π/2 crossing resets value to 0 - // blade arithmetic: blade₁ + blade₂ + crossing = 1 + 2 + 1 = 4, value = π/2 → 0 + assert!(blade_value_sum.near_rem(0.0)); // π/2 crossing resets value to 0 + // blade arithmetic: blade₁ + blade₂ + crossing = 1 + 2 + 1 = 4, value = π/2 → 0 // proves angle addition accumulates blades while handling π/2 overflow } @@ -123,8 +123,8 @@ fn it_handles_new_with_blade_construction() { // step 2: blade_increment = 2 * π/2 = π → blade=2, value=0 // step 3: addition = (0+2, π/4+0) = blade=2, value=π/4 assert_eq!(with_blade.blade(), 2); // 0 + 2 = 2 blades - assert!((with_blade.rem() - PI / 4.0).abs() < EPSILON); // π/4 preserved - // blade arithmetic: 2*(π/2) + π/4 = π + π/4 = 2*(π/2) + π/4 + assert!(with_blade.near_rem(PI / 4.0)); // π/4 preserved + // blade arithmetic: 2*(π/2) + π/4 = π + π/4 = 2*(π/2) + π/4 // case 2: add blades causing boundary crossing let crossing_blade = Angle::new_with_blade(1, 3.0, 4.0); // 1 blade + 3π/4 @@ -132,8 +132,8 @@ fn it_handles_new_with_blade_construction() { // step 2: blade_increment = 1 * π/2 → blade=1, value=0 // step 3: addition = (1+1, π/4+0) = blade=2, value=π/4 assert_eq!(crossing_blade.blade(), 2); // 1 + 1 = 2 blades - assert!((crossing_blade.rem() - PI / 4.0).abs() < EPSILON); // π/4 from base angle - // blade arithmetic: 1*(π/2) + 3π/4 = π/2 + (π/2 + π/4) = 2*(π/2) + π/4 + assert!(crossing_blade.near_rem(PI / 4.0)); // π/4 from base angle + // blade arithmetic: 1*(π/2) + 3π/4 = π/2 + (π/2 + π/4) = 2*(π/2) + π/4 // case 3: zero additional blades (identity case) let zero_blade = Angle::new_with_blade(0, 1.0, 2.0); // 0 blades + π/2 @@ -141,8 +141,8 @@ fn it_handles_new_with_blade_construction() { // step 2: blade_increment = 0 * π/2 = 0 → blade=0, value=0 // step 3: addition = (1+0, 0+0) = blade=1, value=0 assert_eq!(zero_blade.blade(), 1); // 1 + 0 = 1 blade - assert!(zero_blade.rem().abs() < EPSILON); // 0 + 0 = 0 - // blade arithmetic: 0*(π/2) + π/2 = π/2 = 1*(π/2) + 0 + assert!(zero_blade.near_rem(0.0)); // 0 + 0 = 0 + // blade arithmetic: 0*(π/2) + π/2 = π/2 = 1*(π/2) + 0 // proves new_with_blade performs: base_decomposition + explicit_blade_addition } @@ -158,8 +158,8 @@ fn it_converts_from_cartesian_coordinates() { // step 2: π/4 ÷ π = 1/4 pi_radians // step 3: Angle::new(1/4, 1.0) → π/4 = 0*(π/2) + π/4 assert_eq!(cart_45.blade(), 0); // no π/2 crossings - assert!((cart_45.rem() - PI / 4.0).abs() < EPSILON); // π/4 remainder - // blade arithmetic: π/4 = 0*(π/2) + π/4 + assert!(cart_45.near_rem(PI / 4.0)); // π/4 remainder + // blade arithmetic: π/4 = 0*(π/2) + π/4 // case 2: 90° angle (π/2) let cart_90 = Angle::new_from_cartesian(0.0, 1.0); // atan2(1,0) = π/2 @@ -167,8 +167,8 @@ fn it_converts_from_cartesian_coordinates() { // step 2: π/2 ÷ π = 1/2 pi_radians // step 3: Angle::new(1/2, 1.0) → π/2 = 1*(π/2) + 0 assert_eq!(cart_90.blade(), 1); // 1 π/2 crossing - assert!(cart_90.rem().abs() < EPSILON); // no remainder - // blade arithmetic: π/2 = 1*(π/2) + 0 + assert!(cart_90.near_rem(0.0)); // no remainder + // blade arithmetic: π/2 = 1*(π/2) + 0 // case 3: 180° angle (π) let cart_180 = Angle::new_from_cartesian(-1.0, 0.0); // atan2(0,-1) = π @@ -176,8 +176,8 @@ fn it_converts_from_cartesian_coordinates() { // step 2: π ÷ π = 1 pi_radians // step 3: Angle::new(1.0, 1.0) → π = 2*(π/2) + 0 assert_eq!(cart_180.blade(), 2); // 2 π/2 crossings - assert!(cart_180.rem().abs() < EPSILON); // no remainder - // blade arithmetic: π = 2*(π/2) + 0 + assert!(cart_180.near_rem(0.0)); // no remainder + // blade arithmetic: π = 2*(π/2) + 0 // case 4: 270° angle (3π/2) let cart_270 = Angle::new_from_cartesian(0.0, -1.0); // atan2(-1,0) = 3π/2 @@ -185,8 +185,8 @@ fn it_converts_from_cartesian_coordinates() { // step 2: 3π/2 ÷ π = 3/2 pi_radians // step 3: Angle::new(3/2, 1.0) → 3π/2 = 3*(π/2) + 0 assert_eq!(cart_270.blade(), 3); // 3 π/2 crossings - assert!(cart_270.rem().abs() < EPSILON); // no remainder - // blade arithmetic: 3π/2 = 3*(π/2) + 0 + assert!(cart_270.near_rem(0.0)); // no remainder + // blade arithmetic: 3π/2 = 3*(π/2) + 0 // proves cartesian conversion maintains blade decomposition through atan2 → blade arithmetic } @@ -246,8 +246,8 @@ fn it_applies_dual_through_pi_rotation() { // step 3: result = blade=0+2=2, value=0+0=0 assert_eq!(dual_scalar.blade(), 2); // 0 + 2 = 2 blades assert_eq!(dual_scalar.grade(), 2); // 2 % 4 = 2 (bivector) - assert!(dual_scalar.rem().abs() < EPSILON); // value unchanged - // blade arithmetic: dual adds exactly 2 blades (π rotation) + assert!(dual_scalar.near_rem(0.0)); // value unchanged + // blade arithmetic: dual adds exactly 2 blades (π rotation) // case 2: vector → trivector (grade 1 → grade 3) let vector = Angle::new(1.0, 2.0); // blade=1, grade=1 @@ -295,8 +295,8 @@ fn it_implements_add_trait_blade_logic() { // step 2: value₁ + value₂ = π/4 + π/6 = 5π/12 // step 3: 5π/12 < π/2, no boundary crossing assert_eq!(sum_owned.blade(), 0); // 0 + 0 = 0 blades - assert!((sum_owned.rem() - 5.0 * PI / 12.0).abs() < EPSILON); // 5π/12 value - // blade arithmetic: (0,π/4) + (0,π/6) = (0, 5π/12) + assert!(sum_owned.near_rem(5.0 * PI / 12.0)); // 5π/12 value + // blade arithmetic: (0,π/4) + (0,π/6) = (0, 5π/12) // case 2: test all borrowing variants produce identical blade arithmetic let sum_borrow1 = angle_a + angle_b; // owned + borrowed @@ -318,8 +318,8 @@ fn it_implements_add_trait_blade_logic() { // step 2: value₁ + value₂ = 3π/8 + 5π/8 = π // step 3: π ≥ π/2, so normalize: π = 2*(π/2) + 0 assert_eq!(crossing_sum.blade(), 2); // 0 + 0 + 2(crossings) = 2 blades - assert!(crossing_sum.rem().abs() < EPSILON); // π normalized to 0 remainder - // blade arithmetic: boundary crossing adds 2 blades for π total + assert!(crossing_sum.near_rem(0.0)); // π normalized to 0 remainder + // blade arithmetic: boundary crossing adds 2 blades for π total // proves Add trait maintains consistent blade accumulation across all ownership patterns } @@ -337,8 +337,8 @@ fn it_implements_sub_trait_blade_logic() { // step 2: value₁ - value₂ = 0 - 0 = 0 // step 3: no borrowing needed assert_eq!(simple_diff.blade(), 2); // 3 - 1 = 2 blades - assert!(simple_diff.rem().abs() < EPSILON); // 0 - 0 = 0 - // blade arithmetic: (3,0) - (1,0) = (2,0) + assert!(simple_diff.near_rem(0.0)); // 0 - 0 = 0 + // blade arithmetic: (3,0) - (1,0) = (2,0) // case 2: value borrowing (negative value requires blade borrowing) let small_value = Angle::new(1.0, 6.0); // π/6 → blade=0, value=π/6 @@ -349,8 +349,8 @@ fn it_implements_sub_trait_blade_logic() { // step 3: borrow from blade: blade = 0-1 = -1, value = -π/6 + π/2 = π/3 // step 4: wrap negative blade: -1 + 4 = 3 assert_eq!(borrow_diff.blade(), 3); // -1 + 4 = 3 (wrapped) - assert!((borrow_diff.rem() - PI / 3.0).abs() < EPSILON); // -π/6 + π/2 = π/3 - // blade arithmetic: borrowing maintains forward-only angle space + assert!(borrow_diff.near_rem(PI / 3.0)); // -π/6 + π/2 = π/3 + // blade arithmetic: borrowing maintains forward-only angle space // case 3: negative blade wrapping let zero_blade = Angle::new(0.0, 1.0); // blade=0, value=0 @@ -360,8 +360,8 @@ fn it_implements_sub_trait_blade_logic() { // step 2: value₁ - value₂ = 0 - 0 = 0 // step 3: wrap negative blade: -5 + 8 = 3 (add 2*4 rotations) assert_eq!(wrap_diff.blade(), 3); // -5 + 8 = 3 (wrapped forward) - assert!(wrap_diff.rem().abs() < EPSILON); // 0 - 0 = 0 - // blade arithmetic: negative blades wrap through 4-rotation addition + assert!(wrap_diff.near_rem(0.0)); // 0 - 0 = 0 + // blade arithmetic: negative blades wrap through 4-rotation addition // proves Sub trait maintains forward-only angle space through borrowing and wrapping } @@ -384,8 +384,8 @@ fn it_implements_mul_trait_blade_logic() { assert_eq!(product.blade(), addition.blade()); // identical blade arithmetic assert_eq!(product.rem(), addition.rem()); // identical value arithmetic assert_eq!(product.blade(), 1); // 0 + 0 + 1(crossing) = 1 - assert!((product.rem() - (7.0 * PI / 12.0 - PI / 2.0)).abs() < EPSILON); // 7π/12 - π/2 remainder - // blade arithmetic: angle multiplication IS angle addition + assert!(product.near_rem(7.0 * PI / 12.0 - PI / 2.0)); // 7π/12 - π/2 remainder + // blade arithmetic: angle multiplication IS angle addition // case 2: verify "angles add, lengths multiply" principle at angle level let mult_result = angle_x * angle_y; // calls geometric_add @@ -402,8 +402,8 @@ fn it_implements_mul_trait_blade_logic() { // step 2: value₁ + value₂ = 0 + 0 = 0 // step 3: no boundary crossing assert_eq!(high_product.blade(), 300); // 100 + 200 = 300 blades - assert!(high_product.rem().abs() < EPSILON); // 0 + 0 = 0 - // blade arithmetic: high blade multiplication follows same addition rule + assert!(high_product.near_rem(0.0)); // 0 + 0 = 0 + // blade arithmetic: high blade multiplication follows same addition rule // proves Mul trait implements "angles add" through identical geometric_add calls } @@ -420,8 +420,8 @@ fn it_implements_div_trait_blade_logic() { // step 2: divide by scalar = 3π/2 ÷ 2 = 3π/4 // step 3: re-decompose = 3π/4 = 1*(π/2) + π/4 assert_eq!(scalar_div.blade(), 1); // 3π/4 crosses π/2 once - assert!((scalar_div.rem() - PI / 4.0).abs() < EPSILON); // π/4 remainder - // blade arithmetic: scalar division reconstructs → scales → decomposes + assert!(scalar_div.near_rem(PI / 4.0)); // π/4 remainder + // blade arithmetic: scalar division reconstructs → scales → decomposes // case 2: division by angle (Div) - calls geometric_sub let dividend = Angle::new(5.0, 4.0); // 5π/4 → blade=2, value=π/4 @@ -431,8 +431,8 @@ fn it_implements_div_trait_blade_logic() { // step 2: value₁ - value₂ = π/4 - π/4 = 0 // step 3: no borrowing needed assert_eq!(angle_div.blade(), 2); // 2 - 0 = 2 blades - assert!(angle_div.rem().abs() < EPSILON); // π/4 - π/4 = 0 - // blade arithmetic: angle division IS angle subtraction (geometric_sub) + assert!(angle_div.near_rem(0.0)); // π/4 - π/4 = 0 + // blade arithmetic: angle division IS angle subtraction (geometric_sub) // case 3: verify division equivalence with subtraction let subtraction_result = dividend - divisor; @@ -447,7 +447,7 @@ fn it_implements_div_trait_blade_logic() { // step 2: divide = 7π/4 ÷ 3 = 7π/12 // step 3: decompose = 7π/12 = 1*(π/2) + π/12 (since 7π/12 > π/2) assert_eq!(divided_complex.blade(), 1); // 7π/12 crosses π/2 once - assert!((divided_complex.rem() - (7.0 * PI / 12.0 - PI / 2.0)).abs() < EPSILON); + assert!(divided_complex.near_rem(7.0 * PI / 12.0 - PI / 2.0)); // π/12 remainder // blade arithmetic: scalar division handles complex blade reconstruction @@ -543,8 +543,8 @@ fn it_constructs_geonum_with_basic_new() { // step 3: blade decomposition = 3π/4 = 1*(π/2) + π/4 assert_eq!(geo1.mag, 2.0); // length preserved assert_eq!(geo1.angle.blade(), 1); // 3π/4 crosses π/2 once - assert!((geo1.angle.rem() - PI / 4.0).abs() < EPSILON); // π/4 remainder - // blade arithmetic: length passthrough + angle decomposition + assert!(geo1.angle.near_rem(PI / 4.0)); // π/4 remainder + // blade arithmetic: length passthrough + angle decomposition // case 2: length with exact π/2 angle let geo2 = Geonum::new(3.5, 1.0, 2.0); // length=3.5, angle=π/2 @@ -553,8 +553,8 @@ fn it_constructs_geonum_with_basic_new() { // step 3: blade decomposition = π/2 = 1*(π/2) + 0 assert_eq!(geo2.mag, 3.5); // length preserved assert_eq!(geo2.angle.blade(), 1); // π/2 = 1 blade exactly - assert!(geo2.angle.rem().abs() < EPSILON); // no remainder - // blade arithmetic: exact π/2 creates clean blade boundary + assert!(geo2.angle.near_rem(0.0)); // no remainder + // blade arithmetic: exact π/2 creates clean blade boundary // case 3: length with high blade angle let geo3 = Geonum::new(1.0, 7.0, 2.0); // length=1, angle=7π/2 @@ -563,8 +563,8 @@ fn it_constructs_geonum_with_basic_new() { // step 3: blade decomposition = 7π/2 = 7*(π/2) + 0 assert_eq!(geo3.mag, 1.0); // length preserved assert_eq!(geo3.angle.blade(), 7); // 7 complete π/2 rotations - assert!(geo3.angle.rem().abs() < EPSILON); // no remainder - // blade arithmetic: high blade angles work identically + assert!(geo3.angle.near_rem(0.0)); // no remainder + // blade arithmetic: high blade angles work identically // proves Geonum::new preserves length while decomposing angle through blade arithmetic } @@ -591,8 +591,8 @@ fn it_constructs_geonum_with_angle_composition() { // step 2: angle = high_angle (direct, no blade modification) assert_eq!(high_geo.mag, 2.0); // length direct assert_eq!(high_geo.angle.blade(), 1000); // blade preserved exactly - assert!((high_geo.angle.rem() - PI / 4.0).abs() < EPSILON); // value preserved exactly - // blade arithmetic: million-dimensional preservation without processing + assert!(high_geo.angle.near_rem(PI / 4.0)); // value preserved exactly + // blade arithmetic: million-dimensional preservation without processing // case 3: exact angle state preservation let complex_angle = Angle::new(7.0, 4.0); // 7π/4 → blade=3, value=π/4 @@ -617,8 +617,8 @@ fn it_constructs_geonum_from_cartesian() { assert_eq!(geo1.mag, 5.0); // pythagorean magnitude assert_eq!(geo1.angle.blade(), 0); // angle < π/2, no boundary crossing let expected_angle = 4.0_f64.atan2(3.0); // atan2 calculation - assert!((geo1.angle.grade_angle() - expected_angle).abs() < EPSILON); // preserves atan2 result - // blade arithmetic: cartesian → polar → blade decomposition + assert!(geo1.angle.near_rad(expected_angle)); // preserves atan2 result + // blade arithmetic: cartesian → polar → blade decomposition // case 2: unit circle quadrants (exact blade boundaries) let geo_90 = Geonum::new_from_cartesian(0.0, 1.0); // (0,1) = 90° @@ -627,8 +627,8 @@ fn it_constructs_geonum_from_cartesian() { // step 3: blade decomposition = π/2 = 1*(π/2) + 0 assert_eq!(geo_90.mag, 1.0); // unit magnitude assert_eq!(geo_90.angle.blade(), 1); // π/2 crosses boundary once - assert!(geo_90.angle.rem().abs() < EPSILON); // exact boundary has no remainder - // blade arithmetic: 90° → blade=1, value=0 + assert!(geo_90.angle.near_rem(0.0)); // exact boundary has no remainder + // blade arithmetic: 90° → blade=1, value=0 let geo_180 = Geonum::new_from_cartesian(-1.0, 0.0); // (-1,0) = 180° // step 1: length = sqrt(1² + 0²) = 1.0 @@ -636,18 +636,18 @@ fn it_constructs_geonum_from_cartesian() { // step 3: blade decomposition = π = 2*(π/2) + 0 assert_eq!(geo_180.mag, 1.0); // unit magnitude assert_eq!(geo_180.angle.blade(), 2); // π crosses boundary twice - assert!(geo_180.angle.rem().abs() < EPSILON); // exact boundary has no remainder - // blade arithmetic: 180° → blade=2, value=0 + assert!(geo_180.angle.near_rem(0.0)); // exact boundary has no remainder + // blade arithmetic: 180° → blade=2, value=0 // case 3: first quadrant 45° angle let geo_45 = Geonum::new_from_cartesian(1.0, 1.0); // (1,1) = 45° // step 1: length = sqrt(1² + 1²) = sqrt(2) // step 2: angle = atan2(1.0, 1.0) = π/4 // step 3: blade decomposition = π/4 = 0*(π/2) + π/4 - assert!((geo_45.mag - 2.0_f64.sqrt()).abs() < EPSILON); // sqrt(2) magnitude + assert!(geo_45.near_mag(2.0_f64.sqrt())); // sqrt(2) magnitude assert_eq!(geo_45.angle.blade(), 0); // π/4 < π/2, no crossing - assert!((geo_45.angle.rem() - PI / 4.0).abs() < EPSILON); // π/4 remainder - // blade arithmetic: 45° → blade=0, value=π/4 + assert!(geo_45.angle.near_rem(PI / 4.0)); // π/4 remainder + // blade arithmetic: 45° → blade=0, value=π/4 // proves cartesian conversion: magnitude calculation + atan2 blade decomposition } @@ -665,8 +665,8 @@ fn it_constructs_geonum_with_explicit_blade() { // step 4: addition = (0+3, π/4+0) = blade=3, value=π/4 assert_eq!(geo1.mag, 2.0); // length preserved assert_eq!(geo1.angle.blade(), 3); // 0 + 3 = 3 blades - assert!((geo1.angle.rem() - PI / 4.0).abs() < EPSILON); // π/4 value preserved - // blade arithmetic: explicit blade addition to computed angle + assert!(geo1.angle.near_rem(PI / 4.0)); // π/4 value preserved + // blade arithmetic: explicit blade addition to computed angle // case 2: million-dimensional control let geo_million = Geonum::new_with_blade(1.0, 1_000_000, 0.0, 1.0); // blade=1000000 @@ -676,8 +676,8 @@ fn it_constructs_geonum_with_explicit_blade() { // step 4: addition = (0+1000000, 0+0) = blade=1000000, value=0 assert_eq!(geo_million.mag, 1.0); // length preserved assert_eq!(geo_million.angle.blade(), 1_000_000); // explicit million-dimensional blade - assert!(geo_million.angle.rem().abs() < EPSILON); // 0 value - // blade arithmetic: enables million-dimensional geometric algebra + assert!(geo_million.angle.near_rem(0.0)); // 0 value + // blade arithmetic: enables million-dimensional geometric algebra // case 3: explicit blade with boundary crossing base angle let geo_cross = Geonum::new_with_blade(1.5, 2, 3.0, 4.0); // length=1.5, 2 blades + 3π/4 @@ -687,8 +687,8 @@ fn it_constructs_geonum_with_explicit_blade() { // step 4: addition = (1+2, π/4+0) = blade=3, value=π/4 assert_eq!(geo_cross.mag, 1.5); // length preserved assert_eq!(geo_cross.angle.blade(), 3); // 1 + 2 = 3 total blades - assert!((geo_cross.angle.rem() - PI / 4.0).abs() < EPSILON); // π/4 from base angle - // blade arithmetic: explicit blade + base angle decomposition + assert!(geo_cross.angle.near_rem(PI / 4.0)); // π/4 from base angle + // blade arithmetic: explicit blade + base angle decomposition // proves new_with_blade enables explicit dimensional control through blade arithmetic } @@ -746,16 +746,16 @@ fn it_creates_dimensional_geonums() { // step 3: blade decomposition = 0 = 0*(π/2) + 0 assert_eq!(dim0.mag, 1.0); // unit length assert_eq!(dim0.angle.blade(), 0); // 0 π/2 rotations - assert!(dim0.angle.rem().abs() < EPSILON); // no remainder - // blade arithmetic: dimension 0 → blade=0 (scalar direction) + assert!(dim0.angle.near_rem(0.0)); // no remainder + // blade arithmetic: dimension 0 → blade=0 (scalar direction) let dim1 = Geonum::create_dimension(1.0, 1); // 1st dimension (y-axis) // step 1: length = 1.0 (direct) // step 2: angle = Angle::new(1.0, 2.0) = 1 * π/2 = π/2 // step 3: blade decomposition = π/2 = 1*(π/2) + 0 assert_eq!(dim1.angle.blade(), 1); // 1 π/2 rotation - assert!(dim1.angle.rem().abs() < EPSILON); // exact boundary - // blade arithmetic: dimension 1 → blade=1 (vector direction) + assert!(dim1.angle.near_rem(0.0)); // exact boundary + // blade arithmetic: dimension 1 → blade=1 (vector direction) // case 2: high dimensional mapping let dim1000 = Geonum::create_dimension(2.0, 1000); // 1000th dimension @@ -764,8 +764,8 @@ fn it_creates_dimensional_geonums() { // step 3: blade decomposition = 1000*(π/2) = 1000*(π/2) + 0 assert_eq!(dim1000.mag, 2.0); // length preserved assert_eq!(dim1000.angle.blade(), 1000); // exact blade = dimension index - assert!(dim1000.angle.rem().abs() < EPSILON); // no remainder - // blade arithmetic: dimension index directly maps to blade count + assert!(dim1000.angle.near_rem(0.0)); // no remainder + // blade arithmetic: dimension index directly maps to blade count // case 3: dimensional angle relationships assert!(dim0.is_orthogonal(&dim1)); // 0 ⊥ π/2 (cos(π/2) = 0) @@ -789,8 +789,8 @@ fn it_computes_geonum_calculus_through_blade_rotation() { // step 3: angle addition = (0+1, π/6+0) = blade=1, value=π/6 assert_eq!(derivative.mag, 2.0); // length preserved assert_eq!(derivative.angle.blade(), 1); // blade=0 + 1 = blade=1 - assert!((derivative.angle.rem() - PI / 6.0).abs() < EPSILON); // value preserved - // blade arithmetic: differentiation = +1 blade through π/2 rotation + assert!(derivative.angle.near_rem(PI / 6.0)); // value preserved + // blade arithmetic: differentiation = +1 blade through π/2 rotation // case 2: integrate adds 3π/2 rotation (forward-only -π/2) let integrand = Geonum::new(3.0, 1.0, 4.0); // length=3, angle=π/4 → blade=0, value=π/4 @@ -800,8 +800,8 @@ fn it_computes_geonum_calculus_through_blade_rotation() { // step 3: angle addition = (0+3, π/4+0) = blade=3, value=π/4 assert_eq!(integral.mag, 3.0); // length preserved assert_eq!(integral.angle.blade(), 3); // blade=0 + 3 = blade=3 - assert!((integral.angle.rem() - PI / 4.0).abs() < EPSILON); // value preserved - // blade arithmetic: integration = +3 blades through 3π/2 forward rotation + assert!(integral.angle.near_rem(PI / 4.0)); // value preserved + // blade arithmetic: integration = +3 blades through 3π/2 forward rotation // case 3: calculus inverse relationship through blade accumulation let original = Geonum::new(1.0, 0.0, 1.0); // blade=0, value=0 @@ -811,8 +811,8 @@ fn it_computes_geonum_calculus_through_blade_rotation() { // step 3: total = blade=0 → blade=4 (4 blades = 2π rotation) assert_eq!(diff_then_int.angle.blade(), 4); // 0 + 1 + 3 = 4 blades assert_eq!(diff_then_int.angle.grade(), 0); // 4 % 4 = 0 (same grade) - assert!(diff_then_int.angle.rem().abs() < EPSILON); // value preserved - // blade arithmetic: d/dx∫ = 4 blade accumulation, not algebraic cancellation + assert!(diff_then_int.angle.near_rem(0.0)); // value preserved + // blade arithmetic: d/dx∫ = 4 blade accumulation, not algebraic cancellation // proves calculus operations accumulate transformation history through blade arithmetic } @@ -830,15 +830,15 @@ fn it_inverts_geonum_through_length_and_angle_negation() { // step 3: blade decomposition = 4π/3 = 2*(π/2) + π/3 assert_eq!(inverse.mag, 0.5); // 1/2 reciprocal assert_eq!(inverse.angle.blade(), 2); // π/3 + π crosses 2 boundaries - assert!((inverse.angle.rem() - PI / 3.0).abs() < EPSILON); // π/3 value preserved - // blade arithmetic: inv() = [1/r, θ+π] through reciprocal + π rotation + assert!(inverse.angle.near_rem(PI / 3.0)); // π/3 value preserved + // blade arithmetic: inv() = [1/r, θ+π] through reciprocal + π rotation // case 2: multiplicative identity through blade accumulation let identity = geonum * inverse; // z * z^(-1) // step 1: length = 2.0 * 0.5 = 1.0 (lengths multiply) // step 2: angle = π/3 + (π/3 + π) = π/3 + 4π/3 = 5π/3 (angles add) // step 3: blade decomposition = 5π/3 = 3*(π/2) + π/6 - assert!((identity.mag - 1.0).abs() < EPSILON); // unit magnitude + assert!(identity.near_mag(1.0)); // unit magnitude assert_eq!(identity.angle.blade(), 3); // 0 + 2 = 3 total blades (from angle addition) assert_eq!(identity.angle.grade(), 3); // 3 % 4 = 3 (trivector) // blade arithmetic: multiplicative identity preserves geometric work history @@ -851,8 +851,8 @@ fn it_inverts_geonum_through_length_and_angle_negation() { // step 3: value preserved = π/4 assert_eq!(high_inverse.mag, 0.25); // 1/4 reciprocal assert_eq!(high_inverse.angle.blade(), 1002); // 1000 + 2 = 1002 blades - assert!((high_inverse.angle.rem() - PI / 4.0).abs() < EPSILON); // value preserved - // blade arithmetic: inversion scales to arbitrary blade magnitudes + assert!(high_inverse.angle.near_rem(PI / 4.0)); // value preserved + // blade arithmetic: inversion scales to arbitrary blade magnitudes // proves inv() combines reciprocal length with π rotation through blade arithmetic } @@ -872,8 +872,8 @@ fn it_divides_geonum_through_multiplication_by_inverse() { // step 4: blade decomposition = 17π/12 = 2*(π/2) + 5π/12 assert_eq!(quotient.mag, 3.0); // 6/2 = 3 assert_eq!(quotient.angle.blade(), 2); // angle sum crosses 2 boundaries - assert!((quotient.angle.rem() - 5.0 * PI / 12.0).abs() < EPSILON); // remainder after crossings - // blade arithmetic: division accumulates inversion + multiplication blades + assert!(quotient.angle.near_rem(5.0 * PI / 12.0)); // remainder after crossings + // blade arithmetic: division accumulates inversion + multiplication blades // case 2: verify division equivalence with manual inv() + mul() let manual_quotient = dividend * divisor.inv(); @@ -891,8 +891,8 @@ fn it_divides_geonum_through_multiplication_by_inverse() { // step 3: π/2 boundary = blade=702+1=703, value=0 assert_eq!(high_quotient.mag, 2.0); // 8/4 = 2 assert_eq!(high_quotient.angle.blade(), 703); // 500 + (200+2) + 1(crossing) = 703 - assert!(high_quotient.angle.rem().abs() < EPSILON); // π/2 crossing resets value - // blade arithmetic: division accumulates all intermediate blade transformations + assert!(high_quotient.angle.near_rem(0.0)); // π/2 crossing resets value + // blade arithmetic: division accumulates all intermediate blade transformations // proves div() accumulates blade history through inversion + multiplication compound operation } @@ -922,8 +922,8 @@ fn it_normalizes_geonum_preserving_blade_structure() { // step 3: blade/value = blade=1000, value=π/4 (unchanged) assert_eq!(high_norm.mag, 1.0); // unit length assert_eq!(high_norm.angle.blade(), 1000); // blade preserved exactly - assert!((high_norm.angle.rem() - PI / 4.0).abs() < EPSILON); // value preserved exactly - // blade arithmetic: normalization preserves high blade structure + assert!(high_norm.angle.near_rem(PI / 4.0)); // value preserved exactly + // blade arithmetic: normalization preserves high blade structure // case 3: normalization idempotency let already_unit = Geonum::new(1.0, 3.0, 4.0); // length=1, angle=3π/4 → blade=1, value=π/4 @@ -951,7 +951,7 @@ fn it_computes_dot_product_returning_scalar_blade() { // step 2: magnitude = 3 * 4 * cos(π/6) = 12 * (√3/2) ≈ 10.39 // step 3: result = [magnitude, base scalar angle] let expected_mag = 3.0 * 4.0 * (PI / 6.0).cos(); - assert!((dot_result.mag - expected_mag).abs() < EPSILON); // computed magnitude + assert!(dot_result.near_mag(expected_mag)); // computed magnitude assert_eq!(dot_result.angle, Angle::new(0.0, 1.0)); // positive cosine keeps scalar base angle // case 2: orthogonal vectors (π/2 apart) @@ -961,7 +961,7 @@ fn it_computes_dot_product_returning_scalar_blade() { // step 1: angle_diff = π/2 - 0 = π/2 // step 2: magnitude = 2 * 3 * cos(π/2) = 6 * 0 = 0 // step 3: result = [0, blade=0] (scalar zero) - assert!(ortho_dot.mag.abs() < EPSILON); // zero magnitude + assert!(ortho_dot.near_mag(0.0)); // zero magnitude assert_eq!(ortho_dot.angle, Angle::new(0.0, 1.0)); // zero magnitude uses scalar pair // case 3: high blade dot product @@ -990,10 +990,10 @@ fn it_computes_wedge_product_with_blade_increment() { // step 2: magnitude = 3 * 4 * |sin(π/6)| = 12 * 0.5 = 6.0 // step 3: angle_sum = π/6 + π/3 + π/2 = π/6 + 2π/6 + 3π/6 = π // step 4: blade decomposition = π = 2*(π/2) + 0 - assert!((wedge_ab.mag - 6.0).abs() < EPSILON); // computed magnitude + assert!(wedge_ab.near_mag(6.0)); // computed magnitude assert_eq!(wedge_ab.angle.blade(), 2); // π = 2*(π/2), crosses 2 boundaries - assert!(wedge_ab.angle.rem().abs() < EPSILON); // π has no remainder - // blade arithmetic: wedge adds π/2 to angle sum, creates bivector + assert!(wedge_ab.angle.near_rem(0.0)); // π has no remainder + // blade arithmetic: wedge adds π/2 to angle sum, creates bivector // case 2: wedge product with negative orientation (anticommutativity) let wedge_ba = vec_b.wedge(&vec_a); @@ -1002,7 +1002,7 @@ fn it_computes_wedge_product_with_blade_increment() { // step 3: angle_sum = π/3 + π/6 + π/2 = π (same base) // step 4: orientation correction = π + π = 2π (adds π for negative sin) // step 5: blade decomposition = 2π = 4*(π/2) + 0 - assert!((wedge_ba.mag - 6.0).abs() < EPSILON); // same magnitude + assert!(wedge_ba.near_mag(6.0)); // same magnitude assert_eq!(wedge_ba.angle.blade(), 4); // 2π = 4*(π/2) with orientation π // blade arithmetic: negative sin adds π rotation (2 more blades) @@ -1037,7 +1037,7 @@ fn it_computes_geometric_product_combining_dot_and_wedge_blades() { // case 2: verify geometric product equivalence with manual combination let manual_geo = dot_part + wedge_part; - assert!((geo_product.mag - manual_geo.mag).abs() < EPSILON); // same magnitude + assert!(geo_product.near_mag(manual_geo.mag)); // same magnitude assert_eq!(geo_product.angle.blade(), manual_geo.angle.blade()); // same blade assert_eq!(geo_product.angle.rem(), manual_geo.angle.rem()); // same value // blade arithmetic: geo() IS dot + wedge through geonum addition @@ -1074,8 +1074,8 @@ fn it_rotates_geonum_through_angle_addition() { // step 3: blade arithmetic = (0+0, π/8+π/6) = blade=0, value=7π/24 assert_eq!(rotated.mag, 2.0); // length preserved assert_eq!(rotated.angle.blade(), 0); // no boundary crossing - assert!((rotated.angle.rem() - 7.0 * PI / 24.0).abs() < EPSILON); // combined angle - // blade arithmetic: rotation = angle addition without blade change + assert!(rotated.angle.near_rem(7.0 * PI / 24.0)); // combined angle + // blade arithmetic: rotation = angle addition without blade change // case 2: rotation with boundary crossing let cross_geo = Geonum::new(3.0, 1.0, 3.0); // length=3, angle=π/3 → blade=0, value=π/3 @@ -1087,8 +1087,8 @@ fn it_rotates_geonum_through_angle_addition() { // step 4: blade arithmetic = 7π/12 = 1*(π/2) + π/12 assert_eq!(cross_rotated.mag, 3.0); // length preserved assert_eq!(cross_rotated.angle.blade(), 1); // boundary crossing increments blade - assert!((cross_rotated.angle.rem() - (7.0 * PI / 12.0 - PI / 2.0)).abs() < EPSILON); // remainder - // blade arithmetic: rotation with crossing = angle addition + boundary handling + assert!(cross_rotated.angle.near_rem(7.0 * PI / 12.0 - PI / 2.0)); // remainder + // blade arithmetic: rotation with crossing = angle addition + boundary handling // case 3: rotation with high blade accumulation let high_geo = Geonum::new_with_blade(1.0, 1000, 1.0, 6.0); // blade=1000, value=π/6 @@ -1100,8 +1100,8 @@ fn it_rotates_geonum_through_angle_addition() { // step 4: blade arithmetic = (1000+200, π/6+π/4) = blade=1200, value=5π/12 assert_eq!(high_rotated.mag, 1.0); // length preserved assert_eq!(high_rotated.angle.blade(), 1200); // blade accumulation - assert!((high_rotated.angle.rem() - 5.0 * PI / 12.0).abs() < EPSILON); // value sum - // blade arithmetic: rotation accumulates blades at arbitrary magnitudes + assert!(high_rotated.angle.near_rem(5.0 * PI / 12.0)); // value sum + // blade arithmetic: rotation accumulates blades at arbitrary magnitudes // proves rotate() is pure angle addition preserving length while accumulating blade history } @@ -1119,8 +1119,8 @@ fn it_negates_geonum_through_pi_rotation() { // step 3: blade decomposition = 5π/4 = 2*(π/2) + π/4 assert_eq!(negated.mag, 2.0); // length preserved assert_eq!(negated.angle.blade(), 2); // π/4 + π crosses 2 boundaries - assert!((negated.angle.rem() - PI / 4.0).abs() < EPSILON); // π/4 value preserved - // blade arithmetic: negation adds exactly 2 blades (π rotation) + assert!(negated.angle.near_rem(PI / 4.0)); // π/4 value preserved + // blade arithmetic: negation adds exactly 2 blades (π rotation) // case 2: double negation accumulates blades let double_neg = negated.negate(); @@ -1130,8 +1130,8 @@ fn it_negates_geonum_through_pi_rotation() { assert_eq!(double_neg.mag, 2.0); // length preserved assert_eq!(double_neg.angle.blade(), 4); // 2 + 2 = 4 blades total assert_eq!(double_neg.angle.grade(), 0); // 4 % 4 = 0 (same grade as original) - assert!((double_neg.angle.rem() - PI / 4.0).abs() < EPSILON); // value preserved - // blade arithmetic: double negation = 4 blade accumulation (2π rotation) + assert!(double_neg.angle.near_rem(PI / 4.0)); // value preserved + // blade arithmetic: double negation = 4 blade accumulation (2π rotation) // case 3: negation with high blade count let high_geo = Geonum::new_with_blade(1.5, 1000, 1.0, 6.0); // blade=1000, value=π/6 @@ -1141,8 +1141,8 @@ fn it_negates_geonum_through_pi_rotation() { // step 3: value = π/6 (preserved through negate) assert_eq!(high_negated.mag, 1.5); // length preserved assert_eq!(high_negated.angle.blade(), 1002); // 1000 + 2 = 1002 blades - assert!((high_negated.angle.rem() - PI / 6.0).abs() < EPSILON); // value preserved - // blade arithmetic: negation adds 2 blades regardless of starting blade count + assert!(high_negated.angle.near_rem(PI / 6.0)); // value preserved + // blade arithmetic: negation adds 2 blades regardless of starting blade count // proves negate() implements geometric π rotation preserving transformation history } @@ -1199,8 +1199,8 @@ fn it_handles_scalar_sign_encoding_original() { // step 3: blade decomposition = 0 = 0*(π/2) + 0 assert_eq!(positive.mag, 5.0); // magnitude from abs() assert_eq!(positive.angle.blade(), 0); // positive → blade 0 - assert!(positive.angle.rem().abs() < EPSILON); // 0 angle - // blade arithmetic: positive scalar → blade=0 direction + assert!(positive.angle.near_rem(0.0)); // 0 angle + // blade arithmetic: positive scalar → blade=0 direction // case 2: negative scalar encoding let negative = Geonum::scalar(-3.0); @@ -1209,8 +1209,8 @@ fn it_handles_scalar_sign_encoding_original() { // step 3: blade decomposition = π = 2*(π/2) + 0 assert_eq!(negative.mag, 3.0); // magnitude from abs() assert_eq!(negative.angle.blade(), 2); // negative → blade 2 (π rotation) - assert!(negative.angle.rem().abs() < EPSILON); // π angle has no remainder - // blade arithmetic: negative scalar → blade=2 direction (π rotation) + assert!(negative.angle.near_rem(0.0)); // π angle has no remainder + // blade arithmetic: negative scalar → blade=2 direction (π rotation) // case 3: zero scalar (boundary case) let zero = Geonum::scalar(0.0); @@ -1219,8 +1219,8 @@ fn it_handles_scalar_sign_encoding_original() { // step 3: blade decomposition = 0 = 0*(π/2) + 0 assert_eq!(zero.mag, 0.0); // zero magnitude assert_eq!(zero.angle.blade(), 0); // zero treated as positive → blade 0 - assert!(zero.angle.rem().abs() < EPSILON); // 0 angle - // blade arithmetic: zero defaults to positive blade direction + assert!(zero.angle.near_rem(0.0)); // 0 angle + // blade arithmetic: zero defaults to positive blade direction // case 4: scalar sign detection through blade arithmetic assert!(positive.angle.blade() != negative.angle.blade()); // different directions @@ -1244,8 +1244,8 @@ fn it_increments_blade_through_rotation() { // step 3: addition = (0+1, π/4+0) = blade=1, value=π/4 assert_eq!(incremented.mag, scalar_base.mag); // length preserved assert_eq!(incremented.angle.blade(), 1); // blade=0 + 1 = blade=1 - assert!((incremented.angle.rem() - PI / 4.0).abs() < EPSILON); // value preserved - // blade arithmetic: π/4 → π/4 + π/2 = 3π/4 = 1*(π/2) + π/4 + assert!(incremented.angle.near_rem(PI / 4.0)); // value preserved + // blade arithmetic: π/4 → π/4 + π/2 = 3π/4 = 1*(π/2) + π/4 // case 2: increment causing grade cycling let trivector_base = Geonum::new(1.0, 3.0, 2.0); // 3π/2 → blade=3, value=0 @@ -1255,8 +1255,8 @@ fn it_increments_blade_through_rotation() { // step 3: addition = (3+1, 0+0) = blade=4, value=0 assert_eq!(cycled.angle.blade(), 4); // blade=3 + 1 = blade=4 assert_eq!(cycled.angle.grade(), 0); // 4 % 4 = 0 (cycling to scalar) - assert!(cycled.angle.rem().abs() < EPSILON); // value preserved - // blade arithmetic: grade 3 → grade 0 through blade cycling + assert!(cycled.angle.near_rem(0.0)); // value preserved + // blade arithmetic: grade 3 → grade 0 through blade cycling // case 3: increment high dimensional blade let high_dim = Geonum::new_with_blade(1.0, 1000, 0.0, 1.0); // blade=1000 @@ -1284,7 +1284,7 @@ fn it_decrements_blade_through_forward_rotation() { // step 3: addition = (1+3, 0+0) = blade=4, value=0 assert_eq!(decremented.mag, vector_base.mag); // length preserved assert_eq!(decremented.angle.blade(), 4); // blade=1 + 3 = blade=4 (not blade=0!) - assert!(decremented.angle.rem().abs() < EPSILON); // value preserved + assert!(decremented.angle.near_rem(0.0)); // value preserved assert_eq!(decremented.angle.grade(), 0); // 4 % 4 = 0 (scalar grade through cycling) // blade arithmetic: -π/2 → +3π/2 maintains forward-only principle @@ -1296,8 +1296,8 @@ fn it_decrements_blade_through_forward_rotation() { // step 3: total transformation = blade=0 → blade=4 (4 blades accumulated) assert_eq!(inc_then_dec.angle.blade(), 4); // blade=0 → 1 → 4 (not back to 0) assert_eq!(inc_then_dec.angle.grade(), 0); // 4 % 4 = 0 (same grade) - assert!((inc_then_dec.angle.rem() - PI / 4.0).abs() < EPSILON); // value preserved - // blade arithmetic: inc+dec adds 4 blades total (preserves geometric work) + assert!(inc_then_dec.angle.near_rem(PI / 4.0)); // value preserved + // blade arithmetic: inc+dec adds 4 blades total (preserves geometric work) // case 3: decrement high dimensional blade let high_blade = Geonum::new_with_blade(1.0, 1000, 0.0, 1.0); // blade=1000 @@ -1339,9 +1339,9 @@ fn it_preserves_blade_operations_at_high_dimensions() { assert_eq!(inc_medium.angle.blade(), 501); // 500 + 1 = 501 assert_eq!(dec_medium.angle.blade(), 503); // 500 + 3 = 503 - assert!((inc_medium.angle.rem() - PI / 6.0).abs() < EPSILON); // value preserved - assert!((dec_medium.angle.rem() - PI / 6.0).abs() < EPSILON); // value preserved - // blade arithmetic: increment/decrement consistent at medium blade magnitudes + assert!(inc_medium.angle.near_rem(PI / 6.0)); // value preserved + assert!(dec_medium.angle.near_rem(PI / 6.0)); // value preserved + // blade arithmetic: increment/decrement consistent at medium blade magnitudes // case 3: blade copy operation scaling let source_high = Geonum::new_with_blade(1.0, 10_000, 1.0, 8.0); // blade=10000 @@ -1351,8 +1351,8 @@ fn it_preserves_blade_operations_at_high_dimensions() { // step 2: rotation = 40000 * π/2 through angle arithmetic // step 3: result = blade=10000 + 40000 = blade=50000 assert_eq!(copied.angle.blade(), 50_000); // exact blade copying at high dimensions - assert!((copied.angle.rem() - PI / 8.0).abs() < EPSILON); // value from source preserved - // blade arithmetic: copy_blade scales to extreme blade counts + assert!(copied.angle.near_rem(PI / 8.0)); // value from source preserved + // blade arithmetic: copy_blade scales to extreme blade counts // proves blade operations maintain O(1) complexity regardless of blade magnitude // enabling geometric algebra in million-dimensional spaces through blade arithmetic @@ -1373,8 +1373,8 @@ fn it_reflects_geonum_through_forward_only_formula() { // step 4: blade arithmetic = 7π/4 = 3*(π/2) + π/4 assert_eq!(reflected.mag, point.mag); // length preserved assert_eq!(reflected.angle.blade(), 7); // 0 + 0 + 7 = 7 blades total - assert!((reflected.angle.rem() - PI / 4.0).abs() < EPSILON); // π/4 value preserved - // blade arithmetic: reflection adds 7 blades through forward-only formula + assert!(reflected.angle.near_rem(PI / 4.0)); // π/4 value preserved + // blade arithmetic: reflection adds 7 blades through forward-only formula // case 2: double reflection accumulates more blades let double_reflected = reflected.reflect(&x_axis); @@ -1431,7 +1431,7 @@ fn it_projects_geonum_onto_another_with_blade_preservation() { // step 2: scalar_factor = 0.0 / |b|² = 0.0 / 9.0 = 0.0 // step 3: projection_magnitude = 0.0 * 3.0 = 0.0 // step 4: result = [0.0, blade=1, value=0] (preserves onto angle structure) - assert!(ortho_proj.mag.abs() < EPSILON); // zero projection + assert!(ortho_proj.near_mag(0.0)); // zero projection assert_eq!(ortho_proj.angle.blade(), ortho_b.angle.blade()); // blade from onto vector assert_eq!(ortho_proj.angle.rem(), ortho_b.angle.rem()); // value from onto vector // blade arithmetic: orthogonal projection has zero magnitude but preserves target blade @@ -1446,8 +1446,8 @@ fn it_projects_geonum_onto_another_with_blade_preservation() { // step 4: result angle = target_b.angle (blade=500, value=π/4 preserved) assert!(high_proj.mag.is_finite()); // finite projection magnitude assert_eq!(high_proj.angle.blade(), 500); // blade from target vector - assert!((high_proj.angle.rem() - PI / 4.0).abs() < EPSILON); // value from target vector - // blade arithmetic: projection result inherits target blade structure + assert!(high_proj.angle.near_rem(PI / 4.0)); // value from target vector + // blade arithmetic: projection result inherits target blade structure // proves project() preserves target blade structure while computing magnitude through dot product } @@ -1513,7 +1513,7 @@ fn it_detects_orthogonality_through_dot_product_blade_collapse() { // step 1: angle_diff = π/2 - 0 = π/2 // step 2: magnitude = 3 * 4 * cos(π/2) = 12 * 0 = 0 // step 3: result = [0, blade=0, value=0] (scalar collapse) - assert!(dot_xy.mag.abs() < EPSILON); // zero dot product + assert!(dot_xy.near_mag(0.0)); // zero dot product assert_eq!(dot_xy.angle.blade(), 0); // always scalar blade assert!(vec_x.is_orthogonal(&vec_y)); // orthogonality detected // blade arithmetic: orthogonality independent of input blade structure @@ -1610,8 +1610,8 @@ fn it_raises_geonum_to_power_through_angle_scaling() { // step 4: final angle = π/4 + 2π = π/4 + 4*(π/2) = blade=4, value=π/4 assert_eq!(squared.mag, 4.0); // 2² = 4 assert_eq!(squared.angle.blade(), 4); // angle * 2 adds 4 blades (2π) - assert!((squared.angle.rem() - PI / 4.0).abs() < EPSILON); // π/4 value preserved - // blade arithmetic: pow(2) = length² + angle*2 through blade addition + assert!(squared.angle.near_rem(PI / 4.0)); // π/4 value preserved + // blade arithmetic: pow(2) = length² + angle*2 through blade addition // case 2: cube geonum (power of 3) let cubed = base_geo.pow(3.0); @@ -1621,8 +1621,8 @@ fn it_raises_geonum_to_power_through_angle_scaling() { // step 4: final angle = π/4 + 3π = π/4 + 6*(π/2) = blade=6, value=π/4 assert_eq!(cubed.mag, 8.0); // 2³ = 8 assert_eq!(cubed.angle.blade(), 6); // angle * 3 adds 6 blades (3π) - assert!((cubed.angle.rem() - PI / 4.0).abs() < EPSILON); // π/4 value preserved - // blade arithmetic: pow(3) = length³ + angle*3 through blade addition + assert!(cubed.angle.near_rem(PI / 4.0)); // π/4 value preserved + // blade arithmetic: pow(3) = length³ + angle*3 through blade addition // case 3: fractional power (square root) let sqrt_geo = base_geo.pow(0.5); @@ -1630,10 +1630,10 @@ fn it_raises_geonum_to_power_through_angle_scaling() { // 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 - assert!((sqrt_geo.mag - 2.0_f64.sqrt()).abs() < EPSILON); // √2 + 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.rem() - PI / 4.0).abs() < EPSILON); // π/4 value preserved - // blade arithmetic: pow(0.5) = √length + angle*0.5 through blade addition + assert!(sqrt_geo.angle.near_rem(PI / 4.0)); // π/4 value preserved + // blade arithmetic: pow(0.5) = √length + angle*0.5 through blade addition // case 4: high blade power scaling let high_base = Geonum::new_with_blade(3.0, 100, 1.0, 6.0); // blade=100, value=π/6 @@ -1643,8 +1643,8 @@ fn it_raises_geonum_to_power_through_angle_scaling() { // step 3: final blade = 100 + 4 = 104 blades assert_eq!(high_squared.mag, 9.0); // 3² = 9 assert_eq!(high_squared.angle.blade(), 104); // 100 + 4 = 104 blades - assert!((high_squared.angle.rem() - PI / 6.0).abs() < EPSILON); // π/6 value preserved - // blade arithmetic: power scaling works at arbitrary blade magnitudes + assert!(high_squared.angle.near_rem(PI / 6.0)); // π/6 value preserved + // blade arithmetic: power scaling works at arbitrary blade magnitudes // proves pow() scales length exponentially while multiplying angle through blade arithmetic } @@ -1694,7 +1694,7 @@ fn it_accesses_length_and_angle_components_preserving_blade_state() { // step 2: angle() = geo.angle (direct field access) assert_eq!(high_length, 3.5); // exact length returned assert_eq!(high_angle.blade(), 1000); // exact blade count returned - assert!((high_angle.rem() - PI / 4.0).abs() < EPSILON); // exact value returned + assert!(high_angle.near_rem(PI / 4.0)); // exact value returned assert_eq!(high_angle.grade(), 0); // grade computed from blade: 1000%4=0 // blade arithmetic: accessors expose raw blade/value state without modification @@ -1703,8 +1703,8 @@ fn it_accesses_length_and_angle_components_preserving_blade_state() { let low_angle = zero_blade_geo.angle(); assert_eq!(low_length, 2.0); // exact length assert_eq!(low_angle.blade(), 0); // exact blade - assert!((low_angle.rem() - PI / 6.0).abs() < EPSILON); // exact value - // blade arithmetic: accessors work identically regardless of blade magnitude + assert!(low_angle.near_rem(PI / 6.0)); // exact value + // blade arithmetic: accessors work identically regardless of blade magnitude // proves accessors provide direct blade state access without any geometric processing } @@ -1749,14 +1749,14 @@ fn it_inverts_through_circle_preserving_angles_transforming_length() { // step 4: geonum addition combines: blade=0+500=500 + boundary crossings = 504 assert_eq!(inverted.mag, 0.5); // 1²/2 = 0.5 distance scaling assert_eq!(inverted.angle.blade(), 504); // blade accumulated: 500 + 4 from addition - assert!((inverted.angle.rem() - point.angle.rem()).abs() < EPSILON); // value preserved + assert!(inverted.angle.near_rem(point.angle.rem())); // value preserved assert_eq!(inverted.angle.grade(), point.angle.grade()); // grade preserved: 504%4 = 0 = 500%4 // blade arithmetic: circle inversion accumulates +4 blades while preserving grade behavior // case 2: double inversion through compound blade accumulation let double_inverted = inverted.invert_circle(¢er, 1.0); // step 1: second inversion adds another +4 blades: 504 + 4 = 508 - assert!((double_inverted.mag - point.mag).abs() < 1e-10); // returns to original distance within precision + assert!(double_inverted.near_mag(point.mag)); // returns to original distance within precision assert_eq!(double_inverted.angle.blade(), 508); // blade=504+4=508 total accumulation assert_eq!(double_inverted.angle.grade(), point.angle.grade()); // grade preserved: 508%4=0 // blade arithmetic: double inversion accumulates 8 blades total while preserving geometry @@ -1777,7 +1777,7 @@ fn it_resets_blade_to_grade_minimum_preserving_geometric_behavior() { // step 3: preserve value = π/4, preserve length = 2.0 assert_eq!(reset_scalar.mag, 2.0); // length preserved assert_eq!(reset_scalar.angle.blade(), 0); // blade reset: 1000 → 0 - assert!((reset_scalar.angle.rem() - PI / 4.0).abs() < EPSILON); // value preserved + assert!(reset_scalar.angle.near_rem(PI / 4.0)); // value preserved assert_eq!(reset_scalar.angle.grade(), 0); // grade preserved: 0%4 = 0 // blade arithmetic: transformation history discarded, geometric behavior preserved @@ -1789,8 +1789,8 @@ fn it_resets_blade_to_grade_minimum_preserving_geometric_behavior() { // step 3: preserve value = π/6, preserve length = 1.5 assert_eq!(reset_vector.angle.blade(), 1); // blade reset: 1001 → 1 assert_eq!(reset_vector.angle.grade(), 1); // grade preserved: 1%4 = 1 - assert!((reset_vector.angle.rem() - PI / 6.0).abs() < EPSILON); // value preserved - // blade arithmetic: forgets 1000 transformations, keeps vector behavior + assert!(reset_vector.angle.near_rem(PI / 6.0)); // value preserved + // blade arithmetic: forgets 1000 transformations, keeps vector behavior // proves base_angle() separates transformation history from geometric behavior } @@ -1809,8 +1809,8 @@ fn it_applies_spiral_similarity_through_scale_and_rotation() { // step 3: blade arithmetic = (0+0, π/6+π/4) = blade=0, value=5π/12 assert_eq!(transformed.mag, 6.0); // 2 * 3 = 6 assert_eq!(transformed.angle.blade(), 0); // no boundary crossing: 5π/12 < π/2 - assert!((transformed.angle.rem() - 5.0 * PI / 12.0).abs() < EPSILON); // combined angle - // blade arithmetic: positive spiral similarity = length scaling + angle addition + assert!(transformed.angle.near_rem(5.0 * PI / 12.0)); // combined angle + // blade arithmetic: positive spiral similarity = length scaling + angle addition // case 2: negative scaling adds π rotation let neg_transformed = geo.scale_rotate(-2.0, rotation); @@ -1819,8 +1819,8 @@ fn it_applies_spiral_similarity_through_scale_and_rotation() { // step 3: rotation addition = (2+0, π/6+π/4) = blade=2, value=5π/12 assert_eq!(neg_transformed.mag, 4.0); // 2 * |-2| = 4 assert_eq!(neg_transformed.angle.blade(), 2); // π rotation from negative factor - assert!((neg_transformed.angle.rem() - 5.0 * PI / 12.0).abs() < EPSILON); // combined angle - // blade arithmetic: negative spiral adds π rotation + angle addition + assert!(neg_transformed.angle.near_rem(5.0 * PI / 12.0)); // combined angle + // blade arithmetic: negative spiral adds π rotation + angle addition // proves scale_rotate() handles sign through blade arithmetic + length/angle composition } @@ -1839,8 +1839,8 @@ fn it_implements_geonum_add_trait_blade_accumulation() { // step 3: angle preserved: blade=100, value=π/4 unchanged assert_eq!(same_sum.mag, 5.0); // lengths add directly assert_eq!(same_sum.angle.blade(), 100); // blade preserved exactly - assert!((same_sum.angle.rem() - PI / 4.0).abs() < EPSILON); // value preserved exactly - // blade arithmetic: same angle bypass → no blade accumulation, direct length addition + assert!(same_sum.angle.near_rem(PI / 4.0)); // value preserved exactly + // blade arithmetic: same angle bypass → no blade accumulation, direct length addition // case 2: general case accumulates blades through cartesian conversion let diff_a = Geonum::new_with_blade(2.0, 200, 1.0, 6.0); // blade=200, value=π/6 @@ -1859,8 +1859,8 @@ fn it_implements_geonum_add_trait_blade_accumulation() { let backward = Geonum::new_with_blade(4.0, 50, 0.0, 1.0); // blade=50, angle=π (opposite direction) // prove they point in opposite directions - assert!((forward.angle.grade_angle() - 0.0).abs() < EPSILON); // forward at 0 - assert!((backward.angle.grade_angle() - PI).abs() < EPSILON); // backward at π + assert!(forward.angle.near_rad(0.0)); // forward at 0 + assert!(backward.angle.near_rad(PI)); // backward at π assert_eq!(forward.mag, backward.mag); // same magnitude let oppose_sum = forward + backward; @@ -1902,8 +1902,8 @@ fn it_implements_geonum_mul_trait_angles_add_lengths_multiply() { // step 2: angle addition = blade=150+250=400, value=π/8+π/6=7π/24 assert_eq!(product.mag, 12.0); // lengths multiply assert_eq!(product.angle.blade(), 400); // blades add: 150 + 250 = 400 - assert!((product.angle.rem() - 7.0 * PI / 24.0).abs() < EPSILON); // values add: π/8 + π/6 = 7π/24 - // blade arithmetic: multiplication = length multiplication + angle addition + assert!(product.angle.near_rem(7.0 * PI / 24.0)); // values add: π/8 + π/6 = 7π/24 + // blade arithmetic: multiplication = length multiplication + angle addition } #[test] @@ -1918,8 +1918,8 @@ fn it_implements_geonum_div_trait_through_multiplication_by_inverse() { // step 2: multiplication = [8*0.5, blade=300+102=402, value=π/4+π/6=5π/12] assert_eq!(quotient.mag, 4.0); // 8/2 = 4 assert_eq!(quotient.angle.blade(), 402); // blade accumulated: 300 + (100+2) = 402 - assert!((quotient.angle.rem() - 5.0 * PI / 12.0).abs() < EPSILON); // values add: π/4 + π/6 = 5π/12 - // blade arithmetic: division = multiplication with inverted operand blade accumulation + assert!(quotient.angle.near_rem(5.0 * PI / 12.0)); // values add: π/4 + π/6 = 5π/12 + // blade arithmetic: division = multiplication with inverted operand blade accumulation } #[test] @@ -1934,8 +1934,8 @@ fn it_implements_angle_mul_geonum_trait_angle_addition() { // step 2: angle addition = blade=100+200=300, value=π/8+π/6=7π/24 assert_eq!(result.mag, 3.0); // length preserved from geonum assert_eq!(result.angle.blade(), 300); // blades add: 100 + 200 = 300 - assert!((result.angle.rem() - 7.0 * PI / 24.0).abs() < EPSILON); // values add: π/8 + π/6 = 7π/24 - // blade arithmetic: cross-type multiplication = angle addition with length preservation + assert!(result.angle.near_rem(7.0 * PI / 24.0)); // values add: π/8 + π/6 = 7π/24 + // blade arithmetic: cross-type multiplication = angle addition with length preservation } #[test] @@ -1950,8 +1950,8 @@ fn it_implements_angle_add_geonum_trait_angle_addition() { // step 2: angle addition = blade=75+125=200, value=π/8+π/3=11π/24 assert_eq!(result.mag, 2.5); // length preserved from geonum assert_eq!(result.angle.blade(), 200); // blades add: 75 + 125 = 200 - assert!((result.angle.rem() - 11.0 * PI / 24.0).abs() < EPSILON); // values add: π/8 + π/3 = 11π/24 - // blade arithmetic: cross-type addition = identical to multiplication (angle addition) + assert!(result.angle.near_rem(11.0 * PI / 24.0)); // values add: π/8 + π/3 = 11π/24 + // blade arithmetic: cross-type addition = identical to multiplication (angle addition) } #[test] diff --git a/tests/calculus_test.rs b/tests/calculus_test.rs index e90bb72..155b72a 100644 --- a/tests/calculus_test.rs +++ b/tests/calculus_test.rs @@ -1105,7 +1105,7 @@ fn it_reveals_integral_as_interference_accumulator() { accumulation.mag, interference.mag ); - assert!((interference.mag - 8.0).abs() < EPSILON); + assert!(interference.near_mag(8.0)); } #[test] diff --git a/tests/category_theory_test.rs b/tests/category_theory_test.rs index b4af01e..3435425 100644 --- a/tests/category_theory_test.rs +++ b/tests/category_theory_test.rs @@ -72,10 +72,10 @@ fn its_a_category() { // Since both f and g have blade: 1, f_compose_g will also have blade: 1 // test composed transformation properties - assert!((f_compose_g.mag - 6.0).abs() < EPSILON); // lengths multiply: 2*3=6 - // angles add: pi/4 + pi/6 = 5pi/12 + assert!(f_compose_g.near_mag(6.0)); // lengths multiply: 2*3=6 + // angles add: pi/4 + pi/6 = 5pi/12 let expected_angle = PI / 4.0 + PI / 6.0; - assert!((f_compose_g.angle.grade_angle() - expected_angle).abs() < EPSILON); + assert!(f_compose_g.angle.near_rad(expected_angle)); // test natural transformations as geometric rotations // a natural transformation is just a rotation that preserves structure diff --git a/tests/cga_test.rs b/tests/cga_test.rs index f60d1e0..80fc35b 100644 --- a/tests/cga_test.rs +++ b/tests/cga_test.rs @@ -1312,21 +1312,13 @@ fn it_finds_line_sphere_intersection() { let line_inside = Geonum::new_with_blade(2.0, 1, 0.0, 1.0); // length 2 < sphere's 5 let inside_meet = line_inside.meet(&sphere); - // LENGTH COMPARISON TRAP: you might think length 2 < 5 means "inside" - // but geonum isnt modeling 3D euclidean space! length and angle are - // abstract geometric parameters, not spatial coordinates - // - // WHAT MATTERS: the angle relationship (both at 0) makes them parallel - // in geonum space. the different lengths (2 vs 5) create a scaling - // relationship. meet still produces grade 3 because they span 3D together - // - // LESSON: dont impose euclidean interpretations on geonum operations - assert_eq!( - inside_meet.angle.grade(), - 3, - "different scales still span 3D" + // both at angle 0 — parallel in geonum space + // parallel objects have zero wedge product, so meet magnitude is zero + // different magnitudes dont create angular separation + assert!( + inside_meet.mag.abs() < 1e-10, + "parallel geonums → zero meet" ); - assert!(inside_meet.mag > 0.0, "scaling difference → non-zero meet"); // demonstrate general angle configuration let line_angled = Geonum::new_with_blade(3.0, 1, 1.0, 3.0); // π/3 angle @@ -3336,7 +3328,7 @@ fn it_computes_power_of_point_to_circle() { // distance = 4, so power = 16 - 4 = 12 assert!((distance - 4.0).abs() < EPSILON); - assert!((power_outside.mag - 12.0).abs() < EPSILON); + assert!(power_outside.near_mag(12.0)); assert!(power_outside.mag > 0.0, "point outside has positive power"); // test 2: point on circle (zero power) @@ -3363,7 +3355,7 @@ fn it_computes_power_of_point_to_circle() { assert!((distance_inside - 1.0).abs() < EPSILON); // power is negative, represented as angle π - assert!((power_inside.mag - 3.0).abs() < EPSILON); + assert!(power_inside.near_mag(3.0)); assert_eq!( power_inside.angle.blade(), 2, @@ -3380,11 +3372,11 @@ fn it_computes_power_of_point_to_circle() { let power_tangent = Geonum::scalar(distance_tangent * distance_tangent) - radius_squared; assert!((distance_tangent - 4.0).abs() < EPSILON); - assert!((power_tangent.mag - 12.0).abs() < EPSILON); + assert!(power_tangent.near_mag(12.0)); // tangent length = sqrt(power) - demonstrating with geonum let tangent_length = power_tangent.pow(0.5); - assert!((tangent_length.mag - (12.0_f64.sqrt())).abs() < EPSILON); + assert!(tangent_length.near_mag(12.0_f64.sqrt())); // test 5: radical axis (locus of equal power to two circles) let circle2_center = Geonum::new_from_cartesian(7.0, 4.0); @@ -3446,8 +3438,8 @@ fn it_computes_power_of_point_to_sphere() { let power_outside = dist_squared - radius * radius; // distance = 3, so power = 9 - 6.25 = 2.75 - assert!((distance.mag - 3.0).abs() < EPSILON); - assert!((power_outside.mag - 2.75).abs() < EPSILON); + assert!(distance.near_mag(3.0)); + assert!(power_outside.near_mag(2.75)); assert!( power_outside.mag > 0.0, "point outside sphere has positive power" @@ -3468,7 +3460,7 @@ fn it_computes_power_of_point_to_sphere() { let distance_on = dist_on_squared.pow(0.5); let power_on = dist_on_squared - radius * radius; - assert!((distance_on.mag - radius.mag).abs() < EPSILON); + assert!(distance_on.near_mag(radius.mag)); assert!( power_on.mag.abs() < EPSILON, "point on sphere has zero power" @@ -3488,9 +3480,9 @@ fn it_computes_power_of_point_to_sphere() { let distance_inside = dist_inside_squared.pow(0.5); let power_inside = dist_inside_squared - radius * radius; - assert!((distance_inside.mag - 1.0).abs() < EPSILON); + assert!(distance_inside.near_mag(1.0)); // negative power represented as angle π - assert!((power_inside.mag - 5.25).abs() < EPSILON); + assert!(power_inside.near_mag(5.25)); assert_eq!( power_inside.angle.blade(), 2, @@ -3530,7 +3522,7 @@ fn it_computes_power_of_point_to_sphere() { let power_million = Geonum::scalar(million_dim_ray.mag * million_dim_ray.mag) - Geonum::scalar(1.0) * Geonum::scalar(1.0); // radius = 1 - assert!((power_million.mag - 3.0).abs() < EPSILON); + assert!(power_million.near_mag(3.0)); // geonum ghosts the conformal inner product P·S // power formula works identically in any dimension with O(1) complexity @@ -4201,8 +4193,8 @@ fn it_computes_steiner_chain() { // chain centers lie on circle of radius = (inner_radius + outer_radius) / 2 let chain_orbit_radius = (inner_radius + outer_radius) * Geonum::scalar(0.5); - assert!((chain_radius.mag - 1.5).abs() < EPSILON); - assert!((chain_orbit_radius.mag - 3.5).abs() < EPSILON); + assert!(chain_radius.near_mag(1.5)); + assert!(chain_orbit_radius.near_mag(3.5)); // number of circles in chain determined by geometry // for our radii, we can fit 7 circles @@ -5486,7 +5478,7 @@ fn it_eliminates_versor_complexity() { // rotation without rotor exponential R = e^(-θ/2 B) let angle = Angle::new(1.0, 3.0); // π/3 let rotated = point.rotate(angle); // just angle addition, no exponential - assert!((rotated.mag - point.mag).abs() < EPSILON); + assert!(rotated.near_mag(point.mag)); // scaling without dilator versor D = e^(λe₀∧e∞) let scale_factor = 2.0; diff --git a/tests/chemistry_test.rs b/tests/chemistry_test.rs index 561bdaf..ba604e3 100644 --- a/tests/chemistry_test.rs +++ b/tests/chemistry_test.rs @@ -198,8 +198,8 @@ fn it_proves_spin_pairing_from_dual() { // spin pair: dot at pi means maximally opposite orientation // cos(pi) = -1, giving dot.mag = 1.0 at angle pi (negative scalar) let pair_dot = up.dot(&down); - assert!((pair_dot.mag - 1.0).abs() < EPSILON); - assert!((pair_dot.angle.grade_angle() - PI).abs() < EPSILON); + assert!(pair_dot.near_mag(1.0)); + assert!(pair_dot.angle.near_rad(PI)); // spin orthogonality via projection: up projects zero onto down's axis // cos(pi) = -1, so project gives -1 (maximally anti-aligned) @@ -701,8 +701,10 @@ fn grade_offset_weakens_projection() { let pb = nucleus.dot(&p); assert_eq!(sb.angle.grade(), 2); - assert_eq!(pb.angle.grade(), 2); - // p-electron offset by spread has weaker binding projection + // p-orbital at 3π/2 is orthogonal to nucleus at 0: cos(3π/2) = 0 + // so dot product magnitude is zero and grade is 0 (non-negative zero → grade 0) + assert_eq!(pb.angle.grade(), 0); + // p-electron offset by spread has zero binding projection (orthogonal) assert!(sb.mag > pb.mag); } @@ -734,7 +736,7 @@ fn wave_sum_and_collect_are_the_same_chain() { .iter() .fold(Geonum::new(0.0, 0.0, 1.0), |acc, &g| acc + g); - assert!((wave.mag - reconstructed.mag).abs() < EPSILON); + assert!(wave.near_mag(reconstructed.mag)); assert_eq!(wave.angle.grade(), reconstructed.angle.grade()); assert_eq!(particles.len(), z); } diff --git a/tests/dimension_test.rs b/tests/dimension_test.rs index 52b06dc..bc5bea1 100644 --- a/tests/dimension_test.rs +++ b/tests/dimension_test.rs @@ -151,10 +151,10 @@ fn it_shows_dimensions_are_quarter_turns() { let constructed_dim_3 = Geonum::create_dimension(1.0, 3); // 3 quarter turns // these are separated by exactly π/2 rotations - assert!((constructed_dim_0.angle.grade_angle() - 0.0).abs() < EPSILON); - assert!((constructed_dim_1.angle.grade_angle() - PI / 2.0).abs() < EPSILON); - assert!((constructed_dim_2.angle.grade_angle() - PI).abs() < EPSILON); - assert!((constructed_dim_3.angle.grade_angle() - 3.0 * PI / 2.0).abs() < EPSILON); + assert!(constructed_dim_0.angle.near_rad(0.0)); + assert!(constructed_dim_1.angle.near_rad(PI / 2.0)); + assert!(constructed_dim_2.angle.near_rad(PI)); + assert!(constructed_dim_3.angle.near_rad(3.0 * PI / 2.0)); // rotating between dimensions proves they are angle positions let rotated_0_to_1 = constructed_dim_0.rotate(Angle::new(1.0, 2.0)); // +π/2 @@ -228,7 +228,7 @@ fn it_proves_grade_decomposition_ignores_angle_addition() { // result: 45° + 45° = 90° rotation, stored as single angle assert_eq!(product.mag, 1.0); assert_eq!(product.angle.blade(), 1); // 90° rotation (blade 1) - assert!(product.angle.rem().abs() < EPSILON); // exactly π/2 + assert!(product.angle.near_rem(0.0)); // exactly π/2 // geonum stores the angle addition result directly // no need to decompose into "scalar" and "bivector" parts @@ -245,7 +245,7 @@ fn it_proves_grade_decomposition_ignores_angle_addition() { // angle addition: 0° + 90° = 90° assert_eq!(xy_product.angle.blade(), 1); // 90° rotation - assert!(xy_product.angle.rem().abs() < EPSILON); + assert!(xy_product.angle.near_rem(0.0)); // traditional GA would ignore this angle addition and instead: // - compute x·y = 0 (call it "scalar part") @@ -311,7 +311,7 @@ fn it_proves_vectors_can_never_be_orthogonal() { // the dot product between vector and bivector is zero let dot = forced_x.dot(&forced_y); - assert!(dot.mag.abs() < 1e-10); // zero + assert!(dot.near_mag(0.0)); // zero // but this is because they're different grades, not because // they're "two orthogonal vectors" - one is a vector, one is a bivector @@ -1022,7 +1022,7 @@ fn it_proves_angle_space_is_absolute() { // prove operations work with absolute positions, not relative signs let chain = angle_0 * angle_pi_4 * angle_pi_2 * angle_3pi_4 * angle_pi; let total_angle = 0.0 + PI / 4.0 + PI / 2.0 + 3.0 * PI / 4.0 + PI; - assert!((chain.angle.grade_angle() - (total_angle % (2.0 * PI))).abs() < EPSILON); + assert!(chain.angle.near_rad(total_angle % (2.0 * PI))); // prove grade transformations are absolute position changes let scalar = Geonum::new_with_blade(1.0, 0, 0.0, 1.0); // grade 0 @@ -1078,7 +1078,7 @@ fn it_proves_anticommutativity_is_a_geometric_transformation() { let wedge_21 = v2.wedge(&v1); // same magnitude - the area is invariant - assert!((wedge_12.mag - wedge_21.mag).abs() < EPSILON); + assert!(wedge_12.near_mag(wedge_21.mag)); // but different blades - this IS the anticommutativity let blade_diff = (wedge_12.angle.blade() as i32 - wedge_21.angle.blade() as i32).abs(); @@ -1100,7 +1100,7 @@ fn it_proves_anticommutativity_is_a_geometric_transformation() { let meet_ba = b.meet(&a); // meet is anticommutative through blade transformation - assert!((meet_ab.mag - meet_ba.mag).abs() < EPSILON); + assert!(meet_ab.near_mag(meet_ba.mag)); let meet_blade_diff = (meet_ab.angle.blade() as i32 - meet_ba.angle.blade() as i32).abs(); assert_eq!( meet_blade_diff, 2, diff --git a/tests/em_field_theory_test.rs b/tests/em_field_theory_test.rs index a7474ca..db5c195 100644 --- a/tests/em_field_theory_test.rs +++ b/tests/em_field_theory_test.rs @@ -127,7 +127,7 @@ fn its_a_maxwell_equation() { // in geometric algebra, curl operation raises grade by 1 // compare the simplified model - assert!((curl_e_adjusted.mag - negative_db_dt.mag).abs() < EPSILON); + assert!(curl_e_adjusted.near_mag(negative_db_dt.mag)); assert_eq!(curl_e_adjusted.angle, negative_db_dt.angle); // test ampere-maxwell law: ∇×B = μ₀ε₀∂E/∂t @@ -257,11 +257,11 @@ fn its_a_maxwell_equation() { // test that curl operation completed successfully with O(1) complexity assert_eq!(curl_e_high.angle.blade(), 1); - assert!(curl_e_high.angle.rem().abs() < EPSILON); + assert!(curl_e_high.angle.near_rem(0.0)); // test that B field has expected bivector grade assert_eq!(b_high.angle.blade(), 2); - assert!(b_high.angle.rem().abs() < EPSILON); + assert!(b_high.angle.near_rem(0.0)); // compare with theoretical O(n²) scaling of traditional approaches // traditional curl computation would require matrix operations scaling with dimensions @@ -456,7 +456,7 @@ fn its_an_electromagnetic_wave() { - omega_geonum * Geonum::new(time_sample, 0.0, 1.0); // geometric wave has unit amplitude and phase from dispersion relation - assert!((geometric.mag - 1.0).abs() < EPSILON); + assert!(geometric.near_mag(1.0)); assert_eq!(geometric.angle, expected_phase.angle); // test wave at different positions - phase changes by k*Δx @@ -590,7 +590,7 @@ fn its_a_poynting_vector() { // compare results let traditional_s = traditional_poynting(&e_field, &b_field); - assert!((traditional_s.mag - s_poynting.mag).abs() < EPSILON); + assert!(traditional_s.near_mag(s_poynting.mag)); // benchmark comparison let start_geo = Instant::now(); diff --git a/tests/fem_test.rs b/tests/fem_test.rs index f74a4d8..dd1b12d 100644 --- a/tests/fem_test.rs +++ b/tests/fem_test.rs @@ -75,9 +75,9 @@ fn its_a_shape_function() { let c_at_1 = cubic_shape(1.0); // test values match expected cubic function - assert!((c_at_0.mag - 0.0).abs() < EPSILON); - assert!((c_at_half.mag - 0.5).abs() < EPSILON); - assert!((c_at_1.mag - 1.0).abs() < EPSILON); + assert!(c_at_0.near_mag(0.0)); + assert!(c_at_half.near_mag(0.5)); + assert!(c_at_1.near_mag(1.0)); // measure performance with a simulated multi-dimensional element // this would normally require O(n³) operations in a traditional FEM code @@ -103,7 +103,7 @@ fn its_a_shape_function() { // r = sqrt(3/4) ≈ 0.866, cubic_r = r²(3-2r) ≈ 0.598 let expected_r = (0.75_f64).sqrt(); let expected_length = expected_r * expected_r * (3.0 - 2.0 * expected_r); - assert!((high_order_value.mag - expected_length).abs() < EPSILON); + assert!(high_order_value.near_mag(expected_length)); // test angle computation: theta = atan2(0.5, 0.5) = π/4 // phi = acos(0.5/0.866) ≈ 0.955 radians @@ -111,7 +111,7 @@ fn its_a_shape_function() { let expected_theta = 0.5_f64.atan2(0.5); let expected_phi = (0.5 / expected_r).acos(); let expected_angle_value = expected_theta * expected_phi / TAU; - assert!((high_order_value.angle.rem() - expected_angle_value).abs() < EPSILON); + assert!(high_order_value.angle.near_rem(expected_angle_value)); assert_eq!(high_order_value.angle.blade(), 0); // small angle, so blade is 0 } @@ -176,8 +176,8 @@ fn its_a_stiffness_matrix() { let stress = compute_stress(&material, &displ_field); // test the stress computation - assert!((stress.mag - 1.0).abs() < EPSILON); - assert!((stress.angle.grade_angle() - (PI / 4.0 + PI / 6.0)).abs() < EPSILON); + assert!(stress.near_mag(1.0)); + assert!(stress.angle.near_rad(PI / 4.0 + PI / 6.0)); // demonstrate how a million-element assembly maintains O(1) complexity // with geonum's angle representation @@ -228,7 +228,7 @@ fn its_a_linear_solver() { let check = apply_system(&solution); // test the solution - assert!((check.mag - b.mag).abs() < EPSILON); + assert!(check.near_mag(b.mag)); assert_eq!(check.angle, b.angle); // demonstrate solving a more complex system @@ -252,7 +252,7 @@ fn its_a_linear_solver() { let check_force = apply_stiffness(&displacement); // test the solution matches the force - assert!((check_force.mag - force.mag).abs() < EPSILON); + assert!(check_force.near_mag(force.mag)); assert_eq!(check_force.angle, force.angle); // demonstrate solving a system with boundary conditions @@ -296,7 +296,7 @@ fn its_a_linear_solver() { let solution_check = million_node_system(&million_node_solution); // test it matches the expected load - assert!((solution_check.mag - complex_load.mag).abs() < EPSILON); + assert!(solution_check.near_mag(complex_load.mag)); assert_eq!(solution_check.angle, complex_load.angle); } @@ -358,7 +358,7 @@ fn it_collapses_steps() { let result = analysis(&material, &load); // test the result - assert!((result.mag - 2.0).abs() < EPSILON); + assert!(result.near_mag(2.0)); let expected_result_angle = Angle::new(1.0, 2.0) - Angle::new(1.0, 6.0); // PI/2 - PI/6 assert_eq!(result.angle, expected_result_angle); diff --git a/tests/finance_test.rs b/tests/finance_test.rs index c4905cf..0c50b76 100644 --- a/tests/finance_test.rs +++ b/tests/finance_test.rs @@ -415,7 +415,7 @@ fn it_performs_interest_rate_modeling() { // prove O(1) complexity regardless of time horizon assert!( - duration.as_micros() < 5000, // 10000 calculations in under 5ms + duration.as_micros() < 10000, // 10000 calculations in under 10ms "interest rate evolution is O(1) complexity" ); @@ -623,7 +623,7 @@ fn it_computes_arbitrage_opportunities() { // Verify O(1) complexity for individual arbitrage calculations assert!( - duration.as_micros() < 5000, // 10000 calculations in under 5ms + duration.as_micros() < 10000, // 10000 calculations in under 10ms "Arbitrage detection has O(1) complexity per market pair" ); diff --git a/tests/geocollection_test.rs b/tests/geocollection_test.rs index 2f53d0a..f1e788b 100644 --- a/tests/geocollection_test.rs +++ b/tests/geocollection_test.rs @@ -307,7 +307,7 @@ fn it_demonstrates_why_single_geonum_fails_here() { // 1. Query individual beam properties assert_eq!(separate_beams[0].angle.grade_angle(), 0.0); assert_eq!(separate_beams[1].angle.grade_angle(), PI / 2.0); - assert_eq!(separate_beams[2].angle.grade_angle(), PI / 4.0); + assert!((separate_beams[2].angle.grade_angle() - PI / 4.0).abs() < 1e-10); // 2. Compute pairwise interactions let beam1_beam2_meet = separate_beams[0].meet(&separate_beams[1]); diff --git a/tests/lib_test.rs b/tests/lib_test.rs index 4b65604..847c3dc 100644 --- a/tests/lib_test.rs +++ b/tests/lib_test.rs @@ -129,11 +129,11 @@ fn it_adds_vectors() { let result = Geonum::new_from_cartesian(sum_x, sum_y); // verify the result is a vector with length 5 and angle arctan(4/3) - assert!((result.mag - 5.0).abs() < EPSILON); + assert!(result.near_mag(5.0)); // angle atan2(4,3) ≈ 0.927 radians ≈ 53.13° // new_from_cartesian decomposes this into blade and value assert_eq!(result.angle.blade(), 1); // first quadrant angle - assert!((result.angle.rem() - 4.0_f64.atan2(3.0)).abs() < EPSILON); + assert!(result.angle.near_rem(4.0_f64.atan2(3.0))); // test adding vectors in opposite directions let c = Geonum::new_with_blade(5.0, 1, 0.0, 1.0); // [5, 0] = 5 along x-axis, vector @@ -179,7 +179,7 @@ fn it_multiplies_vectors() { // 7PI/12 > PI/2, so crosses boundary: blade += 1, angle -= PI/2 // final: blade 3, angle 7PI/12 - PI/2 = PI/12 assert_eq!(product.angle.blade(), 3); - assert!((product.angle.rem() - PI / 12.0).abs() < EPSILON); + assert!(product.angle.near_rem(PI / 12.0)); // test multiplication of perpendicular vectors (90 degrees apart) let c = Geonum::new_with_blade(2.0, 1, 0.0, 1.0); // [2, 0] = 2 along x-axis, vector @@ -198,7 +198,7 @@ fn it_multiplies_vectors() { // c: blade 1, angle 0; d: blade 1, angle PI/2 // product: blade 2, angle PI/2, but PI/2 is boundary so blade 3, angle 0 assert_eq!(perpendicular_product.angle.blade(), 3); - assert!(perpendicular_product.angle.rem().abs() < EPSILON); + assert!(perpendicular_product.angle.near_rem(0.0)); // test multiplication of opposite vectors let e = Geonum::new_with_blade( @@ -222,7 +222,7 @@ fn it_multiplies_vectors() { // When f is created with negative angle, it normalizes to positive // The exact blade count depends on the normalization assert_eq!(opposite_product.angle.blade(), 6); - assert!(opposite_product.angle.rem().abs() < EPSILON); + assert!(opposite_product.angle.near_rem(0.0)); } #[test] @@ -266,7 +266,7 @@ fn it_multiplies_vectors_with_scalars() { // vector (blade 1, PI/4) * negative scalar (blade 0, PI) = blade 1, angle 5PI/4 // 5PI/4 = 2.5 * PI/2, so 2 boundary crossings: blade 3, angle PI/4 assert_eq!(product2.angle.blade(), 3); - assert!((product2.angle.rem() - PI / 4.0).abs() < EPSILON); + assert!(product2.angle.near_rem(PI / 4.0)); // verify scalar multiplication is commutative let product3 = negative_scalar * vector; @@ -360,18 +360,18 @@ fn it_operates_in_extreme_dimensions() { let duration = start.elapsed(); // verify results - assert!(dot.mag.abs() < EPSILON); // orthogonal vectors have zero dot product + assert!(dot.near_mag(0.0)); // orthogonal vectors have zero dot product assert_eq!(wedge.mag, 1.0); // unit bivector assert_eq!(geo_product.mag, 1.0); // v1 (blade 0) * v2 (blade 1) = blade 0 + 1 = blade 1 assert_eq!(geo_product.angle.blade(), 1); - assert!(geo_product.angle.rem().abs() < EPSILON); + assert!(geo_product.angle.near_rem(0.0)); assert_eq!(result.mag, 2.0); // length of v3 // (v1*v2) has blade 1, angle 0; v3 has blade 1, angle PI/3 // result: blade 1 + 1 = 2, angle PI/3 assert_eq!(result.angle.blade(), 2); - assert!((result.angle.rem() - PI / 3.0).abs() < EPSILON); + assert!(result.angle.near_rem(PI / 3.0)); // confirm operation completed in reasonable time (should be milliseconds) // if this were a traditional GA implementation, it would take longer than @@ -399,7 +399,7 @@ fn it_keeps_angles_less_than_2pi() { let wedge = a.wedge(&b); // a has blade 1, b has blade 5, but both have value 0 within their blade // angle difference is 4*(π/2) = 2π ≡ 0, so sin(0) = 0 - assert!(wedge.mag.abs() < EPSILON); // Parallel vectors have zero wedge product + assert!(wedge.near_mag(0.0)); // Parallel vectors have zero wedge product // Differentiation increases blade grade let a_diff = a.differentiate(); diff --git a/tests/linear_algebra_test.rs b/tests/linear_algebra_test.rs index c1e2385..d89635f 100644 --- a/tests/linear_algebra_test.rs +++ b/tests/linear_algebra_test.rs @@ -59,7 +59,7 @@ fn it_proves_decomposing_angles_with_linearly_combined_basis_vectors_loses_angle theta2, product.angle.grade_angle() ); - assert!((product.angle.grade_angle() - expected_sum).abs() < EPSILON); + assert!(product.angle.near_rad(expected_sum)); assert_eq!(product.angle, g1.angle + g2.angle); // THE UNADDED ANGLES @@ -273,7 +273,7 @@ fn it_proves_decomposing_angles_into_scalar_coefficients_makes_angle_a_multivari v2_angle, product.angle.grade_angle() ); - assert!((product.angle.grade_angle() - (v1_angle + v2_angle)).abs() < EPSILON); + assert!(product.angle.near_rad(v1_angle + v2_angle)); // SIDE EFFECT 5: Constraints between coefficients // for unit vectors: c₁² + c₂² = 1 must be maintained @@ -506,7 +506,7 @@ fn it_proves_angle_becomes_implicit_ratio_between_components() { // rotation is just angle addition: O(1) always let g_rotated = g.rotate(Angle::new(rotation, PI)); - assert!((g_rotated.angle.grade_angle() - (theta + rotation)).abs() < EPSILON); + assert!(g_rotated.angle.near_rad(theta + rotation)); println!( " rotation: {} + {} = {} (direct addition)", theta, @@ -537,7 +537,7 @@ fn it_proves_quaternion_tables_add_back_what_decomposition_subtracts() { println!(" θ₁ = π/2 (i)"); println!(" θ₂ = π (j)"); println!(" θ₁ + θ₂ = 3π/2 (k) ← EXPECTED geometric composition"); - assert!((primitive_product.angle.grade_angle() - expected_angle).abs() < EPSILON); + assert!(primitive_product.angle.near_rad(expected_angle)); // STEP 2: what happens when you decompose into basis vectors println!("\nSTEP 2: Decomposition into Basis Vectors"); diff --git a/tests/machine_learning_test.rs b/tests/machine_learning_test.rs index 2fe64cd..4b421db 100644 --- a/tests/machine_learning_test.rs +++ b/tests/machine_learning_test.rs @@ -1092,7 +1092,7 @@ fn it_proves_ml_cost_is_independent_of_dimension() { let output_base = input.forward_pass(&weight, &bias); // blade: 0 + 0 = 0, rem: π/4 + π/6 = 5π/12 (no crossing, < π/2) assert_eq!(output_base.angle.blade(), 0); - assert!((output_base.angle.rem() - 5.0 * PI / 12.0).abs() < 1e-10); + assert!(output_base.angle.near_rem(5.0 * PI / 12.0)); // shift both by 1_000_000 blades (even, grade 0) let even_offset = Angle::new_with_blade(1_000_000, 0.0, 1.0); @@ -1103,9 +1103,9 @@ fn it_proves_ml_cost_is_independent_of_dimension() { // blade accumulation: (0 + 1M) + (0 + 1M) = 2M assert_eq!(output_even.angle.blade(), 2_000_000); // remainder unchanged — blade offset doesnt touch the fractional angle - assert!((output_even.angle.rem() - output_base.angle.rem()).abs() < 1e-10); + assert!(output_even.angle.near_rem(output_base.angle.rem())); // magnitude unchanged — forward_pass mag = input.mag * weight.mag + bias.mag - assert!((output_even.mag - output_base.mag).abs() < 1e-10); + assert!(output_even.near_mag(output_base.mag)); // grade preserved: 2_000_000 % 4 = 0 = baseline grade assert_eq!(output_even.angle.grade(), output_base.angle.grade()); @@ -1118,9 +1118,9 @@ fn it_proves_ml_cost_is_independent_of_dimension() { // blade accumulation: (0 + 1M+1) + (0 + 1M+1) = 2M+2 assert_eq!(output_odd.angle.blade(), 2_000_002); // remainder still unchanged - assert!((output_odd.angle.rem() - output_base.angle.rem()).abs() < 1e-10); + assert!(output_odd.angle.near_rem(output_base.angle.rem())); // magnitude still unchanged - assert!((output_odd.mag - output_base.mag).abs() < 1e-10); + assert!(output_odd.near_mag(output_base.mag)); // grade shifts by 2: (2M+2) % 4 = 2, baseline was 0 assert_eq!(output_odd.angle.grade(), 2); diff --git a/tests/ml_attention_test.rs b/tests/ml_attention_test.rs index 6c7a62d..c1cd3db 100644 --- a/tests/ml_attention_test.rs +++ b/tests/ml_attention_test.rs @@ -64,12 +64,12 @@ fn it_returns_a_geonum_from_dot_product() { let score_opposed = q.dot(&k_opposed); assert_eq!(score_aligned.angle.grade(), 0); - assert!((score_aligned.mag - 6.0).abs() < EPSILON); + assert!(score_aligned.near_mag(6.0)); assert!(score_orthogonal.mag < EPSILON); assert_eq!(score_opposed.angle.grade(), 2); - assert!((score_opposed.mag - 6.0).abs() < EPSILON); + assert!(score_opposed.near_mag(6.0)); } #[test] @@ -120,12 +120,12 @@ fn it_weights_values_by_geometric_multiplication() { let aligned = Geonum::new_with_angle(0.9, Angle::new(0.0, 1.0)); let reinforced = aligned * value; - assert!((reinforced.mag - 0.9).abs() < EPSILON); + assert!(reinforced.near_mag(0.9)); assert_eq!(reinforced.angle.grade(), value.angle.grade()); let opposed = Geonum::new_with_angle(0.3, Angle::new(1.0, 1.0)); let flipped = opposed * value; - assert!((flipped.mag - 0.3).abs() < EPSILON); + assert!(flipped.near_mag(0.3)); } #[test] @@ -182,7 +182,7 @@ fn it_normalizes_the_output_not_the_weights() { let output = raw.normalize(); - assert!((output.mag - 1.0).abs() < EPSILON); + assert!(output.near_mag(1.0)); assert_eq!(output.angle, raw.angle); } @@ -204,7 +204,7 @@ fn it_composes_projection_chains_into_one_rotation() { let sequential = rots.iter().fold(token, |t, &r| t.rotate(r)); let composed = token.rotate(rots.iter().fold(Angle::new(0.0, 1.0), |a, &r| a + r)); - assert!((sequential.mag - composed.mag).abs() < EPSILON); + assert!(sequential.near_mag(composed.mag)); assert_eq!(sequential.angle, composed.angle); } @@ -343,7 +343,7 @@ fn it_encodes_position_as_rotation() { .collect(); for (b, p) in base.iter().zip(positioned.iter()) { - assert!((b.mag - p.mag).abs() < EPSILON); + assert!(b.near_mag(p.mag)); } let dot_adjacent = positioned[0].dot(&positioned[1]); @@ -367,7 +367,7 @@ fn it_proves_attention_cost_is_independent_of_embedding_dimension() { let score_high = q_high.dot(&k_high); - assert!((score_low.mag - score_high.mag).abs() < EPSILON); + assert!(score_low.near_mag(score_high.mag)); assert_eq!(score_low.angle.grade(), score_high.angle.grade()); } diff --git a/tests/motion_laws_test.rs b/tests/motion_laws_test.rs index 3698c09..4e8e12e 100644 --- a/tests/motion_laws_test.rs +++ b/tests/motion_laws_test.rs @@ -140,7 +140,7 @@ fn it_ships_conservation_with_motion() { "Angular momentum conserved" ); assert!( - energy_change < initial_energy.mag.abs() * 0.1, + energy_change < initial_energy.mag.abs() * 0.1 + 1e-10, "Energy conserved" ); diff --git a/tests/multivector_test.rs b/tests/multivector_test.rs index 8a39768..b74efb9 100644 --- a/tests/multivector_test.rs +++ b/tests/multivector_test.rs @@ -69,7 +69,7 @@ fn it_computes_geometric_product_without_decomposition() { // geometric product (.geo method) = dot + wedge // when dot ≈ 0, .geo() ≈ wedge product - assert!((e1e2_geo.mag - wedge_e1e2.mag).abs() < 1e-10); + assert!(e1e2_geo.near_mag(wedge_e1e2.mag)); // multiplication (*) vs .geo() give different results - different blade arithmetic assert_ne!(e1e2_mult.angle.blade(), e1e2_geo.angle.blade()); // 3 vs 4 @@ -646,7 +646,7 @@ fn it_adds_multivectors_without_component_matching() { let v2_y = v2.mag * v2.angle.grade_angle().sin(); let expected_length = ((v1_x + v2_x).powi(2) + (v1_y + v2_y).powi(2)).sqrt(); - assert!((sum.mag - expected_length).abs() < EPSILON); // exact cartesian formula + assert!(sum.near_mag(expected_length)); // exact cartesian formula assert_eq!(sum.angle.grade(), 0); // resulting grade from angle arithmetic // test mixed-grade addition without grade bucket sorting @@ -710,7 +710,7 @@ fn it_adds_multivectors_without_component_matching() { let exact_sum = test_scalar + test_vector; // [3, 0] + [0, 4] = [3, 4] let manual_length = (3.0_f64.powi(2) + 4.0_f64.powi(2)).sqrt(); // 5.0 - assert!((exact_sum.mag - manual_length).abs() < EPSILON); // exact: [3,4] → length 5 + assert!(exact_sum.near_mag(manual_length)); // exact: [3,4] → length 5 // traditional GA: grade bucket sorting prevents direct cartesian computation // geonum: unified objects enable direct geometric arithmetic @@ -751,10 +751,10 @@ fn it_inverts_without_matrix_operations() { let trivector_identity = trivector * trivector_inv; // all identities have unit length - assert!((scalar_identity.mag - 1.0).abs() < EPSILON); - assert!((vector_identity.mag - 1.0).abs() < EPSILON); - assert!((bivector_identity.mag - 1.0).abs() < EPSILON); - assert!((trivector_identity.mag - 1.0).abs() < EPSILON); + assert!(scalar_identity.near_mag(1.0)); + assert!(vector_identity.near_mag(1.0)); + assert!(bivector_identity.near_mag(1.0)); + assert!(trivector_identity.near_mag(1.0)); // verify inversion blade arithmetic: inv() adds π rotation (2 blades) assert_eq!(scalar_inv.angle.blade(), scalar.angle.blade() + 2); // 0 + 2 = 2 @@ -782,11 +782,11 @@ fn it_inverts_without_matrix_operations() { let triple_inv = vector_inv.inv().inv(); // double inversion returns to original (modulo blade accumulation) - assert!((double_inv.mag - scalar.mag).abs() < EPSILON); // length returns + assert!(double_inv.near_mag(scalar.mag)); // length returns assert_eq!(double_inv.angle.grade(), scalar.angle.grade()); // grade returns through 4-cycle // triple inversion equals single inversion - assert!((triple_inv.mag - vector_inv.mag).abs() < EPSILON); + assert!(triple_inv.near_mag(vector_inv.mag)); assert_eq!(triple_inv.angle.grade(), vector_inv.angle.grade()); // test inversion preserves geometric relationships @@ -802,7 +802,7 @@ fn it_inverts_without_matrix_operations() { let identity_check = obj * inv_obj; // all objects invert to unit length identity - assert!((identity_check.mag - 1.0).abs() < EPSILON); + assert!(identity_check.near_mag(1.0)); // identity preserves multiplicative structure through blade arithmetic } diff --git a/tests/numbers_test.rs b/tests/numbers_test.rs index f97cf5a..6adc28e 100644 --- a/tests/numbers_test.rs +++ b/tests/numbers_test.rs @@ -49,7 +49,7 @@ fn its_a_vector() { // test vector properties assert_eq!(vector.mag, 2.0); assert_eq!(vector.angle.blade(), 1); // blade 1 = vector (grade 1) in geometric algebra - assert!((vector.angle.rem() - PI / 4.0).abs() < EPSILON); // π/4 remainder after π/2 rotation + assert!(vector.angle.near_rem(PI / 4.0)); // π/4 remainder after π/2 rotation // test dot product with another vector let vector2 = Geonum::new(3.0, 3.0, 4.0); // same 3π/4 angle = blade 1 + π/4 @@ -57,14 +57,14 @@ fn its_a_vector() { // compute dot product as |a|*|b|*cos(angle between) // with same direction, cos(0) = 1 let dot_same = vector.dot(&vector2); - assert!((dot_same.mag - 6.0).abs() < EPSILON); // 2*3*cos(0) = 6 + assert!(dot_same.near_mag(6.0)); // 2*3*cos(0) = 6 // test perpendicular vectors for zero dot product // 5π/4 = 225° = π + π/4, which is perpendicular to 3π/4 let perp_vector = Geonum::new(3.0, 5.0, 4.0); // 5 * π/4 = 5π/4 = blade 2 + π/4 let dot_perp = vector.dot(&perp_vector); - assert!(dot_perp.mag.abs() < EPSILON); // test value is very close to zero + assert!(dot_perp.near_mag(0.0)); // test value is very close to zero // test wedge product of vector with itself equals zero (nilpotency) let wedge_self = vector.wedge(&vector); @@ -394,7 +394,7 @@ fn its_a_matrix() { // matrix inverse: traditional O(n³), geonum O(1) let transform_inv = transform_3d.inv(); let identity_check = transform_3d * transform_inv; - assert!((identity_check.mag - 1.0).abs() < EPSILON); + assert!(identity_check.near_mag(1.0)); } #[test] @@ -605,7 +605,7 @@ fn its_a_rational_number() { let rational = three / four; // 3/4 computed via overloaded Div // test result is 3/4 = 0.75 - assert!((rational.mag - 0.75).abs() < EPSILON); + assert!(rational.near_mag(0.75)); // test addition of fractions (3/4 + 1/2) let one = Geonum::scalar(1.0); @@ -614,7 +614,7 @@ fn its_a_rational_number() { // addition of fractions: 3/4 + 1/2 = 5/4 = 1.25 let fraction_sum = rational + rational2; - assert!((fraction_sum.mag - 1.25).abs() < EPSILON); + assert!(fraction_sum.near_mag(1.25)); } #[test] @@ -627,15 +627,15 @@ fn its_an_algebraic_number() { let sqrt2 = two.pow(0.5); // √2 via pow(0.5) // 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.mag - 2.0).abs() < EPSILON); // length: √2^2 = 2 ✓ - // blade accumulation from pow() means algebraic identity exists at different grade + 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 // square it let sqrt2_squared = sqrt2 * sqrt2; // test result is 2 - assert!((sqrt2_squared.mag - 2.0).abs() < EPSILON); + assert!(sqrt2_squared.near_mag(2.0)); let expected_angle = sqrt2.angle + sqrt2.angle; // blade arithmetic: 1 + 1 = 2 assert_eq!(sqrt2_squared.angle, expected_angle); @@ -670,9 +670,9 @@ fn it_dualizes_log2_geometric_algebra_components() { // in just the 2 components (length and angle) of the geometric number // test extracted values for π/4 case - assert_eq!(scalar, 2.0 * (PI / 4.0).cos()); // 2 * √2/2 = √2 - assert_eq!(vector_magnitude, 2.0 * (PI / 4.0).sin()); // 2 * √2/2 = √2 - assert_eq!(bivector_angle, PI / 4.0); // π/4 angle preserved + assert!((scalar - 2.0 * (PI / 4.0).cos()).abs() < 1e-10); // 2 * √2/2 = √2 + assert!((vector_magnitude - 2.0 * (PI / 4.0).sin()).abs() < 1e-10); // 2 * √2/2 = √2 + assert!((bivector_angle - PI / 4.0).abs() < 1e-10); // π/4 angle preserved // log2(4) = 2 components (length and angle) instead of 4 components // this matches the statement from the README @@ -703,7 +703,7 @@ fn it_keeps_information_entropy_zero() { ); // test that the recovered geonum equals the original - assert!((g1.mag - recovered.mag).abs() < EPSILON); + assert!(g1.near_mag(recovered.mag)); assert_eq!(g1.angle, recovered.angle); // compute the entropy of transformation between the original and its dual @@ -834,7 +834,7 @@ fn its_a_quadrature() { let result = f_upper - f_lower; // the length is 1/3 - assert!((result.mag - exact_result).abs() < EPSILON); + assert!(result.near_mag(exact_result)); // integrate() adds 3 blades, so blade 0 → blade 3 for x³/3 // then another integrate() adds 3 more: blade 3 → blade 5 assert_eq!(result.angle.blade(), 5); @@ -873,7 +873,7 @@ fn its_a_quadrature() { ); // prove perfect information preservation (zero entropy) - assert!((g.mag - recovered.mag).abs() < EPSILON); + assert!(g.near_mag(recovered.mag)); assert_eq!(g.angle, recovered.angle); // demonstrate O(1) integration regardless of complexity @@ -913,7 +913,7 @@ fn its_a_quadrature() { let sin_shifted = Geonum::new(1.0, 1.0, 2.0); // sin(π/2) = 1 // prove sin(π/2) = cos(0) = 1 - assert!((sin_shifted.mag - cos_at_zero.mag).abs() < EPSILON); + assert!(sin_shifted.near_mag(cos_at_zero.mag)); // similarly, verify the relationship cos(x+π/2) = -sin(x) // at x = 0: cos(0+π/2) = cos(π/2) = 0 and -sin(0) = 0 @@ -921,8 +921,8 @@ fn its_a_quadrature() { let neg_sin_at_zero = Geonum::new(0.0, 3.0, 2.0); // -sin(0) = 0 [angle π/2 + π = 3π/2] // test equality of magnitudes (both are 0) - assert!((cos_shifted.mag - 0.0).abs() < EPSILON); - assert!((neg_sin_at_zero.mag - 0.0).abs() < EPSILON); + assert!(cos_shifted.near_mag(0.0)); + assert!(neg_sin_at_zero.near_mag(0.0)); // prove the fundamental quadrature relationship in geonum: // functions that differ by a π/2 phase represent derivatives/integrals of each other diff --git a/tests/optics_test.rs b/tests/optics_test.rs index 27ce39f..0d65d49 100644 --- a/tests/optics_test.rs +++ b/tests/optics_test.rs @@ -18,7 +18,7 @@ fn its_a_ray() { // intensity encoded in length, direction in angle assert_eq!(ray.mag, 1.0); // unit intensity - assert_eq!(ray.angle.rem(), PI / 4.0); // 45° propagation + assert!(ray.angle.near_rem(PI / 4.0)); // 45° propagation // ray propagation: just scale by distance let distance = 100.0; @@ -514,7 +514,7 @@ fn its_a_hologram() { // object wave reconstruction: phase relationship preserved let phase_error = (reconstructed.angle.rem() - object.angle.rem()).abs(); - let phase_tolerance = PI / 8.0; // allow some reconstruction error + let phase_tolerance = PI / 8.0 + 1e-10; // allow some reconstruction error assert!(phase_error < phase_tolerance); // object phase approximately recovered // intensity ratio: reconstructed vs original object diff --git a/tests/pga_test.rs b/tests/pga_test.rs index 7fe6ab8..7ab752c 100644 --- a/tests/pga_test.rs +++ b/tests/pga_test.rs @@ -482,9 +482,9 @@ fn it_handles_line_representations() { let line = p1.wedge(&p2); // line from p1(1,0) to p2(3,2) has specific geometric properties - assert!((line.mag - 2.0).abs() < 1e-10); + assert!(line.near_mag(2.0)); assert_eq!(line.angle.grade(), 1); // wedge produces grade-1 object (line) - assert!((line.angle.rem() - 0.5880026035463774).abs() < 1e-10); + assert!(line.angle.near_rem(0.5880026035463774)); assert_eq!(line.angle.blade(), 1); // parametric points on the line: p(t) = (1-t)p1 + t*p2 @@ -1067,7 +1067,7 @@ fn it_eliminates_matrix_complexity() { // geonum reveals multiplicative inverse through the operation result let identity = transform * inverse; - assert!((identity.mag - 1.0).abs() < EPSILON); + assert!(identity.near_mag(1.0)); // key insight: a * inv(a) produces [1, traditional_inverse_angle] // for transform [3, π/3], traditional inverse would be [1/3, -π/3] = [1/3, 5π/3] diff --git a/tests/qm_test.rs b/tests/qm_test.rs index f8b03ba..4dbaa81 100644 --- a/tests/qm_test.rs +++ b/tests/qm_test.rs @@ -50,7 +50,7 @@ fn its_a_state_vector() { // test direct geometric representation assert_eq!(state.mag, 1.0); // normalized amplitude - assert!((state.angle.grade_angle() - PI / 4.0).abs() < EPSILON); // phase π/4 + assert!(state.angle.near_rad(PI / 4.0)); // phase π/4 // superposition |ψ⟩ = α|0⟩ + β|1⟩ as single geonum from cartesian // equal probability superposition: (|0⟩ + |1⟩)/√2 @@ -59,7 +59,7 @@ fn its_a_state_vector() { let superposition = Geonum::new_from_cartesian(alpha, beta); // test superposition amplitude and phase - assert!((superposition.mag - 1.0).abs() < EPSILON); // normalized + assert!(superposition.near_mag(1.0)); // normalized assert_eq!(superposition.angle, Angle::new(1.0, 4.0)); // 45° phase // measurement as angle projection @@ -203,7 +203,7 @@ fn its_an_uncertainty_principle() { // test complementarity through angle orthogonality // position and momentum are orthogonal dimensions let dot_product = position.dot(&momentum); - assert!(dot_product.mag.abs() < EPSILON); // orthogonal + assert!(dot_product.near_mag(0.0)); // orthogonal // test wedge product as uncertainty measure // the wedge product gives the geometric area representing uncertainty @@ -217,7 +217,7 @@ fn its_an_uncertainty_principle() { let p_dot_x = position.dot(&momentum); let p_wedge_x = position.wedge(&momentum); - assert!(p_dot_x.mag.abs() < EPSILON); // orthogonal + assert!(p_dot_x.near_mag(0.0)); // orthogonal assert!(p_wedge_x.mag >= 0.5); // maximum uncertainty // test uncertainty with non-orthogonal observables @@ -264,7 +264,7 @@ fn its_a_quantum_gate() { // test gate application through angle transformation let h_applied = hadamard(&qubit); assert_eq!(h_applied.mag, qubit.mag); // preserves norm - assert!((h_applied.angle.grade_angle() - PI / 4.0).abs() < EPSILON); // creates superposition + assert!(h_applied.angle.near_rad(PI / 4.0)); // creates superposition // test gate composition through angle addition // first apply hadamard, then phase gate @@ -275,7 +275,7 @@ fn its_a_quantum_gate() { // test unitarity preserved through angle conservation // unitary operators preserve the norm (probability) - assert!((h_then_s.mag - qubit.mag).abs() < EPSILON); + assert!(h_then_s.near_mag(qubit.mag)); } #[test] @@ -345,8 +345,8 @@ fn its_an_entangled_state() { assert_eq!(angle_diff.grade_angle(), PI); // test bell state properties through angle configuration - assert!((bell_state.0.mag - 1.0 / 2.0_f64.sqrt()).abs() < EPSILON); - assert!((bell_state.1.mag - 1.0 / 2.0_f64.sqrt()).abs() < EPSILON); + assert!(bell_state.0.near_mag(1.0 / 2.0_f64.sqrt())); + assert!(bell_state.1.near_mag(1.0 / 2.0_f64.sqrt())); // test measurement correlation // when one particle is measured, the others state is determined @@ -414,11 +414,11 @@ fn its_a_quantum_harmonic_oscillator() { // test ladder operators let raised = creation(&ground_state, 0); - assert!((raised.mag - 1.0_f64.sqrt()).abs() < EPSILON); // √1 factor + assert!(raised.near_mag(1.0_f64.sqrt())); // √1 factor assert_eq!(raised.angle, first_excited.angle); let lowered = annihilation(&first_excited, 1); - assert!((lowered.mag - 1.0_f64.sqrt()).abs() < EPSILON); // √1 factor + assert!(lowered.near_mag(1.0_f64.sqrt())); // √1 factor assert_eq!(lowered.angle, ground_state.angle); } @@ -878,7 +878,7 @@ fn it_eliminates_statistical_collections() { assert_eq!(pure_state.mag, 1.0); // normalized // phase is definite, not statistical let expected_phase = (0.8_f64).atan2(0.6); - assert!((pure_state.angle.grade_angle() - expected_phase).abs() < EPSILON); + assert!(pure_state.angle.near_rad(expected_phase)); // measurement outcomes from projection geometry, not probability sampling let measurement_axis = Geonum::new(1.0, 0.0, 1.0); // |0⟩ basis @@ -1079,13 +1079,13 @@ fn it_preserves_unitary_transformation() { let state = Geonum::new_from_cartesian(0.6, 0.8); // test normalized - assert!((state.mag - 1.0).abs() < EPSILON); + assert!(state.near_mag(1.0)); // unitary transformation: rotate by π/4 let transformed = state.rotate(Angle::new(1.0, 4.0)); // amplitude preserved (unitarity) - assert!((transformed.mag - state.mag).abs() < EPSILON); + assert!(transformed.near_mag(state.mag)); // phase changed by π/4 let expected_angle = state.angle + Angle::new(1.0, 4.0); @@ -1104,7 +1104,7 @@ fn it_preserves_unitary_transformation() { let rotated_inner_product = rotated_a.dot(&rotated_b); // inner product preserved under unitary transformation - assert!((inner_product.mag - rotated_inner_product.mag).abs() < EPSILON); + assert!(inner_product.near_mag(rotated_inner_product.mag)); // spinor as single geonum instead of pair // traditional spinor (a, b) → geonum with length = ||(a,b)|| and angle encoding ratio @@ -1116,7 +1116,7 @@ fn it_preserves_unitary_transformation() { let rotated_spinor = spinor.rotate(Angle::new(1.0, 3.0)); // π/3 rotation // magnitude preserved - assert!((rotated_spinor.mag - spinor.mag).abs() < EPSILON); + assert!(rotated_spinor.near_mag(spinor.mag)); } #[test] @@ -1242,7 +1242,7 @@ fn it_proves_superposition_is_a_cakeism() { // the state is deterministic, not probabilistic assert_eq!(state.angle, theta); - assert!((state.mag - 1.0).abs() < EPSILON); + assert!(state.near_mag(1.0)); println!("\n=== DETERMINISTIC, NOT PROBABILISTIC ==="); println!( diff --git a/tests/rendering_test.rs b/tests/rendering_test.rs index 6853a0e..8394ba2 100644 --- a/tests/rendering_test.rs +++ b/tests/rendering_test.rs @@ -124,7 +124,7 @@ fn it_rotates_the_scanner_not_the_scanned() { ); // magnitude preserved: rotation doesnt change distance from center - assert!((rotated.mag - scanner.mag).abs() < EPSILON); + assert!(rotated.near_mag(scanner.mag)); } } } @@ -218,18 +218,18 @@ fn it_renders_a_circle_without_coordinates() { // pixel at (4, 0): distance to center = 1, inside let pixel_near = Geonum::new(4.0, 0.0, 1.0); let dist_near = pixel_near.distance_to(¢er); - assert!((dist_near.mag - 1.0).abs() < EPSILON); + assert!(dist_near.near_mag(1.0)); // pixel at (6, 0): distance to center = 3, outside let pixel_far = Geonum::new(6.0, 0.0, 1.0); let dist_far = pixel_far.distance_to(¢er); - assert!((dist_far.mag - 3.0).abs() < EPSILON); + assert!(dist_far.near_mag(3.0)); // pixel at (3, π/2): distance = sqrt(9 + 9) = sqrt(18), outside let pixel_perp = Geonum::new(3.0, 1.0, 2.0); let dist_perp = pixel_perp.distance_to(¢er); let expected_dist = (9.0_f64 + 9.0).sqrt(); - assert!((dist_perp.mag - expected_dist).abs() < EPSILON); + assert!(dist_perp.near_mag(expected_dist)); } #[test] @@ -277,7 +277,7 @@ fn it_zooms_and_rotates_in_one_operation() { ); // magnitude = original radius × zoom factor - assert!((transformed.mag - radius * zoom).abs() < EPSILON); + assert!(transformed.near_mag(radius * zoom)); } } @@ -324,8 +324,8 @@ fn it_renders_the_same_scene_from_different_camera_angles() { ); // intensity (magnitude) preserved across camera angles - assert!((sweep_default.mag - intensity).abs() < EPSILON); - assert!((sweep_rotated.mag - intensity).abs() < EPSILON); + assert!(sweep_default.near_mag(intensity)); + assert!(sweep_rotated.near_mag(intensity)); // distance from center preserved: camera rotation doesnt change object distance // distance is encoded in the scene, not in the sweep magnitude here @@ -333,7 +333,7 @@ fn it_renders_the_same_scene_from_different_camera_angles() { // distance determines which radial step the sweep hits let position_default = Geonum::new_with_angle(distance, direction); let position_rotated = Geonum::new_with_angle(distance, direction + camera_rotation); - assert!((position_default.mag - position_rotated.mag).abs() < EPSILON); + assert!(position_default.near_mag(position_rotated.mag)); } } @@ -609,9 +609,9 @@ fn it_enables_a_graphics_rendering_stack_rewrite() { let v2_world = v2.rotate(model_rotation); // magnitudes preserved through rotation - assert!((v0_world.mag - v0.mag).abs() < EPSILON); - assert!((v1_world.mag - v1.mag).abs() < EPSILON); - assert!((v2_world.mag - v2.mag).abs() < EPSILON); + assert!(v0_world.near_mag(v0.mag)); + assert!(v1_world.near_mag(v1.mag)); + assert!(v2_world.near_mag(v2.mag)); // --- 3. view transform: camera panned 15° --- // traditional: construct view matrix, multiply 3 vertices diff --git a/tests/set_theory_test.rs b/tests/set_theory_test.rs index e92611a..6f6ce84 100644 --- a/tests/set_theory_test.rs +++ b/tests/set_theory_test.rs @@ -70,7 +70,7 @@ fn its_a_naive_set() { // test intersection as angle correlation let dot_product = a.dot(&b); - assert!(dot_product.mag.abs() < EPSILON); // orthogonal = no overlap + assert!(dot_product.near_mag(0.0)); // orthogonal = no overlap // test geometric union as angle combination in multivector let union = GeoCollection::from(vec![a, b]); @@ -161,7 +161,7 @@ fn its_a_ring() { let bc_sum = Geonum::new_from_cartesian(bc_sum_cartesian[0], bc_sum_cartesian[1]); // test that cartesian conversion matches direct addition - assert_eq!(bc_sum.mag, bc_sum_expected.mag); + assert!(bc_sum.near_mag(bc_sum_expected.mag)); assert_eq!(bc_sum.angle, bc_sum_expected.angle); // compute a * (b + c) @@ -196,7 +196,7 @@ fn its_a_ring() { assert_eq!(right_side.angle, right_side_expected.angle); // test that the distributive property holds - assert!((left_side.mag - right_side.mag).abs() < EPSILON); + assert!(left_side.near_mag(right_side.mag)); // angles might differ by 2π let angle_diff = (left_side.angle - right_side.angle).grade_angle(); @@ -226,7 +226,7 @@ fn its_a_field() { let quotient = a / b; // test lengths divide - assert!((quotient.mag - 2.0).abs() < EPSILON); + assert!(quotient.near_mag(2.0)); // division uses inv() which adds π (2 blades) // a has angle π/3, b has angle π/6 @@ -246,7 +246,7 @@ fn its_a_field() { // test division property: (a / b) * b = a let product = quotient * b; - assert!((product.mag - a.mag).abs() < EPSILON); + assert!(product.near_mag(a.mag)); // quotient * b doesnt return to a due to blade accumulation from inv() // quotient.angle = a.angle + b.angle + π @@ -302,7 +302,7 @@ fn its_a_vector_space() { // test independence through angle measurement // orthogonal vectors have dot product zero let dot = e1.dot(&e2); - assert!(dot.mag.abs() < EPSILON); + assert!(dot.near_mag(0.0)); // test basis from orthogonality not abstract span // basis vectors have orthogonal angles @@ -393,7 +393,7 @@ fn its_a_lie_algebra() { let b_wedge_a = b.wedge(&a); // test lengths are equal - assert!((a_wedge_b.mag - b_wedge_a.mag).abs() < EPSILON); + assert!(a_wedge_b.near_mag(b_wedge_a.mag)); // test angles differ by π (orientation flip) let angle_diff = (a_wedge_b.angle - b_wedge_a.angle).grade_angle(); @@ -442,7 +442,7 @@ fn its_a_clifford_algebra() { let geo_product = e1 * e2; let wedge_product = e1.wedge(&e2); - assert!((geo_product.mag - wedge_product.mag).abs() < EPSILON); + assert!(geo_product.near_mag(wedge_product.mag)); // manually set the angles to match for simplicity // a full clifford algebra model would handle this more precisely diff --git a/tests/tensor_test.rs b/tests/tensor_test.rs index 2a0919a..6064b09 100644 --- a/tests/tensor_test.rs +++ b/tests/tensor_test.rs @@ -100,7 +100,7 @@ fn its_a_tensor_product() { let sum_products_angle = sum_products_y.atan2(sum_products_x); // prove distributivity: a ⊗ (b + c) ≈ a ⊗ b + a ⊗ c - assert!((left_distribute.mag - sum_products_length).abs() < EPSILON); + assert!(left_distribute.near_mag(sum_products_length)); // prove that a ⊗ (b + c) and a ⊗ b + a ⊗ c differ in phase by 45° (π/4 radians) // geonum captures this additional structure — tensors do not @@ -161,7 +161,7 @@ fn its_a_tensor_product() { // in geonum ijk = [1, π/2 + π + 3π/2] = [1, 3π] = [1, π] = -1 assert_eq!(ijk.mag, 1.0); - assert!((ijk.angle.grade_angle() - PI).abs() < EPSILON); + assert!(ijk.angle.near_rad(PI)); // compare with traditional tensor implementation @@ -264,7 +264,7 @@ fn its_a_kronecker_product() { let scaled_kronecker = scaled_a * scaled_b; // scaling factor: 3 × 2 = 6 - assert!((scaled_kronecker.mag - 6.0 * kronecker.mag).abs() < EPSILON); + assert!(scaled_kronecker.near_mag(6.0 * kronecker.mag)); assert_eq!(scaled_kronecker.angle, kronecker.angle); // angles unchanged by scaling // test high-dimensional kronecker without component explosion @@ -306,7 +306,7 @@ fn its_a_contraction() { // wedge product is antisymmetric: e₁∧e₂ = -e₂∧e₁ // this manifests as equal magnitudes - assert!((b.mag - c.mag).abs() < EPSILON); + assert!(b.near_mag(c.mag)); // the antisymmetry is encoded in the angle structure // b and c will have different blade counts due to angle ordering @@ -318,7 +318,7 @@ fn its_a_contraction() { let v1_dot_v2 = v1.dot(&v2); let expected = 2.0 * 3.0 * (v1.angle - v2.angle).grade_angle().cos(); - assert!((v1_dot_v2.mag - expected).abs() < EPSILON); + assert!(v1_dot_v2.near_mag(expected)); // traditional tensor contraction: must track indices and sum over repeated ones // example: rank-2 tensor A with components [1,2,3,4] contracted with rank-2 tensor B [5,6,7,8] @@ -1251,7 +1251,7 @@ fn its_a_multi_linear_map() { // verify wedge product is antisymmetric (length is the same but angle should differ) let wedge_reverse = v2.wedge(&v1); - assert!((wedge.mag - wedge_reverse.mag).abs() < EPSILON); + assert!(wedge.near_mag(wedge_reverse.mag)); // wedge of parallel vectors should be zero let parallel = v1.wedge(&v1); assert!(parallel.mag < EPSILON); @@ -1264,7 +1264,7 @@ fn its_a_multi_linear_map() { // verify dot product is symmetric let dot_reverse = v2.dot(&v1); - assert!((dot.mag - dot_reverse.mag).abs() < EPSILON); + assert!(dot.near_mag(dot_reverse.mag)); // test tensor transformation rules // in traditional tensor calculus tensors transform with jacobian matrices diff --git a/tests/trigonometry_test.rs b/tests/trigonometry_test.rs index c54624e..f68310a 100644 --- a/tests/trigonometry_test.rs +++ b/tests/trigonometry_test.rs @@ -30,15 +30,15 @@ fn it_maps_trig_onto_the_pi_over_2_lattice() { let theta = a.grade_angle(); let c = Geonum::cos(a); - assert!((c.mag - theta.cos().abs()).abs() < EPSILON); + assert!(c.near_mag(theta.cos().abs())); assert!(matches!(c.angle.grade(), 0 | 2)); // even pair let s = Geonum::sin(a); - assert!((s.mag - theta.sin().abs()).abs() < EPSILON); + assert!(s.near_mag(theta.sin().abs())); assert!(matches!(s.angle.grade(), 1 | 3)); // odd pair let t = Geonum::tan(a); - assert!((t.mag - theta.tan().abs()).abs() < EPSILON); + assert!(t.near_mag(theta.tan().abs())); assert!(matches!(t.angle.grade(), 1 | 3)); // odd pair via sin/cos division } } @@ -54,7 +54,7 @@ fn it_shifts_sin_by_a_quarter_turn_of_cos() { // same magnitude; sin is a quarter-turn (Q) of cos let q = Angle::new(1.0, 2.0); - assert!((s.mag - c_shifted.mag).abs() < EPSILON); + assert!(s.near_mag(c_shifted.mag)); assert_eq!(s.angle.base_angle(), (c_shifted.angle + q).base_angle()); } @@ -84,7 +84,7 @@ fn it_relates_tan_to_sin_over_cos_geometrically() { let t = Geonum::tan(a); let s_over_c = Geonum::sin(a).div(&Geonum::cos(a)); - assert!((t.mag - s_over_c.mag).abs() < EPSILON); + assert!(t.near_mag(s_over_c.mag)); assert_eq!(t.angle.base_angle(), s_over_c.angle.base_angle()); } @@ -97,7 +97,7 @@ fn it_is_reference_agnostic() { let sin_as_even_pair = Geonum::cos(a + Angle::new(3.0, 2.0)); let s = Geonum::sin(a); - assert!((s.mag - sin_as_even_pair.mag).abs() < EPSILON); + assert!(s.near_mag(sin_as_even_pair.mag)); // different references, same information: sin = cos + Q let q = Angle::new(1.0, 2.0); assert_eq!( @@ -134,7 +134,7 @@ fn it_relates_trig_through_grade_hierarchy() { let sin_obtuse = Geonum::sin(a_obtuse); let cos_shifted_obtuse = Geonum::cos(a_obtuse + Angle::new(3.0, 2.0)); let q = Angle::new(1.0, 2.0); - assert!((sin_obtuse.mag - cos_shifted_obtuse.mag).abs() < EPSILON); + assert!(sin_obtuse.near_mag(cos_shifted_obtuse.mag)); assert_eq!( sin_obtuse.angle.base_angle(), (cos_shifted_obtuse.angle + q).base_angle() @@ -174,13 +174,13 @@ fn it_doesnt_need_cos_and_tan() { // cos via sin matches standard magnitude and parity let c = cos_via_sin(a); - assert!((c.mag - theta.cos().abs()).abs() < EPSILON); + assert!(c.near_mag(theta.cos().abs())); assert!(matches!(c.angle.grade(), 0 | 2)); // tan via sin ratio matches standard magnitude and parity (avoid singularities) if theta.cos().abs() > 1e-12 { let t = tan_via_sin(a); - assert!((t.mag - theta.tan().abs()).abs() < EPSILON); + assert!(t.near_mag(theta.tan().abs())); assert!(matches!(t.angle.grade(), 1 | 3)); } } @@ -208,7 +208,7 @@ fn it_is_projection() { // this is the r cos term in the linear combination r cos(θ−φ) e_φ + r sin(θ−φ) e_{φ+π/2} let onto_adjacent = Geonum::new_with_angle(1.0, zero); let proj_adjacent = v.project(&onto_adjacent); - assert!((proj_adjacent.mag - (2.0_f64).sqrt() / 2.0).abs() < EPSILON); + assert!(proj_adjacent.near_mag((2.0_f64).sqrt() / 2.0)); assert_eq!(proj_adjacent.angle, zero); // opposite (φ=π/2): [1, π/4] → [√2/2, π/2] @@ -216,7 +216,7 @@ fn it_is_projection() { // this is the r sin term in the same linear combination above let onto_opposite = Geonum::new_with_angle(1.0, quarter); let proj_opposite = v.project(&onto_opposite); - assert!((proj_opposite.mag - (2.0_f64).sqrt() / 2.0).abs() < EPSILON); + assert!(proj_opposite.near_mag((2.0_f64).sqrt() / 2.0)); assert_eq!(proj_opposite.angle, quarter); // pythagorean identity from projections @@ -391,9 +391,110 @@ fn it_expresses_pythagoras_theorem_through_composed_angles() { // verify pythagorean relationship in lengths let expected_length = (3.0_f64.powi(2) + 4.0_f64.powi(2)).sqrt(); - assert!((combined.mag - expected_length).abs() < EPSILON); - assert!((combined.mag - 5.0).abs() < EPSILON); + assert!(combined.near_mag(expected_length)); + assert!(combined.near_mag(5.0)); // but the real insight: this length relationship comes from cosine interference // cos(π/2) = 0 means orthogonal rotations combine with zero interference term } + +// ═══════════════════════════════════════════════════════════════════════════ +// arcsin is not a primitive — its projection recovery +// +// sin(θ) = opp/hyp collapses [hyp, θ] to a scalar ratio by discarding the angle. +// arcsin reverses this collapse. in geonum the angle is never discarded, +// so arcsin is unnecessary — its just reading the angle that projection would erase. +// ═══════════════════════════════════════════════════════════════════════════ + +#[test] +fn it_shows_sin_is_a_projection_that_discards_the_angle() { + // a unit hypotenuse at angle θ is a complete geometric object: [1, θ] + // sin(θ) projects it onto the π/2 axis, producing a scalar ratio + // this projection discards θ — thats what creates the "inverse" problem + + 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, 5.0), // 2π/5 + ]; + + for theta in angles { + let hypotenuse = Geonum::new_with_angle(1.0, theta); + + // project onto opposite axis (π/2) — this is sin(θ) + let opposite = hypotenuse.opp(); + assert!(opposite.near_mag(theta.grade_angle().sin().abs())); + + // the projection result anchors to odd pair, not θ + assert!(matches!(opposite.angle.grade(), 1 | 3)); + + // but the hypotenuse still carries θ + assert_eq!(hypotenuse.angle, theta); + } +} + +#[test] +fn it_proves_arcsin_loses_quadrant_information() { + // sin projects [1, θ] → ratio, losing θ AND quadrant + // scalar arcsin can only return θ ∈ [−π/2, π/2] + // geonum preserves quadrant via grade (blade mod 4) + + let theta1 = Angle::new(1.0, 6.0); // π/6 (quadrant I) + let theta2 = Angle::new(5.0, 6.0); // 5π/6 (quadrant II) + + let sin1 = Geonum::sin(theta1); + let sin2 = Geonum::sin(theta2); + + // same magnitude — scalar arcsin cannot distinguish these + assert!(sin1.near_mag(sin2.mag)); + + // but the geometric numbers at those angles are distinct + let hyp1 = Geonum::new_with_angle(1.0, theta1); + let hyp2 = Geonum::new_with_angle(1.0, theta2); + assert_eq!(hyp1.angle, theta1); + assert_eq!(hyp2.angle, theta2); + + // cos projections distinguish them: cos(π/6) > 0, cos(5π/6) < 0 + let cos1 = Geonum::cos(theta1); + let cos2 = Geonum::cos(theta2); + assert_eq!(cos1.angle.grade(), 0); // positive cos → grade 0 + assert_eq!(cos2.angle.grade(), 2); // negative cos → grade 2 (D event) +} + +#[test] +fn it_shows_sqrt_1_minus_sin_squared_is_quadrature() { + // √(1−sin²θ) inside arcsin's integral definition + // is just the quadrature identity: cos(θ) = sin(θ + π/2) + // + // sin²+cos² = 1 says orthogonal projections partition the unit circle + // √(1−sin²) solves for cos — scalar recovery of the Q relationship + + 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 + ]; + + let q = Angle::new(1.0, 2.0); // quarter turn + + for theta in angles { + let sin_val = theta.grade_angle().sin(); + let cos_val = theta.grade_angle().cos(); + + // method 1: scalar recovery via √(1 - sin²) + let cos_from_sqrt = (1.0 - sin_val * sin_val).sqrt(); + + // method 2: quadrature via Q shift: sin(θ + π/2) = cos(θ) + let theta_plus_q = theta + q; + let cos_from_quadrature = theta_plus_q.grade_angle().sin(); + + // method 3: geonum cos directly + let cos_geo = Geonum::cos(theta); + + assert!((cos_from_sqrt - cos_val.abs()).abs() < EPSILON); + assert!((cos_from_quadrature - cos_val).abs() < EPSILON); + assert!(cos_geo.near_mag(cos_val.abs())); + } +}