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).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        // 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.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        // Compute random Fourier features
224        let projection = x.dot(kernel_weights);
225
226        let n_samples = x.nrows();
227        let n_features = self.config.n_components;
228
229        // Basic features
230        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                // Basic kernel features
237                output[[i, j]] = normalizer * projection[[i, j]].cos();
238
239                // Derivative features (approximate)
240                output[[i, j + n_features]] = -normalizer * projection[[i, j]].sin();
241
242                // Second derivative features (approximate)
243                output[[i, j + 2 * n_features]] = -normalizer * projection[[i, j]].cos();
244            }
245        }
246
247        Ok(output)
248    }
249}
250
251impl PhysicsInformedKernel<Trained> {
252    /// Get boundary data
253    pub fn boundary_data(&self) -> &Array2<Float> {
254        self.boundary_data.as_ref().unwrap()
255    }
256
257    /// Evaluate PDE residual for a given system
258    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                // Heat equation: ∂u/∂t - α∇²u = 0
269                // Approximate derivatives using finite differences
270                let alpha = 0.1; // Thermal diffusivity
271
272                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                // Wave equation: ∂²u/∂t² - c²∇²u = 0
301                let c = 1.0; // Wave speed
302
303                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                // Burgers' equation: ∂u/∂t + u∂u/∂x - ν∇²u = 0
332                let nu = 0.01; // Viscosity
333
334                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                // Default: simple Laplacian
369                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/// Multiscale Kernel for hierarchical phenomena
380///
381/// Implements multiscale kernel methods for problems involving multiple spatial
382/// or temporal scales (e.g., turbulence, molecular dynamics, climate modeling).
383///
384/// # Mathematical Background
385///
386/// Uses wavelets or hierarchical representations:
387/// K(x, x') = Σ_l w_l K_l(x, x')
388/// where K_l are kernels at different scales l.
389///
390/// # Examples
391///
392/// ```rust,ignore
393/// use sklears_kernel_approximation::scientific_computing_kernels::MultiscaleKernel;
394/// use scirs2_core::ndarray::array;
395/// use sklears_core::traits::{Fit, Transform};
396///
397/// let kernel = MultiscaleKernel::with_scales(vec![0.1, 1.0, 10.0]);
398/// let X = array![[1.0, 2.0], [3.0, 4.0]];
399/// let fitted = kernel.fit(&X, &()).unwrap();
400/// let features = fitted.transform(&X).unwrap();
401/// ```
402#[derive(Debug, Clone)]
403pub struct MultiscaleKernel<State = Untrained> {
404    /// Scales for different levels
405    scales: Vec<Float>,
406    /// Number of components per scale
407    n_components_per_scale: usize,
408
409    // Fitted attributes
410    scale_weights: Option<Vec<Array2<Float>>>,
411
412    _state: PhantomData<State>,
413}
414
415impl MultiscaleKernel<Untrained> {
416    /// Create with specified scales
417    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    /// Set number of components per scale
427    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        // Generate random weights for each scale
457        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    /// Get scales
510    pub fn scales(&self) -> &[Float] {
511        &self.scales
512    }
513
514    /// Get weight for a specific scale
515    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        // Data: [x, t] coordinates
536        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); // 3 * n_components
550    }
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        // Test solution (simple linear function)
592        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); // 3 scales * 10 components
612    }
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]]; // Only 1 column, need at least 2
638
639        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}