Skip to content

Fuzzy ODE Solver Quick Start Guide

Overview

FuzzyODESolver solves ordinary differential equations (ODEs) with fuzzy initial conditions and/or fuzzy parameters, computing solutions as α-level sets that form fuzzy envelopes over time.

Key Concept:

dy/dt = f(t, y, parameters)

Where:
- y₀ can be fuzzy (e.g., "around 10")
- parameters can be fuzzy (e.g., "growth rate ≈ 0.5")
- Solution y(t) is fuzzy at each time point

Applications: - Modeling uncertainty in initial conditions - Systems with imprecise parameters - Epidemiological models with uncertain rates - Population dynamics with fuzzy carrying capacity - Any ODE with incomplete/imprecise information

Advantages: - Quantify uncertainty propagation - Multiple scenarios in single solution - α-level interpretation (confidence intervals) - No need for probabilistic assumptions


1. Basic Concepts

What is a Fuzzy ODE?

A fuzzy ODE is a differential equation where: 1. Initial conditions are fuzzy numbers (not single values) 2. Parameters can be fuzzy 3. Solution is a fuzzy-valued function

Example:

Regular ODE:  dy/dt = 0.5y(1 - y/100),  y(0) = 10
Fuzzy ODE:    dy/dt = r·y(1 - y/K),     y₀ ≈ 10 ± 2, r ≈ 0.5 ± 0.05

Fuzzy Numbers

Fuzzy numbers represent imprecise values using membership functions:

from fuzzy_systems.dynamics.fuzzy_ode import FuzzyNumber

# Triangular: "approximately 10, spread 2"
num1 = FuzzyNumber.triangular(center=10, spread=2)
# Support: [8, 12], peak at 10

# Gaussian: "around 50, sigma 5"
num2 = FuzzyNumber.gaussian(mean=50, sigma=5)

# Trapezoidal: "between 20-30, certainty 23-27"
num3 = FuzzyNumber.trapezoidal(a=20, b=23, c=27, d=30)

α-Cuts (Alpha Levels)

An α-cut extracts an interval from a fuzzy number at confidence level α:

α = 1.0 → Core (most likely values)
α = 0.5 → Medium confidence
α = 0.0 → Support (all possible values)

The solver computes α-cuts for multiple levels, creating fuzzy envelopes.


2. Creating Fuzzy Numbers

Triangular Fuzzy Numbers

from fuzzy_systems.dynamics.fuzzy_ode import FuzzyNumber

# Symmetric triangular
y0 = FuzzyNumber.triangular(center=10, spread=2)
# Represents: "around 10, ±2"
# Support: [8, 12], peak at 10

# Check membership
print(y0.membership(10))    # 1.0 (core)
print(y0.membership(9))     # 0.5
print(y0.membership(8))     # 0.0 (boundary)

# Extract α-cut
interval = y0.alpha_cut(alpha=0.5)
print(interval)  # (9.0, 11.0)

Gaussian Fuzzy Numbers

# Gaussian: smoother, no hard boundaries
param = FuzzyNumber.gaussian(
    mean=0.5,
    sigma=0.1,
    n_sigmas=3  # Support extends 3 sigmas
)
# Support: [0.2, 0.8] (3σ from mean)

Trapezoidal Fuzzy Numbers

# Trapezoidal: plateau in the middle
capacity = FuzzyNumber.trapezoidal(
    a=80,   # Left boundary
    b=90,   # Start of plateau
    c=110,  # End of plateau
    d=120   # Right boundary
)
# Core (α=1.0): [90, 110]
# Support (α=0.0): [80, 120]

3. Defining the ODE Function

The ODE function must accept (t, y, **params):

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

    Args:
        t: Time (scalar)
        y: State vector (array)
        r: Parameter 1
        K: Parameter 2

    Returns:
        dy/dt (array, same shape as y)
    """
    return r * y * (1 - y / K)

Important: - Signature must be (t, y, **params) - y is always an array (even for 1D systems) - Return array with same shape as y - Parameters passed via **params (unpacked dict)


4. Creating the Solver

Basic Setup

from fuzzy_systems.dynamics.fuzzy_ode import FuzzyODESolver, FuzzyNumber

# Define ODE
def logistic(t, y, r, K):
    return r * y * (1 - y / K)

# Create fuzzy initial condition
y0_fuzzy = FuzzyNumber.triangular(center=10, spread=2)

# Create fuzzy parameter
r_fuzzy = FuzzyNumber.triangular(center=0.5, spread=0.05)

# Create solver
solver = FuzzyODESolver(
    ode_func=logistic,
    t_span=(0, 50),
    initial_condition=[y0_fuzzy],       # List of FuzzyNumber or float
    params={'r': r_fuzzy, 'K': 100},    # Dict with FuzzyNumber or float
    n_alpha_cuts=11,                    # Number of α-levels (2-50)
    method='RK45',                      # Integration method
    var_names=['population']            # Variable names (optional)
)

Solver Parameters

  • ode_func: ODE function with signature (t, y, **params)
  • t_span: Time interval (t_start, t_end)
  • initial_condition: List of initial values (FuzzyNumber or float)
  • params: Dict of parameters (FuzzyNumber or float)
  • n_alpha_cuts: Number of α-levels (default: 11)
  • More = smoother envelopes, slower computation
  • Typical range: 5-25
  • method: Integration method (default: 'RK45')
  • Options: 'RK45', 'RK23', 'DOP853', 'Radau', 'BDF', 'LSODA'
  • RK45 = good default (Runge-Kutta 4th/5th order)
  • t_eval: Time points for evaluation (optional)
  • If None, uses 100 points uniformly spaced
  • n_jobs: Parallel workers (default: -1 = all cores)
  • rtol, atol: Numerical tolerances (default: 1e-6, 1e-9)
  • var_names: List of variable names for plots

5. Solving Fuzzy ODEs

Method 1: Standard (Default)

# Solve with standard method
solution = solver.solve(
    method='standard',
    n_grid_points=20,  # Points per dimension per α-level
    verbose=True
)

Characteristics: - Most accurate - Slowest (explores full grid) - Best for low-dimensional problems (1-3 variables) - Grid size: n_grid_points^(n_vars + n_fuzzy_params) per α

# Solve with Monte Carlo sampling
solution = solver.solve(
    method='monte_carlo',
    n_samples=1000,     # Number of random samples
    random_seed=42,     # For reproducibility
    verbose=True
)

Characteristics: - Fastest for high-dimensional problems - Scalable (10-400x faster in high dimensions) - Stochastic (results vary slightly) - Best for: many fuzzy parameters, multi-variable systems

Method 3: Hierarchical

# Solve with hierarchical optimization
solution = solver.solve(
    method='hierarchical',
    verbose=True
)

Characteristics: - 3-5x faster than standard - Deterministic (same results every time) - Good middle ground - Reuses computations from higher α-levels


6. Working with Solutions

Solution Object

# Solve
solution = solver.solve(method='monte_carlo')

# Access attributes
print(solution.t)            # Time points
print(solution.y_min.shape)  # (n_alpha, n_vars, n_time)
print(solution.y_max.shape)  # (n_alpha, n_vars, n_time)
print(solution.alphas)       # α-levels used
print(solution.var_names)    # Variable names

Get Specific α-Level

# Extract envelope for specific α
y_min_05, y_max_05 = solution.get_alpha_level(alpha=0.5)
# y_min_05.shape: (n_vars, n_time)
# y_max_05.shape: (n_vars, n_time)

# Plot specific α-level
import matplotlib.pyplot as plt

plt.figure(figsize=(10, 6))
plt.fill_between(solution.t, y_min_05[0], y_max_05[0],
                 alpha=0.3, label='α=0.5')
plt.xlabel('Time')
plt.ylabel('Population')
plt.legend()
plt.show()

7. Visualization

Plot Fuzzy Solution

# Plot with all α-levels
solution.plot(
    var_idx=0,                # Variable index (0 for first)
    alpha_levels=None,        # None = all, or list like [0.0, 0.5, 1.0]
    show=True
)

Custom Multi-Variable Plot

import matplotlib.pyplot as plt

# For multi-variable system
fig, axes = plt.subplots(1, 2, figsize=(14, 6))

for var_idx in range(2):
    ax = axes[var_idx]

    # Plot each α-level
    cmap = plt.cm.Blues
    for i, alpha in enumerate(solution.alphas):
        y_min, y_max = solution.get_alpha_level(alpha)
        color = cmap(0.3 + 0.7 * alpha)

        ax.fill_between(
            solution.t,
            y_min[var_idx],
            y_max[var_idx],
            alpha=0.3,
            color=color,
            label=f'α={alpha:.1f}' if i % 3 == 0 else None
        )

    ax.set_xlabel('Time', fontsize=12)
    ax.set_ylabel(solution.var_names[var_idx], fontsize=12)
    ax.set_title(f'{solution.var_names[var_idx]} Evolution')
    ax.legend()
    ax.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

8. Exporting Results

Export to CSV

# Export specific α-level to CSV
solution.to_csv('fuzzy_solution.csv', alpha=0.5)

# Export with European format
solution.to_csv('solution.csv', alpha=1.0, sep=';', decimal=',')

CSV Format:

time,population_min,population_max
0.000000,8.000000,12.000000
0.500000,8.234100,12.765900
1.000000,8.521234,13.478766
...

Export to DataFrame

# Convert to pandas DataFrame
df = solution.to_dataframe(alpha=0.5)
print(df.head())

# Access metadata
print(df.attrs['alpha_level'])
print(df.attrs['n_alpha_levels'])
print(df.attrs['var_names'])

# Further processing
df['y_mean'] = (df['y0_min'] + df['y0_max']) / 2
df['y_width'] = df['y0_max'] - df['y0_min']

9. Complete Example: Logistic Growth

import numpy as np
import matplotlib.pyplot as plt
from fuzzy_systems.dynamics.fuzzy_ode import FuzzyODESolver, FuzzyNumber

# ============================================================================
# Define ODE: Logistic growth model
# ============================================================================
def logistic(t, y, r, K):
    """dy/dt = r * y * (1 - y/K)"""
    return r * y * (1 - y / K)

# ============================================================================
# Scenario A: Fuzzy Initial Condition Only
# ============================================================================
y0_fuzzy = FuzzyNumber.triangular(center=10, spread=4)

solver_a = FuzzyODESolver(
    ode_func=logistic,
    t_span=(0, 50),
    initial_condition=[y0_fuzzy],
    params={'r': 0.2, 'K': 100},
    n_alpha_cuts=15,
    var_names=['population']
)

sol_a = solver_a.solve(method='monte_carlo', n_samples=1000, verbose=True)
sol_a.plot()
plt.title('Scenario A: Fuzzy Initial Condition')

# ============================================================================
# Scenario B: Fuzzy Carrying Capacity
# ============================================================================
K_fuzzy = FuzzyNumber.triangular(center=100, spread=20)

solver_b = FuzzyODESolver(
    ode_func=logistic,
    t_span=(0, 50),
    initial_condition=[10.0],  # Crisp y0
    params={'r': 0.2, 'K': K_fuzzy},
    n_alpha_cuts=15,
    var_names=['population']
)

sol_b = solver_b.solve(method='monte_carlo', verbose=True)
sol_b.plot()
plt.title('Scenario B: Fuzzy Carrying Capacity')

# ============================================================================
# Scenario C: Fuzzy Growth Rate
# ============================================================================
r_fuzzy = FuzzyNumber.triangular(center=0.2, spread=0.04)

solver_c = FuzzyODESolver(
    ode_func=logistic,
    t_span=(0, 50),
    initial_condition=[10.0],
    params={'r': r_fuzzy, 'K': 100},
    n_alpha_cuts=15,
    var_names=['population']
)

sol_c = solver_c.solve(method='monte_carlo', verbose=True)
sol_c.plot()
plt.title('Scenario C: Fuzzy Growth Rate')

# ============================================================================
# Scenario D: Everything Fuzzy
# ============================================================================
y0_fuzzy_d = FuzzyNumber.triangular(center=10, spread=4)
r_fuzzy_d = FuzzyNumber.triangular(center=0.2, spread=0.04)
K_fuzzy_d = FuzzyNumber.triangular(center=100, spread=20)

solver_d = FuzzyODESolver(
    ode_func=logistic,
    t_span=(0, 50),
    initial_condition=[y0_fuzzy_d],
    params={'r': r_fuzzy_d, 'K': K_fuzzy_d},
    n_alpha_cuts=15,
    var_names=['population']
)

sol_d = solver_d.solve(method='monte_carlo', n_samples=2000, verbose=True)
sol_d.plot()
plt.title('Scenario D: All Parameters Fuzzy')

# ============================================================================
# Compare All Scenarios
# ============================================================================
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
axes = axes.flatten()

scenarios = [
    (sol_a, 'Scenario A: y₀ Fuzzy'),
    (sol_b, 'Scenario B: K Fuzzy'),
    (sol_c, 'Scenario C: r Fuzzy'),
    (sol_d, 'Scenario D: All Fuzzy')
]

for ax, (sol, title) in zip(axes, scenarios):
    cmap = plt.cm.Blues
    for i, alpha in enumerate(sol.alphas):
        y_min, y_max = sol.get_alpha_level(alpha)
        color = cmap(0.3 + 0.7 * alpha)
        ax.fill_between(sol.t, y_min[0], y_max[0],
                       alpha=0.3, color=color)

    ax.set_xlabel('Time', fontsize=11)
    ax.set_ylabel('Population', fontsize=11)
    ax.set_title(title, fontweight='bold')
    ax.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

# Export results
sol_d.to_csv('logistic_fuzzy_all.csv', alpha=0.5)

10. Complete Example: SIR Epidemic Model

import numpy as np
import matplotlib.pyplot as plt
from fuzzy_systems.dynamics.fuzzy_ode import FuzzyODESolver, FuzzyNumber

# ============================================================================
# SIR Model with Fuzzy Parameters
# ============================================================================
def sir_model(t, y, beta, gamma):
    """
    SIR epidemic model

    S: Susceptible
    I: Infected
    R: Recovered

    dS/dt = -beta * S * I
    dI/dt = beta * S * I - gamma * I
    dR/dt = gamma * I
    """
    S, I, R = y
    N = S + I + R  # Total population (constant)

    dS = -beta * S * I / N
    dI = beta * S * I / N - gamma * I
    dR = gamma * I

    return np.array([dS, dI, dR])

# ============================================================================
# Setup with Fuzzy Transmission Rate
# ============================================================================
# Initial conditions (crisp)
S0 = 990.0
I0 = 10.0
R0 = 0.0

# Fuzzy transmission rate: "beta around 0.5 ± 0.1"
beta_fuzzy = FuzzyNumber.triangular(center=0.5, spread=0.1)

# Recovery rate (crisp): 1/14 (14-day recovery)
gamma_crisp = 1.0 / 14.0

# Create solver
solver = FuzzyODESolver(
    ode_func=sir_model,
    t_span=(0, 160),
    initial_condition=[S0, I0, R0],
    params={'beta': beta_fuzzy, 'gamma': gamma_crisp},
    n_alpha_cuts=11,
    var_names=['Susceptible', 'Infected', 'Recovered']
)

# Solve
solution = solver.solve(method='monte_carlo', n_samples=1500, verbose=True)

# ============================================================================
# Visualize All Compartments
# ============================================================================
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
compartments = ['Susceptible', 'Infected', 'Recovered']
colors_map = [plt.cm.Reds, plt.cm.Oranges, plt.cm.Greens]

for idx, (ax, name, cmap) in enumerate(zip(axes, compartments, colors_map)):
    for i, alpha in enumerate(solution.alphas):
        y_min, y_max = solution.get_alpha_level(alpha)
        color = cmap(0.3 + 0.7 * alpha)
        ax.fill_between(
            solution.t,
            y_min[idx],
            y_max[idx],
            alpha=0.4,
            color=color,
            label=f'α={alpha:.1f}' if i % 3 == 0 else None
        )

    ax.set_xlabel('Days', fontsize=12)
    ax.set_ylabel('Population', fontsize=12)
    ax.set_title(f'{name} Population', fontweight='bold', fontsize=13)
    ax.legend(loc='best')
    ax.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

# ============================================================================
# Export Peak Infection Time (α=0.5)
# ============================================================================
y_min_05, y_max_05 = solution.get_alpha_level(alpha=0.5)
I_envelope = (y_min_05[1] + y_max_05[1]) / 2
peak_time = solution.t[np.argmax(I_envelope)]
peak_value = np.max(I_envelope)

print(f"\nPeak Infection (α=0.5):")
print(f"  Time: {peak_time:.1f} days")
print(f"  Value: {peak_value:.0f} infected")

# Export
solution.to_csv('sir_fuzzy_beta.csv', alpha=0.5)

11. Tips and Best Practices

Choosing the Right Method

Method Best For Speed Accuracy Stochastic
Standard 1-2 variables, few fuzzy params Slow Highest No
Monte Carlo High dimensions, many fuzzy params Fast Good Yes
Hierarchical Medium problems, deterministic Medium High No

Recommendations: - 1-2 fuzzy inputs → standard (n_grid_points=20) - 3+ fuzzy inputs → monte_carlo (n_samples=1000-5000) - Need deterministic → hierarchical

Choosing n_alpha_cuts

# Coarse (fast, less smooth)
n_alpha_cuts=5   # [0.0, 0.25, 0.5, 0.75, 1.0]

# Standard (good balance)
n_alpha_cuts=11  # [0.0, 0.1, ..., 0.9, 1.0]

# Fine (smooth, slower)
n_alpha_cuts=25  # [0.0, 0.04, ..., 0.96, 1.0]

Choosing Fuzzy Number Types

Triangular: - Simple, easy to interpret - Use when: "approximately X, ±Y" - Example: FuzzyNumber.triangular(center=10, spread=2)

Gaussian: - Smooth, no hard boundaries - Use when: uncertainty is continuous - Example: FuzzyNumber.gaussian(mean=10, sigma=2)

Trapezoidal: - Plateau of maximum certainty - Use when: "between X and Y, most likely Z to W" - Example: FuzzyNumber.trapezoidal(a=8, b=9, c=11, d=12)

Performance Optimization

# Fast exploration (low accuracy)
sol = solver.solve(method='monte_carlo', n_samples=500)

# Standard (good balance)
sol = solver.solve(method='monte_carlo', n_samples=2000)

# High accuracy (slower)
sol = solver.solve(method='monte_carlo', n_samples=10000)

# Use parallel processing (default)
solver = FuzzyODESolver(..., n_jobs=-1)  # All cores

Numerical Stability

# Stiff systems: use appropriate solver
solver = FuzzyODESolver(
    ...,
    method='BDF',  # Better for stiff ODEs
    rtol=1e-5,
    atol=1e-8
)

# Non-stiff, high accuracy
solver = FuzzyODESolver(
    ...,
    method='DOP853',  # 8th order Runge-Kutta
    rtol=1e-8,
    atol=1e-10
)

12. Common Patterns

Pattern 1: Uncertain Initial Conditions

# Measurement uncertainty in starting population
y0 = FuzzyNumber.gaussian(mean=100, sigma=10)

solver = FuzzyODESolver(
    ode_func=my_ode,
    t_span=(0, 50),
    initial_condition=[y0],
    params={'rate': 0.1}  # Known parameter
)

Pattern 2: Parameter Uncertainty

# Unknown growth rate, measured as "around 0.5"
rate = FuzzyNumber.triangular(center=0.5, spread=0.1)

solver = FuzzyODESolver(
    ode_func=my_ode,
    t_span=(0, 50),
    initial_condition=[100.0],  # Known initial condition
    params={'rate': rate}
)

Pattern 3: Multiple Uncertainties

# Both initial condition and parameters are fuzzy
y0 = FuzzyNumber.triangular(center=100, spread=20)
rate = FuzzyNumber.triangular(center=0.5, spread=0.1)
capacity = FuzzyNumber.gaussian(mean=1000, sigma=100)

solver = FuzzyODESolver(
    ode_func=logistic,
    t_span=(0, 50),
    initial_condition=[y0],
    params={'r': rate, 'K': capacity}
)

13. Common Issues and Solutions

Problem Cause Solution
Very wide envelopes Too much uncertainty Reduce fuzzy spreads, check α-cuts
Solution doesn't match crisp Numerical issues Increase n_samples (MC) or n_grid_points (standard)
Slow computation High dimensions Use method='monte_carlo' with fewer samples
Envelopes cross α-levels not monotonic Check fuzzy number definitions
Memory error Too many grid points Reduce n_grid_points or use Monte Carlo
NaN values ODE solver failed Check ODE function, adjust rtol/atol

Debugging Tips

# Test ODE function with crisp values
y_test = np.array([10.0])
dydt = my_ode(0, y_test, r=0.5, K=100)
print(f"dy/dt at t=0: {dydt}")

# Test fuzzy number
y0 = FuzzyNumber.triangular(center=10, spread=2)
print(f"Support: {y0.support}")
print(f"α=0.5 cut: {y0.alpha_cut(0.5)}")
print(f"Membership at 10: {y0.membership(10)}")

# Solve with verbose
sol = solver.solve(method='monte_carlo', verbose=True)
# Shows progress and statistics

14. Advanced: Multi-Variable Systems

# Lotka-Volterra Predator-Prey with fuzzy parameters
def lotka_volterra(t, y, alpha, beta, delta, gamma):
    """
    Predator-prey model
    x: prey, y: predator
    """
    x, y_pred = y
    dx = alpha * x - beta * x * y_pred
    dy = delta * x * y_pred - gamma * y_pred
    return np.array([dx, dy])

# Fuzzy parameters
alpha_fuzzy = FuzzyNumber.triangular(center=1.0, spread=0.1)
beta_fuzzy = FuzzyNumber.triangular(center=0.1, spread=0.02)
delta_fuzzy = FuzzyNumber.triangular(center=0.075, spread=0.015)
gamma_fuzzy = FuzzyNumber.triangular(center=1.5, spread=0.15)

# Fuzzy initial conditions
x0_fuzzy = FuzzyNumber.triangular(center=40, spread=5)
y0_fuzzy = FuzzyNumber.triangular(center=9, spread=2)

solver = FuzzyODESolver(
    ode_func=lotka_volterra,
    t_span=(0, 50),
    initial_condition=[x0_fuzzy, y0_fuzzy],
    params={
        'alpha': alpha_fuzzy,
        'beta': beta_fuzzy,
        'delta': delta_fuzzy,
        'gamma': gamma_fuzzy
    },
    n_alpha_cuts=11,
    var_names=['Prey', 'Predator']
)

# Use Monte Carlo for high-dimensional problem
solution = solver.solve(method='monte_carlo', n_samples=5000, verbose=True)

# Plot both variables
fig, axes = plt.subplots(1, 2, figsize=(14, 6))

for idx, name in enumerate(['Prey', 'Predator']):
    solution.plot(var_idx=idx, ax=axes[idx], show=False)
    axes[idx].set_title(f'{name} Population')

plt.tight_layout()
plt.show()

15. Comparison with Other Approaches

Fuzzy ODE vs Monte Carlo Simulation

Aspect Fuzzy ODE Monte Carlo
Uncertainty Possibilistic Probabilistic
Input Fuzzy numbers Probability distributions
Output α-level envelopes Confidence intervals
Assumptions None (membership) Statistical (distribution)
Interpretation Possibility Probability

When to Use Fuzzy ODE

Use Fuzzy ODE when: - Data is imprecise, not random - Expert knowledge is qualitative ("around", "approximately") - No probability distributions available - Want worst-case/best-case scenarios

Use Monte Carlo when: - Have probability distributions - Data is from random processes - Need statistical confidence intervals


References

  • Barros, L. C., Bassanezi, R. C., & Lodwick, W. A. (2017). A First Course in Fuzzy Logic, Fuzzy Dynamical Systems, and Biomathematics. Springer.
  • Kaleva, O. (1987). "Fuzzy differential equations." Fuzzy Sets and Systems, 24(3), 301-317.
  • Bede, B., & Gal, S. G. (2005). "Generalizations of the differentiability of fuzzy-number-valued functions with applications to fuzzy differential equations." Fuzzy Sets and Systems, 151(3), 581-599.