Skip to content

Commit c7273e4

Browse files
committed
Add FactorEncoding abstraction
1 parent 838a3af commit c7273e4

10 files changed

Lines changed: 42 additions & 18 deletions

File tree

crates/within/src/domain.rs

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,29 @@ impl<T> Loading<T> {
7878
}
7979
}
8080

81+
/// Mapping between caller-visible factor labels and numerical level positions.
82+
///
83+
/// This is an identity mapping for the existing dense `u32` API. Later
84+
/// encodings can store an explicit mapping without changing `TermMeta`.
85+
#[derive(Debug, Clone)]
86+
pub(crate) struct FactorEncoding {
87+
n_levels: usize,
88+
}
89+
90+
impl FactorEncoding {
91+
fn identity(n_levels: usize) -> Self {
92+
Self { n_levels }
93+
}
94+
95+
pub(crate) fn n_levels(&self) -> usize {
96+
self.n_levels
97+
}
98+
}
99+
81100
/// Per-term metadata; coefficient `c` of `level` lives at `offset + c · n_levels + level`.
82101
#[derive(Debug, Clone)]
83102
pub(crate) struct TermMeta {
84-
pub n_levels: usize,
103+
pub(crate) encoding: FactorEncoding,
85104
pub offset: usize,
86105
/// Non-decreasing in the design's internal row order (fixed at construction).
87106
pub sorted: bool,
@@ -90,17 +109,21 @@ pub(crate) struct TermMeta {
90109
}
91110

92111
impl TermMeta {
112+
pub fn n_levels(&self) -> usize {
113+
self.encoding.n_levels()
114+
}
115+
93116
pub fn n_columns(&self) -> usize {
94117
self.columns.len()
95118
}
96119

97120
pub fn n_dofs(&self) -> usize {
98-
self.n_columns() * self.n_levels
121+
self.n_columns() * self.n_levels()
99122
}
100123

101124
/// Global DOF base of coefficient column `column`.
102125
pub fn column_base(&self, column: usize) -> usize {
103-
self.offset + column * self.n_levels
126+
self.offset + column * self.n_levels()
104127
}
105128
}
106129

@@ -209,8 +232,9 @@ impl<'a> Design<'a> {
209232
sorted &= v >= prev;
210233
prev = v;
211234
}
235+
let encoding = FactorEncoding::identity(max as usize + 1);
212236
let meta = TermMeta {
213-
n_levels: max as usize + 1,
237+
encoding,
214238
offset,
215239
sorted,
216240
columns,
@@ -228,7 +252,7 @@ impl<'a> Design<'a> {
228252
let dominant = (0..terms.len()).max_by_key(|&q| terms[q].n_dofs());
229253
let (frame, obs_perm) = match dominant {
230254
Some(d) if locality_sort && !terms[d].sorted && u32::try_from(n_obs).is_ok() => {
231-
let perm = stable_argsort(frame.level_column(d), terms[d].n_levels);
255+
let perm = stable_argsort(frame.level_column(d), terms[d].n_levels());
232256
let sorted_frame = frame.permuted(&perm);
233257
// Factors nested in the dominant one come out sorted, keeping coalesced scatter.
234258
for (q, meta) in terms.iter_mut().enumerate() {

crates/within/src/domain/cross_tab.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ pub(crate) fn find_all_active_levels(design: &Design<'_>) -> Vec<Vec<bool>> {
2626
let mut active: Vec<Vec<bool>> = design
2727
.terms
2828
.iter()
29-
.map(|f| vec![false; f.n_levels])
29+
.map(|f| vec![false; f.n_levels()])
3030
.collect();
3131
// Factor-outer/obs-inner: all writes for a factor land in one `active[f]` buffer.
3232
for (f, col) in active.iter_mut().enumerate() {

crates/within/src/domain/level_moments.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,9 @@ impl LevelMoments {
7070
let v = covariates.len();
7171
let mut moments = Self {
7272
intercept: v < meta.columns.len(),
73-
w_sum: vec![0.0; meta.n_levels],
74-
mean: vec![0.0; meta.n_levels * v],
75-
comoment: vec![0.0; meta.n_levels * tri_len(v)],
73+
w_sum: vec![0.0; meta.n_levels()],
74+
mean: vec![0.0; meta.n_levels() * v],
75+
comoment: vec![0.0; meta.n_levels() * tri_len(v)],
7676
covariates,
7777
};
7878
let zs: Vec<&[f64]> = moments

crates/within/src/operator/design/gather.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ pub(crate) fn gather_apply(
2121

2222
for_each_chunk(dst, |chunk, row_start| {
2323
for (q, t) in design.terms.iter().enumerate() {
24-
let (offset, n_levels) = (t.offset, t.n_levels);
24+
let (offset, n_levels) = (t.offset, t.n_levels());
2525
let levels = design.level_column(q);
2626
let col = |c: usize| &src[offset + c * n_levels..offset + (c + 1) * n_levels];
2727
match &*t.columns {

crates/within/src/operator/design/scatter.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ pub(super) fn scatter_apply(
5252
}
5353
columns => {
5454
for (c, loading) in columns.iter().enumerate() {
55-
let start = c * t.n_levels;
56-
let slot = &mut block[start..start + t.n_levels];
55+
let start = c * t.n_levels();
56+
let slot = &mut block[start..start + t.n_levels()];
5757
match loading {
5858
Loading::Constant => {
5959
scatter_term::<1>(slot, t, levels, parallel, scratch, |i| [base(i)]);
@@ -80,7 +80,7 @@ fn scatter_term<const C: usize>(
8080
scratch: &[AtomicF64],
8181
values: impl Fn(usize) -> [f64; C] + Sync,
8282
) {
83-
let n_levels = meta.n_levels;
83+
let n_levels = meta.n_levels();
8484
debug_assert_eq!(block.len(), C * n_levels);
8585
match ScatterStrategy::pick(parallel, C * n_levels, meta.sorted) {
8686
ScatterStrategy::Sequential => scatter_sequential::<C>(block, n_levels, levels, &values),

crates/within/src/operator/design/tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ mod slope_design_tests {
307307
for (q, t) in design.terms.iter().enumerate() {
308308
let levels = design.level_column(q);
309309
for (c, loading) in t.columns.iter().enumerate() {
310-
let base = t.offset + c * t.n_levels;
310+
let base = t.offset + c * t.n_levels();
311311
for (i, &lev) in levels.iter().enumerate() {
312312
d[i][base + lev as usize] = match loading {
313313
Loading::Constant => 1.0,

crates/within/src/operator/schwarz.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ fn build_diagonal(
221221
let w = |uid: usize| weights.map_or(1.0, |ws| ws[uid]);
222222
for (column, loading) in term.columns.iter().enumerate() {
223223
let base = term.column_base(column);
224-
let slice = &mut diag[base..base + term.n_levels];
224+
let slice = &mut diag[base..base + term.n_levels()];
225225
match loading {
226226
Loading::Constant => {
227227
for (uid, &level) in levels.iter().enumerate() {

crates/within/src/solver.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ impl CoefficientLayout {
143143
.iter()
144144
.map(|t| TermLayout {
145145
offset: t.offset,
146-
n_levels: t.n_levels,
146+
n_levels: t.n_levels(),
147147
n_columns: t.n_columns(),
148148
})
149149
.collect();

crates/within/src/solver/reparam.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ impl TermReparam {
8080
unidentified: &mut Vec<CoefficientAddress>,
8181
) -> Self {
8282
let meta = &design.terms[term];
83-
let (offset, n_levels) = (meta.offset, meta.n_levels);
83+
let (offset, n_levels) = (meta.offset, meta.n_levels());
8484
let mut intercept_column = None;
8585
let mut slope_columns = Vec::new();
8686
for (column, loading) in meta.columns.iter().enumerate() {

crates/within/src/solver/reparam/tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ fn build_whitens_each_slope_bearing_term() {
4040
.filter_map(|c| c.covariate())
4141
.map(|&k| design.loading_column(k as usize))
4242
.collect();
43-
for level in 0..meta.n_levels {
43+
for level in 0..meta.n_levels() {
4444
let obs: Vec<usize> = (0..levels.len())
4545
.filter(|&i| levels[i] as usize == level)
4646
.collect();

0 commit comments

Comments
 (0)