Skip to main content

sklears_kernel_approximation/
scientific_computing_kernels.rs

1//! Scientific Computing Kernel Methods
2//!
3//! This module implements kernel methods for scientific computing applications,
4//! including physics-informed kernels, differential equation kernels, conservation
5//! law kernels, and multiscale methods.
6//!
7//! # References
8//! - Raissi et al. (2019): "Physics-informed neural networks"
9//! - Karniadakis et al. (2021): "Physics-informed machine learning"
10//! - Chen & Sideris (2020): "Finite element method with interpolation at the nodes"
11//! - E & Yu (2018): "The Deep Ritz Method"
12
13use 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/// Configuration for physics-informed kernels
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct PhysicsInformedConfig {
28    /// Type of physical system
29    pub system_type: PhysicalSystem,
30    /// Number of random features
31    pub n_components: usize,
32    /// Kernel bandwidth
33    pub bandwidth: Float,
34    /// Weight for physics loss
35    pub physics_weight: Float,
36    /// Weight for data loss
37    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/// Types of physical systems
53#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
54pub enum PhysicalSystem {
55    /// Heat equation: ∂u/∂t = α∇²u
56    HeatEquation,
57    /// Wave equation: ∂²u/∂t² = c²∇²u
58    WaveEquation,
59    /// Burgers' equation: ∂u/∂t + u∂u/∂x = ν∇²u
60    BurgersEquation,
61    /// Navier-Stokes equations
62    NavierStokes,
63    /// Schrödinger equation: iℏ∂ψ/∂t = Ĥψ
64    Schrodinger,
65    /// Custom PDE (user-defined)
66    Custom,
67}
68
69/// Physics-Informed Kernel
70///
71/// Implements kernel methods that incorporate physical laws and differential
72/// equations into the learning process. Useful for solving PDEs and modeling
73/// physical systems with sparse data.
74///
75/// # Mathematical Background
76///
77/// For a PDE: N\[u\](x,t) = 0 (e.g., heat equation, wave equation)
78/// The kernel incorporates both data fitting and PDE residual minimization:
79///
80/// Loss = λ_data * ||u - u_data||² + λ_physics * ||N\[u\]||²
81///
82/// # Examples
83///
84/// ```rust,ignore
85/// use sklears_kernel_approximation::scientific_computing_kernels::{
86///     PhysicsInformedKernel, PhysicsInformedConfig, PhysicalSystem
87/// };
88/// use scirs2_core::ndarray::array;
89/// use sklears_core::traits::{Fit, Transform};
90///
91/// let config = PhysicsInformedConfig {
92///     system_type: PhysicalSystem::HeatEquation,
93///     ..Default::default()
94/// };
95///
96/// let pinn = PhysicsInformedKernel::new(config);
97/// let X = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]]; // (x, t) coordinates
98/// let fitted = pinn.fit(&X, &()).unwrap();
99/// let features = fitted.transform(&X).unwrap();
100/// ```
101#[derive(Debug, Clone)]
102pub struct PhysicsInformedKernel<State = Untrained> {
103    config: PhysicsInformedConfig,
104
105    // Fitted attributes
106    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    /// Create a new physics-informed kernel
115    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    /// Create with default configuration
126    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    /// Set number of components
140    pub fn n_components(mut self, n: usize) -> Self {
141        self.config.n_components = n;
142        self
143    }
144
145    /// Set physics weight
146    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        // Generate random Fourier features for the kernel
175        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        // Generate derivative weights for different orders
183        let mut derivative_weights = Vec::new();
184
185        // First derivatives (spatial and temporal)
186        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        // Second derivatives (Laplacian)
194        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        // Compute random Fourier features
227        let projection = x.dot(kernel_weights);
228
229        let n_samples = x.nrows();
230        let n_features = self.config.n_components;
231
232        // Basic features
233        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                // Basic kernel features
240                output[[i, j]] = normalizer * projection[[i, j]].cos();
241
242                // Derivative features (approximate)
243                output[[i, j + n_features]] = -normalizer * projection[[i, j]].sin();
244
245                // Second derivative features (approximate)
246                output[[i, j + 2 * n_features]] = -normalizer * projection[[i, j]].cos();
247            }
248        }
249
250        Ok(output)
251    }
252}
253
254impl PhysicsInformedKernel<Trained> {
255    /// Get boundary data
256    pub fn boundary_data(&self) -> &Array2<Float> {
257        self.boundary_data
258            .as_ref()
259            .expect("operation should succeed")
260    }
261
262    /// Evaluate PDE residual for a given system
263    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                // Heat equation: ∂u/∂t - α∇²u = 0
274                // Approximate derivatives using finite differences
275                let alpha = 0.1; // Thermal diffusivity
276
277                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                // Wave equation: ∂²u/∂t² - c²∇²u = 0
306                let c = 1.0; // Wave speed
307
308                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                // Burgers' equation: ∂u/∂t + u∂u/∂x - ν∇²u = 0
337                let nu = 0.01; // Viscosity
338
339                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                // Default: simple Laplacian
374                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/// Multiscale Kernel for hierarchical phenomena
385///
386/// Implements multiscale kernel methods for problems involving multiple spatial
387/// or temporal scales (e.g., turbulence, molecular dynamics, climate modeling).
388///
389/// # Mathematical Background
390///
391/// Uses wavelets or hierarchical representations:
392/// K(x, x') = Σ_l w_l K_l(x, x')
393/// where K_l are kernels at different scales l.
394///
395/// # Examples
396///
397/// ```rust,ignore
398/// use sklears_kernel_approximation::scientific_computing_kernels::MultiscaleKernel;
399/// use scirs2_core::ndarray::array;
400/// use sklears_core::traits::{Fit, Transform};
401///
402/// let kernel = MultiscaleKernel::with_scales(vec![0.1, 1.0, 10.0]);
403/// let X = array![[1.0, 2.0], [3.0, 4.0]];
404/// let fitted = kernel.fit(&X, &()).unwrap();
405/// let features = fitted.transform(&X).unwrap();
406/// ```
407#[derive(Debug, Clone)]
408pub struct MultiscaleKernel<State = Untrained> {
409    /// Scales for different levels
410    scales: Vec<Float>,
411    /// Number of components per scale
412    n_components_per_scale: usize,
413
414    // Fitted attributes
415    scale_weights: Option<Vec<Array2<Float>>>,
416
417    _state: PhantomData<State>,
418}
419
420impl MultiscaleKernel<Untrained> {
421    /// Create with specified scales
422    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    /// Set number of components per scale
432    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        // Generate random weights for each scale
462        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    /// Get scales
518    pub fn scales(&self) -> &[Float] {
519        &self.scales
520    }
521
522    /// Get weight for a specific scale
523    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        // Data: [x, t] coordinates
544        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); // 3 * n_components
558    }
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        // Test solution (simple linear function)
600        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); // 3 scales * 10 components
622    }
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]]; // Only 1 column, need at least 2
648
649        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}