1use scirs2_core::ndarray::{Array1, Array2};
14use scirs2_core::random::essentials::Normal;
15use scirs2_core::random::thread_rng;
16use serde::{Deserialize, Serialize};
17use sklears_core::{
18 error::{Result, SklearsError},
19 prelude::{Fit, Transform},
20 traits::{Estimator, Trained, Untrained},
21 types::Float,
22};
23use std::marker::PhantomData;
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct PhysicsInformedConfig {
28 pub system_type: PhysicalSystem,
30 pub n_components: usize,
32 pub bandwidth: Float,
34 pub physics_weight: Float,
36 pub data_weight: Float,
38}
39
40impl Default for PhysicsInformedConfig {
41 fn default() -> Self {
42 Self {
43 system_type: PhysicalSystem::HeatEquation,
44 n_components: 100,
45 bandwidth: 1.0,
46 physics_weight: 1.0,
47 data_weight: 1.0,
48 }
49 }
50}
51
52#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
54pub enum PhysicalSystem {
55 HeatEquation,
57 WaveEquation,
59 BurgersEquation,
61 NavierStokes,
63 Schrodinger,
65 Custom,
67}
68
69#[derive(Debug, Clone)]
102pub struct PhysicsInformedKernel<State = Untrained> {
103 config: PhysicsInformedConfig,
104
105 kernel_weights: Option<Array2<Float>>,
107 derivative_weights: Option<Vec<Array2<Float>>>,
108 boundary_data: Option<Array2<Float>>,
109
110 _state: PhantomData<State>,
111}
112
113impl PhysicsInformedKernel<Untrained> {
114 pub fn new(config: PhysicsInformedConfig) -> Self {
116 Self {
117 config,
118 kernel_weights: None,
119 derivative_weights: None,
120 boundary_data: None,
121 _state: PhantomData,
122 }
123 }
124
125 pub fn with_system(system: PhysicalSystem) -> Self {
127 Self {
128 config: PhysicsInformedConfig {
129 system_type: system,
130 ..Default::default()
131 },
132 kernel_weights: None,
133 derivative_weights: None,
134 boundary_data: None,
135 _state: PhantomData,
136 }
137 }
138
139 pub fn n_components(mut self, n: usize) -> Self {
141 self.config.n_components = n;
142 self
143 }
144
145 pub fn physics_weight(mut self, weight: Float) -> Self {
147 self.config.physics_weight = weight;
148 self
149 }
150}
151
152impl Estimator for PhysicsInformedKernel<Untrained> {
153 type Config = PhysicsInformedConfig;
154 type Error = SklearsError;
155 type Float = Float;
156
157 fn config(&self) -> &Self::Config {
158 &self.config
159 }
160}
161
162impl Fit<Array2<Float>, ()> for PhysicsInformedKernel<Untrained> {
163 type Fitted = PhysicsInformedKernel<Trained>;
164
165 fn fit(self, x: &Array2<Float>, _y: &()) -> Result<Self::Fitted> {
166 if x.nrows() == 0 || x.ncols() < 2 {
167 return Err(SklearsError::InvalidInput(
168 "Input must have at least 2 columns (spatial + temporal)".to_string(),
169 ));
170 }
171
172 let boundary_data = x.clone();
173
174 let mut rng = thread_rng();
176 let normal = Normal::new(0.0, 1.0).expect("operation should succeed");
177
178 let kernel_weights = Array2::from_shape_fn((x.ncols(), self.config.n_components), |_| {
179 rng.sample(normal) * (2.0 * self.config.bandwidth).sqrt()
180 });
181
182 let mut derivative_weights = Vec::new();
184
185 for _ in 0..x.ncols() {
187 let weights = Array2::from_shape_fn((x.ncols(), self.config.n_components), |_| {
188 rng.sample(normal) * (2.0 * self.config.bandwidth).sqrt()
189 });
190 derivative_weights.push(weights);
191 }
192
193 for _ in 0..x.ncols() {
195 let weights = Array2::from_shape_fn((x.ncols(), self.config.n_components), |_| {
196 rng.sample(normal) * (2.0 * self.config.bandwidth).sqrt()
197 });
198 derivative_weights.push(weights);
199 }
200
201 Ok(PhysicsInformedKernel {
202 config: self.config,
203 kernel_weights: Some(kernel_weights),
204 derivative_weights: Some(derivative_weights),
205 boundary_data: Some(boundary_data),
206 _state: PhantomData,
207 })
208 }
209}
210
211impl Transform<Array2<Float>, Array2<Float>> for PhysicsInformedKernel<Trained> {
212 fn transform(&self, x: &Array2<Float>) -> Result<Array2<Float>> {
213 let kernel_weights = self
214 .kernel_weights
215 .as_ref()
216 .expect("operation should succeed");
217
218 if x.ncols() != kernel_weights.nrows() {
219 return Err(SklearsError::InvalidInput(format!(
220 "Feature dimension mismatch: expected {}, got {}",
221 kernel_weights.nrows(),
222 x.ncols()
223 )));
224 }
225
226 let projection = x.dot(kernel_weights);
228
229 let n_samples = x.nrows();
230 let n_features = self.config.n_components;
231
232 let mut output = Array2::zeros((n_samples, n_features * 3));
234
235 let normalizer = (2.0 / n_features as Float).sqrt();
236
237 for i in 0..n_samples {
238 for j in 0..n_features {
239 output[[i, j]] = normalizer * projection[[i, j]].cos();
241
242 output[[i, j + n_features]] = -normalizer * projection[[i, j]].sin();
244
245 output[[i, j + 2 * n_features]] = -normalizer * projection[[i, j]].cos();
247 }
248 }
249
250 Ok(output)
251 }
252}
253
254impl PhysicsInformedKernel<Trained> {
255 pub fn boundary_data(&self) -> &Array2<Float> {
257 self.boundary_data
258 .as_ref()
259 .expect("operation should succeed")
260 }
261
262 pub fn pde_residual(
264 &self,
265 x: &Array2<Float>,
266 solution: &Array1<Float>,
267 ) -> Result<Array1<Float>> {
268 let n_samples = x.nrows();
269 let mut residual = Array1::zeros(n_samples);
270
271 match self.config.system_type {
272 PhysicalSystem::HeatEquation => {
273 let alpha = 0.1; for i in 1..(n_samples - 1) {
278 let dt = if i > 0 {
279 x[[i, 1]] - x[[i - 1, 1]]
280 } else {
281 0.01
282 };
283 let dx = if i > 0 {
284 x[[i, 0]] - x[[i - 1, 0]]
285 } else {
286 0.01
287 };
288
289 let du_dt = if dt > 0.0 {
290 (solution[i] - solution[i - 1]) / dt
291 } else {
292 0.0
293 };
294
295 let d2u_dx2 = if dx > 0.0 && i > 0 && i < n_samples - 1 {
296 (solution[i + 1] - 2.0 * solution[i] + solution[i - 1]) / (dx * dx)
297 } else {
298 0.0
299 };
300
301 residual[i] = du_dt - alpha * d2u_dx2;
302 }
303 }
304 PhysicalSystem::WaveEquation => {
305 let c = 1.0; for i in 1..(n_samples - 1) {
309 let dt = if i > 0 {
310 x[[i, 1]] - x[[i - 1, 1]]
311 } else {
312 0.01
313 };
314 let dx = if i > 0 {
315 x[[i, 0]] - x[[i - 1, 0]]
316 } else {
317 0.01
318 };
319
320 let d2u_dt2 = if dt > 0.0 && i > 0 && i < n_samples - 1 {
321 (solution[i + 1] - 2.0 * solution[i] + solution[i - 1]) / (dt * dt)
322 } else {
323 0.0
324 };
325
326 let d2u_dx2 = if dx > 0.0 && i > 0 && i < n_samples - 1 {
327 (solution[i + 1] - 2.0 * solution[i] + solution[i - 1]) / (dx * dx)
328 } else {
329 0.0
330 };
331
332 residual[i] = d2u_dt2 - c * c * d2u_dx2;
333 }
334 }
335 PhysicalSystem::BurgersEquation => {
336 let nu = 0.01; for i in 1..(n_samples - 1) {
340 let dt = if i > 0 {
341 x[[i, 1]] - x[[i - 1, 1]]
342 } else {
343 0.01
344 };
345 let dx = if i > 0 {
346 x[[i, 0]] - x[[i - 1, 0]]
347 } else {
348 0.01
349 };
350
351 let du_dt = if dt > 0.0 {
352 (solution[i] - solution[i - 1]) / dt
353 } else {
354 0.0
355 };
356
357 let du_dx = if dx > 0.0 {
358 (solution[i] - solution[i - 1]) / dx
359 } else {
360 0.0
361 };
362
363 let d2u_dx2 = if dx > 0.0 && i > 0 && i < n_samples - 1 {
364 (solution[i + 1] - 2.0 * solution[i] + solution[i - 1]) / (dx * dx)
365 } else {
366 0.0
367 };
368
369 residual[i] = du_dt + solution[i] * du_dx - nu * d2u_dx2;
370 }
371 }
372 _ => {
373 for i in 1..(n_samples - 1) {
375 residual[i] = solution[i + 1] - 2.0 * solution[i] + solution[i - 1];
376 }
377 }
378 }
379
380 Ok(residual)
381 }
382}
383
384#[derive(Debug, Clone)]
408pub struct MultiscaleKernel<State = Untrained> {
409 scales: Vec<Float>,
411 n_components_per_scale: usize,
413
414 scale_weights: Option<Vec<Array2<Float>>>,
416
417 _state: PhantomData<State>,
418}
419
420impl MultiscaleKernel<Untrained> {
421 pub fn with_scales(scales: Vec<Float>) -> Self {
423 Self {
424 scales,
425 n_components_per_scale: 50,
426 scale_weights: None,
427 _state: PhantomData,
428 }
429 }
430
431 pub fn n_components_per_scale(mut self, n: usize) -> Self {
433 self.n_components_per_scale = n;
434 self
435 }
436}
437
438impl Estimator for MultiscaleKernel<Untrained> {
439 type Config = ();
440 type Error = SklearsError;
441 type Float = Float;
442
443 fn config(&self) -> &Self::Config {
444 &()
445 }
446}
447
448impl Fit<Array2<Float>, ()> for MultiscaleKernel<Untrained> {
449 type Fitted = MultiscaleKernel<Trained>;
450
451 fn fit(self, x: &Array2<Float>, _y: &()) -> Result<Self::Fitted> {
452 if x.nrows() == 0 || x.ncols() == 0 {
453 return Err(SklearsError::InvalidInput(
454 "Input array cannot be empty".to_string(),
455 ));
456 }
457
458 let mut rng = thread_rng();
459 let normal = Normal::new(0.0, 1.0).expect("operation should succeed");
460
461 let mut scale_weights = Vec::new();
463
464 for &scale in &self.scales {
465 let weights = Array2::from_shape_fn((x.ncols(), self.n_components_per_scale), |_| {
466 rng.sample(normal) * (2.0 / scale).sqrt()
467 });
468 scale_weights.push(weights);
469 }
470
471 Ok(MultiscaleKernel {
472 scales: self.scales,
473 n_components_per_scale: self.n_components_per_scale,
474 scale_weights: Some(scale_weights),
475 _state: PhantomData,
476 })
477 }
478}
479
480impl Transform<Array2<Float>, Array2<Float>> for MultiscaleKernel<Trained> {
481 fn transform(&self, x: &Array2<Float>) -> Result<Array2<Float>> {
482 let scale_weights = self
483 .scale_weights
484 .as_ref()
485 .expect("operation should succeed");
486
487 if scale_weights.is_empty() {
488 return Err(SklearsError::InvalidInput(
489 "No scale weights available".to_string(),
490 ));
491 }
492
493 let n_samples = x.nrows();
494 let n_scales = self.scales.len();
495 let total_features = n_scales * self.n_components_per_scale;
496
497 let mut output = Array2::zeros((n_samples, total_features));
498
499 let normalizer = (2.0 / self.n_components_per_scale as Float).sqrt();
500
501 for (scale_idx, weights) in scale_weights.iter().enumerate() {
502 let projection = x.dot(weights);
503
504 for i in 0..n_samples {
505 for j in 0..self.n_components_per_scale {
506 let feature_idx = scale_idx * self.n_components_per_scale + j;
507 output[[i, feature_idx]] = normalizer * projection[[i, j]].cos();
508 }
509 }
510 }
511
512 Ok(output)
513 }
514}
515
516impl MultiscaleKernel<Trained> {
517 pub fn scales(&self) -> &[Float] {
519 &self.scales
520 }
521
522 pub fn scale_weight(&self, scale_idx: usize) -> Option<&Array2<Float>> {
524 self.scale_weights.as_ref().and_then(|w| w.get(scale_idx))
525 }
526}
527
528#[cfg(test)]
529mod tests {
530 use super::*;
531 use scirs2_core::ndarray::array;
532
533 #[test]
534 fn test_physics_informed_kernel_basic() {
535 let config = PhysicsInformedConfig {
536 system_type: PhysicalSystem::HeatEquation,
537 n_components: 20,
538 ..Default::default()
539 };
540
541 let pinn = PhysicsInformedKernel::new(config);
542
543 let X = array![
545 [0.0, 0.0],
546 [0.5, 0.0],
547 [1.0, 0.0],
548 [0.0, 0.1],
549 [0.5, 0.1],
550 [1.0, 0.1]
551 ];
552
553 let fitted = pinn.fit(&X, &()).expect("operation should succeed");
554 let features = fitted.transform(&X).expect("operation should succeed");
555
556 assert_eq!(features.nrows(), 6);
557 assert_eq!(features.ncols(), 60); }
559
560 #[test]
561 fn test_different_physical_systems() {
562 let systems = vec![
563 PhysicalSystem::HeatEquation,
564 PhysicalSystem::WaveEquation,
565 PhysicalSystem::BurgersEquation,
566 ];
567
568 let X = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]];
569
570 for system in systems {
571 let pinn = PhysicsInformedKernel::with_system(system).n_components(20);
572 let fitted = pinn.fit(&X, &()).expect("operation should succeed");
573 let features = fitted.transform(&X).expect("operation should succeed");
574
575 assert_eq!(features.nrows(), 3);
576 }
577 }
578
579 #[test]
580 fn test_pde_residual() {
581 let config = PhysicsInformedConfig {
582 system_type: PhysicalSystem::HeatEquation,
583 n_components: 20,
584 ..Default::default()
585 };
586
587 let pinn = PhysicsInformedKernel::new(config);
588 let X = array![
589 [0.0, 0.0],
590 [0.5, 0.0],
591 [1.0, 0.0],
592 [0.0, 0.1],
593 [0.5, 0.1],
594 [1.0, 0.1]
595 ];
596
597 let fitted = pinn.fit(&X, &()).expect("operation should succeed");
598
599 let solution = array![0.0, 0.5, 1.0, 0.0, 0.5, 1.0];
601
602 let residual = fitted
603 .pde_residual(&X, &solution)
604 .expect("operation should succeed");
605
606 assert_eq!(residual.len(), 6);
607 assert!(residual.iter().all(|&r| r.is_finite()));
608 }
609
610 #[test]
611 fn test_multiscale_kernel() {
612 let scales = vec![0.1, 1.0, 10.0];
613 let kernel = MultiscaleKernel::with_scales(scales).n_components_per_scale(10);
614
615 let X = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]];
616
617 let fitted = kernel.fit(&X, &()).expect("operation should succeed");
618 let features = fitted.transform(&X).expect("operation should succeed");
619
620 assert_eq!(features.nrows(), 3);
621 assert_eq!(features.ncols(), 30); }
623
624 #[test]
625 fn test_multiscale_scales() {
626 let scales = vec![0.5, 2.0, 8.0];
627 let kernel = MultiscaleKernel::with_scales(scales.clone());
628
629 let X = array![[1.0], [2.0]];
630
631 let fitted = kernel.fit(&X, &()).expect("operation should succeed");
632
633 assert_eq!(fitted.scales(), &scales[..]);
634 }
635
636 #[test]
637 fn test_empty_input_error() {
638 let pinn = PhysicsInformedKernel::with_system(PhysicalSystem::HeatEquation);
639 let empty: Array2<Float> = Array2::zeros((0, 0));
640
641 assert!(pinn.fit(&empty, &()).is_err());
642 }
643
644 #[test]
645 fn test_insufficient_columns_error() {
646 let pinn = PhysicsInformedKernel::with_system(PhysicalSystem::HeatEquation);
647 let data = array![[1.0]]; assert!(pinn.fit(&data, &()).is_err());
650 }
651
652 #[test]
653 fn test_physics_weight_setting() {
654 let pinn = PhysicsInformedKernel::with_system(PhysicalSystem::HeatEquation)
655 .physics_weight(2.0)
656 .n_components(30);
657
658 assert_eq!(pinn.config.physics_weight, 2.0);
659 assert_eq!(pinn.config.n_components, 30);
660 }
661}