188 lines
5.6 KiB
Rust
188 lines
5.6 KiB
Rust
//! 1-D Fokker–Planck (Kolmogorov forward) equation
|
||||
|
|
//! =================================================
|
|||
|
|
//!
|
|||
|
|
//! Solves
|
|||
|
|
//!
|
|||
|
|
//! ```text
|
|||
|
|
//! ∂_t m(x, t) + ∂_x [μ(x) m(x, t)] - (1/2) ∂_xx [σ²(x) m(x, t)] = 0, t > 0
|
|||
|
|
//! m(x, 0) = m_0(x), Dirichlet boundary m = 0 on the box ends.
|
|||
|
|
//! ```
|
|||
|
|
//!
|
|||
|
|
//! Discretisation: forward Euler in time, conservative central differences in
|
|||
|
|
//! space. The CFL-type stability condition `Δt · (max|μ|/Δx + max σ²/Δx²) ≤ 1`
|
|||
|
|
//! must hold; the routine returns an error when it would be violated.
|
|||
|
|
|
|||
|
|
use crate::core::{OptimizrError, Result};
|
|||
|
|
use ndarray::Array1;
|
|||
|
|
|
|||
|
|
#[derive(Clone, Debug)]
|
|||
|
|
pub struct FokkerPlanckConfig {
|
|||
|
|
pub n_x: usize,
|
|||
|
|
pub x_min: f64,
|
|||
|
|
pub x_max: f64,
|
|||
|
|
pub n_t: usize,
|
|||
|
|
pub t_horizon: f64,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
impl FokkerPlanckConfig {
|
|||
|
|
pub fn validate(&self) -> Result<()> {
|
|||
|
|
if self.n_x < 5 {
|
|||
|
|
return Err(OptimizrError::InvalidParameter("n_x must be ≥ 5".into()));
|
|||
|
|
}
|
|||
|
|
if !(self.x_max > self.x_min) {
|
|||
|
|
return Err(OptimizrError::InvalidParameter("x_max > x_min required".into()));
|
|||
|
|
}
|
|||
|
|
if self.n_t == 0 {
|
|||
|
|
return Err(OptimizrError::InvalidParameter("n_t must be > 0".into()));
|
|||
|
|
}
|
|||
|
|
if !(self.t_horizon > 0.0) {
|
|||
|
|
return Err(OptimizrError::InvalidParameter("t_horizon must be > 0".into()));
|
|||
|
|
}
|
|||
|
|
Ok(())
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[derive(Clone, Debug)]
|
|||
|
|
pub struct FokkerPlanckResult {
|
|||
|
|
pub x_grid: Array1<f64>,
|
|||
|
|
pub time_grid: Array1<f64>,
|
|||
|
|
/// Density at each `(t_k, x_i)` flattened in row-major order
|
|||
|
|
/// `density[k * n_x + i]`.
|
|||
|
|
pub density: Vec<f64>,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
pub fn solve_fokker_planck_1d<Mu, Sigma2, M0>(
|
|||
|
|
drift: Mu,
|
|||
|
|
diffusion_sq: Sigma2,
|
|||
|
|
initial_density: M0,
|
|||
|
|
cfg: &FokkerPlanckConfig,
|
|||
|
|
) -> Result<FokkerPlanckResult>
|
|||
|
|
where
|
|||
|
|
Mu: Fn(f64) -> f64,
|
|||
|
|
Sigma2: Fn(f64) -> f64,
|
|||
|
|
M0: Fn(f64) -> f64,
|
|||
|
|
{
|
|||
|
|
cfg.validate()?;
|
|||
|
|
let nx = cfg.n_x;
|
|||
|
|
let nt = cfg.n_t;
|
|||
|
|
let dx = (cfg.x_max - cfg.x_min) / (nx - 1) as f64;
|
|||
|
|
let dt = cfg.t_horizon / nt as f64;
|
|||
|
|
let x_grid: Array1<f64> = Array1::from_iter((0..nx).map(|i| cfg.x_min + i as f64 * dx));
|
|||
|
|
let time_grid: Array1<f64> = Array1::from_iter((0..=nt).map(|k| k as f64 * dt));
|
|||
|
|
|
|||
|
|
// Stability check (very mild upper bound on coefficients sampled on the grid).
|
|||
|
|
let mut max_mu = 0.0_f64;
|
|||
|
|
let mut max_sig = 0.0_f64;
|
|||
|
|
for &x in x_grid.iter() {
|
|||
|
|
max_mu = max_mu.max(drift(x).abs());
|
|||
|
|
max_sig = max_sig.max(diffusion_sq(x).abs());
|
|||
|
|
}
|
|||
|
|
let cfl = dt * (max_mu / dx + max_sig / (dx * dx));
|
|||
|
|
if cfl > 1.0 {
|
|||
|
|
return Err(OptimizrError::NumericalError(format!(
|
|||
|
|
"CFL condition violated: dt·(|μ|/dx + σ²/dx²) = {cfl:.3} > 1"
|
|||
|
|
)));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let mut density = vec![0.0; nx * (nt + 1)];
|
|||
|
|
for i in 0..nx {
|
|||
|
|
density[i] = initial_density(x_grid[i]).max(0.0);
|
|||
|
|
}
|
|||
|
|
// Renormalise initial density to mass 1 (trapezoidal).
|
|||
|
|
let mut mass = 0.0;
|
|||
|
|
for i in 0..nx - 1 {
|
|||
|
|
mass += 0.5 * dx * (density[i] + density[i + 1]);
|
|||
|
|
}
|
|||
|
|
if mass > 0.0 {
|
|||
|
|
for i in 0..nx {
|
|||
|
|
density[i] /= mass;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for k in 0..nt {
|
|||
|
|
let off = k * nx;
|
|||
|
|
let new_off = (k + 1) * nx;
|
|||
|
|
// Boundaries enforced to zero
|
|||
|
|
density[new_off] = 0.0;
|
|||
|
|
density[new_off + nx - 1] = 0.0;
|
|||
|
|
for i in 1..nx - 1 {
|
|||
|
|
let x_im = x_grid[i - 1];
|
|||
|
|
let x_ip = x_grid[i + 1];
|
|||
|
|
let m_im = density[off + i - 1];
|
|||
|
|
let m_i = density[off + i];
|
|||
|
|
let m_ip = density[off + i + 1];
|
|||
|
|
let mu_im = drift(x_im);
|
|||
|
|
let mu_ip = drift(x_ip);
|
|||
|
|
let s_im = diffusion_sq(x_im);
|
|||
|
|
let s_i = diffusion_sq(x_grid[i]);
|
|||
|
|
let s_ip = diffusion_sq(x_ip);
|
|||
|
|
|
|||
|
|
let drift_term = (mu_ip * m_ip - mu_im * m_im) / (2.0 * dx);
|
|||
|
|
let diff_term = (s_ip * m_ip - 2.0 * s_i * m_i + s_im * m_im) / (dx * dx);
|
|||
|
|
density[new_off + i] = m_i - dt * drift_term + 0.5 * dt * diff_term;
|
|||
|
|
if density[new_off + i] < 0.0 {
|
|||
|
|
density[new_off + i] = 0.0; // positivity safeguard
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Ok(FokkerPlanckResult {
|
|||
|
|
x_grid,
|
|||
|
|
time_grid,
|
|||
|
|
density,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[cfg(test)]
|
|||
|
|
mod tests {
|
|||
|
|
use super::*;
|
|||
|
|
use std::f64::consts::PI;
|
|||
|
|
|
|||
|
|
/// Pure diffusion `μ = 0, σ² = 1` with Gaussian initial condition centred
|
|||
|
|
/// at 0 should remain centred and stay non-negative; total mass should be
|
|||
|
|
/// approximately conserved before any boundary loss.
|
|||
|
|
#[test]
|
|||
|
|
fn pure_diffusion_keeps_mean_at_zero() {
|
|||
|
|
let cfg = FokkerPlanckConfig {
|
|||
|
|
n_x: 401,
|
|||
|
|
x_min: -8.0,
|
|||
|
|
x_max: 8.0,
|
|||
|
|
n_t: 8000,
|
|||
|
|
t_horizon: 0.5,
|
|||
|
|
};
|
|||
|
|
let res = solve_fokker_planck_1d(
|
|||
|
|
|_| 0.0,
|
|||
|
|
|_| 1.0,
|
|||
|
|
|x| (-(x * x) / 2.0).exp() / (2.0 * PI).sqrt(),
|
|||
|
|
&cfg,
|
|||
|
|
)
|
|||
|
|
.unwrap();
|
|||
|
|
let nx = cfg.n_x;
|
|||
|
|
let off = cfg.n_t * nx;
|
|||
|
|
let dx = (cfg.x_max - cfg.x_min) / (nx - 1) as f64;
|
|||
|
|
let mut mean = 0.0;
|
|||
|
|
let mut mass = 0.0;
|
|||
|
|
for i in 0..nx {
|
|||
|
|
let x = cfg.x_min + i as f64 * dx;
|
|||
|
|
let m = res.density[off + i];
|
|||
|
|
mean += x * m * dx;
|
|||
|
|
mass += m * dx;
|
|||
|
|
}
|
|||
|
|
assert!(mass > 0.5, "lost too much mass: {mass}");
|
|||
|
|
assert!(mean.abs() < 0.05, "mean drifted: {mean}");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn cfl_violation_is_detected() {
|
|||
|
|
let cfg = FokkerPlanckConfig {
|
|||
|
|
n_x: 11,
|
|||
|
|
x_min: 0.0,
|
|||
|
|
x_max: 1.0,
|
|||
|
|
n_t: 1,
|
|||
|
|
t_horizon: 1.0,
|
|||
|
|
};
|
|||
|
|
let res = solve_fokker_planck_1d(|_| 0.0, |_| 1.0, |_| 1.0, &cfg);
|
|||
|
|
assert!(res.is_err());
|
|||
|
|
}
|
|||
|
|
}
|