Skip to main content

horon_engine/
constants.rs

1//! Shared fixed-point constants for the engine.
2//!
3//! All constants are constructed as exact rational values using
4//! FixedPoint integer division. No floats are used.
5
6use g_math::fixed_point::FixedPoint;
7
8/// Below this many nodes, `nearest_semantic` stays on the brute-force scan:
9/// a VP-tree build costs O(n log n) distance evaluations, which small stores
10/// never amortize (see `docs/SEMANTIC_INDEX.md`).
11pub const SEMANTIC_INDEX_MIN_NODES: usize = 256;
12
13/// Maximum number of dimension slices the semantic index caches at once.
14/// Eviction is deterministic (lowest key first), not wall-clock LRU.
15pub const SEMANTIC_INDEX_MAX_SLICES: usize = 16;
16
17/// Minimum population for `find_outliers`: z-scores over fewer nodes
18/// are statistically meaningless, so smaller populations return no outliers.
19pub const OUTLIER_MIN_POPULATION: usize = 5;
20
21/// Neighborhood size for `find_outliers`: each node's outlier score is
22/// its average distance to this many nearest peers (capped at population−1).
23pub const OUTLIER_KNN: usize = 10;
24
25/// 0.5 = 1/2
26#[inline]
27pub fn half() -> FixedPoint {
28    FixedPoint::from_int(1) / FixedPoint::from_int(2)
29}
30
31/// Largest norm/ratio the model represents: `1 − 10⁻¹²`.
32///
33/// This bounds the greatest expressible hyperbolic distance at
34/// `2·atanh(1 − 10⁻¹²) ≈ 28.3`, which with the default τ = 1 is roughly 27
35/// levels of nesting. The limit is a deliberate margin, not an arithmetic
36/// one: `atanh`/`tanh` round-trip at 0 ULP out to `1 − 10⁻¹⁸`, and `1 − r²`
37/// stays representable until about `1 − 10⁻¹⁹`, so this sits six orders of
38/// magnitude clear of the floor.
39///
40/// It was 0.99 until 2026-07-30, which capped distance at 5.29 — about seven
41/// levels — and, worse, made `ensure_in_disk` collapse anything deeper back
42/// onto this value, so norms cycled instead of growing. See
43/// `docs/HYPERBOLIC_INDEX.md`.
44#[inline]
45pub fn near_boundary() -> FixedPoint {
46    FixedPoint::from_str("0.999999999999")
47}
48
49/// 0.99999 = 99999/100000 — tanh saturation bound
50#[inline]
51pub fn near_one() -> FixedPoint {
52    FixedPoint::from_int(99999) / FixedPoint::from_int(100000)
53}
54
55/// 0.0001 = 1/10000 — standard epsilon for near-zero checks
56#[inline]
57pub fn epsilon() -> FixedPoint {
58    FixedPoint::from_int(1) / FixedPoint::from_int(10000)
59}
60
61/// 0.00001 = 1/100000 — small epsilon for origin detection
62#[inline]
63pub fn small_epsilon() -> FixedPoint {
64    FixedPoint::from_int(1) / FixedPoint::from_int(100000)
65}
66
67/// How close to the boundary a point may sit before it is projected back in:
68/// `ensure_in_disk` rescales when `1 − ‖p‖² < 10⁻¹²`.
69///
70/// Matched to [`near_boundary`] so a rescaled point lands *inside* the
71/// margin and does not immediately re-trigger. Was 1/1000, which fired at
72/// ‖p‖ ≈ 0.9995 — reachable by depth 8 — and rescaled all the way back to
73/// 0.99, producing an observable 4-cycle in the norms
74/// (0.99 → 0.9963 → 0.9986 → 0.9995 → 0.99) rather than a depth limit.
75#[inline]
76pub fn boundary_margin() -> FixedPoint {
77    FixedPoint::from_str("0.000000000001")
78}
79
80/// Below this, the Möbius denominator `|1 − p̄q|²` is treated as degenerate.
81///
82/// Purely a division-safety bound, not a geometric one. `dist_sq ≤ 4` for any
83/// two points in the disk, so the quotient stays inside Q64.64 as long as the
84/// denominator exceeds `4 / (2⁶³−1) ≈ 8` ULP; 16 ULP doubles that margin.
85///
86/// This was `epsilon()² = 10⁻⁸` until 2026-07-30 — ten orders of magnitude
87/// above the real floor. Since `|1 − p̄q|² = (1 − ‖p‖²)² + ‖p − q‖²` for
88/// points at equal radius, ordinary sibling geometry at depth 11 evaluates to
89/// ~3.5e-9 and tripped the guard, making the kernel return the saturation
90/// value for *every* pair from that depth on. Every node became equidistant,
91/// so nearest-neighbour ranking became arbitrary — the real cause of what
92/// looked like a spatial-index limit.
93#[inline]
94pub fn min_safe_denominator() -> FixedPoint {
95    FixedPoint::from_raw(16)
96}
97
98/// 0.3 = 3/10 — region radius for hash table origin bucket
99#[inline]
100pub fn region_radius() -> FixedPoint {
101    FixedPoint::from_int(3) / FixedPoint::from_int(10)
102}
103
104/// Pi, parsed from string for maximum precision
105#[inline]
106pub fn pi() -> FixedPoint {
107    // 20 digits of pi — enough for any gMath profile
108    FixedPoint::from_str("3.14159265358979323846")
109}
110
111/// Two * pi
112#[inline]
113pub fn two_pi() -> FixedPoint {
114    FixedPoint::from_int(2) * pi()
115}
116
117/// Golden angle = pi * (3 - sqrt(5))
118#[inline]
119pub fn golden_angle() -> FixedPoint {
120    pi() * (FixedPoint::from_int(3) - FixedPoint::from_int(5).sqrt())
121}
122
123/// Safe atanh: clamps input to (-0.99, 0.99) then calls gMath's .atanh()
124#[inline]
125pub fn safe_atanh(x: FixedPoint) -> FixedPoint {
126    let max = near_boundary();
127    let clamped = if x > max {
128        max
129    } else if x < -max {
130        -max
131    } else {
132        x
133    };
134    clamped.atanh()
135}
136
137/// Default Sarkar embedding scale factor τ = 1.0.
138/// Controls parent-child hyperbolic distance. Q64.64 supports depth ~44/τ.
139#[inline]
140pub fn default_tau() -> FixedPoint {
141    FixedPoint::from_int(1)
142}
143
144/// Quantization helper: converts a FixedPoint to an i32 by multiplying
145/// by 1000 and rounding to the nearest integer.
146/// Replaces the pattern `(x.to_f32() * 1000.0).round() as i32`
147#[inline]
148pub fn quantize_1000(x: FixedPoint) -> i32 {
149    let scaled = x * FixedPoint::from_int(1000);
150    // Round to nearest integer: add 0.5 (or subtract 0.5 if negative) then truncate
151    let rounded = if scaled.is_negative() {
152        scaled - half()
153    } else {
154        scaled + half()
155    };
156    rounded.to_int()
157}
158
159/// High-resolution quantization for node-identity position signatures:
160/// 2^20 steps per unit (coordinates live inside the unit disk, so the
161/// result fits an i32 with room to spare).
162///
163/// Node identity must NOT use `quantize_1000`: at 1/1000 resolution two
164/// depth-2 cousins in *different branches* can quantize to identical
165/// signatures at a few hundred nodes (silent data
166/// crossover). 2^-20 resolution pushes the same birthday bound past
167/// millions of nodes, and rainbow fan-out banding guarantees the true
168/// positions are distinct.
169#[inline]
170pub fn quantize_position(x: FixedPoint) -> i32 {
171    let scaled = x * FixedPoint::from_int(1 << 20);
172    let rounded = if scaled.is_negative() {
173        scaled - half()
174    } else {
175        scaled + half()
176    };
177    rounded.to_int()
178}