Skip to main content

temporal_neural_solver/baselines/
numpy_style.rs

1//! NumPy-style neural network implementation
2//!
3//! This mimics how a typical Python/NumPy implementation would work,
4//! providing another baseline for comparison.
5
6use ndarray::{Array1, Array2, Axis};
7use ndarray_rand::RandomExt;
8use rand_distr::Normal;
9use std::time::{Duration, Instant};
10
11/// NumPy-style implementation using ndarray operations
12pub struct NumpyStyleNetwork {
13    weights1: Array2<f32>,
14    bias1: Array1<f32>,
15    weights2: Array2<f32>,
16    bias2: Array1<f32>,
17}
18
19impl NumpyStyleNetwork {
20    pub fn new_standard() -> Self {
21        // Initialize like NumPy would (similar to sklearn's MLPClassifier)
22        let scale1 = (2.0 / 128.0_f32).sqrt();
23        let scale2 = (2.0 / 32.0_f32).sqrt();
24
25        let dist1 = Normal::new(0.0, scale1).unwrap();
26        let dist2 = Normal::new(0.0, scale2).unwrap();
27
28        Self {
29            weights1: Array2::random((32, 128), dist1),
30            bias1: Array1::zeros(32),
31            weights2: Array2::random((4, 32), dist2),
32            bias2: Array1::zeros(4),
33        }
34    }
35
36    /// Forward pass using ndarray broadcasting (like NumPy)
37    pub fn forward(&self, input: &Array1<f32>) -> Array1<f32> {
38        // Layer 1: W1 @ x + b1
39        let z1 = self.weights1.dot(input) + &self.bias1;
40
41        // ReLU activation (element-wise maximum with 0)
42        let a1 = z1.mapv(|x| x.max(0.0));
43
44        // Layer 2: W2 @ a1 + b2
45        let z2 = self.weights2.dot(&a1) + &self.bias2;
46
47        z2
48    }
49
50    /// Batch forward pass (like NumPy's vectorized operations)
51    pub fn forward_batch(&self, inputs: &Array2<f32>) -> Array2<f32> {
52        // inputs shape: (batch_size, 128)
53        // outputs shape: (batch_size, 4)
54
55        let batch_size = inputs.shape()[0];
56        let mut outputs = Array2::zeros((batch_size, 4));
57
58        // Process each sample (NumPy would vectorize this)
59        for (i, input_row) in inputs.axis_iter(Axis(0)).enumerate() {
60            let input_vec = input_row.to_owned();
61            let output = self.forward(&input_vec);
62            outputs.row_mut(i).assign(&output);
63        }
64
65        outputs
66    }
67
68    /// Predict with timing
69    pub fn predict_timed(&self, input: &Array1<f32>) -> (Array1<f32>, Duration) {
70        let start = Instant::now();
71        let output = self.forward(input);
72        let duration = start.elapsed();
73        (output, duration)
74    }
75
76    /// Batch predict with timing
77    pub fn predict_batch_timed(&self, inputs: &Array2<f32>) -> (Array2<f32>, Duration) {
78        let start = Instant::now();
79        let outputs = self.forward_batch(inputs);
80        let duration = start.elapsed();
81        (outputs, duration)
82    }
83
84    /// Gradient computation (for completeness, like sklearn)
85    pub fn compute_gradients(&self, input: &Array1<f32>, target: &Array1<f32>) -> f32 {
86        let prediction = self.forward(input);
87
88        // Mean squared error loss
89        let diff = &prediction - target;
90        let loss = diff.mapv(|x| x * x).sum() / prediction.len() as f32;
91
92        loss
93    }
94}
95
96/// NumPy-style with manual loop unrolling (optimized NumPy equivalent)
97pub struct OptimizedNumpyStyle {
98    // Store as contiguous memory like NumPy arrays
99    w1_data: Vec<f32>,  // 32 x 128
100    b1_data: Vec<f32>,  // 32
101    w2_data: Vec<f32>,  // 4 x 32
102    b2_data: Vec<f32>,  // 4
103}
104
105impl OptimizedNumpyStyle {
106    pub fn new_standard() -> Self {
107        let scale1 = (2.0 / 128.0_f32).sqrt();
108        let scale2 = (2.0 / 32.0_f32).sqrt();
109
110        use rand::Rng;
111        let mut rng = rand::thread_rng();
112
113        // Initialize like NumPy
114        let w1_data: Vec<f32> = (0..32*128)
115            .map(|_| rng.gen::<f32>() * scale1 * 2.0 - scale1)
116            .collect();
117
118        let w2_data: Vec<f32> = (0..4*32)
119            .map(|_| rng.gen::<f32>() * scale2 * 2.0 - scale2)
120            .collect();
121
122        Self {
123            w1_data,
124            b1_data: vec![0.0; 32],
125            w2_data,
126            b2_data: vec![0.0; 4],
127        }
128    }
129
130    /// NumPy-style dot product with manual implementation
131    pub fn forward(&self, input: &[f32; 128]) -> [f32; 4] {
132        // Layer 1: 128 -> 32 with ReLU
133        let mut hidden = [0.0f32; 32];
134
135        for i in 0..32 {
136            let mut sum = self.b1_data[i];
137
138            // Manual dot product (like NumPy's internal implementation)
139            for j in 0..128 {
140                sum += self.w1_data[i * 128 + j] * input[j];
141            }
142
143            // ReLU
144            hidden[i] = if sum > 0.0 { sum } else { 0.0 };
145        }
146
147        // Layer 2: 32 -> 4
148        let mut output = [0.0f32; 4];
149
150        for i in 0..4 {
151            let mut sum = self.b2_data[i];
152
153            for j in 0..32 {
154                sum += self.w2_data[i * 32 + j] * hidden[j];
155            }
156
157            output[i] = sum;
158        }
159
160        output
161    }
162
163    pub fn predict_timed(&self, input: &[f32; 128]) -> ([f32; 4], Duration) {
164        let start = Instant::now();
165        let output = self.forward(input);
166        let duration = start.elapsed();
167        (output, duration)
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn test_numpy_style_network() {
177        let network = NumpyStyleNetwork::new_standard();
178        let input = Array1::from_vec(vec![0.1; 128]);
179        let (output, duration) = network.predict_timed(&input);
180
181        assert_eq!(output.len(), 4);
182        println!("NumPy-style latency: {:?}", duration);
183    }
184
185    #[test]
186    fn test_numpy_batch_processing() {
187        let network = NumpyStyleNetwork::new_standard();
188        let inputs = Array2::from_shape_vec((10, 128), vec![0.1; 10 * 128]).unwrap();
189        let (outputs, duration) = network.predict_batch_timed(&inputs);
190
191        assert_eq!(outputs.shape(), &[10, 4]);
192        println!("NumPy batch latency: {:?}", duration);
193    }
194
195    #[test]
196    fn test_optimized_numpy_style() {
197        let network = OptimizedNumpyStyle::new_standard();
198        let input = [0.1f32; 128];
199        let (output, duration) = network.predict_timed(&input);
200
201        assert_eq!(output.len(), 4);
202        println!("Optimized NumPy-style latency: {:?}", duration);
203    }
204}