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