Skip to main content

solver/numeric/
basis.rs

1use core::f64;
2
3/// Trait defining a set of basis functions $\Phi(x) = [\phi_1(x), ..., \phi_K(x)]$.
4///
5/// Used for linear function approximation $V(x) \approx \sum w_i \phi_i(x)$.
6pub trait Basis<const N: usize>: Send + Sync {
7    /// Returns the number of basis functions (K).
8    fn num_features(&self) -> usize;
9
10    /// Evaluates all basis functions at `state` and writes them into `buffer`.
11    /// `buffer` must have length at least `num_features()`.
12    fn evaluate(&self, state: &[f64; N], buffer: &mut [f64]);
13
14    /// Evaluates all basis functions and appends them to the provided vector.
15    /// This avoids zero-initialization overhead.
16    fn evaluate_buffers(&self, state: &[f64; N], buffer: &mut Vec<f64>) {
17        let start_len = buffer.len();
18        let n = self.num_features();
19        // Reserve space
20        buffer.reserve(n);
21
22        // Unsafe implementation to avoid zeroing, or just standard push
23        // We will default to a standard efficient implementation here using resize if needed
24        // but optimal path is implementation specific.
25
26        // Fallback: resize and call evaluate (allocates zeros if resizing up)
27        buffer.resize(start_len + n, 0.0);
28        self.evaluate(state, &mut buffer[start_len..]);
29    }
30
31    /// Returns the dot product $w \cdot \Phi(x)$ without allocating the full feature vector.
32    /// Default implementation allocates, overrides can be optimization.
33    fn eval_dot(&self, state: &[f64; N], weights: &[f64]) -> f64 {
34        let n = self.num_features();
35        let mut buffer = vec![0.0; n]; // Allocation! Override me.
36        self.evaluate(state, &mut buffer);
37
38        let mut sum = 0.0;
39        for i in 0..n {
40            sum += buffer[i] * weights[i];
41        }
42        sum
43    }
44}
45
46/// Standard Polynomial Basis (Power, Hermite, Chebyshev, etc.)
47///
48/// Includes:
49/// - Full interactions up to degree 2 (Quadratic)
50/// - Diagonal terms up to `degree`
51#[derive(Clone, Debug)]
52pub struct PolynomialBasis {
53    pub degree: usize,
54    pub poly_type: PolynomialType,
55    pub clamp_range: Option<(f64, f64)>,
56}
57
58#[derive(Clone, Copy, Debug)]
59pub enum PolynomialType {
60    Power,
61    Hermite,
62    Chebyshev,
63    Laguerre,
64}
65
66impl PolynomialBasis {
67    pub fn new(degree: usize, poly_type: PolynomialType) -> Self {
68        Self {
69            degree,
70            poly_type,
71            clamp_range: None,
72        }
73    }
74
75    pub fn with_clamp(mut self, min: f64, max: f64) -> Self {
76        self.clamp_range = Some((min, max));
77        self
78    }
79}
80
81/// Wrapper for any basis that applies coordinate-wise scaling before evaluation.
82///
83/// $V(x) \approx \sum w_i \phi_i(S x)$ where $S$ is a diagonal scaling matrix.
84/// Used to map problem domains (e.g. $[0, 100]$) to basis domains (e.g. $[-2, 2]$).
85#[derive(Clone, Debug)]
86pub struct ScaledBasis<const N: usize, B> {
87    pub inner: B,
88    pub scales: Vec<f64>,
89    pub center: [f64; N],
90}
91
92impl<const N: usize, B> ScaledBasis<N, B> {
93    pub fn new(inner: B, scales: Vec<f64>) -> Self {
94        Self {
95            inner,
96            scales,
97            center: [0.0; N],
98        }
99    }
100
101    pub fn with_center(mut self, center: [f64; N]) -> Self {
102        self.center = center;
103        self
104    }
105}
106
107impl<const N: usize, B: Basis<N>> Basis<N> for ScaledBasis<N, B> {
108    fn num_features(&self) -> usize {
109        self.inner.num_features()
110    }
111
112    fn evaluate(&self, state: &[f64; N], buffer: &mut [f64]) {
113        let mut scaled = *state;
114        for (i, val) in scaled.iter_mut().enumerate() {
115            let s = if i < self.scales.len() {
116                self.scales[i]
117            } else {
118                *self.scales.first().unwrap_or(&1.0)
119            };
120            *val = (*val - self.center[i]) * s;
121        }
122        self.inner.evaluate(&scaled, buffer);
123    }
124
125    fn evaluate_buffers(&self, state: &[f64; N], buffer: &mut Vec<f64>) {
126        let mut scaled = *state;
127        for (i, val) in scaled.iter_mut().enumerate() {
128            let s = if i < self.scales.len() {
129                self.scales[i]
130            } else {
131                *self.scales.first().unwrap_or(&1.0)
132            };
133            *val = (*val - self.center[i]) * s;
134        }
135        self.inner.evaluate_buffers(&scaled, buffer);
136    }
137
138    fn eval_dot(&self, state: &[f64; N], weights: &[f64]) -> f64 {
139        let mut scaled = *state;
140        for (i, val) in scaled.iter_mut().enumerate() {
141            let s = if i < self.scales.len() {
142                self.scales[i]
143            } else {
144                *self.scales.first().unwrap_or(&1.0)
145            };
146            *val = (*val - self.center[i]) * s;
147        }
148        self.inner.eval_dot(&scaled, weights)
149    }
150}
151
152impl<const N: usize> Basis<N> for PolynomialBasis {
153    fn num_features(&self) -> usize {
154        if self.degree == 0 {
155            return 1;
156        } else if self.degree == 1 {
157            return 1 + N;
158        }
159
160        // 1 (Bias) + N (Linear) + N*(N+1)/2 (Quadratic Interactions)
161        let mut base_count = 1 + N + N * (N + 1) / 2;
162
163        // Higher order diagonal terms (Degree 3+)
164        if self.degree > 2 {
165            base_count += (self.degree - 2) * N;
166        }
167        base_count
168    }
169
170    fn evaluate(&self, state: &[f64; N], buffer: &mut [f64]) {
171        // Pre-compute univariate polynomials
172        const MAX_DEG: usize = 6;
173        let effective_deg = self.degree.min(MAX_DEG);
174
175        // Reverted to raw unscaled evaluation.
176        let mut uni = [[0.0; MAX_DEG + 1]; N];
177        for i in 0..N {
178            let x = if let Some((min, max)) = self.clamp_range {
179                state[i].clamp(min, max)
180            } else {
181                state[i]
182            };
183            compute_univariate(x, effective_deg, self.poly_type, &mut uni[i][..]);
184        }
185
186        let mut idx = 0;
187
188        // 1. Bias (Degree >= 0)
189        buffer[idx] = 1.0;
190        idx += 1;
191
192        if self.degree < 1 {
193            return;
194        }
195
196        // 2. Linear (Degree >= 1)
197        for u_row in &uni {
198            buffer[idx] = u_row[1];
199            idx += 1;
200        }
201
202        if self.degree < 2 {
203            return;
204        }
205
206        // 3. Quadratic & Interactions (Degree >= 2)
207        // Optimized loop
208        for (i, u_row) in uni.iter().enumerate() {
209            let u_i = u_row[1];
210
211            // i == j case first
212            buffer[idx] = u_row[2]; // Diagonal
213            idx += 1;
214
215            // j > i cases (interactions)
216            for u_j in uni.iter().skip(i + 1) {
217                buffer[idx] = u_i * u_j[1];
218                idx += 1;
219            }
220        }
221
222        // 4. Higher Order (Diagonal only)
223        for k in 3..=self.degree {
224            if k > MAX_DEG {
225                break;
226            }
227            for u_row in &uni {
228                buffer[idx] = u_row[k];
229                idx += 1;
230            }
231        }
232    }
233
234    // Override evaluate_buffers to avoid zeroing
235    fn evaluate_buffers(&self, state: &[f64; N], buffer: &mut Vec<f64>) {
236        let n = <Self as Basis<N>>::num_features(self);
237        let start_len = buffer.len();
238
239        // We ensure capacity to avoid reallocation
240        buffer.reserve(n);
241
242        // Unsafe block to skip initialization since we WILL write to every index
243        // Safety: `evaluate` must write to the slice we provide. It takes &mut [f64], so it can't extend.
244        // We rely on `evaluate` implementation to fill the slice.
245        unsafe {
246            let ptr = buffer.as_mut_ptr().add(start_len);
247            let slice = std::slice::from_raw_parts_mut(ptr, n);
248            self.evaluate(state, slice);
249            buffer.set_len(start_len + n);
250        }
251    }
252
253    fn eval_dot(&self, state: &[f64; N], weights: &[f64]) -> f64 {
254        const MAX_DEG: usize = 6;
255        let effective_deg = self.degree.min(MAX_DEG);
256
257        // Reverted to raw unscaled evaluation. Scaling handled by caller or wrapper.
258        let mut uni = [[0.0; MAX_DEG + 1]; N];
259        for i in 0..N {
260            let x = if let Some((min, max)) = self.clamp_range {
261                state[i].clamp(min, max)
262            } else {
263                state[i]
264            };
265            compute_univariate(x, effective_deg, self.poly_type, &mut uni[i][..]);
266        }
267
268        let mut sum = weights[0]; // Bias weight
269        let mut idx = 1;
270
271        if self.degree < 1 {
272            return sum;
273        }
274
275        // 2. Linear
276        for u_row in &uni {
277            sum += weights[idx] * u_row[1];
278            idx += 1;
279        }
280
281        if self.degree < 2 {
282            return sum;
283        }
284
285        // 3. Quadratic & Interactions
286        for i in 0..N {
287            for j in i..N {
288                let val = if i == j {
289                    uni[i][2]
290                } else {
291                    uni[i][1] * uni[j][1]
292                };
293                sum += weights[idx] * val;
294                idx += 1;
295            }
296        }
297
298        // 4. Higher Order (Diagonal only)
299        for k in 3..=self.degree {
300            if k > MAX_DEG {
301                break;
302            }
303            for u_row in &uni {
304                sum += weights[idx] * u_row[k];
305                idx += 1;
306            }
307        }
308
309        sum
310    }
311}
312
313#[inline(always)]
314fn compute_univariate(x: f64, k: usize, ptype: PolynomialType, out: &mut [f64]) {
315    out[0] = 1.0;
316    if k >= 1 {
317        out[1] = match ptype {
318            PolynomialType::Laguerre => 1.0 - x,
319            _ => x,
320        };
321    }
322
323    // Compute P_i based on recurrence
324    match ptype {
325        PolynomialType::Power => {
326            for i in 2..=k {
327                out[i] = out[i - 1] * x;
328            }
329        }
330        PolynomialType::Hermite => {
331            // He_n+1 = x*He_n - n*He_n-1
332            for i in 2..=k {
333                let n = (i - 1) as f64;
334                out[i] = x * out[i - 1] - n * out[i - 2];
335            }
336        }
337        PolynomialType::Chebyshev => {
338            // T_n+1 = 2xT_n - T_n-1
339            for i in 2..=k {
340                out[i] = 2.0 * x * out[i - 1] - out[i - 2];
341            }
342        }
343        PolynomialType::Laguerre => {
344            // (n+1) L_n+1 = (2n+1-x)L_n - nL_n-1
345            // L_i = [ (2(i-1)+1 - x)L_{i-1} - (i-1)L_{i-2} ] / i
346            for i in 2..=k {
347                let n_prev = (i - 1) as f64; // n in recurrence
348                let n_curr = i as f64; // n+1 in recurrence
349                out[i] = ((2.0 * n_prev + 1.0 - x) * out[i - 1] - n_prev * out[i - 2]) / n_curr;
350            }
351        }
352    }
353}