py4u blog

SciPy - Integration of a Differential Equation for Curve Fit

Many real-world systems—from population growth in biology to chemical reactions in chemistry, and from electrical circuits in engineering to heat transfer in physics—are governed by differential equations (DEs). These equations describe how a quantity changes over time or space. However, experimental data often only provides measurements of the system’s state at discrete points (e.g., concentration at specific times, temperature at specific positions). To connect these observations to the underlying DE model, we need to fit the DE’s parameters such that the model’s predictions align with the data.

This blog post explores how to use SciPy, a Python library for scientific computing, to integrate a differential equation and fit its parameters to experimental data. We will cover core concepts, step-by-step implementation, common challenges, and best practices to ensure robust and accurate results.

2026-07

Table of Contents#

  1. Background: Differential Equations and Curve Fitting
  2. SciPy Tools for ODE Integration and Curve Fitting
  3. Step-by-Step Example: Fitting a Logistic Growth Model
  4. Common Challenges and Solutions
  5. Best Practices
  6. Conclusion
  7. References

Background: Differential Equations and Curve Fitting#

What Are Differential Equations?#

A differential equation (DE) relates a function to its derivatives. For dynamic systems, we often encounter ordinary differential equations (ODEs), which involve derivatives with respect to a single independent variable (e.g., time ( t )).

Example: The logistic growth equation, which models population growth with a carrying capacity ( K ):
[ \frac{dy}{dt} = r \cdot y \left(1 - \frac{y}{K}\right) ]
Here, ( y(t) ) is the population at time ( t ), ( r ) is the growth rate, and ( K ) is the maximum sustainable population (carrying capacity).

Curve Fitting for ODEs#

Curve fitting aims to find model parameters (e.g., ( r ) and ( K ) in the logistic equation) that minimize the difference between the model’s predictions and experimental data. For ODE-based models, this requires:

  1. Integrating the ODE over the domain of the data (e.g., time points) to generate model predictions.
  2. Optimizing parameters to minimize the residual error between predictions and data.

SciPy Tools for ODE Integration and Curve Fitting#

SciPy provides two key tools for this workflow:

The scipy.integrate.solve_ivp function numerically integrates ODEs. It supports multiple solvers (e.g., RK45 for non-stiff problems, Radau for stiff problems) and is more flexible than the legacy odeint function.

Key Features:

  • Handles initial value problems (IVPs) of the form ( \frac{dy}{dt} = f(t, y, \theta) ), where ( \theta ) are parameters.
  • Allows specifying the time points at which to evaluate the solution (t_eval).
  • Supports event detection and stiff/non-stiff solvers.

2. Curve Fitting: curve_fit#

The scipy.optimize.curve_fit function fits a model to data using non-linear least squares. It minimizes the sum of squared residuals between the model’s predictions and the observed data.

Key Features:

  • Takes a model function, independent variable data, and dependent variable data.
  • Returns optimized parameters and their covariance matrix (for uncertainty estimates).

Step-by-Step Example: Fitting a Logistic Growth Model#

Let’s walk through fitting the logistic growth ODE to synthetic experimental data. We will:

  1. Generate noisy synthetic data from a known logistic model.
  2. Define the ODE and a wrapper function to integrate it.
  3. Use curve_fit to estimate the parameters ( r ) (growth rate) and ( K ) (carrying capacity).

Step 1: Import Libraries#

import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import solve_ivp
from scipy.optimize import curve_fit

Step 2: Define the ODE#

The logistic ODE is ( \frac{dy}{dt} = r \cdot y \left(1 - \frac{y}{K}\right) ). We define it as a function with signature f(t, y, r, K) (required by solve_ivp):

def logistic_ode(t, y, r, K):
    """Logistic ODE: dy/dt = r*y*(1 - y/K)"""
    return r * y * (1 - y / K)

Step 3: Define the Model Function for Curve Fitting#

To use curve_fit, we need a model function that:

  • Takes the independent variable (time ( t )) and parameters (( r, K )) as inputs.
  • Integrates the ODE to return predictions ( y(t) ).

We fix the initial condition ( y_0 ) (e.g., initial population) for simplicity (we could also fit ( y_0 ) as a parameter).

def model(t, r, K):
    """Wrapper to integrate the logistic ODE and return y(t)"""
    y0 = [y0_true]  # Initial condition (fixed here; could be a parameter)
    # Integrate ODE from t[0] to t[-1], evaluate at t_eval=t
    sol = solve_ivp(
        fun=logistic_ode,
        t_span=[t[0], t[-1]],  # Time interval
        y0=y0,                  # Initial condition
        args=(r, K),            # Parameters passed to ODE
        t_eval=t,               # Evaluate solution at input time points
        method='RK45'           # Solver (non-stiff problems)
    )
    return sol.y[0]  # sol.y is shape (n_states, n_times); extract population

Step 4: Generate Synthetic Data#

We generate noisy data using known parameters to test the fitting process:

# True parameters (unknown in real scenarios)
r_true = 0.5       # Growth rate
K_true = 100       # Carrying capacity
y0_true = 10       # Initial population
t = np.linspace(0, 10, 50)  # Time points (0 to 10 units)
 
# Generate noise-free data by integrating the ODE
y_true = model(t, r_true, K_true)
 
# Add Gaussian noise (simulate experimental error)
np.random.seed(42)  # For reproducibility
y_obs = y_true + np.random.normal(loc=0, scale=2, size=len(t))  # Noise: mean=0, std=2

Step 5: Fit Parameters with curve_fit#

We use curve_fit to estimate ( r ) and ( K ). We provide initial guesses for the parameters (p0) to guide the optimization:

# Initial parameter guesses (based on data inspection)
p0 = [0.3, 80]  # [r_guess, K_guess]
 
# Fit the model to data
popt, pcov = curve_fit(
    f=model,       # Model function
    xdata=t,       # Independent variable (time)
    ydata=y_obs,   # Dependent variable (observed population)
    p0=p0          # Initial parameter guesses
)
 
# Extract optimized parameters
r_fit, K_fit = popt
print(f"Optimized parameters: r = {r_fit:.3f}, K = {K_fit:.3f}")
print(f"True parameters:      r = {r_true:.3f}, K = {K_true:.3f}")

Output:

Optimized parameters: r = 0.502, K = 99.873
True parameters:      r = 0.500, K = 100.000

The optimized parameters are very close to the true values!

Step 6: Visualize Results#

Plot the observed data, true model, and fitted model:

plt.figure(figsize=(10, 6))
plt.scatter(t, y_obs, label='Noisy Data', color='red', alpha=0.5)
plt.plot(t, y_true, label='True Model', color='blue', linestyle='--')
plt.plot(t, model(t, r_fit, K_fit), label='Fitted Model', color='green')
plt.xlabel('Time')
plt.ylabel('Population')
plt.legend()
plt.title('Logistic Growth Model Fit')
plt.show()

Expected Plot: The fitted model (green) should closely overlap the true model (blue) and match the noisy data (red points).

Step 7: Uncertainty Estimation#

The covariance matrix pcov from curve_fit gives parameter uncertainties (standard errors):

perr = np.sqrt(np.diag(pcov))  # Standard errors of parameters
print(f"Parameter uncertainties: r = ±{perr[0]:.3f}, K = ±{perr[1]:.3f}")

Output:

Parameter uncertainties: r = ±0.012, K = ±0.891

Common Challenges and Solutions#

1. Stiff ODEs#

Problem: Some ODEs (e.g., chemical reactions with fast and slow processes) are "stiff," causing standard solvers (e.g., RK45) to fail or run slowly.
Solution: Use stiff solvers like 'Radau' or 'BDF' in solve_ivp:

sol = solve_ivp(..., method='Radau')  # For stiff problems

2. Poor Initial Guesses#

Problem: curve_fit may converge to a local minimum if initial parameter guesses are far from the true values.
Solution:

  • Inspect data to guess parameters (e.g., ( K ) is the asymptote of logistic growth).
  • Use bounds to restrict parameters (e.g., ( K > 0 )):
    popt, pcov = curve_fit(..., bounds=([0, 50], [1, 150]))  # r ∈ [0,1], K ∈ [50,150]

3. Overfitting#

Problem: Using too many parameters can lead to overfitting (model fits noise instead of signal).
Solution:

  • Start with simple models; add complexity only if necessary.
  • Validate with residual plots: residuals (data - model) should be randomly distributed.

4. ODE Integration Errors#

Problem: solve_ivp may fail to integrate (e.g., due to unstable ODEs or large step sizes).
Solution:

  • Adjust solver tolerances (rtol, atol) for stricter integration:
    sol = solve_ivp(..., rtol=1e-6, atol=1e-8)  # Tighter tolerances
  • Check for solver warnings (e.g., RuntimeWarning for failed convergence).

Best Practices#

  1. Start Simple: Begin with the simplest ODE model that captures the system’s behavior before adding complexity.
  2. Validate ODE Integration: Test the ODE integrator with known parameters to ensure it works as expected.
  3. Initialize Parameters Carefully: Use domain knowledge or data inspection to choose initial guesses.
  4. Check Residuals: Plot residuals to ensure the model captures the data trend (no patterns = good fit).
  5. Use Stiff Solvers When Needed: Switch to 'Radau' or 'BDF' if the ODE is stiff.
  6. Document Parameters: Clearly define parameters (units, physical meaning) for reproducibility.

Conclusion#

Integrating differential equations and fitting their parameters to data is a powerful technique for modeling dynamic systems. SciPy’s solve_ivp and curve_fit provide a robust workflow to achieve this. By following best practices—such as validating ODE integration, choosing appropriate solvers, and carefully initializing parameters—you can accurately infer model parameters from experimental data.

This approach is widely applicable in science and engineering, enabling insights into processes like population dynamics, chemical kinetics, and thermal transport.

References#

  • SciPy Documentation:
  • Press, W. H., Teukolsky, S. A., Vetterling, W. T., & Flannery, B. P. (2007). Numerical Recipes: The Art of Scientific Computing (3rd ed.). Cambridge University Press.
  • Ermentrout, G. B. (2002). Simulating, Analyzing, and Animating Dynamical Systems: A Guide to XPPAUT for Researchers and Students. SIAM.