Skip to main content

oxiphysics_gpu/
neural_physics.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Neural network-based physics acceleration (CPU mock).
5//!
6//! Provides a simple feed-forward neural network with multiple activation
7//! functions, forward-pass inference, MSE loss, ML force potentials, and
8//! collision probability prediction — all on CPU as a GPU mock backend.
9
10// ── Activation type ──────────────────────────────────────────────────────────
11
12/// Activation function used in a neural layer.
13#[derive(Debug, Clone, Copy, PartialEq)]
14pub enum ActivationType {
15    /// Rectified linear unit: `max(0, x)`.
16    Relu,
17    /// Hyperbolic tangent.
18    Tanh,
19    /// Logistic sigmoid: `1 / (1 + exp(-x))`.
20    Sigmoid,
21    /// Identity / no activation.
22    Linear,
23}
24
25// ── Layer and network structs ─────────────────────────────────────────────────
26
27/// A single fully-connected neural network layer.
28#[derive(Debug, Clone)]
29pub struct NeuralLayer {
30    /// Weight matrix: `weights[out][in]`.
31    pub weights: Vec<Vec<f32>>,
32    /// Bias vector, one entry per output neuron.
33    pub biases: Vec<f32>,
34    /// Activation function applied after the linear transform.
35    pub activation: ActivationType,
36}
37
38/// A feed-forward neural network composed of stacked [`NeuralLayer`]s.
39#[derive(Debug, Clone)]
40pub struct NeuralNet {
41    /// The ordered list of layers.
42    pub layers: Vec<NeuralLayer>,
43    /// Expected size of the input vector.
44    pub input_size: usize,
45    /// Size of the network's output.
46    pub output_size: usize,
47}
48
49// ── Activation functions ──────────────────────────────────────────────────────
50
51/// Evaluate activation function `act` at scalar `x`.
52pub fn activate(x: f32, act: &ActivationType) -> f32 {
53    match act {
54        ActivationType::Relu => x.max(0.0),
55        ActivationType::Tanh => x.tanh(),
56        ActivationType::Sigmoid => 1.0 / (1.0 + (-x).exp()),
57        ActivationType::Linear => x,
58    }
59}
60
61/// Evaluate the derivative of activation function `act` at *pre-activation* `x`.
62pub fn activate_derivative(x: f32, act: &ActivationType) -> f32 {
63    match act {
64        ActivationType::Relu => {
65            if x > 0.0 {
66                1.0
67            } else {
68                0.0
69            }
70        }
71        ActivationType::Tanh => {
72            let t = x.tanh();
73            1.0 - t * t
74        }
75        ActivationType::Sigmoid => {
76            let s = 1.0 / (1.0 + (-x).exp());
77            s * (1.0 - s)
78        }
79        ActivationType::Linear => 1.0,
80    }
81}
82
83// ── Inference ─────────────────────────────────────────────────────────────────
84
85/// Run a forward pass through `net`, returning the output vector.
86///
87/// # Panics
88/// Panics in debug mode if `input.len() != net.input_size`.
89pub fn forward_pass(net: &NeuralNet, input: &[f32]) -> Vec<f32> {
90    debug_assert_eq!(input.len(), net.input_size);
91    let mut current: Vec<f32> = input.to_vec();
92    for layer in &net.layers {
93        let n_out = layer.biases.len();
94        let mut next = Vec::with_capacity(n_out);
95        for o in 0..n_out {
96            let mut sum = layer.biases[o];
97            for (i, &inp) in current.iter().enumerate() {
98                if i < layer.weights[o].len() {
99                    sum += layer.weights[o][i] * inp;
100                }
101            }
102            next.push(activate(sum, &layer.activation));
103        }
104        current = next;
105    }
106    current
107}
108
109// ── Loss ──────────────────────────────────────────────────────────────────────
110
111/// Mean squared error between `predicted` and `target` vectors.
112///
113/// Returns 0 if either slice is empty or lengths differ.
114pub fn mse_loss(predicted: &[f32], target: &[f32]) -> f32 {
115    if predicted.is_empty() || predicted.len() != target.len() {
116        return 0.0;
117    }
118    let n = predicted.len() as f32;
119    predicted
120        .iter()
121        .zip(target.iter())
122        .map(|(p, t)| (p - t) * (p - t))
123        .sum::<f32>()
124        / n
125}
126
127// ── Physics applications ──────────────────────────────────────────────────────
128
129/// Predict interatomic forces using a neural network potential.
130///
131/// For each atom, concatenates its position `[x, y, z]` with its type index,
132/// runs a forward pass, and interprets the first three output components as the
133/// predicted force `[fx, fy, fz]`.
134pub fn neural_force_prediction(
135    net: &NeuralNet,
136    positions: &[[f32; 3]],
137    types: &[u32],
138) -> Vec<[f32; 3]> {
139    positions
140        .iter()
141        .zip(types.iter())
142        .map(|(pos, &atom_type)| {
143            let mut inp = Vec::with_capacity(net.input_size);
144            inp.push(pos[0]);
145            inp.push(pos[1]);
146            inp.push(pos[2]);
147            inp.push(atom_type as f32);
148            // Pad or truncate to net.input_size
149            inp.resize(net.input_size, 0.0);
150            let out = forward_pass(net, &inp);
151            let fx = out.first().copied().unwrap_or(0.0);
152            let fy = out.get(1).copied().unwrap_or(0.0);
153            let fz = out.get(2).copied().unwrap_or(0.0);
154            [fx, fy, fz]
155        })
156        .collect()
157}
158
159/// Predict collision probability between two spheres using a neural network.
160///
161/// Input features: relative displacement `[dx, dy, dz]`, radii `[ra, rb]`.
162/// Returns a scalar in `[0, 1]`.
163pub fn neural_collision_check(
164    net: &NeuralNet,
165    pos_a: [f32; 3],
166    pos_b: [f32; 3],
167    radii: [f32; 2],
168) -> f32 {
169    let dx = pos_b[0] - pos_a[0];
170    let dy = pos_b[1] - pos_a[1];
171    let dz = pos_b[2] - pos_a[2];
172    let mut inp = vec![dx, dy, dz, radii[0], radii[1]];
173    inp.resize(net.input_size, 0.0);
174    let out = forward_pass(net, &inp);
175    // Clamp output to [0, 1]
176    out.first().copied().unwrap_or(0.0).clamp(0.0, 1.0)
177}
178
179/// Run a batched GPU-style forward pass for multiple input vectors.
180pub fn gpu_neural_batch_forward(net: &NeuralNet, batch: &[Vec<f32>]) -> Vec<Vec<f32>> {
181    batch.iter().map(|inp| forward_pass(net, inp)).collect()
182}
183
184// ── Network construction ──────────────────────────────────────────────────────
185
186/// Create a fully-connected network with the given layer sizes and random weights.
187///
188/// `layer_sizes` must contain at least 2 entries (input + output).
189/// All hidden layers use `activation`; the output layer uses `Linear`.
190pub fn create_network(layer_sizes: &[usize], activation: ActivationType) -> NeuralNet {
191    use rand::RngExt;
192    assert!(
193        layer_sizes.len() >= 2,
194        "Need at least input and output sizes"
195    );
196
197    let mut rng = rand::rng();
198    let mut layers = Vec::new();
199
200    for i in 0..layer_sizes.len() - 1 {
201        let n_in = layer_sizes[i];
202        let n_out = layer_sizes[i + 1];
203        let is_last = i == layer_sizes.len() - 2;
204        let act = if is_last {
205            ActivationType::Linear
206        } else {
207            activation
208        };
209
210        let scale = (2.0_f32 / n_in as f32).sqrt();
211        let weights: Vec<Vec<f32>> = (0..n_out)
212            .map(|_| (0..n_in).map(|_| rng.random_range(-scale..scale)).collect())
213            .collect();
214        let biases: Vec<f32> = (0..n_out).map(|_| 0.0_f32).collect();
215        layers.push(NeuralLayer {
216            weights,
217            biases,
218            activation: act,
219        });
220    }
221
222    NeuralNet {
223        input_size: layer_sizes[0],
224        output_size: *layer_sizes.last().expect("collection should not be empty"),
225        layers,
226    }
227}
228
229// ── Tests ─────────────────────────────────────────────────────────────────────
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234
235    fn simple_net() -> NeuralNet {
236        // 2 → 3 → 1
237        create_network(&[2, 3, 1], ActivationType::Relu)
238    }
239
240    #[test]
241    fn test_activate_relu_positive() {
242        assert!((activate(2.0, &ActivationType::Relu) - 2.0).abs() < 1e-6);
243    }
244
245    #[test]
246    fn test_activate_relu_negative() {
247        assert!((activate(-1.0, &ActivationType::Relu)).abs() < 1e-6);
248    }
249
250    #[test]
251    fn test_activate_relu_zero() {
252        assert!((activate(0.0, &ActivationType::Relu)).abs() < 1e-6);
253    }
254
255    #[test]
256    fn test_activate_tanh_zero() {
257        assert!((activate(0.0, &ActivationType::Tanh)).abs() < 1e-6);
258    }
259
260    #[test]
261    fn test_activate_sigmoid_zero() {
262        assert!((activate(0.0, &ActivationType::Sigmoid) - 0.5).abs() < 1e-6);
263    }
264
265    #[test]
266    fn test_activate_linear() {
267        assert!((activate(3.125, &ActivationType::Linear) - 3.125).abs() < 1e-6);
268    }
269
270    #[test]
271    fn test_activate_derivative_relu_positive() {
272        assert!((activate_derivative(1.0, &ActivationType::Relu) - 1.0).abs() < 1e-6);
273    }
274
275    #[test]
276    fn test_activate_derivative_relu_negative() {
277        assert!((activate_derivative(-1.0, &ActivationType::Relu)).abs() < 1e-6);
278    }
279
280    #[test]
281    fn test_activate_derivative_tanh_zero() {
282        assert!((activate_derivative(0.0, &ActivationType::Tanh) - 1.0).abs() < 1e-6);
283    }
284
285    #[test]
286    fn test_activate_derivative_sigmoid_zero() {
287        assert!((activate_derivative(0.0, &ActivationType::Sigmoid) - 0.25).abs() < 1e-5);
288    }
289
290    #[test]
291    fn test_activate_derivative_linear() {
292        assert!((activate_derivative(99.0, &ActivationType::Linear) - 1.0).abs() < 1e-6);
293    }
294
295    #[test]
296    fn test_mse_loss_zero() {
297        let a = vec![1.0, 2.0, 3.0];
298        assert!((mse_loss(&a, &a)).abs() < 1e-6);
299    }
300
301    #[test]
302    fn test_mse_loss_known() {
303        let p = vec![0.0, 0.0];
304        let t = vec![1.0, 1.0];
305        assert!((mse_loss(&p, &t) - 1.0).abs() < 1e-6);
306    }
307
308    #[test]
309    fn test_mse_loss_empty() {
310        assert!((mse_loss(&[], &[])).abs() < 1e-6);
311    }
312
313    #[test]
314    fn test_mse_loss_length_mismatch() {
315        assert!((mse_loss(&[1.0], &[1.0, 2.0])).abs() < 1e-6);
316    }
317
318    #[test]
319    fn test_create_network_sizes() {
320        let net = create_network(&[4, 8, 8, 3], ActivationType::Relu);
321        assert_eq!(net.input_size, 4);
322        assert_eq!(net.output_size, 3);
323        assert_eq!(net.layers.len(), 3);
324    }
325
326    #[test]
327    fn test_create_network_layer_dims() {
328        let net = create_network(&[3, 5, 2], ActivationType::Tanh);
329        assert_eq!(net.layers[0].weights.len(), 5);
330        assert_eq!(net.layers[0].weights[0].len(), 3);
331        assert_eq!(net.layers[1].weights.len(), 2);
332        assert_eq!(net.layers[1].weights[0].len(), 5);
333    }
334
335    #[test]
336    fn test_create_network_output_activation_linear() {
337        let net = create_network(&[2, 4, 1], ActivationType::Relu);
338        assert_eq!(
339            net.layers.last().unwrap().activation,
340            ActivationType::Linear
341        );
342    }
343
344    #[test]
345    fn test_forward_pass_output_size() {
346        let net = simple_net();
347        let out = forward_pass(&net, &[0.5, -0.3]);
348        assert_eq!(out.len(), 1);
349    }
350
351    #[test]
352    fn test_forward_pass_deterministic() {
353        let net = simple_net();
354        let a = forward_pass(&net, &[1.0, 0.0]);
355        let b = forward_pass(&net, &[1.0, 0.0]);
356        assert_eq!(a, b);
357    }
358
359    #[test]
360    fn test_forward_pass_zero_input() {
361        let net = simple_net();
362        let out = forward_pass(&net, &[0.0, 0.0]);
363        assert_eq!(out.len(), 1);
364    }
365
366    #[test]
367    fn test_forward_pass_sigmoid_net() {
368        let net = create_network(&[2, 2, 1], ActivationType::Sigmoid);
369        let out = forward_pass(&net, &[0.0, 0.0]);
370        // Output of sigmoid net on zero input should be in range [0,1] roughly
371        assert!(out[0].is_finite());
372    }
373
374    #[test]
375    fn test_neural_force_prediction_shape() {
376        let net = create_network(&[4, 8, 3], ActivationType::Relu);
377        let positions = vec![[1.0_f32, 0.0, 0.0], [0.0, 1.0, 0.0]];
378        let types = vec![0u32, 1];
379        let forces = neural_force_prediction(&net, &positions, &types);
380        assert_eq!(forces.len(), 2);
381    }
382
383    #[test]
384    fn test_neural_force_prediction_finite() {
385        let net = create_network(&[4, 6, 3], ActivationType::Tanh);
386        let positions = vec![[0.0_f32; 3]];
387        let types = vec![0u32];
388        let forces = neural_force_prediction(&net, &positions, &types);
389        assert!(forces[0][0].is_finite());
390        assert!(forces[0][1].is_finite());
391        assert!(forces[0][2].is_finite());
392    }
393
394    #[test]
395    fn test_neural_collision_check_range() {
396        let net = create_network(&[5, 4, 1], ActivationType::Sigmoid);
397        let prob = neural_collision_check(&net, [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 0.5]);
398        assert!((0.0..=1.0).contains(&prob));
399    }
400
401    #[test]
402    fn test_neural_collision_check_zero_sep() {
403        let net = create_network(&[5, 4, 1], ActivationType::Sigmoid);
404        let prob = neural_collision_check(&net, [0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [1.0, 1.0]);
405        assert!((0.0..=1.0).contains(&prob));
406    }
407
408    #[test]
409    fn test_gpu_neural_batch_forward_shape() {
410        let net = create_network(&[3, 4, 2], ActivationType::Relu);
411        let batch: Vec<Vec<f32>> = vec![
412            vec![1.0, 2.0, 3.0],
413            vec![0.0, 0.0, 0.0],
414            vec![-1.0, 0.5, 0.1],
415        ];
416        let results = gpu_neural_batch_forward(&net, &batch);
417        assert_eq!(results.len(), 3);
418        for r in &results {
419            assert_eq!(r.len(), 2);
420        }
421    }
422
423    #[test]
424    fn test_gpu_neural_batch_forward_empty() {
425        let net = create_network(&[2, 2, 1], ActivationType::Linear);
426        let results = gpu_neural_batch_forward(&net, &[]);
427        assert!(results.is_empty());
428    }
429
430    #[test]
431    fn test_create_network_two_layers() {
432        let net = create_network(&[1, 1], ActivationType::Linear);
433        assert_eq!(net.layers.len(), 1);
434        assert_eq!(net.input_size, 1);
435        assert_eq!(net.output_size, 1);
436    }
437
438    #[test]
439    fn test_network_weights_finite() {
440        let net = create_network(&[5, 10, 3], ActivationType::Relu);
441        for layer in &net.layers {
442            for row in &layer.weights {
443                for &w in row {
444                    assert!(w.is_finite());
445                }
446            }
447        }
448    }
449
450    #[test]
451    fn test_forward_pass_tanh_bounded() {
452        let net = create_network(&[2, 4, 1], ActivationType::Tanh);
453        let out = forward_pass(&net, &[100.0, -100.0]);
454        // tanh saturates; linear output should still be finite
455        assert!(out[0].is_finite());
456    }
457
458    #[test]
459    fn test_mse_loss_asymmetric() {
460        let p = vec![2.0_f32, 0.0];
461        let t = vec![0.0_f32, 2.0];
462        // (4 + 4) / 2 = 4
463        assert!((mse_loss(&p, &t) - 4.0).abs() < 1e-5);
464    }
465
466    #[test]
467    fn test_neural_force_empty_input() {
468        let net = create_network(&[4, 4, 3], ActivationType::Linear);
469        let forces = neural_force_prediction(&net, &[], &[]);
470        assert!(forces.is_empty());
471    }
472
473    #[test]
474    fn test_batch_forward_single_item() {
475        let net = create_network(&[2, 3, 1], ActivationType::Relu);
476        let batch = vec![vec![0.5_f32, -0.5]];
477        let out = gpu_neural_batch_forward(&net, &batch);
478        assert_eq!(out.len(), 1);
479        assert_eq!(out[0].len(), 1);
480    }
481}