datacortex_core/mixer/
logistic.rs1const K: i32 = 128;
19
20const SQUASH_SIZE: usize = 16384;
23const SQUASH_OFFSET: i32 = 8192;
24
25static SQUASH_TABLE: [u16; SQUASH_SIZE] = {
26 let mut table = [0u16; SQUASH_SIZE];
27 let mut i = 0usize;
28 while i < SQUASH_SIZE {
29 let d = i as i32 - SQUASH_OFFSET;
30 let abs_d = if d < 0 { -d } else { d };
31 let p = 2048 + (d * 2047) / (K + abs_d);
32 table[i] = if p < 1 {
33 1
34 } else if p > 4095 {
35 4095
36 } else {
37 p as u16
38 };
39 i += 1;
40 }
41 table
42};
43
44static STRETCH_TABLE: [i16; 4097] = {
48 let mut table = [0i16; 4097];
49 let mut p = 0usize;
50 while p <= 4096 {
51 let c = p as i32 - 2048;
52 let abs_c = if c < 0 { -c } else { c };
53
54 let d = if abs_c >= 2047 {
55 if c >= 0 { 8191i32 } else { -8191i32 }
56 } else {
57 (c * K) / (2047 - abs_c)
58 };
59
60 table[p] = if d > 8191 {
61 8191
62 } else if d < -8191 {
63 -8191
64 } else {
65 d as i16
66 };
67 p += 1;
68 }
69 table
70};
71
72#[inline(always)]
76pub fn stretch(p: u32) -> i32 {
77 STRETCH_TABLE[p.min(4096) as usize] as i32
78}
79
80#[inline(always)]
84pub fn squash(d: i32) -> u32 {
85 let idx = (d + SQUASH_OFFSET).clamp(0, (SQUASH_SIZE - 1) as i32) as usize;
86 SQUASH_TABLE[idx] as u32
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 #[test]
94 fn squash_at_zero_is_half() {
95 let p = squash(0);
96 assert_eq!(p, 2048, "squash(0) should be exactly 2048, got {p}");
97 }
98
99 #[test]
100 fn stretch_at_half_is_zero() {
101 let d = stretch(2048);
102 assert_eq!(d, 0, "stretch(2048) should be 0, got {d}");
103 }
104
105 #[test]
106 fn squash_output_in_range() {
107 for d in -10000..=10000 {
108 let p = squash(d);
109 assert!(
110 (1..=4095).contains(&p),
111 "squash({d}) = {p}, out of [1, 4095]"
112 );
113 }
114 }
115
116 #[test]
117 fn stretch_output_bounded() {
118 for p in 1..=4095u32 {
119 let d = stretch(p);
120 assert!(
121 (-8191..=8191).contains(&d),
122 "stretch({p}) = {d}, out of bounds"
123 );
124 }
125 }
126
127 #[test]
128 fn squash_is_monotonic() {
129 let mut prev = squash(-10000);
130 for d in -9999..=10000 {
131 let p = squash(d);
132 assert!(p >= prev, "squash not monotonic at d={d}: {prev} > {p}");
133 prev = p;
134 }
135 }
136
137 #[test]
138 fn stretch_is_monotonic() {
139 let mut prev = stretch(1);
140 for p in 2..=4095u32 {
141 let d = stretch(p);
142 assert!(d >= prev, "stretch not monotonic at p={p}: {prev} > {d}");
143 prev = d;
144 }
145 }
146
147 #[test]
148 fn roundtrip_squash_stretch() {
149 let mut max_diff = 0u32;
155 for p in 100..=3996u32 {
156 let d = stretch(p);
157 let p2 = squash(d);
158 let diff = (p2 as i32 - p as i32).unsigned_abs();
159 if diff > max_diff {
160 max_diff = diff;
161 }
162 }
163 assert!(
165 max_diff <= 35,
166 "max roundtrip error {max_diff} in range [100, 3996]"
167 );
168 }
169
170 #[test]
171 fn roundtrip_stretch_squash() {
172 for d in -1500..=1500 {
175 let p = squash(d);
176 let d2 = stretch(p);
177 let diff = (d2 - d).unsigned_abs();
178 assert!(
179 diff <= 30,
180 "roundtrip error: d={d}, squash={p}, stretch(squash)={d2}, diff={diff}"
181 );
182 }
183 }
184
185 #[test]
186 fn symmetry() {
187 for p in 1..=4095u32 {
189 let d1 = stretch(p);
190 let d2 = stretch(4096 - p);
191 assert_eq!(
192 d1,
193 -d2,
194 "asymmetry at p={p}: stretch({p})={d1}, stretch({})={d2}",
195 4096 - p,
196 );
197 }
198 }
199
200 #[test]
201 fn squash_extremes() {
202 assert!(squash(-10000) <= 100, "squash(-10000) = {}", squash(-10000));
203 assert!(squash(10000) >= 3996, "squash(10000) = {}", squash(10000));
204 }
205
206 #[test]
207 fn stretch_extremes() {
208 assert!(stretch(1) < -60, "stretch(1) = {}", stretch(1));
209 assert!(stretch(4095) > 60, "stretch(4095) = {}", stretch(4095));
210 }
211}