Integro-Differential Equations
Integro-differential equations (IDEs) combine differential and integral operators,
modeling systems where the rate of change depends not only on the current state but on
a weighted accumulation of the entire past. They appear in viscoelasticity (hereditary
materials), population dynamics with age structure, heat conduction with memory, and
radiative transfer. Numra provides the numra-ide crate with quadrature-based Volterra
solvers and an efficient Prony series solver for sum-of-exponentials kernels.
Mathematical Formulation
Section titled “Mathematical Formulation”A Volterra integro-differential equation has the form:
where:
- is the local (instantaneous) right-hand side,
- is the memory kernel encoding history dependence,
- the integral accumulates contributions from the entire past .
Convolution Kernels
Section titled “Convolution Kernels”A kernel is a convolution kernel if it depends only on the time difference:
Convolution kernels can be computed more efficiently and are the most common type in physical applications.
Comparison with Other Memory-Dependent DEs
Section titled “Comparison with Other Memory-Dependent DEs”| Equation Type | Memory Structure | Computational Cost |
|---|---|---|
| ODE | No memory | total |
| DDE | Discrete delay points | total |
| FDE | Power-law kernel (Caputo) | total |
| IDE | General kernel | total |
| IDE (Prony) | Sum of exponentials | total |
The IdeSystem Trait
Section titled “The IdeSystem Trait”Every IDE in Numra implements the IdeSystem trait:
pub trait IdeSystem<S: Scalar> { /// Dimension of the state space. fn dim(&self) -> usize;
/// Evaluate the local right-hand side f(t, y). fn rhs(&self, t: S, y: &[S], f: &mut [S]);
/// Evaluate the memory kernel K(t, s, y(s)). fn kernel(&self, t: S, s: S, y_s: &[S], k: &mut [S]);
/// Whether K depends only on (t-s), not t and s separately. fn is_convolution_kernel(&self) -> bool { false }}The rhs method evaluates the non-integral part, and kernel evaluates the integrand
at a specific past time given the solution at that time.
Memory Kernels
Section titled “Memory Kernels”Numra provides several built-in kernel types via the Kernel trait:
Exponential Kernel
Section titled “Exponential Kernel”Models the Maxwell element in viscoelasticity. Memory decays exponentially.
use numra_ide::kernels::ExponentialKernel;
let kernel = ExponentialKernel::new(2.0, 0.5);// K(0) = 2.0, K(2) = 2 * exp(-1) ~ 0.736Power-Law Kernel
Section titled “Power-Law Kernel”Models fractional memory effects. Singular at (regularized to zero in Numra). For , this is a weakly singular kernel.
use numra_ide::kernels::PowerLawKernel;
let kernel = PowerLawKernel::new(1.0, 0.5);// K(1) = 1.0, K(4) = 0.5Prony Series (Sum of Exponentials)
Section titled “Prony Series (Sum of Exponentials)”The most computationally efficient kernel type because the integral can be updated recursively without storing the full history.
use numra_ide::kernels::PronyKernel;
// Two-term generalized Maxwell modellet kernel = PronyKernel::two_term(1.0, 0.5, 2.0, 1.0);// K(0) = 1 + 2 = 3, K(2) = exp(-1) + 2*exp(-2) ~ 0.639Mittag-Leffler Kernel
Section titled “Mittag-Leffler Kernel”Generalizes both exponential () and power-law kernels.
use numra_ide::kernels::MittagLefflerKernel;
let kernel = MittagLefflerKernel::relaxation(1.0, 0.5);Solvers
Section titled “Solvers”VolterraSolver (Trapezoidal + Euler)
Section titled “VolterraSolver (Trapezoidal + Euler)”The basic solver uses composite trapezoidal quadrature for the integral and explicit Euler for the time step:
The integral is approximated with the trapezoidal rule over all stored history points.
- Time stepping: Explicit Euler
- Quadrature: Composite trapezoidal
- Cost per step: kernel evaluations
- Total cost:
VolterraRK4Solver (Trapezoidal + RK4)
Section titled “VolterraRK4Solver (Trapezoidal + RK4)”An improved solver that replaces explicit Euler with classical 4th-order Runge-Kutta for the time-stepping:
- Time stepping: Classical RK4
- Quadrature: Composite trapezoidal
- Accuracy: Significantly better than Euler for the same step size
- Cost per step: kernel evaluations (four RK stages)
PronySolver
Section titled “PronySolver”For kernels that are sums of exponentials, the PronySolver achieves cost
per step by recursively updating auxiliary variables. The key insight is that for
the integral satisfies , converting the IDE into an augmented ODE system.
Solver Options
Section titled “Solver Options”use numra_ide::IdeOptions;
let opts = IdeOptions::default() .dt(0.01) // Time step .max_steps(100_000) // Maximum number of steps .tol(1e-10) // Tolerance for implicit iteration .quad_points(4); // Quadrature points per step| Option | Default | Description |
|---|---|---|
dt | 0.01 | Time step size |
max_steps | 100,000 | Maximum number of steps |
tol | 1e-10 | Convergence tolerance |
max_iter | 100 | Maximum iterations per step |
quad_points | 4 | Gauss-Legendre quadrature points per subinterval |
Example: Exponential Memory Decay
Section titled “Example: Exponential Memory Decay”A system with exponential memory kernel modeling a viscoelastic material:
use numra_ide::{IdeSystem, VolterraSolver, IdeSolver, IdeOptions};
struct ExponentialMemory { k: f64 }
impl IdeSystem<f64> for ExponentialMemory { fn dim(&self) -> usize { 1 }
fn rhs(&self, _t: f64, y: &[f64], f: &mut [f64]) { f[0] = -self.k * y[0]; }
fn kernel(&self, t: f64, s: f64, y_s: &[f64], k: &mut [f64]) { k[0] = (-(t - s)).exp() * y_s[0]; }
fn is_convolution_kernel(&self) -> bool { true }}
let system = ExponentialMemory { k: 1.0 };let opts = IdeOptions::default().dt(0.01);
let result = VolterraSolver::solve(&system, 0.0, 5.0, &[1.0], &opts) .expect("Solve failed");
for (i, &t) in result.t.iter().enumerate().step_by(50) { println!("t = {:.2}, y = {:.6}", t, result.y_at(i)[0]);}Higher Accuracy with RK4
Section titled “Higher Accuracy with RK4”For the same problem with improved time-stepping accuracy:
use numra_ide::{IdeSystem, VolterraRK4Solver, IdeSolver, IdeOptions};
// Same system definition as above...
let opts = IdeOptions::default().dt(0.05); // Can use larger step with RK4
let result = VolterraRK4Solver::solve(&system, 0.0, 5.0, &[1.0], &opts) .expect("Solve failed");Example: Viscoelastic Material with Prony Kernel
Section titled “Example: Viscoelastic Material with Prony Kernel”For a generalized Maxwell model with two relaxation times:
use numra_ide::{IdeSystem, VolterraSolver, IdeSolver, IdeOptions};use numra_ide::kernels::PronyKernel;
struct ViscoelasticMaterial { kernel: PronyKernel<f64>,}
impl IdeSystem<f64> for ViscoelasticMaterial { fn dim(&self) -> usize { 1 }
fn rhs(&self, _t: f64, y: &[f64], f: &mut [f64]) { f[0] = -y[0]; }
fn kernel(&self, t: f64, s: f64, y_s: &[f64], k: &mut [f64]) { use numra_ide::Kernel; k[0] = self.kernel.evaluate(t - s) * y_s[0]; }
fn is_convolution_kernel(&self) -> bool { true }}
let material = ViscoelasticMaterial { kernel: PronyKernel::two_term(1.0, 0.5, 0.5, 2.0),};let opts = IdeOptions::default().dt(0.01);
let result = VolterraSolver::solve(&material, 0.0, 10.0, &[1.0], &opts) .expect("Solve failed");Multi-Dimensional Systems
Section titled “Multi-Dimensional Systems”IDEs with coupled components:
use numra_ide::{IdeSystem, VolterraSolver, IdeSolver, IdeOptions};
struct CoupledIDE;
impl IdeSystem<f64> for CoupledIDE { fn dim(&self) -> usize { 2 }
fn rhs(&self, _t: f64, y: &[f64], f: &mut [f64]) { f[0] = -y[0] + 0.1 * y[1]; f[1] = -y[1]; }
fn kernel(&self, t: f64, s: f64, y_s: &[f64], k: &mut [f64]) { let decay = (-(t - s)).exp(); k[0] = 0.5 * decay * y_s[0]; k[1] = 0.2 * decay * y_s[1]; }}
let opts = IdeOptions::default().dt(0.01);let result = VolterraSolver::solve(&CoupledIDE, 0.0, 5.0, &[1.0, 1.0], &opts) .expect("Solve failed");
let y_final = result.y_final().unwrap();println!("y1(5) = {:.6}, y2(5) = {:.6}", y_final[0], y_final[1]);Working with Results
Section titled “Working with Results”IdeResult provides the standard Numra result interface:
let result = VolterraSolver::solve(&system, t0, tf, &y0, &opts)?;
// Final statelet y_final = result.y_final().unwrap();
// Access at indexlet y_i = result.y_at(50);
// Statisticsprintln!("RHS evaluations: {}", result.stats.n_rhs);println!("Kernel evaluations: {}", result.stats.n_kernel);println!("Steps taken: {}", result.stats.n_steps);Choosing a Solver
Section titled “Choosing a Solver”| Kernel Type | Recommended Solver | Cost | Notes |
|---|---|---|---|
| General | VolterraRK4Solver | Most flexible | |
| Simple problems | VolterraSolver | Euler time-stepping | |
| Sum of exponentials | PronySolver | Fastest, limited to SOE kernels | |
| Power-law | VolterraRK4Solver | Consider FDE solver instead |
Practical Considerations
Section titled “Practical Considerations”Quadrature Accuracy
Section titled “Quadrature Accuracy”The trapezoidal rule used for the integral is second-order accurate. For weakly singular kernels (like power-law), the accuracy degrades near . Strategies:
- Use a smaller to compensate
- For power-law kernels with , consider using the FDE solver instead
Cost vs. Accuracy
Section titled “Cost vs. Accuracy”The cost of general Volterra solvers becomes significant for long integrations. To manage computational expense:
- Use
PronySolverwhenever the kernel can be approximated by a sum of exponentials - Many physical kernels (even power-law) can be approximated by Prony series
- Use the coarsest that meets accuracy requirements
Connection to Fractional Calculus
Section titled “Connection to Fractional Calculus”When the kernel has the form , the
Volterra integral becomes the Riemann-Liouville fractional integral, and the IDE reduces
to a fractional differential equation. For such problems, the dedicated FDE solver in
numra-fde is more efficient because it uses optimized Caputo weights.