use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
use std::sync::Arc;
use crate::ode::{solve_ivp, ODEMethod, ODEOptions};
use crate::pde::{PDEError, PDEResult};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum StencilOrder {
Second,
Fourth,
}
#[derive(Debug, Clone)]
pub enum MOLBoundaryCondition {
Dirichlet(f64),
Neumann(f64),
Periodic,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum MOLTimeIntegrator {
RK45,
BDF,
RK23,
}
impl MOLTimeIntegrator {
fn to_ode_method(self) -> ODEMethod {
match self {
MOLTimeIntegrator::RK45 => ODEMethod::RK45,
MOLTimeIntegrator::BDF => ODEMethod::Bdf,
MOLTimeIntegrator::RK23 => ODEMethod::RK23,
}
}
}
#[derive(Debug, Clone)]
pub struct MOLEnhancedOptions {
pub integrator: MOLTimeIntegrator,
pub stencil: StencilOrder,
pub atol: f64,
pub rtol: f64,
pub max_steps: usize,
}
impl Default for MOLEnhancedOptions {
fn default() -> Self {
MOLEnhancedOptions {
integrator: MOLTimeIntegrator::RK45,
stencil: StencilOrder::Second,
atol: 1e-6,
rtol: 1e-3,
max_steps: 10000,
}
}
}
#[derive(Debug, Clone)]
pub struct MOLEnhancedResult {
pub x: Array1<f64>,
pub t: Vec<f64>,
pub u: Vec<Array1<f64>>,
pub n_eval: usize,
pub n_steps: usize,
}
pub fn mol_diffusion_1d(
alpha: f64,
x_range: [f64; 2],
t_range: [f64; 2],
nx: usize,
left_bc: MOLBoundaryCondition,
right_bc: MOLBoundaryCondition,
initial_condition: impl Fn(f64) -> f64 + Send + Sync + 'static,
source: Option<Arc<dyn Fn(f64, f64, f64) -> f64 + Send + Sync>>,
options: &MOLEnhancedOptions,
) -> PDEResult<MOLEnhancedResult> {
if alpha <= 0.0 {
return Err(PDEError::InvalidParameter(
"Diffusion coefficient alpha must be positive".to_string(),
));
}
if nx < 5 {
return Err(PDEError::InvalidGrid(
"Need at least 5 spatial points for MOL".to_string(),
));
}
let dx = (x_range[1] - x_range[0]) / (nx as f64 - 1.0);
let x = Array1::from_shape_fn(nx, |i| x_range[0] + i as f64 * dx);
let mut u0 = Array1::from_shape_fn(nx, |i| initial_condition(x[i]));
apply_mol_bc(&mut u0, &left_bc, &right_bc, dx);
let stencil = options.stencil;
let x_clone = x.clone();
let left_bc_c = left_bc.clone();
let right_bc_c = right_bc.clone();
let rhs = move |t: f64, u: ArrayView1<f64>| -> Array1<f64> {
let n = u.len();
let mut dudt = Array1::zeros(n);
match stencil {
StencilOrder::Second => {
let r = alpha / (dx * dx);
for i in 1..n - 1 {
dudt[i] = r * (u[i + 1] - 2.0 * u[i] + u[i - 1]);
}
}
StencilOrder::Fourth => {
let r = alpha / (12.0 * dx * dx);
for i in 2..n - 2 {
dudt[i] = r
* (-u[i + 2] + 16.0 * u[i + 1] - 30.0 * u[i] + 16.0 * u[i - 1] - u[i - 2]);
}
let r2 = alpha / (dx * dx);
if n > 2 {
dudt[1] = r2 * (u[2] - 2.0 * u[1] + u[0]);
}
if n > 3 {
dudt[n - 2] = r2 * (u[n - 1] - 2.0 * u[n - 2] + u[n - 3]);
}
}
}
if let Some(ref src) = source {
for i in 1..n - 1 {
dudt[i] += src(x_clone[i], t, u[i]);
}
}
apply_mol_bc_rhs(
&mut dudt,
&u,
&left_bc_c,
&right_bc_c,
n,
dx,
alpha,
&x_clone,
t,
&source,
);
dudt
};
run_mol_ode(x, u0, t_range, rhs, options)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum AdvectionScheme {
Upwind,
LaxWendroff,
Central,
}
pub fn mol_advection_1d(
velocity: f64,
x_range: [f64; 2],
t_range: [f64; 2],
nx: usize,
left_bc: MOLBoundaryCondition,
right_bc: MOLBoundaryCondition,
initial_condition: impl Fn(f64) -> f64 + Send + Sync + 'static,
source: Option<Arc<dyn Fn(f64, f64) -> f64 + Send + Sync>>,
scheme: AdvectionScheme,
options: &MOLEnhancedOptions,
) -> PDEResult<MOLEnhancedResult> {
if nx < 5 {
return Err(PDEError::InvalidGrid(
"Need at least 5 spatial points for advection".to_string(),
));
}
let dx = (x_range[1] - x_range[0]) / (nx as f64 - 1.0);
let x = Array1::from_shape_fn(nx, |i| x_range[0] + i as f64 * dx);
let mut u0 = Array1::from_shape_fn(nx, |i| initial_condition(x[i]));
apply_mol_bc(&mut u0, &left_bc, &right_bc, dx);
let x_clone = x.clone();
let left_bc_c = left_bc.clone();
let right_bc_c = right_bc.clone();
let rhs = move |t: f64, u: ArrayView1<f64>| -> Array1<f64> {
let n = u.len();
let mut dudt = Array1::zeros(n);
match scheme {
AdvectionScheme::Upwind => {
if velocity >= 0.0 {
for i in 1..n - 1 {
dudt[i] = -velocity * (u[i] - u[i - 1]) / dx;
}
} else {
for i in 1..n - 1 {
dudt[i] = -velocity * (u[i + 1] - u[i]) / dx;
}
}
}
AdvectionScheme::LaxWendroff => {
for i in 1..n - 1 {
let advection = -velocity * (u[i + 1] - u[i - 1]) / (2.0 * dx);
let diffusion =
velocity.abs() * dx / 2.0 * (u[i + 1] - 2.0 * u[i] + u[i - 1]) / (dx * dx);
dudt[i] = advection + diffusion;
}
}
AdvectionScheme::Central => {
for i in 1..n - 1 {
dudt[i] = -velocity * (u[i + 1] - u[i - 1]) / (2.0 * dx);
}
}
}
if let Some(ref src) = source {
for i in 1..n - 1 {
dudt[i] += src(x_clone[i], t);
}
}
apply_advection_bc_rhs(&mut dudt, &u, &left_bc_c, &right_bc_c, n, dx, velocity);
dudt
};
run_mol_ode(x, u0, t_range, rhs, options)
}
pub struct ReactionDiffusionSystem {
pub n_species: usize,
pub diffusion_coeffs: Vec<f64>,
pub reaction: Arc<dyn Fn(f64, f64, &[f64]) -> Vec<f64> + Send + Sync>,
}
#[derive(Debug, Clone)]
pub struct ReactionDiffusionResult {
pub x: Array1<f64>,
pub t: Vec<f64>,
pub u: Vec<Array2<f64>>,
pub n_eval: usize,
pub n_steps: usize,
}
pub fn mol_reaction_diffusion(
system: &ReactionDiffusionSystem,
x_range: [f64; 2],
t_range: [f64; 2],
nx: usize,
initial_conditions: &[impl Fn(f64) -> f64],
bc_left: &[f64], bc_right: &[f64], options: &MOLEnhancedOptions,
) -> PDEResult<ReactionDiffusionResult> {
let m = system.n_species;
if initial_conditions.len() != m || bc_left.len() != m || bc_right.len() != m {
return Err(PDEError::InvalidParameter(format!(
"Expected {} initial conditions/BCs for {} species",
m, m
)));
}
if nx < 5 {
return Err(PDEError::InvalidGrid(
"Need at least 5 spatial points".to_string(),
));
}
let dx = (x_range[1] - x_range[0]) / (nx as f64 - 1.0);
let x = Array1::from_shape_fn(nx, |i| x_range[0] + i as f64 * dx);
let total_dof = m * nx;
let mut u0 = Array1::zeros(total_dof);
for s in 0..m {
for i in 0..nx {
u0[s * nx + i] = initial_conditions[s](x[i]);
}
u0[s * nx] = bc_left[s];
u0[s * nx + nx - 1] = bc_right[s];
}
let diffusion_coeffs = system.diffusion_coeffs.clone();
let reaction = system.reaction.clone();
let x_clone = x.clone();
let bc_l = bc_left.to_vec();
let bc_r = bc_right.to_vec();
let rhs = move |t: f64, u: ArrayView1<f64>| -> Array1<f64> {
let mut dudt = Array1::zeros(total_dof);
let dx2 = dx * dx;
for s in 0..m {
let offset = s * nx;
let d = diffusion_coeffs[s];
for i in 1..nx - 1 {
dudt[offset + i] =
d * (u[offset + i + 1] - 2.0 * u[offset + i] + u[offset + i - 1]) / dx2;
}
dudt[offset] = 0.0;
dudt[offset + nx - 1] = 0.0;
}
let mut species_vals = vec![0.0; m];
for i in 1..nx - 1 {
for s in 0..m {
species_vals[s] = u[s * nx + i];
}
let r = reaction(x_clone[i], t, &species_vals);
for s in 0..m {
if s < r.len() {
dudt[s * nx + i] += r[s];
}
}
}
dudt
};
let ode_opts = ODEOptions {
method: options.integrator.to_ode_method(),
rtol: options.rtol,
atol: options.atol,
max_steps: options.max_steps,
dense_output: false,
..Default::default()
};
let rhs_arc = Arc::new(rhs);
let rhs_clone = move |t: f64, u: ArrayView1<f64>| -> Array1<f64> { rhs_arc(t, u) };
let result = solve_ivp(rhs_clone, t_range, u0, Some(ode_opts))?;
let mut t_vec = Vec::new();
let mut u_vec = Vec::new();
for (step, y) in result.y.iter().enumerate() {
t_vec.push(result.t[step]);
let mut u_2d = Array2::zeros((m, nx));
for s in 0..m {
for i in 0..nx {
u_2d[[s, i]] = y[s * nx + i];
}
}
u_vec.push(u_2d);
}
Ok(ReactionDiffusionResult {
x,
t: t_vec,
u: u_vec,
n_eval: result.n_eval,
n_steps: result.n_steps,
})
}
pub fn mol_advection_diffusion_1d(
velocity: f64,
diffusion: f64,
x_range: [f64; 2],
t_range: [f64; 2],
nx: usize,
left_bc: MOLBoundaryCondition,
right_bc: MOLBoundaryCondition,
initial_condition: impl Fn(f64) -> f64 + Send + Sync + 'static,
source: Option<Arc<dyn Fn(f64, f64) -> f64 + Send + Sync>>,
options: &MOLEnhancedOptions,
) -> PDEResult<MOLEnhancedResult> {
if diffusion < 0.0 {
return Err(PDEError::InvalidParameter(
"Diffusion coefficient must be non-negative".to_string(),
));
}
if nx < 5 {
return Err(PDEError::InvalidGrid(
"Need at least 5 spatial points".to_string(),
));
}
let dx = (x_range[1] - x_range[0]) / (nx as f64 - 1.0);
let x = Array1::from_shape_fn(nx, |i| x_range[0] + i as f64 * dx);
let mut u0 = Array1::from_shape_fn(nx, |i| initial_condition(x[i]));
apply_mol_bc(&mut u0, &left_bc, &right_bc, dx);
let x_clone = x.clone();
let left_bc_c = left_bc.clone();
let right_bc_c = right_bc.clone();
let rhs = move |t: f64, u: ArrayView1<f64>| -> Array1<f64> {
let n = u.len();
let mut dudt = Array1::zeros(n);
let dx2 = dx * dx;
for i in 1..n - 1 {
let advection = if velocity >= 0.0 {
-velocity * (u[i] - u[i - 1]) / dx
} else {
-velocity * (u[i + 1] - u[i]) / dx
};
let diff = diffusion * (u[i + 1] - 2.0 * u[i] + u[i - 1]) / dx2;
dudt[i] = advection + diff;
}
if let Some(ref src) = source {
for i in 1..n - 1 {
dudt[i] += src(x_clone[i], t);
}
}
apply_advection_bc_rhs(&mut dudt, &u, &left_bc_c, &right_bc_c, n, dx, velocity);
dudt
};
run_mol_ode(x, u0, t_range, rhs, options)
}
fn apply_mol_bc(
u: &mut Array1<f64>,
left: &MOLBoundaryCondition,
right: &MOLBoundaryCondition,
dx: f64,
) {
let n = u.len();
match left {
MOLBoundaryCondition::Dirichlet(val) => u[0] = *val,
MOLBoundaryCondition::Neumann(val) => u[0] = u[1] - dx * val,
MOLBoundaryCondition::Periodic => {
if n > 1 {
u[0] = u[n - 2]; }
}
}
match right {
MOLBoundaryCondition::Dirichlet(val) => u[n - 1] = *val,
MOLBoundaryCondition::Neumann(val) => u[n - 1] = u[n - 2] + dx * val,
MOLBoundaryCondition::Periodic => {
if n > 1 {
u[n - 1] = u[1]; }
}
}
}
#[allow(clippy::too_many_arguments)]
fn apply_mol_bc_rhs(
dudt: &mut Array1<f64>,
u: &ArrayView1<f64>,
left: &MOLBoundaryCondition,
right: &MOLBoundaryCondition,
n: usize,
dx: f64,
alpha: f64,
_x: &Array1<f64>,
_t: f64,
_source: &Option<Arc<dyn Fn(f64, f64, f64) -> f64 + Send + Sync>>,
) {
let dx2 = dx * dx;
match left {
MOLBoundaryCondition::Dirichlet(_) => {
dudt[0] = 0.0;
}
MOLBoundaryCondition::Neumann(val) => {
let ghost = u[1] - 2.0 * dx * val;
dudt[0] = alpha * (u[1] - 2.0 * u[0] + ghost) / dx2;
}
MOLBoundaryCondition::Periodic => {
dudt[0] = alpha * (u[1] - 2.0 * u[0] + u[n - 2]) / dx2;
}
}
match right {
MOLBoundaryCondition::Dirichlet(_) => {
dudt[n - 1] = 0.0;
}
MOLBoundaryCondition::Neumann(val) => {
let ghost = u[n - 2] + 2.0 * dx * val;
dudt[n - 1] = alpha * (ghost - 2.0 * u[n - 1] + u[n - 2]) / dx2;
}
MOLBoundaryCondition::Periodic => {
dudt[n - 1] = alpha * (u[1] - 2.0 * u[n - 1] + u[n - 2]) / dx2;
}
}
}
fn apply_advection_bc_rhs(
dudt: &mut Array1<f64>,
u: &ArrayView1<f64>,
left: &MOLBoundaryCondition,
right: &MOLBoundaryCondition,
n: usize,
dx: f64,
velocity: f64,
) {
match left {
MOLBoundaryCondition::Dirichlet(_) => {
dudt[0] = 0.0;
}
MOLBoundaryCondition::Neumann(_) => {
dudt[0] = 0.0; }
MOLBoundaryCondition::Periodic => {
if velocity >= 0.0 {
dudt[0] = -velocity * (u[0] - u[n - 2]) / dx;
} else {
dudt[0] = -velocity * (u[1] - u[0]) / dx;
}
}
}
match right {
MOLBoundaryCondition::Dirichlet(_) => {
dudt[n - 1] = 0.0;
}
MOLBoundaryCondition::Neumann(_) => {
dudt[n - 1] = 0.0; }
MOLBoundaryCondition::Periodic => {
if velocity >= 0.0 {
dudt[n - 1] = -velocity * (u[n - 1] - u[n - 2]) / dx;
} else {
dudt[n - 1] = -velocity * (u[1] - u[n - 1]) / dx;
}
}
}
}
fn run_mol_ode(
x: Array1<f64>,
u0: Array1<f64>,
t_range: [f64; 2],
rhs: impl Fn(f64, ArrayView1<f64>) -> Array1<f64> + Send + Sync + 'static,
options: &MOLEnhancedOptions,
) -> PDEResult<MOLEnhancedResult> {
let ode_opts = ODEOptions {
method: options.integrator.to_ode_method(),
rtol: options.rtol,
atol: options.atol,
max_steps: options.max_steps,
dense_output: false,
..Default::default()
};
let rhs_arc = Arc::new(rhs);
let rhs_clone = move |t: f64, u: ArrayView1<f64>| -> Array1<f64> { rhs_arc(t, u) };
let result = solve_ivp(rhs_clone, t_range, u0, Some(ode_opts))?;
let t_vec: Vec<f64> = result.t.to_vec();
let u_vec: Vec<Array1<f64>> = result.y.to_vec();
Ok(MOLEnhancedResult {
x,
t: t_vec,
u: u_vec,
n_eval: result.n_eval,
n_steps: result.n_steps,
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
#[test]
fn test_mol_diffusion_constant() {
let result = mol_diffusion_1d(
0.1,
[0.0, 1.0],
[0.0, 0.5],
21,
MOLBoundaryCondition::Dirichlet(1.0),
MOLBoundaryCondition::Dirichlet(1.0),
|_| 1.0,
None,
&MOLEnhancedOptions::default(),
)
.expect("Should succeed");
let last = &result.u[result.u.len() - 1];
for &v in last.iter() {
assert!((v - 1.0).abs() < 0.01, "Should stay at 1.0, got {v}");
}
}
#[test]
fn test_mol_diffusion_decay() {
let alpha = 0.1;
let result = mol_diffusion_1d(
alpha,
[0.0, 1.0],
[0.0, 0.5],
41,
MOLBoundaryCondition::Dirichlet(0.0),
MOLBoundaryCondition::Dirichlet(0.0),
|x| (PI * x).sin(),
None,
&MOLEnhancedOptions::default(),
)
.expect("Should succeed");
let last = &result.u[result.u.len() - 1];
let mid = last.len() / 2;
let exact = (PI * 0.5).sin() * (-PI * PI * alpha * 0.5).exp();
assert!(
(last[mid] - exact).abs() < 0.05,
"MOL diffusion: got {}, expected {exact}",
last[mid]
);
}
#[test]
fn test_mol_diffusion_4th_order() {
let alpha = 0.1;
let opts = MOLEnhancedOptions {
stencil: StencilOrder::Fourth,
..Default::default()
};
let result = mol_diffusion_1d(
alpha,
[0.0, 1.0],
[0.0, 0.3],
41,
MOLBoundaryCondition::Dirichlet(0.0),
MOLBoundaryCondition::Dirichlet(0.0),
|x| (PI * x).sin(),
None,
&opts,
)
.expect("Should succeed");
let last = &result.u[result.u.len() - 1];
let mid = last.len() / 2;
let exact = (PI * 0.5).sin() * (-PI * PI * alpha * 0.3).exp();
assert!(
(last[mid] - exact).abs() < 0.05,
"4th order: got {}, expected {exact}",
last[mid]
);
}
#[test]
fn test_mol_diffusion_with_source() {
let result = mol_diffusion_1d(
0.1,
[0.0, 1.0],
[0.0, 0.5],
21,
MOLBoundaryCondition::Dirichlet(0.0),
MOLBoundaryCondition::Dirichlet(0.0),
|_| 0.0,
Some(Arc::new(|_, _, _| 1.0)),
&MOLEnhancedOptions::default(),
)
.expect("Should succeed");
let last = &result.u[result.u.len() - 1];
let mid = last.len() / 2;
assert!(last[mid] > 0.0, "Source should make interior positive");
}
#[test]
fn test_mol_diffusion_neumann() {
let result = mol_diffusion_1d(
0.01,
[0.0, 1.0],
[0.0, 0.5],
21,
MOLBoundaryCondition::Neumann(0.0),
MOLBoundaryCondition::Neumann(0.0),
|_| 1.0,
None,
&MOLEnhancedOptions::default(),
)
.expect("Should succeed");
let last = &result.u[result.u.len() - 1];
for &v in last.iter() {
assert!((v - 1.0).abs() < 0.05, "Neumann: should stay ~1.0, got {v}");
}
}
#[test]
fn test_mol_diffusion_periodic() {
let result = mol_diffusion_1d(
0.01,
[0.0, 1.0],
[0.0, 0.5],
41,
MOLBoundaryCondition::Periodic,
MOLBoundaryCondition::Periodic,
|x| (2.0 * PI * x).sin(),
None,
&MOLEnhancedOptions::default(),
)
.expect("Should succeed");
assert!(result.u.len() > 1, "Should have multiple time steps");
}
#[test]
fn test_mol_advection_upwind() {
let result = mol_advection_1d(
1.0,
[0.0, 2.0],
[0.0, 0.5],
41,
MOLBoundaryCondition::Dirichlet(0.0),
MOLBoundaryCondition::Dirichlet(0.0),
|x| if x > 0.3 && x < 0.7 { 1.0 } else { 0.0 },
None,
AdvectionScheme::Upwind,
&MOLEnhancedOptions::default(),
)
.expect("Should succeed");
assert!(result.u.len() > 1);
}
#[test]
fn test_mol_advection_lax_wendroff() {
let result = mol_advection_1d(
1.0,
[0.0, 2.0],
[0.0, 0.3],
41,
MOLBoundaryCondition::Dirichlet(0.0),
MOLBoundaryCondition::Dirichlet(0.0),
|x| (PI * x).sin(),
None,
AdvectionScheme::LaxWendroff,
&MOLEnhancedOptions::default(),
)
.expect("Should succeed");
assert!(result.u.len() > 1);
}
#[test]
fn test_mol_advection_periodic() {
let result = mol_advection_1d(
1.0,
[0.0, 1.0],
[0.0, 0.3],
41,
MOLBoundaryCondition::Periodic,
MOLBoundaryCondition::Periodic,
|x| (2.0 * PI * x).sin(),
None,
AdvectionScheme::Upwind,
&MOLEnhancedOptions::default(),
)
.expect("Should succeed");
assert!(result.u.len() > 1);
}
#[test]
fn test_mol_advection_diffusion() {
let result = mol_advection_diffusion_1d(
1.0,
0.01,
[0.0, 1.0],
[0.0, 0.5],
41,
MOLBoundaryCondition::Dirichlet(0.0),
MOLBoundaryCondition::Dirichlet(0.0),
|x| (PI * x).sin(),
None,
&MOLEnhancedOptions::default(),
)
.expect("Should succeed");
assert!(result.u.len() > 1);
}
#[test]
fn test_mol_reaction_diffusion() {
let system = ReactionDiffusionSystem {
n_species: 2,
diffusion_coeffs: vec![0.01, 0.005],
reaction: Arc::new(|_x, _t, u| {
let f = 0.04;
let k = 0.06;
let u_val = u[0];
let v_val = u[1];
vec![
-u_val * v_val * v_val + f * (1.0 - u_val),
u_val * v_val * v_val - (f + k) * v_val,
]
}),
};
fn ic_u(_x: f64) -> f64 {
1.0
}
fn ic_v(x: f64) -> f64 {
if x > 0.4 && x < 0.6 {
0.5
} else {
0.0
}
}
let ics: Vec<fn(f64) -> f64> = vec![ic_u, ic_v];
let result = mol_reaction_diffusion(
&system,
[0.0, 1.0],
[0.0, 1.0],
21,
&ics,
&[1.0, 0.0],
&[1.0, 0.0],
&MOLEnhancedOptions {
integrator: MOLTimeIntegrator::RK45,
..Default::default()
},
)
.expect("Should succeed");
assert!(result.u.len() > 1);
assert_eq!(result.u[0].shape()[0], 2); }
#[test]
fn test_mol_bdf_integrator() {
let result = mol_diffusion_1d(
1.0, [0.0, 1.0],
[0.0, 0.1],
21,
MOLBoundaryCondition::Dirichlet(0.0),
MOLBoundaryCondition::Dirichlet(0.0),
|x| (PI * x).sin(),
None,
&MOLEnhancedOptions {
integrator: MOLTimeIntegrator::BDF,
..Default::default()
},
)
.expect("BDF should succeed");
assert!(result.u.len() > 1);
}
#[test]
fn test_mol_result_fields() {
let result = mol_diffusion_1d(
0.1,
[0.0, 1.0],
[0.0, 0.1],
11,
MOLBoundaryCondition::Dirichlet(0.0),
MOLBoundaryCondition::Dirichlet(0.0),
|x| (PI * x).sin(),
None,
&MOLEnhancedOptions::default(),
)
.expect("Should succeed");
assert_eq!(result.x.len(), 11);
assert!(result.n_eval > 0);
assert!(result.n_steps > 0);
assert!(result.t.len() == result.u.len());
}
}