datacortex_core/mixer/
meta_mixer.rs1const N_BINS: usize = 64;
13
14const TABLE_SIZE: usize = N_BINS * N_BINS;
16
17pub struct MetaMixer {
19 table: Vec<u32>,
22 gru_weight: u32,
25 last_idx: usize,
27 last_p: u32,
29 lr_shift: u32,
31}
32
33impl MetaMixer {
34 pub fn new(gru_weight_pct: u32) -> Self {
37 let mut table = vec![0u32; TABLE_SIZE];
38 for cm_bin in 0..N_BINS {
39 for gru_bin in 0..N_BINS {
40 let cm_center =
41 (cm_bin as u32 * 4095 + (N_BINS as u32 - 1) / 2) / (N_BINS as u32 - 1);
42 let gru_center =
43 (gru_bin as u32 * 4095 + (N_BINS as u32 - 1) / 2) / (N_BINS as u32 - 1);
44 let w = (gru_weight_pct * 256 / 100).min(256);
46 let avg =
47 (cm_center as u64 * (256 - w) as u64 + gru_center as u64 * w as u64) / 256;
48 table[cm_bin * N_BINS + gru_bin] = (avg as u32).clamp(1, 4095);
49 }
50 }
51
52 MetaMixer {
53 table,
54 gru_weight: (gru_weight_pct * 256 / 100).min(256),
55 last_idx: 0,
56 last_p: 2048,
57 lr_shift: 5,
58 }
59 }
60
61 #[inline(always)]
68 pub fn blend(&mut self, p_cm: u32, p_gru: u32) -> u32 {
69 let cm_bin = ((p_cm.min(4095) as u64 * (N_BINS as u64 - 1)) / 4095) as usize;
70 let gru_bin = ((p_gru.min(4095) as u64 * (N_BINS as u64 - 1)) / 4095) as usize;
71 let idx = cm_bin.min(N_BINS - 1) * N_BINS + gru_bin.min(N_BINS - 1);
72 self.last_idx = idx;
73
74 let table_p = self.table[idx];
75
76 let direct = (p_cm as u64 * (256 - self.gru_weight) as u64
78 + p_gru as u64 * self.gru_weight as u64)
79 / 256;
80
81 let blended = (table_p as u64 + direct) / 2;
83 self.last_p = (blended as u32).clamp(1, 4095);
84 self.last_p
85 }
86
87 #[inline(always)]
89 pub fn update(&mut self, bit: u8) {
90 let target = if bit != 0 { 4095u32 } else { 1u32 };
91 let old = self.table[self.last_idx];
92 let delta = (target as i32 - old as i32) >> self.lr_shift;
93 self.table[self.last_idx] = (old as i32 + delta).clamp(1, 4095) as u32;
94 }
95
96 pub fn last_prediction(&self) -> u32 {
98 self.last_p
99 }
100}
101
102impl Default for MetaMixer {
103 fn default() -> Self {
104 Self::new(5) }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn initial_blend_biased_to_cm() {
114 let mut mixer = MetaMixer::new(5);
115 let p = mixer.blend(2048, 2048);
117 assert!(
118 (1900..=2200).contains(&p),
119 "equal inputs should give ~2048, got {p}"
120 );
121 }
122
123 #[test]
124 fn blend_always_in_range() {
125 let mut mixer = MetaMixer::new(5);
126 for cm in [1u32, 100, 1000, 2048, 3000, 4000, 4095] {
127 for gru in [1u32, 100, 1000, 2048, 3000, 4000, 4095] {
128 let p = mixer.blend(cm, gru);
129 assert!(
130 (1..=4095).contains(&p),
131 "out of range: cm={cm}, gru={gru}, got {p}"
132 );
133 }
134 }
135 }
136
137 #[test]
138 fn cm_dominates_at_low_weight() {
139 let mut mixer = MetaMixer::new(5); let p = mixer.blend(3500, 500);
142 assert!(p > 2500, "5% GRU should let CM dominate: got {p}");
144 }
145
146 #[test]
147 fn update_adapts() {
148 let mut mixer = MetaMixer::new(5);
149 for _ in 0..200 {
150 mixer.blend(2048, 2048);
151 mixer.update(1);
152 }
153 let p = mixer.blend(2048, 2048);
154 assert!(p > 2048, "after many 1s, should predict higher: {p}");
155 }
156}