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
33 KiB
33 KiB
In [ ]:
# Import required libraries
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import cm
from mpl_toolkits.mplot3d import Axes3D
import seaborn as sns
# Set style
sns.set_style('whitegrid')
plt.rcParams['figure.figsize'] = (14, 6)
plt.rcParams['font.size'] = 11
print("✅ Libraries loaded successfully")
print("\n📚 This tutorial covers:")
print(" 1. Regime Switching Systems")
print(" 2. Jump Diffusion Processes")
print(" 3. Combined MRSJD Models")
print(" 4. Numerical Methods (Finite Differences, Upwind Schemes)")
print(" 5. Practical Parameter Selection")In [ ]:
# Simulate Ornstein-Uhlenbeck process
def simulate_ou(theta, mu, sigma, x0, T, dt):
"""
Simulate Ornstein-Uhlenbeck process using Euler-Maruyama method
dX_t = θ(μ - X_t)dt + σ dW_t
"""
n_steps = int(T / dt)
t = np.linspace(0, T, n_steps)
X = np.zeros(n_steps)
X[0] = x0
for i in range(1, n_steps):
dW = np.random.normal(0, np.sqrt(dt))
X[i] = X[i-1] + theta * (mu - X[i-1]) * dt + sigma * dW
return t, X
# Example: Temperature control
theta = 0.5 # Mean reversion speed
mu = 20.0 # Target temperature (°C)
sigma = 2.0 # Noise level
x0 = 10.0 # Initial temperature
T = 10.0 # Time horizon (seconds)
dt = 0.01 # Time step
t, X = simulate_ou(theta, mu, sigma, x0, T, dt)
# Plot
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))
# Trajectory
ax1.plot(t, X, linewidth=1.5, color='steelblue', label='Temperature')
ax1.axhline(y=mu, color='red', linestyle='--', label=f'Target μ={mu}')
ax1.fill_between(t, mu-sigma, mu+sigma, alpha=0.2, color='red', label='±σ band')
ax1.set_xlabel('Time (s)')
ax1.set_ylabel('Temperature (°C)')
ax1.set_title('Ornstein-Uhlenbeck Process (Mean-Reverting System)')
ax1.legend()
ax1.grid(alpha=0.3)
# Distribution at equilibrium
equilibrium_samples = X[len(X)//2:] # Second half (near equilibrium)
ax2.hist(equilibrium_samples, bins=30, density=True, alpha=0.7, color='steelblue', edgecolor='black')
# Theoretical distribution: N(μ, σ²/(2θ))
x_range = np.linspace(X.min(), X.max(), 100)
theoretical_std = sigma / np.sqrt(2 * theta)
from scipy.stats import norm
ax2.plot(x_range, norm.pdf(x_range, mu, theoretical_std),
'r-', linewidth=2, label=f'Theory: N({mu:.1f}, {theoretical_std:.2f}²)')
ax2.set_xlabel('Temperature (°C)')
ax2.set_ylabel('Probability Density')
ax2.set_title('Equilibrium Distribution')
ax2.legend()
ax2.grid(alpha=0.3)
plt.tight_layout()
plt.show()
print(f"\n📊 OU Process Analysis:")
print(f" Half-life: {np.log(2)/theta:.2f} seconds")
print(f" Theoretical equilibrium std: {theoretical_std:.2f}")
print(f" Observed equilibrium std: {equilibrium_samples.std():.2f}")In [ ]:
# Simulate regime-switching process
def simulate_regime_switching(Q, regime_params, x0, T, dt):
"""
Simulate regime-switching stochastic process
Args:
Q: Transition rate matrix (N x N)
regime_params: List of (mu, sigma) for each regime
x0: Initial state
T: Time horizon
dt: Time step
"""
n_steps = int(T / dt)
n_regimes = Q.shape[0]
t = np.linspace(0, T, n_steps)
X = np.zeros(n_steps)
regimes = np.zeros(n_steps, dtype=int)
X[0] = x0
regimes[0] = 0 # Start in regime 0
for i in range(1, n_steps):
current_regime = regimes[i-1]
# Check for regime transition
for j in range(n_regimes):
if j != current_regime:
if np.random.rand() < Q[current_regime, j] * dt:
current_regime = j
break
regimes[i] = current_regime
# Evolve state according to current regime
mu, sigma = regime_params[current_regime]
dW = np.random.normal(0, np.sqrt(dt))
X[i] = X[i-1] + mu * dt + sigma * dW
return t, X, regimes
# Example: 3-regime system (Slow/Normal/Fast)
Q = np.array([
[-0.5, 0.3, 0.2], # Slow regime
[ 0.4, -0.7, 0.3], # Normal regime
[ 0.3, 0.4, -0.7] # Fast regime
])
regime_params = [
(0.1, 0.2), # Slow: low drift, low vol
(0.3, 0.4), # Normal: medium drift, medium vol
(0.5, 0.8) # Fast: high drift, high vol
]
t, X, regimes = simulate_regime_switching(Q, regime_params, x0=0.0, T=50.0, dt=0.01)
# Plot
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(14, 10), sharex=True)
# State trajectory
colors = ['blue', 'green', 'red']
for i in range(len(t)-1):
ax1.plot(t[i:i+2], X[i:i+2], color=colors[regimes[i]], alpha=0.8, linewidth=0.8)
ax1.set_ylabel('State X')
ax1.set_title('Regime-Switching Process')
ax1.grid(alpha=0.3)
# Regime evolution
ax2.step(t, regimes, where='post', linewidth=1.5, color='black')
ax2.set_ylabel('Regime')
ax2.set_yticks([0, 1, 2])
ax2.set_yticklabels(['Slow', 'Normal', 'Fast'])
ax2.set_title('Regime Evolution')
ax2.grid(alpha=0.3)
# Regime distribution
regime_counts = np.bincount(regimes, minlength=3) / len(regimes)
ax3.bar([0, 1, 2], regime_counts, color=colors, alpha=0.7, edgecolor='black')
ax3.set_xlabel('Regime')
ax3.set_ylabel('Frequency')
ax3.set_xticks([0, 1, 2])
ax3.set_xticklabels(['Slow', 'Normal', 'Fast'])
ax3.set_title('Regime Distribution')
ax3.grid(alpha=0.3, axis='y')
plt.tight_layout()
plt.show()
# Compute stationary distribution
from scipy.linalg import null_space
pi_stationary = null_space(Q.T)
pi_stationary = pi_stationary / pi_stationary.sum()
print("\n📊 Regime Switching Analysis:")
print(f" Observed frequencies: {regime_counts}")
print(f" Theoretical stationary: {pi_stationary.flatten()}")In [ ]:
# Simulate jump diffusion process
def simulate_jump_diffusion(mu, sigma, lambda_jump, jump_mean, jump_std, x0, T, dt):
"""
Simulate Merton jump diffusion model
dX_t = μ dt + σ dW_t + dJ_t
where J_t is compound Poisson with Normal jumps
"""
n_steps = int(T / dt)
t = np.linspace(0, T, n_steps)
X = np.zeros(n_steps)
jumps = np.zeros(n_steps)
X[0] = x0
for i in range(1, n_steps):
# Diffusion component
dW = np.random.normal(0, np.sqrt(dt))
dX = mu * dt + sigma * dW
# Jump component
n_jumps = np.random.poisson(lambda_jump * dt)
if n_jumps > 0:
jump_sizes = np.random.normal(jump_mean, jump_std, n_jumps)
total_jump = jump_sizes.sum()
dX += total_jump
jumps[i] = total_jump
X[i] = X[i-1] + dX
return t, X, jumps
# Example: System with occasional failures/shocks
mu = 0.5 # Baseline drift
sigma = 0.3 # Continuous volatility
lambda_jump = 2.0 # 2 jumps per time unit (on average)
jump_mean = -0.5 # Negative jumps (failures)
jump_std = 0.2 # Jump size variability
x0 = 10.0 # Initial state
T = 20.0 # Time horizon
dt = 0.01
t, X, jumps = simulate_jump_diffusion(mu, sigma, lambda_jump, jump_mean, jump_std, x0, T, dt)
# Also simulate without jumps for comparison
t_nodiff, X_nodiff, _ = simulate_jump_diffusion(mu, sigma, 0.0, 0.0, 0.0, x0, T, dt)
# Plot
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 10), sharex=True)
# Trajectories comparison
ax1.plot(t, X, linewidth=1.5, color='red', label='With Jumps', alpha=0.8)
ax1.plot(t_nodiff, X_nodiff, linewidth=1.5, color='blue', label='Pure Diffusion', alpha=0.6)
# Mark jump times
jump_times = t[jumps != 0]
jump_values = X[jumps != 0]
ax1.scatter(jump_times, jump_values, color='black', s=50, zorder=5, label='Jump Events', alpha=0.7)
ax1.set_ylabel('State X')
ax1.set_title('Jump Diffusion Process vs Pure Diffusion')
ax1.legend(fontsize=11)
ax1.grid(alpha=0.3)
# Jump sizes over time
ax2.stem(t, jumps, linefmt='red', markerfmt='ro', basefmt=' ', label='Jump Sizes')
ax2.axhline(y=0, color='black', linewidth=0.8)
ax2.set_xlabel('Time')
ax2.set_ylabel('Jump Size')
ax2.set_title('Jump Events')
ax2.grid(alpha=0.3)
plt.tight_layout()
plt.show()
# Statistics
n_observed_jumps = np.sum(jumps != 0)
expected_jumps = lambda_jump * T
avg_jump_size = jumps[jumps != 0].mean() if n_observed_jumps > 0 else 0
print("\n📊 Jump Diffusion Analysis:")
print(f" Expected jumps: {expected_jumps:.1f}")
print(f" Observed jumps: {n_observed_jumps}")
print(f" Average jump size: {avg_jump_size:.3f} (theoretical: {jump_mean})")
print(f" Std of jumps: {jumps[jumps != 0].std() if n_observed_jumps > 0 else 0:.3f} (theoretical: {jump_std})")
print(f"\n Impact: Final value with jumps = {X[-1]:.2f} vs {X_nodiff[-1]:.2f} without jumps")In [ ]:
# Simplified MRSJD simulation (for illustration)
def simulate_mrsjd(Q, regime_params_list, x0, T, dt):
"""
Simulate Markov Regime Switching Jump Diffusion
Each regime has: (mu, sigma, lambda_jump, jump_mean, jump_std)
"""
n_steps = int(T / dt)
n_regimes = Q.shape[0]
t = np.linspace(0, T, n_steps)
X = np.zeros(n_steps)
regimes = np.zeros(n_steps, dtype=int)
jump_events = []
X[0] = x0
regimes[0] = 0
for i in range(1, n_steps):
current_regime = regimes[i-1]
mu, sigma, lam, jmu, jsig = regime_params_list[current_regime]
# Check regime transition
for j in range(n_regimes):
if j != current_regime and np.random.rand() < Q[current_regime, j] * dt:
current_regime = j
break
regimes[i] = current_regime
# Diffusion
dW = np.random.normal(0, np.sqrt(dt))
dX = mu * dt + sigma * dW
# Jumps (regime-dependent)
n_jumps = np.random.poisson(lam * dt)
if n_jumps > 0:
jump_size = np.sum(np.random.normal(jmu, jsig, n_jumps))
dX += jump_size
jump_events.append((t[i], jump_size, current_regime))
X[i] = X[i-1] + dX
return t, X, regimes, jump_events
# Example: 2-regime system with regime-dependent jumps
Q = np.array([
[-0.3, 0.3],
[0.5, -0.5]
])
# Regime 0: Stable (low vol, rare small jumps)
# Regime 1: Volatile (high vol, frequent large jumps)
regime_params_list = [
(0.2, 0.3, 0.5, -0.1, 0.05), # Stable: mu, sigma, lambda, jump_mu, jump_sigma
(0.1, 0.8, 2.0, -0.3, 0.15) # Volatile
]
t, X, regimes, jump_events = simulate_mrsjd(Q, regime_params_list, x0=5.0, T=30.0, dt=0.01)
# Plot
fig, axes = plt.subplots(3, 1, figsize=(14, 12), sharex=True)
# State trajectory
regime_colors = ['blue', 'red']
for i in range(len(t)-1):
axes[0].plot(t[i:i+2], X[i:i+2], color=regime_colors[regimes[i]], alpha=0.8, linewidth=1.0)
# Mark jumps
if jump_events:
jump_t = [j[0] for j in jump_events]
jump_idx = [np.argmin(np.abs(t - jt)) for jt in jump_t]
axes[0].scatter([t[i] for i in jump_idx], [X[i] for i in jump_idx],
color='black', s=60, zorder=5, marker='x', label='Jumps')
axes[0].set_ylabel('State X')
axes[0].set_title('MRSJD: Combined Regime Switching + Jump Diffusion')
axes[0].legend()
axes[0].grid(alpha=0.3)
# Regime evolution
axes[1].step(t, regimes, where='post', linewidth=1.5, color='black')
axes[1].fill_between(t, regimes, alpha=0.3, step='post',
color=['blue' if r==0 else 'red' for r in regimes])
axes[1].set_ylabel('Regime')
axes[1].set_yticks([0, 1])
axes[1].set_yticklabels(['Stable', 'Volatile'])
axes[1].set_title('Regime Transitions')
axes[1].grid(alpha=0.3)
# Jump events by regime
if jump_events:
regime_0_jumps = [j for j in jump_events if j[2] == 0]
regime_1_jumps = [j for j in jump_events if j[2] == 1]
if regime_0_jumps:
axes[2].scatter([j[0] for j in regime_0_jumps], [j[1] for j in regime_0_jumps],
color='blue', s=50, alpha=0.7, label='Stable Regime Jumps')
if regime_1_jumps:
axes[2].scatter([j[0] for j in regime_1_jumps], [j[1] for j in regime_1_jumps],
color='red', s=50, alpha=0.7, label='Volatile Regime Jumps')
axes[2].axhline(y=0, color='black', linewidth=0.8)
axes[2].set_xlabel('Time')
axes[2].set_ylabel('Jump Size')
axes[2].set_title('Jump Events by Regime')
axes[2].legend()
axes[2].grid(alpha=0.3)
plt.tight_layout()
plt.show()
# Statistics
print("\n📊 MRSJD Analysis:")
print(f" Total jumps: {len(jump_events)}")
regime_times = [np.sum(regimes == i) * dt for i in range(2)]
print(f" Time in Stable regime: {regime_times[0]:.1f} ({regime_times[0]/T*100:.1f}%)")
print(f" Time in Volatile regime: {regime_times[1]:.1f} ({regime_times[1]/T*100:.1f}%)")
if jump_events:
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
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
print(f" Average jump size (Stable): {avg_jump_0:.3f}")
print(f" Average jump size (Volatile): {avg_jump_1:.3f}")