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).unwrap();
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.kernel_weights.as_ref().unwrap();
214
215 if x.ncols() != kernel_weights.nrows() {
216 return Err(SklearsError::InvalidInput(format!(
217 "Feature dimension mismatch: expected {}, got {}",
218 kernel_weights.nrows(),
219 x.ncols()
220 )));
221 }
222
223 let projection = x.dot(kernel_weights);
225
226 let n_samples = x.nrows();
227 let n_features = self.config.n_components;
228
229 let mut output = Array2::zeros((n_samples, n_features * 3));
231
232 let normalizer = (2.0 / n_features as Float).sqrt();
233
234 for i in 0..n_samples {
235 for j in 0..n_features {
236 output[[i, j]] = normalizer * projection[[i, j]].cos();
238
239 output[[i, j + n_features]] = -normalizer * projection[[i, j]].sin();
241
242 output[[i, j + 2 * n_features]] = -normalizer * projection[[i, j]].cos();
244 }
245 }
246
247 Ok(output)
248 }
249}
250
251impl PhysicsInformedKernel<Trained> {
252 pub fn boundary_data(&self) -> &Array2<Float> {
254 self.boundary_data.as_ref().unwrap()
255 }
256
257 pub fn pde_residual(
259 &self,
260 x: &Array2<Float>,
261 solution: &Array1<Float>,
262 ) -> Result<Array1<Float>> {
263 let n_samples = x.nrows();
264 let mut residual = Array1::zeros(n_samples);
265
266 match self.config.system_type {
267 PhysicalSystem::HeatEquation => {
268 let alpha = 0.1; for i in 1..(n_samples - 1) {
273 let dt = if i > 0 {
274 x[[i, 1]] - x[[i - 1, 1]]
275 } else {
276 0.01
277 };
278 let dx = if i > 0 {
279 x[[i, 0]] - x[[i - 1, 0]]
280 } else {
281 0.01
282 };
283
284 let du_dt = if dt > 0.0 {
285 (solution[i] - solution[i - 1]) / dt
286 } else {
287 0.0
288 };
289
290 let d2u_dx2 = if dx > 0.0 && i > 0 && i < n_samples - 1 {
291 (solution[i + 1] - 2.0 * solution[i] + solution[i - 1]) / (dx * dx)
292 } else {
293 0.0
294 };
295
296 residual[i] = du_dt - alpha * d2u_dx2;
297 }
298 }
299 PhysicalSystem::WaveEquation => {
300 let c = 1.0; for i in 1..(n_samples - 1) {
304 let dt = if i > 0 {
305 x[[i, 1]] - x[[i - 1, 1]]
306 } else {
307 0.01
308 };
309 let dx = if i > 0 {
310 x[[i, 0]] - x[[i - 1, 0]]
311 } else {
312 0.01
313 };
314
315 let d2u_dt2 = if dt > 0.0 && i > 0 && i < n_samples - 1 {
316 (solution[i + 1] - 2.0 * solution[i] + solution[i - 1]) / (dt * dt)
317 } else {
318 0.0
319 };
320
321 let d2u_dx2 = if dx > 0.0 && i > 0 && i < n_samples - 1 {
322 (solution[i + 1] - 2.0 * solution[i] + solution[i - 1]) / (dx * dx)
323 } else {
324 0.0
325 };
326
327 residual[i] = d2u_dt2 - c * c * d2u_dx2;
328 }
329 }
330 PhysicalSystem::BurgersEquation => {
331 let nu = 0.01; for i in 1..(n_samples - 1) {
335 let dt = if i > 0 {
336 x[[i, 1]] - x[[i - 1, 1]]
337 } else {
338 0.01
339 };
340 let dx = if i > 0 {
341 x[[i, 0]] - x[[i - 1, 0]]
342 } else {
343 0.01
344 };
345
346 let du_dt = if dt > 0.0 {
347 (solution[i] - solution[i - 1]) / dt
348 } else {
349 0.0
350 };
351
352 let du_dx = if dx > 0.0 {
353 (solution[i] - solution[i - 1]) / dx
354 } else {
355 0.0
356 };
357
358 let d2u_dx2 = if dx > 0.0 && i > 0 && i < n_samples - 1 {
359 (solution[i + 1] - 2.0 * solution[i] + solution[i - 1]) / (dx * dx)
360 } else {
361 0.0
362 };
363
364 residual[i] = du_dt + solution[i] * du_dx - nu * d2u_dx2;
365 }
366 }
367 _ => {
368 for i in 1..(n_samples - 1) {
370 residual[i] = solution[i + 1] - 2.0 * solution[i] + solution[i - 1];
371 }
372 }
373 }
374
375 Ok(residual)
376 }
377}
378
379#[derive(Debug, Clone)]
403pub struct MultiscaleKernel<State = Untrained> {
404 scales: Vec<Float>,
406 n_components_per_scale: usize,
408
409 scale_weights: Option<Vec<Array2<Float>>>,
411
412 _state: PhantomData<State>,
413}
414
415impl MultiscaleKernel<Untrained> {
416 pub fn with_scales(scales: Vec<Float>) -> Self {
418 Self {
419 scales,
420 n_components_per_scale: 50,
421 scale_weights: None,
422 _state: PhantomData,
423 }
424 }
425
426 pub fn n_components_per_scale(mut self, n: usize) -> Self {
428 self.n_components_per_scale = n;
429 self
430 }
431}
432
433impl Estimator for MultiscaleKernel<Untrained> {
434 type Config = ();
435 type Error = SklearsError;
436 type Float = Float;
437
438 fn config(&self) -> &Self::Config {
439 &()
440 }
441}
442
443impl Fit<Array2<Float>, ()> for MultiscaleKernel<Untrained> {
444 type Fitted = MultiscaleKernel<Trained>;
445
446 fn fit(self, x: &Array2<Float>, _y: &()) -> Result<Self::Fitted> {
447 if x.nrows() == 0 || x.ncols() == 0 {
448 return Err(SklearsError::InvalidInput(
449 "Input array cannot be empty".to_string(),
450 ));
451 }
452
453 let mut rng = thread_rng();
454 let normal = Normal::new(0.0, 1.0).unwrap();
455
456 let mut scale_weights = Vec::new();
458
459 for &scale in &self.scales {
460 let weights = Array2::from_shape_fn((x.ncols(), self.n_components_per_scale), |_| {
461 rng.sample(normal) * (2.0 / scale).sqrt()
462 });
463 scale_weights.push(weights);
464 }
465
466 Ok(MultiscaleKernel {
467 scales: self.scales,
468 n_components_per_scale: self.n_components_per_scale,
469 scale_weights: Some(scale_weights),
470 _state: PhantomData,
471 })
472 }
473}
474
475impl Transform<Array2<Float>, Array2<Float>> for MultiscaleKernel<Trained> {
476 fn transform(&self, x: &Array2<Float>) -> Result<Array2<Float>> {
477 let scale_weights = self.scale_weights.as_ref().unwrap();
478
479 if scale_weights.is_empty() {
480 return Err(SklearsError::InvalidInput(
481 "No scale weights available".to_string(),
482 ));
483 }
484
485 let n_samples = x.nrows();
486 let n_scales = self.scales.len();
487 let total_features = n_scales * self.n_components_per_scale;
488
489 let mut output = Array2::zeros((n_samples, total_features));
490
491 let normalizer = (2.0 / self.n_components_per_scale as Float).sqrt();
492
493 for (scale_idx, weights) in scale_weights.iter().enumerate() {
494 let projection = x.dot(weights);
495
496 for i in 0..n_samples {
497 for j in 0..self.n_components_per_scale {
498 let feature_idx = scale_idx * self.n_components_per_scale + j;
499 output[[i, feature_idx]] = normalizer * projection[[i, j]].cos();
500 }
501 }
502 }
503
504 Ok(output)
505 }
506}
507
508impl MultiscaleKernel<Trained> {
509 pub fn scales(&self) -> &[Float] {
511 &self.scales
512 }
513
514 pub fn scale_weight(&self, scale_idx: usize) -> Option<&Array2<Float>> {
516 self.scale_weights.as_ref().and_then(|w| w.get(scale_idx))
517 }
518}
519
520#[cfg(test)]
521mod tests {
522 use super::*;
523 use scirs2_core::ndarray::array;
524
525 #[test]
526 fn test_physics_informed_kernel_basic() {
527 let config = PhysicsInformedConfig {
528 system_type: PhysicalSystem::HeatEquation,
529 n_components: 20,
530 ..Default::default()
531 };
532
533 let pinn = PhysicsInformedKernel::new(config);
534
535 let X = array![
537 [0.0, 0.0],
538 [0.5, 0.0],
539 [1.0, 0.0],
540 [0.0, 0.1],
541 [0.5, 0.1],
542 [1.0, 0.1]
543 ];
544
545 let fitted = pinn.fit(&X, &()).unwrap();
546 let features = fitted.transform(&X).unwrap();
547
548 assert_eq!(features.nrows(), 6);
549 assert_eq!(features.ncols(), 60); }
551
552 #[test]
553 fn test_different_physical_systems() {
554 let systems = vec![
555 PhysicalSystem::HeatEquation,
556 PhysicalSystem::WaveEquation,
557 PhysicalSystem::BurgersEquation,
558 ];
559
560 let X = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]];
561
562 for system in systems {
563 let pinn = PhysicsInformedKernel::with_system(system).n_components(20);
564 let fitted = pinn.fit(&X, &()).unwrap();
565 let features = fitted.transform(&X).unwrap();
566
567 assert_eq!(features.nrows(), 3);
568 }
569 }
570
571 #[test]
572 fn test_pde_residual() {
573 let config = PhysicsInformedConfig {
574 system_type: PhysicalSystem::HeatEquation,
575 n_components: 20,
576 ..Default::default()
577 };
578
579 let pinn = PhysicsInformedKernel::new(config);
580 let X = array![
581 [0.0, 0.0],
582 [0.5, 0.0],
583 [1.0, 0.0],
584 [0.0, 0.1],
585 [0.5, 0.1],
586 [1.0, 0.1]
587 ];
588
589 let fitted = pinn.fit(&X, &()).unwrap();
590
591 let solution = array![0.0, 0.5, 1.0, 0.0, 0.5, 1.0];
593
594 let residual = fitted.pde_residual(&X, &solution).unwrap();
595
596 assert_eq!(residual.len(), 6);
597 assert!(residual.iter().all(|&r| r.is_finite()));
598 }
599
600 #[test]
601 fn test_multiscale_kernel() {
602 let scales = vec![0.1, 1.0, 10.0];
603 let kernel = MultiscaleKernel::with_scales(scales).n_components_per_scale(10);
604
605 let X = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]];
606
607 let fitted = kernel.fit(&X, &()).unwrap();
608 let features = fitted.transform(&X).unwrap();
609
610 assert_eq!(features.nrows(), 3);
611 assert_eq!(features.ncols(), 30); }
613
614 #[test]
615 fn test_multiscale_scales() {
616 let scales = vec![0.5, 2.0, 8.0];
617 let kernel = MultiscaleKernel::with_scales(scales.clone());
618
619 let X = array![[1.0], [2.0]];
620
621 let fitted = kernel.fit(&X, &()).unwrap();
622
623 assert_eq!(fitted.scales(), &scales[..]);
624 }
625
626 #[test]
627 fn test_empty_input_error() {
628 let pinn = PhysicsInformedKernel::with_system(PhysicalSystem::HeatEquation);
629 let empty: Array2<Float> = Array2::zeros((0, 0));
630
631 assert!(pinn.fit(&empty, &()).is_err());
632 }
633
634 #[test]
635 fn test_insufficient_columns_error() {
636 let pinn = PhysicsInformedKernel::with_system(PhysicalSystem::HeatEquation);
637 let data = array![[1.0]]; assert!(pinn.fit(&data, &()).is_err());
640 }
641
642 #[test]
643 fn test_physics_weight_setting() {
644 let pinn = PhysicsInformedKernel::with_system(PhysicalSystem::HeatEquation)
645 .physics_weight(2.0)
646 .n_components(30);
647
648 assert_eq!(pinn.config.physics_weight, 2.0);
649 assert_eq!(pinn.config.n_components, 30);
650 }
651}