@@ -13,12 +13,12 @@ use crate::channel::Channel;
1313use crate :: config:: { LsmrOptions , PreconditionerConfig } ;
1414use crate :: domain:: collinearity:: detect_collinear_slopes;
1515use crate :: domain:: level_moments:: TermMoments ;
16- use crate :: domain:: { Design , Effect } ;
16+ use crate :: domain:: { Design , Effect , FactorEncoding } ;
1717use crate :: observation:: ObservationFrame ;
1818use crate :: operator:: design:: gather_apply;
1919use crate :: operator:: schwarz:: { build_preconditioner, Preconditioner } ;
2020use crate :: operator:: DesignOperator ;
21- use crate :: { BuildError , BuildWarning , SolveError , WithinError } ;
21+ use crate :: { BuildError , BuildWarning , FactorLabel , SolveError , WithinError } ;
2222
2323mod reparam;
2424#[ cfg( test) ]
@@ -112,27 +112,47 @@ impl From<&Preconditioner> for PreconditionerInput {
112112}
113113
114114/// One coefficient of the design: a [`Channel`] at one level of its term.
115- #[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
115+ #[ derive( Debug , Clone , PartialEq , Eq ) ]
116116pub struct CoefficientAddress {
117117 /// The coefficient column this address sits in.
118118 pub channel : Channel ,
119- /// Level index within the term (`0..n_levels`).
120- pub level : usize ,
119+ /// Caller-visible factor label.
120+ pub level : FactorLabel ,
121+ }
122+
123+ #[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
124+ struct CoefficientPosition {
125+ channel : Channel ,
126+ level : usize ,
127+ }
128+
129+ impl CoefficientPosition {
130+ fn to_caller_address ( self , design : & Design ) -> CoefficientAddress {
131+ let level = design. terms [ self . channel . term ]
132+ . encoding
133+ . label ( self . level )
134+ . expect ( "coefficient position belongs to its term" ) ;
135+
136+ CoefficientAddress {
137+ channel : self . channel ,
138+ level,
139+ }
140+ }
121141}
122142
123143/// Translates a [`CoefficientAddress`] to its flat index in [`SolveResult::x`]
124- /// and back, so callers need not reconstruct the term-major offset formula
125- /// (`offset + column * n_levels + level`) by hand .
144+ /// and back, including the translation between caller-visible labels and
145+ /// internal compact level positions .
126146#[ derive( Debug , Clone , PartialEq , Eq ) ]
127147pub struct CoefficientLayout {
128148 terms : Vec < TermLayout > ,
129149 n_dofs : usize ,
130150}
131151
132- #[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
152+ #[ derive( Debug , Clone , PartialEq , Eq ) ]
133153struct TermLayout {
134154 offset : usize ,
135- n_levels : usize ,
155+ encoding : FactorEncoding ,
136156 n_columns : usize ,
137157}
138158
@@ -143,7 +163,7 @@ impl CoefficientLayout {
143163 . iter ( )
144164 . map ( |t| TermLayout {
145165 offset : t. offset ,
146- n_levels : t. n_levels ( ) ,
166+ encoding : t. encoding . clone ( ) ,
147167 n_columns : t. n_columns ( ) ,
148168 } )
149169 . collect ( ) ;
@@ -165,7 +185,7 @@ impl CoefficientLayout {
165185
166186 /// Level count of `term`, or `None` if `term` is out of range.
167187 pub fn n_levels ( & self , term : usize ) -> Option < usize > {
168- self . terms . get ( term) . map ( |t| t. n_levels )
188+ self . terms . get ( term) . map ( |t| t. encoding . n_levels ( ) )
169189 }
170190
171191 /// Coefficient-column count of `term` (`intercept? + slopes`, ordered
@@ -174,12 +194,18 @@ impl CoefficientLayout {
174194 self . terms . get ( term) . map ( |t| t. n_columns )
175195 }
176196
177- /// Flat [`SolveResult::x`] index of `at`, or `None` if any coordinate is
178- /// out of range.
179- pub fn index ( & self , at : CoefficientAddress ) -> Option < usize > {
180- let t = self . terms . get ( at. channel . term ) ?;
181- ( at. level < t. n_levels && at. channel . column < t. n_columns )
182- . then ( || t. offset + at. channel . column * t. n_levels + at. level )
197+ /// Flat [`SolveResult::x`] index of `at`, or `None` if its term,
198+ /// column, or caller-visible level label is out of range.
199+ pub fn index ( & self , at : & CoefficientAddress ) -> Option < usize > {
200+ let term = self . terms . get ( at. channel . term ) ?;
201+
202+ if at. channel . column >= term. n_columns {
203+ return None ;
204+ }
205+
206+ let position = term. encoding . position ( & at. level ) ?;
207+
208+ Some ( term. offset + at. channel . column * term. encoding . n_levels ( ) + position)
183209 }
184210
185211 /// The address of flat index `i`, or `None` if `i >= n_dofs`.
@@ -191,12 +217,18 @@ impl CoefficientLayout {
191217 let term = self . terms . partition_point ( |t| t. offset <= i) - 1 ;
192218 let t = & self . terms [ term] ;
193219 let within = i - t. offset ;
220+ let n_levels = t. encoding . n_levels ( ) ;
221+ let level = t
222+ . encoding
223+ . label ( within % n_levels)
224+ . expect ( "coefficient position belongs to the term encoding" ) ;
225+
194226 Some ( CoefficientAddress {
195227 channel : Channel {
196228 term,
197- column : within / t . n_levels ,
229+ column : within / n_levels,
198230 } ,
199- level : within % t . n_levels ,
231+ level,
200232 } )
201233 }
202234}
@@ -207,9 +239,10 @@ impl CoefficientLayout {
207239pub struct SolveResult {
208240 /// Fixed-effect coefficients (length = total DOFs across all factors).
209241 ///
210- /// Term-major: coefficient column `c` of level `level` sits at
211- /// `term_offset + c * n_levels + level`, columns ordered
212- /// `[intercept?, slopes…]`. Slots for unidentified directions hold the
242+ /// Term-major by compact level position `p`: coefficient column `c` sits at
243+ /// `term_offset + c * n_levels + p`, with columns ordered
244+ /// `[intercept?, slopes…]`. Use [`SolveResult::layout`] to translate caller
245+ /// labels to these slots. Slots for unidentified directions hold the
213246 /// minimal-norm value `0`, never NaN; see [`SolveResult::unidentified`].
214247 pub x : Vec < f64 > ,
215248 /// Per-level directions the data cannot identify.
@@ -504,10 +537,15 @@ impl<'a> Solver<'a> {
504537 /// Per-level directions the data cannot identify, shared across all RHS:
505538 /// identification depends only on the design and weights, never on `y`.
506539 fn unidentified ( & self ) -> Vec < CoefficientAddress > {
507- self . reparam
508- . as_ref ( )
509- . map ( |rp| rp. unidentified . clone ( ) )
510- . unwrap_or_default ( )
540+ let Some ( reparam) = & self . reparam else {
541+ return Vec :: new ( ) ;
542+ } ;
543+ reparam
544+ . unidentified
545+ . iter ( )
546+ . copied ( )
547+ . map ( |position| position. to_caller_address ( & self . design ) )
548+ . collect ( )
511549 }
512550
513551 /// Solve for a single RHS vector with the given LSMR tuning.
0 commit comments