1#[derive(Debug, Clone, Copy, PartialEq)]
14pub enum ActivationType {
15 Relu,
17 Tanh,
19 Sigmoid,
21 Linear,
23}
24
25#[derive(Debug, Clone)]
29pub struct NeuralLayer {
30 pub weights: Vec<Vec<f32>>,
32 pub biases: Vec<f32>,
34 pub activation: ActivationType,
36}
37
38#[derive(Debug, Clone)]
40pub struct NeuralNet {
41 pub layers: Vec<NeuralLayer>,
43 pub input_size: usize,
45 pub output_size: usize,
47}
48
49pub 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
61pub 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
83pub 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
109pub 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
127pub 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 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
159pub 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 out.first().copied().unwrap_or(0.0).clamp(0.0, 1.0)
177}
178
179pub 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
184pub 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#[cfg(test)]
232mod tests {
233 use super::*;
234
235 fn simple_net() -> NeuralNet {
236 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 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 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 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}