Files
optimiz-rs/examples/notebooks/03_optimal_control_tutorial.ipynb
T
Melvin Avarez 79f51e4775 Release v0.2.0: Comprehensive DE, Mathematical Toolkit, Optimal Control
Major Features:
• Comprehensive Differential Evolution with 5 strategies (rand1, best1, currenttobest1, rand2, best2)
• Adaptive jDE algorithm for self-tuning F and CR parameters
• Convergence tracking with history records and early stopping
• Mathematical toolkit module (780 lines): gradient, hessian, jacobian, statistics, linear algebra
• Optimal control framework: HJB solvers, regime switching, jump diffusion, MRSJD
• Sparse optimization: Sparse PCA, Box-Tao decomposition, ADMM, Elastic Net
• Rayon parallelization infrastructure (ready for pure Rust objectives)

Performance:
• 74-88× speedup for DE vs SciPy
• 50-100× speedup overall vs pure Python

Refactoring & Cleanup:
• Removed 5 legacy files (de_refactored.rs, hmm_legacy.rs, hmm_refactored.rs, mcmc_legacy.rs, mcmc_refactored.rs)
• Modular architecture with trait-based design
• Generic implementations (no domain-specific code)
• Updated Python bindings for new DE API
• Fixed ALL compilation warnings (0 errors, 0 warnings)

Documentation:
• Updated README with v0.2.0 features and benchmarks
• Created RELEASE_NOTES_v0.2.0.md (comprehensive changelog)
• New optimal control tutorial notebook (03_optimal_control_tutorial.ipynb)
• Updated API examples in README
• Created test_release.py for release validation

Version Bumps:
• Cargo.toml: 0.1.0 → 0.2.0
• pyproject.toml: 0.1.0 → 0.2.0
• python/__init__.py: 0.1.0 → 0.2.0

Breaking Changes:
• DE API: mutation_factor/crossover_rate → f/cr
• DE API: use_adaptive_jde → adaptive
• DE API: strategy names simplified (e.g., 'rand/1/bin' → 'rand1')
• DE returns: (x, fun) tuple instead of dict-like object

Known Items (Post-Release):
• Mathematical toolkit functions available in Rust but not yet exposed to Python
• MCMC Python wrapper needs API update to match new Rust implementation
• Tutorial notebooks need DE API updates

Tests: 34 Rust tests passing, core Python functionality validated with test_release.py
2025-12-10 18:54:32 +01:00

874 lines
33 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "118782fe",
"metadata": {},
"outputs": [],
"source": [
"# Import required libraries\n",
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"from matplotlib import cm\n",
"from mpl_toolkits.mplot3d import Axes3D\n",
"import seaborn as sns\n",
"\n",
"# Set style\n",
"sns.set_style('whitegrid')\n",
"plt.rcParams['figure.figsize'] = (14, 6)\n",
"plt.rcParams['font.size'] = 11\n",
"\n",
"print(\"✅ Libraries loaded successfully\")\n",
"print(\"\\n📚 This tutorial covers:\")\n",
"print(\" 1. Regime Switching Systems\")\n",
"print(\" 2. Jump Diffusion Processes\")\n",
"print(\" 3. Combined MRSJD Models\")\n",
"print(\" 4. Numerical Methods (Finite Differences, Upwind Schemes)\")\n",
"print(\" 5. Practical Parameter Selection\")"
]
},
{
"cell_type": "markdown",
"id": "dcfec9d8",
"metadata": {},
"source": [
"## 2. Mathematical Background <a id=\"math\"></a>\n",
"\n",
"### Stochastic Differential Equations (SDEs)\n",
"\n",
"A general SDE has the form:\n",
"\n",
"$$\n",
"dX_t = \\mu(X_t)dt + \\sigma(X_t)dW_t\n",
"$$\n",
"\n",
"where:\n",
"- $\\mu(X_t)$ = **drift** (deterministic trend)\n",
"- $\\sigma(X_t)$ = **diffusion** (volatility)\n",
"- $dW_t$ = **Wiener process** increment: $dW_t \\sim \\mathcal{N}(0, dt)$\n",
"\n",
"### Key Properties\n",
"\n",
"**Itô's Lemma** (chain rule for SDEs):\n",
"\n",
"For $Y_t = f(X_t)$:\n",
"\n",
"$$\n",
"dY_t = f'(X_t)dX_t + \\frac{1}{2}f''(X_t)\\sigma^2(X_t)dt\n",
"$$\n",
"\n",
"**Feynman-Kac Formula** (connects PDEs to expectations):\n",
"\n",
"$$\n",
"V(x,t) = \\mathbb{E}_x\\left[ \\int_t^T e^{-\\rho(s-t)} L(X_s)ds + e^{-\\rho(T-t)}\\Phi(X_T) \\right]\n",
"$$\n",
"\n",
"satisfies the PDE:\n",
"\n",
"$$\n",
"\\frac{\\partial V}{\\partial t} + \\mu(x)\\frac{\\partial V}{\\partial x} + \\frac{1}{2}\\sigma^2(x)\\frac{\\partial^2 V}{\\partial x^2} - \\rho V + L(x) = 0\n",
"$$\n",
"\n",
"### Example: Ornstein-Uhlenbeck Process\n",
"\n",
"Mean-reverting process:\n",
"\n",
"$$\n",
"dX_t = \\theta(\\mu - X_t)dt + \\sigma dW_t\n",
"$$\n",
"\n",
"- $\\theta$ = speed of mean reversion\n",
"- $\\mu$ = long-term mean\n",
"- $\\sigma$ = volatility\n",
"\n",
"**Half-life**: $t_{1/2} = \\frac{\\ln 2}{\\theta}$"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b25e1746",
"metadata": {},
"outputs": [],
"source": [
"# Simulate Ornstein-Uhlenbeck process\n",
"def simulate_ou(theta, mu, sigma, x0, T, dt):\n",
" \"\"\"\n",
" Simulate Ornstein-Uhlenbeck process using Euler-Maruyama method\n",
" \n",
" dX_t = θ(μ - X_t)dt + σ dW_t\n",
" \"\"\"\n",
" n_steps = int(T / dt)\n",
" t = np.linspace(0, T, n_steps)\n",
" X = np.zeros(n_steps)\n",
" X[0] = x0\n",
" \n",
" for i in range(1, n_steps):\n",
" dW = np.random.normal(0, np.sqrt(dt))\n",
" X[i] = X[i-1] + theta * (mu - X[i-1]) * dt + sigma * dW\n",
" \n",
" return t, X\n",
"\n",
"# Example: Temperature control\n",
"theta = 0.5 # Mean reversion speed\n",
"mu = 20.0 # Target temperature (°C)\n",
"sigma = 2.0 # Noise level\n",
"x0 = 10.0 # Initial temperature\n",
"T = 10.0 # Time horizon (seconds)\n",
"dt = 0.01 # Time step\n",
"\n",
"t, X = simulate_ou(theta, mu, sigma, x0, T, dt)\n",
"\n",
"# Plot\n",
"fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))\n",
"\n",
"# Trajectory\n",
"ax1.plot(t, X, linewidth=1.5, color='steelblue', label='Temperature')\n",
"ax1.axhline(y=mu, color='red', linestyle='--', label=f'Target μ={mu}')\n",
"ax1.fill_between(t, mu-sigma, mu+sigma, alpha=0.2, color='red', label='±σ band')\n",
"ax1.set_xlabel('Time (s)')\n",
"ax1.set_ylabel('Temperature (°C)')\n",
"ax1.set_title('Ornstein-Uhlenbeck Process (Mean-Reverting System)')\n",
"ax1.legend()\n",
"ax1.grid(alpha=0.3)\n",
"\n",
"# Distribution at equilibrium\n",
"equilibrium_samples = X[len(X)//2:] # Second half (near equilibrium)\n",
"ax2.hist(equilibrium_samples, bins=30, density=True, alpha=0.7, color='steelblue', edgecolor='black')\n",
"\n",
"# Theoretical distribution: N(μ, σ²/(2θ))\n",
"x_range = np.linspace(X.min(), X.max(), 100)\n",
"theoretical_std = sigma / np.sqrt(2 * theta)\n",
"from scipy.stats import norm\n",
"ax2.plot(x_range, norm.pdf(x_range, mu, theoretical_std), \n",
" 'r-', linewidth=2, label=f'Theory: N({mu:.1f}, {theoretical_std:.2f}²)')\n",
"\n",
"ax2.set_xlabel('Temperature (°C)')\n",
"ax2.set_ylabel('Probability Density')\n",
"ax2.set_title('Equilibrium Distribution')\n",
"ax2.legend()\n",
"ax2.grid(alpha=0.3)\n",
"\n",
"plt.tight_layout()\n",
"plt.show()\n",
"\n",
"print(f\"\\n📊 OU Process Analysis:\")\n",
"print(f\" Half-life: {np.log(2)/theta:.2f} seconds\")\n",
"print(f\" Theoretical equilibrium std: {theoretical_std:.2f}\")\n",
"print(f\" Observed equilibrium std: {equilibrium_samples.std():.2f}\")"
]
},
{
"cell_type": "markdown",
"id": "78636a0b",
"metadata": {},
"source": [
"## 3. Regime Switching Systems <a id=\"regime\"></a>\n",
"\n",
"### Motivation\n",
"\n",
"Many real systems exhibit **multiple operating modes** or **regimes**:\n",
"- Weather: sunny ↔ rainy ↔ stormy\n",
"- Manufacturing: normal ↔ maintenance ↔ failure\n",
"- Traffic: free-flow ↔ congested ↔ gridlock\n",
"- Economic activity: expansion ↔ recession\n",
"\n",
"### Continuous-Time Markov Chain\n",
"\n",
"The regime $i_t \\in \\{1, 2, ..., N\\}$ follows a Markov chain with **transition rate matrix** $Q$:\n",
"\n",
"$$\n",
"\\mathbb{P}(i_{t+dt} = j | i_t = i) = \n",
"\\begin{cases}\n",
"q_{ij} dt & \\text{if } i \\neq j \\\\\n",
"1 + q_{ii} dt & \\text{if } i = j\n",
"\\end{cases}\n",
"$$\n",
"\n",
"where $q_{ii} = -\\sum_{j \\neq i} q_{ij}$ (rows sum to zero).\n",
"\n",
"### Coupled HJB System\n",
"\n",
"The value function $V^i(x)$ in regime $i$ satisfies:\n",
"\n",
"$$\n",
"\\rho V^i(x) = \\sup_u \\left[ \\mu^i(x,u) (V^i)'(x) + \\frac{1}{2}(\\sigma^i)^2(x,u) (V^i)''(x) + L^i(x,u) + \\sum_{j \\neq i} q_{ij}[V^j(x) - V^i(x)] \\right]\n",
"$$\n",
"\n",
"Key insight: The term $\\sum_{j \\neq i} q_{ij}[V^j(x) - V^i(x)]$ represents the **expected change in value due to regime switching**.\n",
"\n",
"### Stationary Distribution\n",
"\n",
"The long-run probability of being in each regime solves:\n",
"\n",
"$$\n",
"Q^T \\pi = 0, \\quad \\sum_i \\pi_i = 1\n",
"$$\n",
"\n",
"### Parameter Selection Tips\n",
"\n",
"| Parameter | Typical Range | Effect | How to Choose |\n",
"|-----------|--------------|--------|---------------|\n",
"| $q_{ij}$ | 0.1 - 10.0 | Regime persistence | Higher = faster switching. Set $q_{ij} = 1/\\text{expected duration}$ |\n",
"| $\\mu^i$ | Problem-specific | Drift in regime $i$ | Estimate from data or physics |\n",
"| $\\sigma^i$ | $> 0$ | Volatility in regime $i$ | Measure from observations or experiments |\n",
"\n",
"**Example**: If regime 1 typically lasts 5 time units, set $q_{12} \\approx 0.2$."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "79238099",
"metadata": {},
"outputs": [],
"source": [
"# Simulate regime-switching process\n",
"def simulate_regime_switching(Q, regime_params, x0, T, dt):\n",
" \"\"\"\n",
" Simulate regime-switching stochastic process\n",
" \n",
" Args:\n",
" Q: Transition rate matrix (N x N)\n",
" regime_params: List of (mu, sigma) for each regime\n",
" x0: Initial state\n",
" T: Time horizon\n",
" dt: Time step\n",
" \"\"\"\n",
" n_steps = int(T / dt)\n",
" n_regimes = Q.shape[0]\n",
" \n",
" t = np.linspace(0, T, n_steps)\n",
" X = np.zeros(n_steps)\n",
" regimes = np.zeros(n_steps, dtype=int)\n",
" \n",
" X[0] = x0\n",
" regimes[0] = 0 # Start in regime 0\n",
" \n",
" for i in range(1, n_steps):\n",
" current_regime = regimes[i-1]\n",
" \n",
" # Check for regime transition\n",
" for j in range(n_regimes):\n",
" if j != current_regime:\n",
" if np.random.rand() < Q[current_regime, j] * dt:\n",
" current_regime = j\n",
" break\n",
" \n",
" regimes[i] = current_regime\n",
" \n",
" # Evolve state according to current regime\n",
" mu, sigma = regime_params[current_regime]\n",
" dW = np.random.normal(0, np.sqrt(dt))\n",
" X[i] = X[i-1] + mu * dt + sigma * dW\n",
" \n",
" return t, X, regimes\n",
"\n",
"# Example: 3-regime system (Slow/Normal/Fast)\n",
"Q = np.array([\n",
" [-0.5, 0.3, 0.2], # Slow regime\n",
" [ 0.4, -0.7, 0.3], # Normal regime\n",
" [ 0.3, 0.4, -0.7] # Fast regime\n",
"])\n",
"\n",
"regime_params = [\n",
" (0.1, 0.2), # Slow: low drift, low vol\n",
" (0.3, 0.4), # Normal: medium drift, medium vol\n",
" (0.5, 0.8) # Fast: high drift, high vol\n",
"]\n",
"\n",
"t, X, regimes = simulate_regime_switching(Q, regime_params, x0=0.0, T=50.0, dt=0.01)\n",
"\n",
"# Plot\n",
"fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(14, 10), sharex=True)\n",
"\n",
"# State trajectory\n",
"colors = ['blue', 'green', 'red']\n",
"for i in range(len(t)-1):\n",
" ax1.plot(t[i:i+2], X[i:i+2], color=colors[regimes[i]], alpha=0.8, linewidth=0.8)\n",
"\n",
"ax1.set_ylabel('State X')\n",
"ax1.set_title('Regime-Switching Process')\n",
"ax1.grid(alpha=0.3)\n",
"\n",
"# Regime evolution\n",
"ax2.step(t, regimes, where='post', linewidth=1.5, color='black')\n",
"ax2.set_ylabel('Regime')\n",
"ax2.set_yticks([0, 1, 2])\n",
"ax2.set_yticklabels(['Slow', 'Normal', 'Fast'])\n",
"ax2.set_title('Regime Evolution')\n",
"ax2.grid(alpha=0.3)\n",
"\n",
"# Regime distribution\n",
"regime_counts = np.bincount(regimes, minlength=3) / len(regimes)\n",
"ax3.bar([0, 1, 2], regime_counts, color=colors, alpha=0.7, edgecolor='black')\n",
"ax3.set_xlabel('Regime')\n",
"ax3.set_ylabel('Frequency')\n",
"ax3.set_xticks([0, 1, 2])\n",
"ax3.set_xticklabels(['Slow', 'Normal', 'Fast'])\n",
"ax3.set_title('Regime Distribution')\n",
"ax3.grid(alpha=0.3, axis='y')\n",
"\n",
"plt.tight_layout()\n",
"plt.show()\n",
"\n",
"# Compute stationary distribution\n",
"from scipy.linalg import null_space\n",
"pi_stationary = null_space(Q.T)\n",
"pi_stationary = pi_stationary / pi_stationary.sum()\n",
"\n",
"print(\"\\n📊 Regime Switching Analysis:\")\n",
"print(f\" Observed frequencies: {regime_counts}\")\n",
"print(f\" Theoretical stationary: {pi_stationary.flatten()}\")"
]
},
{
"cell_type": "markdown",
"id": "b3751412",
"metadata": {},
"source": [
"## 4. Jump Diffusion Processes <a id=\"jumps\"></a>\n",
"\n",
"### Motivation\n",
"\n",
"Continuous diffusion models fail to capture **sudden, discrete events**:\n",
"- Market crashes/rallies\n",
"- Equipment failures\n",
"- Policy changes\n",
"- Natural disasters\n",
"- Phase transitions\n",
"\n",
"### Lévy Processes and Compound Poisson\n",
"\n",
"A jump diffusion process combines:\n",
"1. **Continuous diffusion**: $\\sigma dW_t$\n",
"2. **Discrete jumps**: $dJ_t = \\sum_{i=1}^{N_t} Y_i$\n",
"\n",
"$$\n",
"dX_t = \\mu dt + \\sigma dW_t + dJ_t\n",
"$$\n",
"\n",
"where:\n",
"- $N_t \\sim \\text{Poisson}(\\lambda t)$ = number of jumps by time $t$\n",
"- $Y_i \\sim F$ = jump size distribution\n",
"- $\\lambda$ = **jump intensity** (expected jumps per unit time)\n",
"\n",
"### HJB with Jump Integral\n",
"\n",
"$$\n",
"\\rho V(x) = \\sup_u \\left[ \\mu(x,u) V'(x) + \\frac{1}{2}\\sigma^2(x,u) V''(x) + L(x,u) + \\lambda \\int [V(x+y) - V(x)] F(dy) \\right]\n",
"$$\n",
"\n",
"The integral term $\\lambda \\mathbb{E}[V(x+Y) - V(x)]$ represents the **expected value change from jumps**.\n",
"\n",
"### Jump Size Distributions\n",
"\n",
"| Distribution | Density | Use Case |\n",
"|--------------|---------|----------|\n",
"| Normal | $\\mathcal{N}(\\mu_j, \\sigma_j^2)$ | Symmetric jumps (up/down equally likely) |\n",
"| Exponential | $\\lambda e^{-\\lambda y}$ | One-sided jumps (failures, crashes) |\n",
"| Laplace | $\\frac{1}{2b}e^{-|y-\\mu|/b}$ | Heavy-tailed jumps |\n",
"| Uniform | $U(a, b)$ | Bounded jumps |\n",
"\n",
"### Parameter Selection\n",
"\n",
"| Parameter | Typical Range | Effect | How to Choose |\n",
"|-----------|--------------|--------|---------------|\n",
"| $\\lambda$ | 0.01 - 5.0 | Jump frequency | Count events per unit time from data |\n",
"| $\\mu_j$ | Problem-specific | Average jump size | Measure typical event magnitude |\n",
"| $\\sigma_j$ | $> 0$ | Jump size variability | Standard deviation of observed jumps |\n",
"\n",
"**Rule of thumb**: If you expect ~1 jump per 10 time units, set $\\lambda = 0.1$."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "733539e5",
"metadata": {},
"outputs": [],
"source": [
"# Simulate jump diffusion process\n",
"def simulate_jump_diffusion(mu, sigma, lambda_jump, jump_mean, jump_std, x0, T, dt):\n",
" \"\"\"\n",
" Simulate Merton jump diffusion model\n",
" \n",
" dX_t = μ dt + σ dW_t + dJ_t\n",
" \n",
" where J_t is compound Poisson with Normal jumps\n",
" \"\"\"\n",
" n_steps = int(T / dt)\n",
" t = np.linspace(0, T, n_steps)\n",
" X = np.zeros(n_steps)\n",
" jumps = np.zeros(n_steps)\n",
" \n",
" X[0] = x0\n",
" \n",
" for i in range(1, n_steps):\n",
" # Diffusion component\n",
" dW = np.random.normal(0, np.sqrt(dt))\n",
" dX = mu * dt + sigma * dW\n",
" \n",
" # Jump component\n",
" n_jumps = np.random.poisson(lambda_jump * dt)\n",
" if n_jumps > 0:\n",
" jump_sizes = np.random.normal(jump_mean, jump_std, n_jumps)\n",
" total_jump = jump_sizes.sum()\n",
" dX += total_jump\n",
" jumps[i] = total_jump\n",
" \n",
" X[i] = X[i-1] + dX\n",
" \n",
" return t, X, jumps\n",
"\n",
"# Example: System with occasional failures/shocks\n",
"mu = 0.5 # Baseline drift\n",
"sigma = 0.3 # Continuous volatility\n",
"lambda_jump = 2.0 # 2 jumps per time unit (on average)\n",
"jump_mean = -0.5 # Negative jumps (failures)\n",
"jump_std = 0.2 # Jump size variability\n",
"x0 = 10.0 # Initial state\n",
"T = 20.0 # Time horizon\n",
"dt = 0.01\n",
"\n",
"t, X, jumps = simulate_jump_diffusion(mu, sigma, lambda_jump, jump_mean, jump_std, x0, T, dt)\n",
"\n",
"# Also simulate without jumps for comparison\n",
"t_nodiff, X_nodiff, _ = simulate_jump_diffusion(mu, sigma, 0.0, 0.0, 0.0, x0, T, dt)\n",
"\n",
"# Plot\n",
"fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 10), sharex=True)\n",
"\n",
"# Trajectories comparison\n",
"ax1.plot(t, X, linewidth=1.5, color='red', label='With Jumps', alpha=0.8)\n",
"ax1.plot(t_nodiff, X_nodiff, linewidth=1.5, color='blue', label='Pure Diffusion', alpha=0.6)\n",
"\n",
"# Mark jump times\n",
"jump_times = t[jumps != 0]\n",
"jump_values = X[jumps != 0]\n",
"ax1.scatter(jump_times, jump_values, color='black', s=50, zorder=5, label='Jump Events', alpha=0.7)\n",
"\n",
"ax1.set_ylabel('State X')\n",
"ax1.set_title('Jump Diffusion Process vs Pure Diffusion')\n",
"ax1.legend(fontsize=11)\n",
"ax1.grid(alpha=0.3)\n",
"\n",
"# Jump sizes over time\n",
"ax2.stem(t, jumps, linefmt='red', markerfmt='ro', basefmt=' ', label='Jump Sizes')\n",
"ax2.axhline(y=0, color='black', linewidth=0.8)\n",
"ax2.set_xlabel('Time')\n",
"ax2.set_ylabel('Jump Size')\n",
"ax2.set_title('Jump Events')\n",
"ax2.grid(alpha=0.3)\n",
"\n",
"plt.tight_layout()\n",
"plt.show()\n",
"\n",
"# Statistics\n",
"n_observed_jumps = np.sum(jumps != 0)\n",
"expected_jumps = lambda_jump * T\n",
"avg_jump_size = jumps[jumps != 0].mean() if n_observed_jumps > 0 else 0\n",
"\n",
"print(\"\\n📊 Jump Diffusion Analysis:\")\n",
"print(f\" Expected jumps: {expected_jumps:.1f}\")\n",
"print(f\" Observed jumps: {n_observed_jumps}\")\n",
"print(f\" Average jump size: {avg_jump_size:.3f} (theoretical: {jump_mean})\")\n",
"print(f\" Std of jumps: {jumps[jumps != 0].std() if n_observed_jumps > 0 else 0:.3f} (theoretical: {jump_std})\")\n",
"print(f\"\\n Impact: Final value with jumps = {X[-1]:.2f} vs {X_nodiff[-1]:.2f} without jumps\")"
]
},
{
"cell_type": "markdown",
"id": "fca7aba3",
"metadata": {},
"source": [
"## 5. Combined MRSJD Models <a id=\"mrsjd\"></a>\n",
"\n",
"### Why Combine Regime Switching and Jumps?\n",
"\n",
"Real systems often exhibit **both**:\n",
"1. **State-dependent behavior** (regimes)\n",
"2. **Sudden shocks** (jumps)\n",
"\n",
"Examples:\n",
"- **Manufacturing**: Normal/maintenance regimes + equipment failures (jumps)\n",
"- **Power grid**: Low/high demand regimes + blackout events (jumps)\n",
"- **Epidemic**: Endemic/outbreak regimes + super-spreader events (jumps)\n",
"\n",
"### Full MRSJD Dynamics\n",
"\n",
"$$\n",
"dX_t = \\mu^{i_t}(X_t)dt + \\sigma^{i_t}(X_t)dW_t + dJ_t^{i_t}\n",
"$$\n",
"\n",
"where:\n",
"- Drift $\\mu^i$ and volatility $\\sigma^i$ depend on current regime $i_t$\n",
"- Jump intensity $\\lambda^i$ and distribution $F^i$ also regime-dependent\n",
"- Regime switches according to $Q$\n",
"\n",
"### Coupled HJB with Both Effects\n",
"\n",
"$$\n",
"\\boxed{\n",
"\\rho V^i(x) = \\sup_u \\left[ \\mu^i V^i_x + \\frac{(\\sigma^i)^2}{2} V^i_{xx} + L^i(x,u) + \\lambda^i \\int [V^i(x+y) - V^i(x)] F^i(dy) + \\sum_{j \\neq i} q_{ij}[V^j(x) - V^i(x)] \\right]\n",
"}\n",
"$$\n",
"\n",
"This is the **most general** formulation combining:\n",
"1. ✅ Diffusion: $(\\sigma^i)^2 V^i_{xx}$\n",
"2. ✅ Jumps: $\\lambda^i \\int [V^i(x+y) - V^i(x)] F^i(dy)$\n",
"3. ✅ Regime Switching: $\\sum_{j \\neq i} q_{ij}[V^j(x) - V^i(x)]$\n",
"4. ✅ Optimal Control: $\\sup_u$\n",
"\n",
"### Numerical Solution: Finite Differences with Upwind Schemes\n",
"\n",
"**Grid discretization**: $x_k = x_{\\min} + k \\Delta x$, $k = 0, ..., N$\n",
"\n",
"**Value function approximation**: $V^i(x_k) \\approx V^i_k$\n",
"\n",
"**Derivatives**:\n",
"- Forward: $V_x \\approx (V_{k+1} - V_k) / \\Delta x$\n",
"- Backward: $V_x \\approx (V_k - V_{k-1}) / \\Delta x$\n",
"- Central: $V_{xx} \\approx (V_{k+1} - 2V_k + V_{k-1}) / (\\Delta x)^2$\n",
"\n",
"**Upwind scheme** (for stability when $\\mu \\neq 0$):\n",
"$$\n",
"V_x \\approx \n",
"\\begin{cases}\n",
"(V_{k+1} - V_k) / \\Delta x & \\text{if } \\mu > 0 \\text{ (forward)} \\\\\n",
"(V_k - V_{k-1}) / \\Delta x & \\text{if } \\mu < 0 \\text{ (backward)}\n",
"\\end{cases}\n",
"$$\n",
"\n",
"**Why upwind?** Prevents numerical oscillations when advection dominates diffusion.\n",
"\n",
"### Algorithm: Value Iteration\n",
"\n",
"```\n",
"1. Initialize V^i_k = 0 for all regimes i and grid points k\n",
"2. Repeat until convergence:\n",
" For each regime i:\n",
" For each grid point k:\n",
" a. Compute derivatives V_x, V_xx\n",
" b. Compute jump integral ∫[V(x+y) - V(x)]F(dy)\n",
" c. Compute regime switching term Σ q_ij[V^j - V^i]\n",
" d. Optimize over control: u* = argmax_u RHS(u)\n",
" e. Update: V^i_k ← RHS(u*) / ρ\n",
"3. Convergence check: ||V_new - V_old|| < tol\n",
"```\n",
"\n",
"### Computational Complexity\n",
"\n",
"- **Per iteration**: $O(N \\cdot M \\cdot K)$\n",
" - $N$ = number of regimes\n",
" - $M$ = grid points\n",
" - $K$ = control discretization\n",
"- **Iterations**: Typically 100-1000\n",
"- **Total**: $O(10^5 - 10^7)$ operations\n",
"\n",
"**Speedup techniques**:\n",
"- Parallel computation across regimes (Rayon)\n",
"- Adaptive grid refinement\n",
"- Policy iteration instead of value iteration\n",
"- Sparse matrix operations"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "f104f849",
"metadata": {},
"outputs": [],
"source": [
"# Simplified MRSJD simulation (for illustration)\n",
"def simulate_mrsjd(Q, regime_params_list, x0, T, dt):\n",
" \"\"\"\n",
" Simulate Markov Regime Switching Jump Diffusion\n",
" \n",
" Each regime has: (mu, sigma, lambda_jump, jump_mean, jump_std)\n",
" \"\"\"\n",
" n_steps = int(T / dt)\n",
" n_regimes = Q.shape[0]\n",
" \n",
" t = np.linspace(0, T, n_steps)\n",
" X = np.zeros(n_steps)\n",
" regimes = np.zeros(n_steps, dtype=int)\n",
" jump_events = []\n",
" \n",
" X[0] = x0\n",
" regimes[0] = 0\n",
" \n",
" for i in range(1, n_steps):\n",
" current_regime = regimes[i-1]\n",
" mu, sigma, lam, jmu, jsig = regime_params_list[current_regime]\n",
" \n",
" # Check regime transition\n",
" for j in range(n_regimes):\n",
" if j != current_regime and np.random.rand() < Q[current_regime, j] * dt:\n",
" current_regime = j\n",
" break\n",
" \n",
" regimes[i] = current_regime\n",
" \n",
" # Diffusion\n",
" dW = np.random.normal(0, np.sqrt(dt))\n",
" dX = mu * dt + sigma * dW\n",
" \n",
" # Jumps (regime-dependent)\n",
" n_jumps = np.random.poisson(lam * dt)\n",
" if n_jumps > 0:\n",
" jump_size = np.sum(np.random.normal(jmu, jsig, n_jumps))\n",
" dX += jump_size\n",
" jump_events.append((t[i], jump_size, current_regime))\n",
" \n",
" X[i] = X[i-1] + dX\n",
" \n",
" return t, X, regimes, jump_events\n",
"\n",
"# Example: 2-regime system with regime-dependent jumps\n",
"Q = np.array([\n",
" [-0.3, 0.3],\n",
" [0.5, -0.5]\n",
"])\n",
"\n",
"# Regime 0: Stable (low vol, rare small jumps)\n",
"# Regime 1: Volatile (high vol, frequent large jumps)\n",
"regime_params_list = [\n",
" (0.2, 0.3, 0.5, -0.1, 0.05), # Stable: mu, sigma, lambda, jump_mu, jump_sigma\n",
" (0.1, 0.8, 2.0, -0.3, 0.15) # Volatile\n",
"]\n",
"\n",
"t, X, regimes, jump_events = simulate_mrsjd(Q, regime_params_list, x0=5.0, T=30.0, dt=0.01)\n",
"\n",
"# Plot\n",
"fig, axes = plt.subplots(3, 1, figsize=(14, 12), sharex=True)\n",
"\n",
"# State trajectory\n",
"regime_colors = ['blue', 'red']\n",
"for i in range(len(t)-1):\n",
" axes[0].plot(t[i:i+2], X[i:i+2], color=regime_colors[regimes[i]], alpha=0.8, linewidth=1.0)\n",
"\n",
"# Mark jumps\n",
"if jump_events:\n",
" jump_t = [j[0] for j in jump_events]\n",
" jump_idx = [np.argmin(np.abs(t - jt)) for jt in jump_t]\n",
" axes[0].scatter([t[i] for i in jump_idx], [X[i] for i in jump_idx], \n",
" color='black', s=60, zorder=5, marker='x', label='Jumps')\n",
"\n",
"axes[0].set_ylabel('State X')\n",
"axes[0].set_title('MRSJD: Combined Regime Switching + Jump Diffusion')\n",
"axes[0].legend()\n",
"axes[0].grid(alpha=0.3)\n",
"\n",
"# Regime evolution\n",
"axes[1].step(t, regimes, where='post', linewidth=1.5, color='black')\n",
"axes[1].fill_between(t, regimes, alpha=0.3, step='post', \n",
" color=['blue' if r==0 else 'red' for r in regimes])\n",
"axes[1].set_ylabel('Regime')\n",
"axes[1].set_yticks([0, 1])\n",
"axes[1].set_yticklabels(['Stable', 'Volatile'])\n",
"axes[1].set_title('Regime Transitions')\n",
"axes[1].grid(alpha=0.3)\n",
"\n",
"# Jump events by regime\n",
"if jump_events:\n",
" regime_0_jumps = [j for j in jump_events if j[2] == 0]\n",
" regime_1_jumps = [j for j in jump_events if j[2] == 1]\n",
" \n",
" if regime_0_jumps:\n",
" axes[2].scatter([j[0] for j in regime_0_jumps], [j[1] for j in regime_0_jumps],\n",
" color='blue', s=50, alpha=0.7, label='Stable Regime Jumps')\n",
" if regime_1_jumps:\n",
" axes[2].scatter([j[0] for j in regime_1_jumps], [j[1] for j in regime_1_jumps],\n",
" color='red', s=50, alpha=0.7, label='Volatile Regime Jumps')\n",
"\n",
"axes[2].axhline(y=0, color='black', linewidth=0.8)\n",
"axes[2].set_xlabel('Time')\n",
"axes[2].set_ylabel('Jump Size')\n",
"axes[2].set_title('Jump Events by Regime')\n",
"axes[2].legend()\n",
"axes[2].grid(alpha=0.3)\n",
"\n",
"plt.tight_layout()\n",
"plt.show()\n",
"\n",
"# Statistics\n",
"print(\"\\n📊 MRSJD Analysis:\")\n",
"print(f\" Total jumps: {len(jump_events)}\")\n",
"regime_times = [np.sum(regimes == i) * dt for i in range(2)]\n",
"print(f\" Time in Stable regime: {regime_times[0]:.1f} ({regime_times[0]/T*100:.1f}%)\")\n",
"print(f\" Time in Volatile regime: {regime_times[1]:.1f} ({regime_times[1]/T*100:.1f}%)\")\n",
"if jump_events:\n",
" avg_jump_0 = np.mean([j[1] for j in jump_events if j[2] == 0]) if len([j for j in jump_events if j[2] == 0]) > 0 else 0\n",
" avg_jump_1 = np.mean([j[1] for j in jump_events if j[2] == 1]) if len([j for j in jump_events if j[2] == 1]) > 0 else 0\n",
" print(f\" Average jump size (Stable): {avg_jump_0:.3f}\")\n",
" print(f\" Average jump size (Volatile): {avg_jump_1:.3f}\")"
]
},
{
"cell_type": "markdown",
"id": "822b77f8",
"metadata": {},
"source": [
"## 6. Practical Parameter Selection Guide <a id=\"params\"></a>\n",
"\n",
"### How to Choose Parameters for Your Problem\n",
"\n",
"#### Step 1: Identify Regimes\n",
"\n",
"Ask: Does the system have distinct \"modes\" or \"states\"?\n",
"\n",
"**Examples**:\n",
"- Manufacturing: Normal / Degraded / Failed\n",
"- Weather: Clear / Cloudy / Storm\n",
"- Network: Low / Medium / High traffic\n",
"\n",
"**Tip**: Start with 2-3 regimes. More regimes = more parameters to estimate.\n",
"\n",
"#### Step 2: Estimate Regime Persistence\n",
"\n",
"**Question**: How long does each regime typically last?\n",
"\n",
"**Formula**: $q_{ij} = \\frac{1}{\\text{expected duration in regime } i}$\n",
"\n",
"**Example**: If \"Normal\" regime lasts ~10 time units:\n",
"- Total exit rate from Normal: $q_{01} + q_{02} = 0.1$\n",
"- Split based on transition probabilities\n",
"\n",
"#### Step 3: Characterize Within-Regime Dynamics\n",
"\n",
"For each regime $i$:\n",
"\n",
"| Parameter | Method | Example |\n",
"|-----------|--------|----------|\n",
"| $\\mu^i$ | Sample mean of increments | $\\bar{\\Delta X} / \\Delta t$ |\n",
"| $\\sigma^i$ | Sample std of increments | $\\text{std}(\\Delta X) / \\sqrt{\\Delta t}$ |\n",
"| $\\lambda^i$ | Count events per time | $N_{\\text{jumps}} / T$ |\n",
"| Jump mean | Average jump size | $\\bar{Y}$ |\n",
"| Jump std | Std of jump sizes | $\\text{std}(Y)$ |\n",
"\n",
"#### Step 4: Validate with Simulations\n",
"\n",
"Before solving the HJB:\n",
"1. Simulate the process with chosen parameters\n",
"2. Check if trajectories \"look right\"\n",
"3. Compare summary statistics to data\n",
"4. Adjust and iterate\n",
"\n",
"### Common Pitfalls and Solutions\n",
"\n",
"| Problem | Symptom | Solution |\n",
"|---------|---------|----------|\n",
"| Too many regimes | Overfitting, unstable estimates | Use 2-3 regimes; combine similar ones |\n",
"| Wrong time scale | Unrealistic dynamics | Match $q_{ij}$ to actual durations |\n",
"| Numerical instability | Oscillations, divergence | Reduce grid spacing, use upwind scheme |\n",
"| Slow convergence | Many iterations needed | Better initial guess, increase tolerance |\n",
"| High dimensionality | Curse of dimensionality | Reduce state space, use approximations |\n",
"\n",
"### Sensitivity Analysis\n",
"\n",
"Always check how results change with parameters:\n",
"1. Vary each parameter by ±20%\n",
"2. Observe impact on optimal policy and value function\n",
"3. Identify which parameters matter most\n",
"4. Focus calibration efforts on sensitive parameters\n",
"\n",
"### When to Use vs. Not Use\n",
"\n",
"✅ **Good fit for HJB optimal control**:\n",
"- Continuous state space (position, temperature, concentration)\n",
"- Known or learnable dynamics\n",
"- Quantifiable objectives\n",
"- Medium-dimensional problems (1-3 state variables)\n",
"- Offline planning acceptable\n",
"\n",
"❌ **Not recommended**:\n",
"- Purely discrete decisions (use dynamic programming)\n",
"- Unknown dynamics (use reinforcement learning)\n",
"- High-dimensional state (>5 variables)\n",
"- Real-time requirements (<1ms response)\n",
"- Purely deterministic problems (use calculus of variations)\n",
"\n",
"### Alternative Approaches\n",
"\n",
"| Method | When to Use | Pros | Cons |\n",
"|--------|-------------|------|------|\n",
"| **LQR/LQG** | Linear dynamics, quadratic cost | Fast, analytical solution | Limited to LQ problems |\n",
"| **MPC** | Need real-time receding horizon | Handles constraints well | Computational cost |\n",
"| **RL (DQN, PPO)** | Unknown dynamics | Model-free, flexible | Sample inefficient |\n",
"| **PID Control** | Simple SISO systems | Easy to tune | No optimality guarantee |\n",
"| **Bang-Bang** | Hard constraints | Simple implementation | Non-smooth control |\n",
"\n",
"### Further Reading\n",
"\n",
"1. **Books**:\n",
" - Fleming & Rishel: \"Deterministic and Stochastic Optimal Control\"\n",
" - Øksendal & Sulem: \"Applied Stochastic Control of Jump Diffusions\"\n",
" - Bertsekas: \"Dynamic Programming and Optimal Control\"\n",
"\n",
"2. **Papers**:\n",
" - Guo & Hernandez-Lerma: \"Continuous-Time Markov Decision Processes\"\n",
" - Pham: \"Continuous-time Stochastic Control and Optimization with Financial Applications\"\n",
"\n",
"3. **Software**:\n",
" - This library (optimizr): Generic optimal control solvers\n",
" - PROPT: MATLAB optimal control toolbox\n",
" - CasADi: Nonlinear optimization and optimal control"
]
},
{
"cell_type": "markdown",
"id": "432ef4af",
"metadata": {},
"source": [
"## Summary and Next Steps\n",
"\n",
"### What We Covered\n",
"\n",
"1. ✅ **Optimal Control Theory**: HJB equations, value functions\n",
"2. ✅ **Regime Switching**: Markov chains, coupled HJB systems\n",
"3. ✅ **Jump Diffusion**: Lévy processes, compound Poisson\n",
"4. ✅ **MRSJD Models**: Combined framework for complex systems\n",
"5. ✅ **Numerical Methods**: Finite differences, upwind schemes, value iteration\n",
"6. ✅ **Parameter Selection**: Practical guidance and sensitivity analysis\n",
"\n",
"### Key Takeaways\n",
"\n",
"- Optimal control finds the **best** policy, not just a good one\n",
"- HJB equations require **solving PDEs** (computational cost)\n",
"- Regime switching captures **state-dependent behavior**\n",
"- Jumps model **sudden events** and tail risk\n",
"- Start simple (2 regimes, pure diffusion) then add complexity\n",
"\n",
"### Exercises for Practice\n",
"\n",
"1. **Temperature Control**: Design an optimal heating/cooling policy to maintain room temperature near 20°C while minimizing energy cost\n",
"\n",
"2. **Inventory Management**: Optimize reorder policy for warehouse with regime-switching demand (normal/holiday)\n",
"\n",
"3. **Robot Navigation**: Find optimal path for robot avoiding obstacles with uncertain dynamics and occasional sensor failures (jumps)\n",
"\n",
"### Next Tutorial\n",
"\n",
"- **Hidden Markov Models (HMM)**: When regime is not directly observable\n",
"- **MCMC Sampling**: Bayesian inference for parameter estimation\n",
"- **Sparse Optimization**: High-dimensional problems with sparsity\n",
"\n",
"---\n",
"\n",
"**Questions?** Open an issue on the repository or consult the API documentation.\n",
"\n",
"**Happy Optimizing! 🚀**"
]
}
],
"metadata": {
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 5
}