Fractional Differential Equations
Fractional differential equations (FDEs) replace integer-order derivatives with
fractional-order derivatives, enabling the modeling of systems with memory effects
and anomalous dynamics. They appear in viscoelastic materials, anomalous diffusion,
electrochemistry, and biological tissue modeling. Numra provides the numra-fde crate
with an L1 scheme solver for Caputo fractional derivatives.
The Caputo Fractional Derivative
Section titled “The Caputo Fractional Derivative”The Caputo fractional derivative of order is defined as:
where is the gamma function. Key properties:
| Property | Caputo Derivative |
|---|---|
| Constant rule | (like ordinary derivatives) |
| Power rule | for |
| Integer limit | (reduces to standard derivative) |
| Memory | Depends on entire history of from to |
| Initial conditions | Standard: (same as ODEs) |
Why Caputo over Riemann-Liouville?
Section titled “Why Caputo over Riemann-Liouville?”The Caputo derivative is preferred for physical modeling because:
- It preserves the meaning of initial conditions ( rather than fractional integrals of at ).
- The Caputo derivative of a constant is zero, matching physical intuition.
- Standard initial value problem formulations carry over directly.
The Memory Effect
Section titled “The Memory Effect”The defining feature of fractional derivatives is non-locality in time. The integral in the Caputo definition means the derivative at time depends on the entire history for . This “long memory” effect distinguishes FDEs from both ODEs (no memory) and DDEs (finite memory at discrete delay points).
Physically, this models:
- Viscoelastic materials: stress depends on the entire strain history
- Anomalous diffusion: particles with power-law waiting times
- Fractional relaxation: decay slower than exponential
The Mittag-Leffler Function
Section titled “The Mittag-Leffler Function”The Mittag-Leffler function generalizes the exponential and is the natural solution to fractional relaxation equations:
When , this reduces to . For , the Mittag-Leffler function decays as a power law for large arguments:
This is slower than exponential decay, capturing the “long tail” behavior of fractional
systems. Numra provides mittag_leffler_1(z, alpha) for numerical evaluation.
The FdeSystem Trait
Section titled “The FdeSystem Trait”Every FDE in Numra implements the FdeSystem trait:
pub trait FdeSystem<S: Scalar> { /// Dimension of the state space. fn dim(&self) -> usize;
/// Fractional order alpha in (0, 1]. fn alpha(&self) -> S;
/// Evaluate the right-hand side f(t, y). fn rhs(&self, t: S, y: &[S], f: &mut [S]);
/// Check if the order is valid (0 < alpha <= 1). fn is_valid_order(&self) -> bool { ... }}The FDE solved is:
The L1 Scheme Solver
Section titled “The L1 Scheme Solver”Numra uses the L1 scheme to discretize the Caputo derivative. The L1 approximation is:
where the weights encode the memory kernel.
Convergence
Section titled “Convergence”The L1 scheme has convergence order :
| Convergence Order | Effective for | |
|---|---|---|
| 0.3 | Error ~ | |
| 0.5 | Error ~ | |
| 0.8 | Error ~ | |
| 1.0 | Error ~ (reduces to Euler) |
As , the scheme becomes more accurate; as , it approaches first-order Euler accuracy.
Computational Cost
Section titled “Computational Cost”Because of the memory effect, the L1 scheme at step must access all previous solution values . This makes the total cost for time steps, compared to for standard ODE methods. The memory requirement is also to store the full solution history.
Solver Options
Section titled “Solver Options”use numra_fde::FdeOptions;
let opts = FdeOptions::default() .dt(0.01) // Time step .max_steps(100_000) // Maximum number of steps .tol(1e-10); // Tolerance for fixed-point iteration| Option | Default | Description |
|---|---|---|
dt | 0.01 | Time step size |
max_steps | 100,000 | Maximum number of steps |
tol | 1e-10 | Convergence tolerance for implicit iteration |
max_iter | 100 | Maximum fixed-point iterations per step |
Example: Fractional Relaxation
Section titled “Example: Fractional Relaxation”The fractional relaxation equation generalizes exponential decay:
The exact solution is , where is the Mittag-Leffler function.
use numra_fde::{FdeSystem, L1Solver, FdeSolver, FdeOptions};
struct FractionalRelaxation { lambda: f64, alpha: f64,}
impl FdeSystem<f64> for FractionalRelaxation { fn dim(&self) -> usize { 1 } fn alpha(&self) -> f64 { self.alpha } fn rhs(&self, _t: f64, y: &[f64], f: &mut [f64]) { f[0] = -self.lambda * y[0]; }}
// Half-order derivative: slower than exponential decaylet system = FractionalRelaxation { lambda: 1.0, alpha: 0.5 };let opts = FdeOptions::default().dt(0.01);
let result = L1Solver::solve(&system, 0.0, 5.0, &[1.0], &opts) .expect("Solve failed");
// Compare numerical and exact solutionsuse numra_fde::mittag_leffler_1;for (i, &t) in result.t.iter().enumerate().step_by(50) { let y_numerical = result.y_at(i)[0]; let y_exact = mittag_leffler_1(-t.powf(0.5), 0.5); println!("t = {:.2}: numerical = {:.6}, exact = {:.6}", t, y_numerical, y_exact);}Comparing Different Fractional Orders
Section titled “Comparing Different Fractional Orders”The fractional order controls how fast the system “forgets”:
use numra_fde::{FdeSystem, L1Solver, FdeSolver, FdeOptions};
struct Relaxation { alpha: f64 }
impl FdeSystem<f64> for Relaxation { fn dim(&self) -> usize { 1 } fn alpha(&self) -> f64 { self.alpha } fn rhs(&self, _t: f64, y: &[f64], f: &mut [f64]) { f[0] = -y[0]; }}
let opts = FdeOptions::default().dt(0.01);
// Compare alpha = 0.3, 0.5, 0.8, 1.0for &alpha in &[0.3, 0.5, 0.8, 1.0] { let system = Relaxation { alpha }; let result = L1Solver::solve(&system, 0.0, 5.0, &[1.0], &opts) .expect("Solve failed");
let y_final = result.y_final().unwrap()[0]; println!("alpha = {:.1}: y(5) = {:.6}", alpha, y_final);}// Output shows: smaller alpha => slower decay (longer memory)Example: Fractional Viscoelasticity
Section titled “Example: Fractional Viscoelasticity”A fractional Maxwell model for viscoelastic stress relaxation:
For a step strain , this simplifies to:
use numra_fde::{FdeSystem, L1Solver, FdeSolver, FdeOptions};
struct FractionalMaxwell { modulus: f64, // E: elastic modulus viscosity: f64, // eta: viscosity alpha: f64, // fractional order}
impl FdeSystem<f64> for FractionalMaxwell { fn dim(&self) -> usize { 1 } fn alpha(&self) -> f64 { self.alpha } fn rhs(&self, _t: f64, y: &[f64], f: &mut [f64]) { f[0] = -(self.modulus / self.viscosity) * y[0]; }}
let model = FractionalMaxwell { modulus: 1.0, viscosity: 10.0, alpha: 0.7, // Sub-exponential relaxation};
let sigma_0 = 1.0; // Initial stress = E * epsilon_0let opts = FdeOptions::default().dt(0.01);
let result = L1Solver::solve(&model, 0.0, 50.0, &[sigma_0], &opts) .expect("Solve failed");
// Stress relaxation follows Mittag-Leffler decayfor (t, y) in result.iter().step_by(100) { println!("t = {:.1}, stress = {:.6}", t, y[0]);}Multi-Dimensional Systems
Section titled “Multi-Dimensional Systems”FDEs with multiple components sharing the same fractional order:
use numra_fde::{FdeSystem, L1Solver, FdeSolver, FdeOptions};
struct TwoDFractional;
impl FdeSystem<f64> for TwoDFractional { fn dim(&self) -> usize { 2 } fn alpha(&self) -> f64 { 0.8 } fn rhs(&self, _t: f64, y: &[f64], f: &mut [f64]) { f[0] = -y[0] + 0.5 * y[1]; f[1] = -2.0 * y[1]; }}
let opts = FdeOptions::default().dt(0.01);let result = L1Solver::solve(&TwoDFractional, 0.0, 5.0, &[1.0, 1.0], &opts) .expect("Solve failed");
let y_final = result.y_final().unwrap();// Component 2 decays faster (larger coefficient)assert!(y_final[0] > y_final[1]);Working with Results
Section titled “Working with Results”FdeResult uses the standard Numra result format:
let result = L1Solver::solve(&system, t0, tf, &y0, &opts)?;
// Final statelet y_final = result.y_final().unwrap();
// Access at indexlet y_i = result.y_at(50);
// Time pointslet t = &result.t;
// Statisticsprintln!("RHS evaluations: {}", result.stats.n_rhs);println!("Steps taken: {}", result.stats.n_steps);Practical Considerations
Section titled “Practical Considerations”Choosing the Time Step
Section titled “Choosing the Time Step”Unlike adaptive ODE solvers, the L1 scheme uses a fixed time step. Guidelines:
- is a good starting point for most problems
- Reduce for higher accuracy (convergence is )
- For close to 0, even coarse steps can be accurate
- For close to 1, you may need finer steps
Long-Time Integration
Section titled “Long-Time Integration”Because of cost, very long integrations can become slow. Strategies:
- Use the coarsest that meets accuracy requirements
- For problems where only the final state matters, reduce
max_stepsaccordingly - Future versions may support fast summation techniques () for the history weights
Validating Fractional Order
Section titled “Validating Fractional Order”The solver rejects orders outside :
// This will return an errorlet system = MySystem { alpha: 1.5 }; // Invalid: alpha > 1let result = L1Solver::solve(&system, 0.0, 1.0, &[1.0], &opts);assert!(result.is_err());Relationship to Other DE Types
Section titled “Relationship to Other DE Types”- When : FDE reduces to an ODE (use
numra-odefor better performance) - When : True fractional dynamics with power-law memory
- FDEs with power-law kernels are closely related to Volterra integro-differential equations (see the IDE chapter)