1#![allow(dead_code)]
16
17pub fn ns_relu(x: f64) -> f64 {
21 x.max(0.0)
22}
23
24pub fn ns_sigmoid(x: f64) -> f64 {
26 1.0 / (1.0 + (-x).exp())
27}
28
29pub fn ns_softmax(x: &[f64]) -> Vec<f64> {
34 if x.is_empty() {
35 return Vec::new();
36 }
37 let max_val = x.iter().copied().fold(f64::NEG_INFINITY, f64::max);
38 let exps: Vec<f64> = x.iter().map(|&v| (v - max_val).exp()).collect();
39 let sum: f64 = exps.iter().sum();
40 exps.iter().map(|&e| e / sum).collect()
41}
42
43pub fn ns_mse_loss(predicted: &[f64], target: &[f64]) -> f64 {
49 if predicted.is_empty() {
50 return 0.0;
51 }
52 let n = predicted.len().min(target.len());
53 predicted[..n]
54 .iter()
55 .zip(target[..n].iter())
56 .map(|(p, t)| (p - t).powi(2))
57 .sum::<f64>()
58 / n as f64
59}
60
61pub fn ns_mae_loss(predicted: &[f64], target: &[f64]) -> f64 {
65 if predicted.is_empty() {
66 return 0.0;
67 }
68 let n = predicted.len().min(target.len());
69 predicted[..n]
70 .iter()
71 .zip(target[..n].iter())
72 .map(|(p, t)| (p - t).abs())
73 .sum::<f64>()
74 / n as f64
75}
76
77pub fn pinn_residual(u: f64, u_xx: f64, source: f64) -> f64 {
83 let _ = u; -u_xx - source
85}
86
87pub fn pinn_boundary_loss(u_boundary: &[f64], u_target: &[f64]) -> f64 {
90 ns_mse_loss(u_boundary, u_target)
91}
92
93#[derive(Debug, Clone)]
100pub struct NeuralLayer {
101 pub weights: Vec<f64>,
103 pub biases: Vec<f64>,
105 pub n_in: usize,
107 pub n_out: usize,
109}
110
111impl NeuralLayer {
112 pub fn new(n_in: usize, n_out: usize) -> Self {
114 Self {
115 weights: vec![0.1; n_out * n_in],
116 biases: vec![0.0; n_out],
117 n_in,
118 n_out,
119 }
120 }
121
122 pub fn forward(&self, input: &[f64]) -> Vec<f64> {
124 let n = self.n_in.min(input.len());
125 (0..self.n_out)
126 .map(|i| {
127 let base = i * self.n_in;
128 let dot: f64 = (0..n).map(|j| self.weights[base + j] * input[j]).sum();
129 dot + self.biases[i]
130 })
131 .collect()
132 }
133
134 pub fn relu_forward(&self, input: &[f64]) -> Vec<f64> {
136 self.forward(input).into_iter().map(ns_relu).collect()
137 }
138
139 pub fn tanh_forward(&self, input: &[f64]) -> Vec<f64> {
141 self.forward(input).into_iter().map(|v| v.tanh()).collect()
142 }
143
144 pub fn output_size(&self) -> usize {
146 self.n_out
147 }
148
149 pub fn input_size(&self) -> usize {
151 self.n_in
152 }
153}
154
155#[derive(Debug, Clone)]
162pub struct GpuNeuralSolver {
163 pub layers: Vec<NeuralLayer>,
165 pub learning_rate: f64,
167}
168
169impl GpuNeuralSolver {
170 pub fn new(layer_sizes: &[usize], lr: f64) -> Self {
175 assert!(
176 layer_sizes.len() >= 2,
177 "Need at least input and output sizes"
178 );
179 let layers = layer_sizes
180 .windows(2)
181 .map(|w| NeuralLayer::new(w[0], w[1]))
182 .collect();
183 Self {
184 layers,
185 learning_rate: lr,
186 }
187 }
188
189 pub fn forward_pass(&self, input: &[f64]) -> Vec<f64> {
191 let mut x: Vec<f64> = input.to_vec();
192 let last = self.layers.len().saturating_sub(1);
193 for (i, layer) in self.layers.iter().enumerate() {
194 x = if i < last {
195 layer.relu_forward(&x)
196 } else {
197 layer.forward(&x)
198 };
199 }
200 x
201 }
202
203 pub fn layer_count(&self) -> usize {
205 self.layers.len()
206 }
207
208 pub fn predict(&self, input: &[f64]) -> Vec<f64> {
210 self.forward_pass(input)
211 }
212}
213
214#[derive(Debug, Clone)]
221pub struct PhysicsNeuralNet {
222 pub solver: GpuNeuralSolver,
224 pub pde_weight: f64,
226 pub bc_weight: f64,
228}
229
230impl PhysicsNeuralNet {
231 pub fn new(layer_sizes: &[usize], pde_weight: f64, bc_weight: f64) -> Self {
233 Self {
234 solver: GpuNeuralSolver::new(layer_sizes, 1e-3),
235 pde_weight,
236 bc_weight,
237 }
238 }
239
240 pub fn total_loss(&self, pde_residual: f64, bc_loss: f64) -> f64 {
243 self.pde_weight * pde_residual.abs() + self.bc_weight * bc_loss
244 }
245
246 pub fn predict(&self, x: f64) -> f64 {
250 let out = self.solver.predict(&[x]);
251 out.first().copied().unwrap_or(0.0)
252 }
253}
254
255#[cfg(test)]
258mod tests {
259 use super::*;
260
261 #[test]
264 fn relu_negative_is_zero() {
265 assert!((ns_relu(-1.0) - 0.0).abs() < 1e-12);
266 }
267
268 #[test]
269 fn relu_positive_identity() {
270 assert!((ns_relu(1.0) - 1.0).abs() < 1e-12);
271 }
272
273 #[test]
274 fn relu_zero_boundary() {
275 assert!((ns_relu(0.0) - 0.0).abs() < 1e-12);
276 }
277
278 #[test]
279 fn relu_large_positive() {
280 assert!((ns_relu(1000.0) - 1000.0).abs() < 1e-8);
281 }
282
283 #[test]
286 fn sigmoid_at_zero_is_half() {
287 assert!((ns_sigmoid(0.0) - 0.5).abs() < 1e-12);
288 }
289
290 #[test]
291 fn sigmoid_large_positive_near_one() {
292 assert!(ns_sigmoid(100.0) > 0.999);
293 }
294
295 #[test]
296 fn sigmoid_large_negative_near_zero() {
297 assert!(ns_sigmoid(-100.0) < 0.001);
298 }
299
300 #[test]
301 fn sigmoid_symmetry() {
302 let s = ns_sigmoid(2.0);
303 assert!((ns_sigmoid(-2.0) - (1.0 - s)).abs() < 1e-12);
304 }
305
306 #[test]
309 fn softmax_sums_to_one() {
310 let x = [1.0, 2.0, 3.0];
311 let s = ns_softmax(&x);
312 let total: f64 = s.iter().sum();
313 assert!((total - 1.0).abs() < 1e-12);
314 }
315
316 #[test]
317 fn softmax_empty_input() {
318 let s = ns_softmax(&[]);
319 assert!(s.is_empty());
320 }
321
322 #[test]
323 fn softmax_single_element() {
324 let s = ns_softmax(&[42.0]);
325 assert!((s[0] - 1.0).abs() < 1e-12);
326 }
327
328 #[test]
329 fn softmax_uniform_input() {
330 let x = [1.0f64; 4];
331 let s = ns_softmax(&x);
332 for &v in &s {
333 assert!((v - 0.25).abs() < 1e-12);
334 }
335 }
336
337 #[test]
338 fn softmax_all_non_negative() {
339 let x = [-3.0, 0.0, 1.0, 5.0];
340 let s = ns_softmax(&x);
341 for &v in &s {
342 assert!(v >= 0.0);
343 }
344 }
345
346 #[test]
349 fn mse_zero_for_identical() {
350 let v = [1.0, 2.0, 3.0];
351 assert!((ns_mse_loss(&v, &v) - 0.0).abs() < 1e-12);
352 }
353
354 #[test]
355 fn mse_known_value() {
356 let pred = [3.0];
357 let target = [1.0];
358 assert!((ns_mse_loss(&pred, &target) - 4.0).abs() < 1e-12);
360 }
361
362 #[test]
363 fn mse_empty_returns_zero() {
364 assert!((ns_mse_loss(&[], &[]) - 0.0).abs() < 1e-12);
365 }
366
367 #[test]
368 fn mse_positive_values() {
369 let pred = [1.0, 2.0];
370 let target = [0.0, 0.0];
371 let loss = ns_mse_loss(&pred, &target);
372 assert!(loss > 0.0);
373 }
374
375 #[test]
378 fn mae_zero_for_identical() {
379 let v = [1.0, 2.0, 3.0];
380 assert!((ns_mae_loss(&v, &v) - 0.0).abs() < 1e-12);
381 }
382
383 #[test]
384 fn mae_known_value() {
385 let pred = [3.0, 1.0];
386 let target = [1.0, 1.0];
387 assert!((ns_mae_loss(&pred, &target) - 1.0).abs() < 1e-12);
389 }
390
391 #[test]
392 fn mae_empty_returns_zero() {
393 assert!((ns_mae_loss(&[], &[]) - 0.0).abs() < 1e-12);
394 }
395
396 #[test]
399 fn neural_layer_output_size() {
400 let layer = NeuralLayer::new(4, 3);
401 assert_eq!(layer.output_size(), 3);
402 }
403
404 #[test]
405 fn neural_layer_input_size() {
406 let layer = NeuralLayer::new(4, 3);
407 assert_eq!(layer.input_size(), 4);
408 }
409
410 #[test]
411 fn neural_layer_forward_output_length() {
412 let layer = NeuralLayer::new(4, 3);
413 let out = layer.forward(&[1.0, 2.0, 3.0, 4.0]);
414 assert_eq!(out.len(), 3);
415 }
416
417 #[test]
418 fn neural_layer_relu_forward_non_negative() {
419 let layer = NeuralLayer::new(2, 4);
420 let out = layer.relu_forward(&[-10.0, -10.0]);
421 for &v in &out {
422 assert!(v >= 0.0);
423 }
424 }
425
426 #[test]
427 fn neural_layer_tanh_bounded() {
428 let layer = NeuralLayer::new(3, 3);
429 let out = layer.tanh_forward(&[1.0, 2.0, 3.0]);
430 for &v in &out {
431 assert!(v > -1.0 && v < 1.0);
432 }
433 }
434
435 #[test]
436 fn neural_layer_zero_input() {
437 let mut layer = NeuralLayer::new(3, 2);
439 layer.weights = vec![0.0; 6];
440 let out = layer.forward(&[0.0, 0.0, 0.0]);
441 for &v in &out {
442 assert!(v.abs() < 1e-12);
443 }
444 }
445
446 #[test]
449 fn solver_layer_count() {
450 let s = GpuNeuralSolver::new(&[4, 8, 8, 2], 1e-3);
451 assert_eq!(s.layer_count(), 3);
452 }
453
454 #[test]
455 fn solver_forward_output_shape() {
456 let s = GpuNeuralSolver::new(&[3, 5, 2], 1e-3);
457 let out = s.forward_pass(&[1.0, 0.0, -1.0]);
458 assert_eq!(out.len(), 2);
459 }
460
461 #[test]
462 fn solver_predict_same_as_forward() {
463 let s = GpuNeuralSolver::new(&[2, 4, 1], 1e-3);
464 let input = [0.5, -0.5];
465 let a = s.forward_pass(&input);
466 let b = s.predict(&input);
467 assert_eq!(a, b);
468 }
469
470 #[test]
471 fn solver_single_layer() {
472 let s = GpuNeuralSolver::new(&[2, 1], 1e-3);
473 let out = s.forward_pass(&[1.0, 1.0]);
474 assert_eq!(out.len(), 1);
475 }
476
477 #[test]
478 fn solver_deep_network_no_panic() {
479 let s = GpuNeuralSolver::new(&[10, 20, 20, 20, 5], 1e-4);
480 let input = vec![0.1; 10];
481 let out = s.forward_pass(&input);
482 assert_eq!(out.len(), 5);
483 }
484
485 #[test]
488 fn pinn_residual_formula() {
489 let r = pinn_residual(0.0, 2.0, 1.0);
491 assert!((r - (-3.0)).abs() < 1e-12);
492 }
493
494 #[test]
495 fn pinn_residual_zero_when_satisfied() {
496 let u_xx = -1.0;
498 let source = 1.0;
499 let r = pinn_residual(0.0, u_xx, source);
500 assert!(r.abs() < 1e-12);
501 }
502
503 #[test]
504 fn pinn_boundary_loss_zero_for_equal() {
505 let v = [1.0, 0.0, -1.0];
506 assert!((pinn_boundary_loss(&v, &v) - 0.0).abs() < 1e-12);
507 }
508
509 #[test]
510 fn pinn_boundary_loss_positive_for_different() {
511 let u_boundary = [1.0, 2.0];
512 let u_target = [0.0, 0.0];
513 assert!(pinn_boundary_loss(&u_boundary, &u_target) > 0.0);
514 }
515
516 #[test]
519 fn pinn_total_loss_formula() {
520 let net = PhysicsNeuralNet::new(&[1, 4, 1], 2.0, 3.0);
521 let loss = net.total_loss(1.0, 1.0);
523 assert!((loss - 5.0).abs() < 1e-12);
524 }
525
526 #[test]
527 fn pinn_total_loss_zero_when_both_zero() {
528 let net = PhysicsNeuralNet::new(&[1, 4, 1], 1.0, 1.0);
529 assert!((net.total_loss(0.0, 0.0) - 0.0).abs() < 1e-12);
530 }
531
532 #[test]
533 fn pinn_predict_returns_scalar() {
534 let net = PhysicsNeuralNet::new(&[1, 8, 1], 1.0, 1.0);
535 let _v = net.predict(0.5); }
537
538 #[test]
539 fn pinn_total_loss_pde_only() {
540 let net = PhysicsNeuralNet::new(&[1, 4, 1], 5.0, 0.0);
541 assert!((net.total_loss(2.0, 100.0) - 10.0).abs() < 1e-12);
542 }
543
544 #[test]
545 fn pinn_total_loss_bc_only() {
546 let net = PhysicsNeuralNet::new(&[1, 4, 1], 0.0, 4.0);
547 assert!((net.total_loss(100.0, 3.0) - 12.0).abs() < 1e-12);
548 }
549}