1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
//! Atom resolution policy and analytic-evaluator rebuild (issue #2236).
//!
//! Widths are derived only from constructor-validated [`SaeAtomGeometryPlan`]s.
//! There is intentionally no evaluator reconstruction from realized widths.
use super::SaeAtomBasisKind;
/// Default per-axis harmonic order for a torus atom (Φ has `(2H+1)^d`
/// columns). Three harmonics per axis gives a 7-column 1-D factor and a
/// 49-column tensor basis at `d=2`, which is the smallest expansion that
/// reliably resolves a non-trivial signal on `T^2` without exploding the
/// design.
pub const SAE_DEFAULT_TORUS_HARMONICS: usize = 3;
/// Duchon nullspace knob `m` for a SAE-manifold atom of latent dimension
/// `dim`, sized so the native reproducing-norm Gram (`PenaltySource::Primary`)
/// on the scale-free polyharmonic basis is well-posed in every dimension.
///
/// `m` maps to the polynomial-nullspace order via `duchon_nullspace_from_m`
/// (`m = 1 → Zero/p=1`, `m = 2 → Linear/p=2`, `m = k+1 → Degree(k)/p=k+1`), so
/// `p_order == m`. The pure-polyharmonic basis the `DuchonCoordinateEvaluator`
/// evaluates uses `s = 0`; sizing the null space by `2·m > dim + 2` keeps the
/// kernel CPD/null-space adequacy comfortable across dimensions, whose smallest
/// integer solution is `m = ⌊dim / 2⌋ + 2`.
///
/// For `dim == 1` this reproduces the historical `m = 2` (constant + linear
/// null space). For `dim ≥ 2` it grows the null space with the latent
/// dimension — the previous hard-coded `m = 2` left the resolved power/order
/// inconsistent with the power-0 evaluator. The seed build and every
/// `DuchonCoordinateEvaluator` refresh read this same derived `m`, so the
/// design `Φ`, its jet, and the penalty stay column-consistent (the issue-247
/// invariant).
pub fn sae_duchon_atom_m(dim: usize) -> usize {
dim / 2 + 2
}
/// Maximum total monomial degree for a Euclidean tangent-patch SAE atom.
///
/// IMPORTANT (#1201): this is **degree 2**, so the `"euclidean"` atom basis is a
/// QUADRATIC patch `{1, t, t²}` at `d_atom = 1` (and `{1, t_a, t_b, t_a², t_a t_b,
/// t_b²}` at `d_atom = 2`), NOT a single straight decoder direction `γ(t) = t·b`.
/// Any comparison that calls the `"euclidean"` atom the "linear" baseline is
/// therefore comparing curved-vs-QUADRATIC, not curved-vs-linear — label such
/// comparisons honestly. (A genuinely linear secant baseline is available as the
/// per-atom hybrid-split LINEAR candidate, `crate::terms::sae::hybrid_split`,
/// which fits `b₀ + (t − t̄)·b₁` exactly; the `"euclidean"` OUTER fit path is the
/// quadratic patch.)
pub const SAE_EUCLIDEAN_PATCH_MAX_DEGREE: usize = 2;
/// Largest explicitly selectable Euclidean-patch degree in the topology race.
/// Seed patches use degree 2 ([`SAE_EUCLIDEAN_PATCH_MAX_DEGREE`]); a structure
/// birth may explicitly persist the degree-3 line candidate. The degree lives
/// in `SaeBasisResolution::Polynomial`, never inferred from decoder width.
pub const SAE_EUCLIDEAN_PATCH_RACE_MAX_DEGREE: usize = 3;
/// Flat-line polynomial degree of a Cylinder `S¹ × ℝ` atom's line axis (axis 1).
/// Mirrors the Euclidean-patch degree so the cylinder's flat factor matches the
/// patch candidate it races against; `Ml = SAE_CYLINDER_LINE_DEGREE + 1`.
pub const SAE_CYLINDER_LINE_DEGREE: usize = 2;
/// Möbius production convention (#2240): circle harmonics on the DOUBLE-COVER
/// angle and the width monomial degree of the deck-invariant basis. Must match
/// the seed builder and the topology-race candidate (`MobiusHarmonicEvaluator::
/// new(3, 2)`, deck-invariant width 10).
pub const SAE_MOBIUS_CIRCLE_HARMONICS: usize = 3;
pub const SAE_MOBIUS_WIDTH_DEGREE: usize = 2;
pub const SAE_MAX_PERIODIC_HARMONICS: usize = 4096;
pub fn sae_periodic_basis_size(n_harmonics: usize) -> Result<usize, String> {
if n_harmonics > SAE_MAX_PERIODIC_HARMONICS {
return Err(format!(
"sae_build_periodic_atom: n_harmonics={n_harmonics} exceeds dense limit {SAE_MAX_PERIODIC_HARMONICS}"
));
}
n_harmonics
.checked_mul(2)
.and_then(|twice| twice.checked_add(1))
.ok_or_else(|| {
format!("sae_build_periodic_atom: basis size overflows for n_harmonics={n_harmonics}")
})
}
/// The canonical basis tokens a caller may name. Single source of truth for
/// both the parser and the error message that rejects everything else, so the
/// two cannot drift apart.
pub const SAE_ATOM_BASIS_TOKENS: [&str; 13] = [
"duchon",
"periodic",
"sphere",
"torus",
"projective_plane",
"klein_bottle",
"linear",
"linear_block",
"euclidean",
"poincare",
"cylinder",
"mobius",
"finite_set",
];
/// Native [`SaeAtomBasisKind`] for a CALLER-SUPPLIED basis token.
///
/// An unrecognized token is an error here, named at the boundary it entered.
/// It used to fall through to `Precomputed(token)`, which meant a typo did not
/// fail at parse: it became a precomputed atom and died much later with "has no
/// analytic sparse-seed chart", a message about a kind the caller never asked
/// for. That is how `"auto"` became `Precomputed("auto")`.
///
/// This function's previous doc asserted that "public and Rust fit front doors
/// validate seed tokens before calling this converter" — an invariant nothing
/// enforced. The converter now enforces it itself, which is the only place it
/// can be guaranteed.
///
/// Deserializing a persisted artifact is the *other* contract, where an
/// unrecognized name genuinely does mean a caller-provided basis; that path
/// uses [`sae_atom_basis_kind_from_artifact_str`].
pub fn sae_atom_basis_kind_from_str(value: &str) -> Result<SaeAtomBasisKind, String> {
match sae_atom_basis_kind_from_artifact_str(value) {
SaeAtomBasisKind::Precomputed(unknown) => Err(format!(
"unknown atom basis {unknown:?}; expected one of {}",
SAE_ATOM_BASIS_TOKENS.join(", ")
)),
known => Ok(known),
}
}
/// Native [`SaeAtomBasisKind`] for a token read back from a PERSISTED artifact.
///
/// Here an unrecognized name is not a typo — a typed native artifact may carry
/// a caller-provided precomputed basis under its own label, and round-tripping
/// it is the whole point. Callers taking user input want
/// [`sae_atom_basis_kind_from_str`] instead, which refuses the same string.
pub fn sae_atom_basis_kind_from_artifact_str(value: &str) -> SaeAtomBasisKind {
match value {
"duchon" => SaeAtomBasisKind::Duchon,
"periodic" => SaeAtomBasisKind::Periodic,
"sphere" => SaeAtomBasisKind::Sphere,
"torus" => SaeAtomBasisKind::Torus,
"projective_plane" => SaeAtomBasisKind::ProjectivePlane,
"klein_bottle" => SaeAtomBasisKind::KleinBottle,
// The genuinely-linear atom is the degree-1 monomial patch, distinct
// from the degree-2 canonical `euclidean` patch.
"linear" => SaeAtomBasisKind::Linear,
// #BSF — `"linear_block"` is a BSF block expressed AS a manifold-SAE atom:
// γ_g(t) = t·D_g with an orthonormal decoder frame D_g and block-level
// (norm-selection or separate-gate) gating. Mathematically it IS the
// `Linear` degree-1 patch (the honest encoding of "BSF ⊂ ManifoldSAE" is a
// frame + gating CONFIG on the linear atom, not a new basis type), so it
// maps to `SaeAtomBasisKind::Linear` for construction/evidence; the
// exact `linear_block` public label is retained by the fit artifact.
"linear_block" => SaeAtomBasisKind::Linear,
"euclidean" => SaeAtomBasisKind::EuclideanPatch,
"poincare" => SaeAtomBasisKind::Poincare,
"cylinder" => SaeAtomBasisKind::Cylinder,
"mobius" => SaeAtomBasisKind::Mobius,
"finite_set" => SaeAtomBasisKind::FiniteSet,
other => SaeAtomBasisKind::Precomputed(other.to_string()),
}
}
/// The canonical lowercase string name of an atom basis kind — the inverse of
/// [`sae_atom_basis_kind_from_str`] for the round-trippable kinds, and the
/// string the python `from_payload` boundary reads under each atom's
/// `"basis_kind"` key and each plan's `"kind"` key. Derived from the FITTED
/// atom (not the user's input metadata) so a structure-search-grown / shrunk
/// dictionary serializes its DISCOVERED per-atom topology, not the input one.
pub fn sae_atom_basis_kind_name(kind: &SaeAtomBasisKind) -> String {
match kind {
SaeAtomBasisKind::Periodic => "periodic".to_string(),
SaeAtomBasisKind::Duchon => "duchon".to_string(),
SaeAtomBasisKind::Sphere => "sphere".to_string(),
SaeAtomBasisKind::Torus => "torus".to_string(),
SaeAtomBasisKind::ProjectivePlane => "projective_plane".to_string(),
SaeAtomBasisKind::KleinBottle => "klein_bottle".to_string(),
// The degree-1 and degree-2 patch families have distinct canonical names.
SaeAtomBasisKind::Linear => "linear".to_string(),
SaeAtomBasisKind::EuclideanPatch => "euclidean".to_string(),
SaeAtomBasisKind::Poincare => "poincare".to_string(),
SaeAtomBasisKind::Cylinder => "cylinder".to_string(),
SaeAtomBasisKind::Mobius => "mobius".to_string(),
// The finite-set (discrete-anchor) candidate is inert scaffolding that is
// not enrolled in the topology race by default, so a discovered dictionary
// never actually carries it (see
// `structure_harvest::finite_set_race_enrolled`). Round-trip it under the
// same `"finite_set"` token the gam-sae inference/harvest paths already
// emit, so serialization stays consistent the moment it is enrolled.
SaeAtomBasisKind::FiniteSet => "finite_set".to_string(),
SaeAtomBasisKind::Precomputed(name) => name.clone(),
}
}