- 07_topology: expanded persistent-homology examples - 10_bsde: reworked BSDE solver walkthrough - 04/05: clear execution counts - gitignore generated plot PNGs (docs snippets + notebook frames)
298 KiB
298 KiB
In [ ]:
# pyright: reportArgumentType=false, reportUnusedImport=false, reportUnusedVariable=false, reportUnusedExpression=false, reportCallIssue=false, reportAttributeAccessIssue=false, reportOptionalMemberAccess=false, reportOperatorIssue=false, reportGeneralTypeIssues=false, reportReturnType=false, reportAssignmentType=false, reportIndexIssue=false, reportDeprecated=false, reportUndefinedVariable=false, reportPrivateImportUsage=false
import numpy as np
import pandas as pd
import time
import matplotlib.pyplot as plt
import seaborn as sns
from typing import Callable, Tuple
import warnings
warnings.filterwarnings('ignore')
# OptimizR (Rust)
from optimizr import (
HMM,
mcmc_sample,
differential_evolution,
grid_search,
mutual_information,
shannon_entropy
)
# Pure Python alternatives
try:
from hmmlearn import hmm
HMMLEARN_AVAILABLE = True
except ImportError:
print("⚠️ hmmlearn not installed. Installing...")
import subprocess
subprocess.run(['pip', 'install', 'hmmlearn'], check=True, capture_output=True)
from hmmlearn import hmm
HMMLEARN_AVAILABLE = True
from scipy.optimize import differential_evolution as scipy_de
from sklearn.metrics import mutual_info_score
from sklearn.model_selection import ParameterGrid
np.random.seed(42)
sns.set_style('whitegrid')
print("✓ All modules loaded!")
print("\n" + "="*60)
print(" BENCHMARK: OptimizR (Rust) vs Pure Python")
print("="*60)
_ = (Callable, Tuple, ParameterGrid,)
✓ All modules loaded!
============================================================
BENCHMARK: OptimizR (Rust) vs Pure Python
============================================================
In [2]:
# pyright: reportArgumentType=false, reportUnusedImport=false, reportUnusedVariable=false, reportUnusedExpression=false, reportCallIssue=false, reportAttributeAccessIssue=false, reportOptionalMemberAccess=false, reportOperatorIssue=false, reportGeneralTypeIssues=false, reportReturnType=false, reportAssignmentType=false, reportIndexIssue=false, reportDeprecated=false, reportUndefinedVariable=false, reportPrivateImportUsage=false
def benchmark_hmm(n_obs_list=(500, 1000, 2500, 5000), n_runs=3):
"""
Benchmark HMM fitting across multiple data sizes.
Note: capped at 5,000 observations to avoid kernel pressure on
typical laptops while still showing the scaling trend clearly.
"""
results = []
for n_obs in n_obs_list:
print(f"\n📊 Testing HMM with {n_obs:,} observations...")
# Generate synthetic 1D data
rng = np.random.default_rng(42)
data = rng.standard_normal(n_obs) * 0.02 + 0.001
data_reshaped = data.reshape(-1, 1) # hmmlearn needs 2D
# Benchmark OptimizR (Rust)
rust_times = []
for _ in range(n_runs):
hmm_rust = HMM(n_states=3)
start = time.perf_counter()
hmm_rust.fit(data, n_iterations=50, tolerance=1e-4)
rust_times.append(time.perf_counter() - start)
rust_mean = float(np.mean(rust_times))
rust_std = float(np.std(rust_times))
# Benchmark hmmlearn (Python/Cython)
python_times = []
for _ in range(n_runs):
hmm_py = hmm.GaussianHMM(
n_components=3,
covariance_type='spherical',
n_iter=50,
tol=1e-4,
random_state=42,
)
start = time.perf_counter()
hmm_py.fit(data_reshaped)
python_times.append(time.perf_counter() - start)
python_mean = float(np.mean(python_times))
python_std = float(np.std(python_times))
speedup = python_mean / rust_mean
results.append({
'n_obs': n_obs,
'rust_time': rust_mean,
'rust_std': rust_std,
'python_time': python_mean,
'python_std': python_std,
'speedup': speedup,
})
print(f" OptimizR: {rust_mean*1000:.1f}ms ± {rust_std*1000:.1f}ms")
print(f" hmmlearn: {python_mean*1000:.1f}ms ± {python_std*1000:.1f}ms")
print(f" 🚀 Speedup: {speedup:.1f}x")
return pd.DataFrame(results)
hmm_results = benchmark_hmm()
print("\n" + "="*60)
print(f"Average HMM speedup: {hmm_results['speedup'].mean():.1f}x")
print("="*60)
Model is not converging. Current: 1265.4250492350661 is not greater than 1265.4417975561971. Delta is -0.016748321130990007 Model is not converging. Current: 1265.4250492350661 is not greater than 1265.4417975561971. Delta is -0.016748321130990007 Model is not converging. Current: 1265.4250492350661 is not greater than 1265.4417975561971. Delta is -0.016748321130990007
📊 Testing HMM with 500 observations... OptimizR: 2.7ms ± 0.8ms hmmlearn: 66.1ms ± 39.3ms 🚀 Speedup: 24.2x 📊 Testing HMM with 1,000 observations...
Model is not converging. Current: 2503.588336673215 is not greater than 2503.589554785923. Delta is -0.001218112708102126 Model is not converging. Current: 2503.588336673215 is not greater than 2503.589554785923. Delta is -0.001218112708102126 Model is not converging. Current: 2503.5883366732173 is not greater than 2503.5895547859236. Delta is -0.0012181127062831365
OptimizR: 5.9ms ± 0.2ms hmmlearn: 58.2ms ± 1.9ms 🚀 Speedup: 9.9x 📊 Testing HMM with 2,500 observations... OptimizR: 13.3ms ± 0.8ms hmmlearn: 127.5ms ± 4.5ms 🚀 Speedup: 9.6x 📊 Testing HMM with 5,000 observations...
Model is not converging. Current: 12470.144154851318 is not greater than 12470.144321406788. Delta is -0.00016655547005939297 Model is not converging. Current: 12470.144154851318 is not greater than 12470.144321406788. Delta is -0.00016655547005939297 Model is not converging. Current: 12470.144154851318 is not greater than 12470.144321406788. Delta is -0.00016655547005939297
OptimizR: 22.8ms ± 1.0ms hmmlearn: 183.2ms ± 2.0ms 🚀 Speedup: 8.0x ============================================================ Average HMM speedup: 12.9x ============================================================
In [3]:
# pyright: reportArgumentType=false, reportUnusedImport=false, reportUnusedVariable=false, reportUnusedExpression=false, reportCallIssue=false, reportAttributeAccessIssue=false, reportOptionalMemberAccess=false, reportOperatorIssue=false, reportGeneralTypeIssues=false, reportReturnType=false, reportAssignmentType=false, reportIndexIssue=false, reportDeprecated=false, reportUndefinedVariable=false, reportPrivateImportUsage=false
def python_mcmc(log_likelihood_fn, initial_params, param_bounds,
n_samples=2000, burn_in=500, proposal_std=0.1, seed=42):
"""Pure NumPy Metropolis-Hastings sampler (single chain)."""
rng = np.random.default_rng(seed)
dim = len(initial_params)
bounds = np.asarray(param_bounds, dtype=float)
current = np.asarray(initial_params, dtype=float).copy()
current_ll = log_likelihood_fn(current.tolist())
total = burn_in + n_samples
samples = np.empty((n_samples, dim))
accepted = 0
out_idx = 0
for i in range(total):
proposal = current + rng.normal(0.0, proposal_std, size=dim)
# reflect at bounds
proposal = np.clip(proposal, bounds[:, 0], bounds[:, 1])
proposal_ll = log_likelihood_fn(proposal.tolist())
if np.log(rng.random()) < (proposal_ll - current_ll):
current = proposal
current_ll = proposal_ll
if i >= burn_in:
accepted += 1
if i >= burn_in:
samples[out_idx] = current
out_idx += 1
return samples
def benchmark_mcmc(n_samples_list=(500, 1000, 2500, 5000), n_runs=3):
"""
Benchmark Metropolis-Hastings on a 2D Gaussian target.
"""
results = []
# Target: 2D Gaussian centered at (1, -1) with unit variance
def log_likelihood(theta):
a, b = theta[0], theta[1]
return -0.5 * ((a - 1.0) ** 2 + (b + 1.0) ** 2)
bounds = [(-5.0, 5.0), (-5.0, 5.0)]
initial = np.array([0.0, 0.0])
for n_samples in n_samples_list:
print(f"\n🔗 Testing MCMC with {n_samples:,} samples...")
# Benchmark OptimizR (Rust)
rust_times = []
for _ in range(n_runs):
start = time.perf_counter()
_ = mcmc_sample(
log_likelihood_fn=log_likelihood,
initial_params=initial,
param_bounds=bounds,
n_samples=n_samples,
burn_in=max(200, n_samples // 5),
proposal_std=0.5,
)
rust_times.append(time.perf_counter() - start)
rust_mean = float(np.mean(rust_times))
rust_std = float(np.std(rust_times))
# Benchmark Pure Python MCMC
python_times = []
for _ in range(n_runs):
start = time.perf_counter()
_ = python_mcmc(
log_likelihood_fn=log_likelihood,
initial_params=initial,
param_bounds=bounds,
n_samples=n_samples,
burn_in=max(200, n_samples // 5),
proposal_std=0.5,
)
python_times.append(time.perf_counter() - start)
python_mean = float(np.mean(python_times))
python_std = float(np.std(python_times))
speedup = python_mean / rust_mean
results.append({
'n_samples': n_samples,
'rust_time': rust_mean,
'rust_std': rust_std,
'python_time': python_mean,
'python_std': python_std,
'speedup': speedup,
})
print(f" OptimizR: {rust_mean*1000:.1f}ms ± {rust_std*1000:.1f}ms")
print(f" Pure Python: {python_mean*1000:.1f}ms ± {python_std*1000:.1f}ms")
print(f" 🚀 Speedup: {speedup:.1f}x")
return pd.DataFrame(results)
mcmc_results = benchmark_mcmc()
print("\n" + "="*60)
print(f"Average MCMC speedup: {mcmc_results['speedup'].mean():.1f}x")
print("="*60)
🔗 Testing MCMC with 500 samples... OptimizR: 1.6ms ± 0.8ms Pure Python: 15.9ms ± 1.4ms 🚀 Speedup: 9.7x 🔗 Testing MCMC with 1,000 samples... OptimizR: 2.2ms ± 0.1ms Pure Python: 17.2ms ± 0.3ms 🚀 Speedup: 7.7x 🔗 Testing MCMC with 2,500 samples... OptimizR: 4.2ms ± 0.1ms Pure Python: 34.4ms ± 4.2ms 🚀 Speedup: 8.2x 🔗 Testing MCMC with 5,000 samples... OptimizR: 11.7ms ± 2.5ms Pure Python: 64.4ms ± 7.6ms 🚀 Speedup: 5.5x ============================================================ Average MCMC speedup: 7.8x ============================================================
In [5]:
# pyright: reportArgumentType=false, reportUnusedImport=false, reportUnusedVariable=false, reportUnusedExpression=false, reportCallIssue=false, reportAttributeAccessIssue=false, reportOptionalMemberAccess=false, reportOperatorIssue=false, reportGeneralTypeIssues=false, reportReturnType=false, reportAssignmentType=false, reportIndexIssue=false, reportDeprecated=false, reportUndefinedVariable=false, reportPrivateImportUsage=false
def rosenbrock(x):
"""N-dimensional Rosenbrock function."""
x = np.asarray(x, dtype=float)
return float(np.sum(100.0 * (x[1:] - x[:-1] ** 2) ** 2 + (1.0 - x[:-1]) ** 2))
def benchmark_de(dimensions=(2, 5, 10), n_runs=3, popsize=15, maxiter=100):
"""
Benchmark Differential Evolution on Rosenbrock.
"""
results = []
for dim in dimensions:
print(f"\n🎯 Testing DE with {dim}D Rosenbrock...")
bounds = [(-5.0, 5.0)] * dim
# Benchmark OptimizR (Rust) — returns (best_x, best_f)
rust_times = []
rust_results = []
for _ in range(n_runs):
start = time.perf_counter()
_, best_f = differential_evolution(
objective_fn=rosenbrock,
bounds=bounds,
popsize=popsize,
maxiter=maxiter,
tol=1e-6,
seed=42,
)
rust_times.append(time.perf_counter() - start)
rust_results.append(best_f)
rust_mean = float(np.mean(rust_times))
rust_std = float(np.std(rust_times))
rust_quality = float(np.mean(rust_results))
# Benchmark SciPy — returns OptimizeResult
python_times = []
python_results = []
for _ in range(n_runs):
start = time.perf_counter()
result = scipy_de(
func=rosenbrock,
bounds=bounds,
popsize=popsize,
maxiter=maxiter,
tol=1e-6,
seed=42,
workers=1,
polish=False,
)
python_times.append(time.perf_counter() - start)
python_results.append(result.fun)
python_mean = float(np.mean(python_times))
python_std = float(np.std(python_times))
python_quality = float(np.mean(python_results))
speedup = python_mean / rust_mean
results.append({
'dimensions': dim,
'rust_time': rust_mean,
'rust_std': rust_std,
'rust_quality': rust_quality,
'python_time': python_mean,
'python_std': python_std,
'python_quality': python_quality,
'speedup': speedup,
})
print(f" OptimizR: {rust_mean*1000:.1f}ms ± {rust_std*1000:.1f}ms (f={rust_quality:.2e})")
print(f" SciPy: {python_mean*1000:.1f}ms ± {python_std*1000:.1f}ms (f={python_quality:.2e})")
print(f" 🚀 Speedup: {speedup:.1f}x")
return pd.DataFrame(results)
de_results = benchmark_de()
print("\n" + "="*60)
print(f"Average DE speedup: {de_results['speedup'].mean():.1f}x")
print("="*60)
🎯 Testing DE with 2D Rosenbrock... OptimizR: 41.1ms ± 1.0ms (f=2.20e-10) SciPy: 252.7ms ± 6.9ms (f=1.67e-27) 🚀 Speedup: 6.2x 🎯 Testing DE with 5D Rosenbrock... OptimizR: 138.2ms ± 1.1ms (f=1.75e+00) SciPy: 582.6ms ± 0.8ms (f=5.15e-04) 🚀 Speedup: 4.2x 🎯 Testing DE with 10D Rosenbrock... OptimizR: 318.5ms ± 7.3ms (f=1.29e+01) SciPy: 1196.5ms ± 3.9ms (f=4.76e+00) 🚀 Speedup: 3.8x ============================================================ Average DE speedup: 4.7x ============================================================
In [6]:
# pyright: reportArgumentType=false, reportUnusedImport=false, reportUnusedVariable=false, reportUnusedExpression=false, reportCallIssue=false, reportAttributeAccessIssue=false, reportOptionalMemberAccess=false, reportOperatorIssue=false, reportGeneralTypeIssues=false, reportReturnType=false, reportAssignmentType=false, reportIndexIssue=false, reportDeprecated=false, reportUndefinedVariable=false, reportPrivateImportUsage=false
def sphere_function(x):
"""Simple sphere function for testing."""
x = np.asarray(x, dtype=float)
return float(np.sum(x ** 2))
def python_grid_search(objective_fn, bounds, n_points):
"""Pure Python grid search implementation."""
grids = [np.linspace(low, high, n_points) for low, high in bounds]
meshes = np.meshgrid(*grids, indexing='ij')
points = np.vstack([m.ravel() for m in meshes]).T
best_value = np.inf
best_params = None
for point in points:
value = objective_fn(point)
if value < best_value:
best_value = value
best_params = point
return best_params, best_value
def benchmark_grid_search(n_points_list=(8, 12, 16), dimensions=(2, 3), n_runs=3,
max_total_evals=20000):
"""Benchmark Grid Search (capped at 20k evals to keep runtime bounded)."""
results = []
for dim in dimensions:
for n_points in n_points_list:
total_evals = n_points ** dim
if total_evals > max_total_evals:
continue
print(f"\n🔍 Testing Grid Search: {dim}D, {n_points} points/dim ({total_evals:,} total)...")
bounds = [(-10.0, 10.0)] * dim
# Benchmark OptimizR (Rust)
rust_times = []
for _ in range(n_runs):
start = time.perf_counter()
_ = grid_search(
objective_fn=sphere_function,
bounds=bounds,
n_points=n_points,
)
rust_times.append(time.perf_counter() - start)
rust_mean = float(np.mean(rust_times))
rust_std = float(np.std(rust_times))
# Benchmark Pure Python
python_times = []
for _ in range(n_runs):
start = time.perf_counter()
_, _ = python_grid_search(
objective_fn=sphere_function,
bounds=bounds,
n_points=n_points,
)
python_times.append(time.perf_counter() - start)
python_mean = float(np.mean(python_times))
python_std = float(np.std(python_times))
speedup = python_mean / rust_mean
results.append({
'dimensions': dim,
'n_points': n_points,
'total_evals': total_evals,
'rust_time': rust_mean,
'rust_std': rust_std,
'python_time': python_mean,
'python_std': python_std,
'speedup': speedup,
})
print(f" OptimizR: {rust_mean*1000:.1f}ms ± {rust_std*1000:.1f}ms")
print(f" Pure Python: {python_mean*1000:.1f}ms ± {python_std*1000:.1f}ms")
print(f" 🚀 Speedup: {speedup:.1f}x")
return pd.DataFrame(results)
grid_results = benchmark_grid_search()
print("\n" + "="*60)
print(f"Average Grid Search speedup: {grid_results['speedup'].mean():.1f}x")
print("="*60)
🔍 Testing Grid Search: 2D, 8 points/dim (64 total)... OptimizR: 0.8ms ± 0.5ms Pure Python: 0.6ms ± 0.0ms 🚀 Speedup: 0.7x 🔍 Testing Grid Search: 2D, 12 points/dim (144 total)... OptimizR: 1.4ms ± 0.3ms Pure Python: 1.1ms ± 0.0ms 🚀 Speedup: 0.7x 🔍 Testing Grid Search: 2D, 16 points/dim (256 total)... OptimizR: 2.1ms ± 0.1ms Pure Python: 1.8ms ± 0.0ms 🚀 Speedup: 0.8x 🔍 Testing Grid Search: 3D, 8 points/dim (512 total)... OptimizR: 3.9ms ± 0.1ms Pure Python: 3.6ms ± 0.0ms 🚀 Speedup: 0.9x 🔍 Testing Grid Search: 3D, 12 points/dim (1,728 total)... OptimizR: 14.2ms ± 1.4ms Pure Python: 11.6ms ± 0.3ms 🚀 Speedup: 0.8x 🔍 Testing Grid Search: 3D, 16 points/dim (4,096 total)... OptimizR: 30.7ms ± 0.0ms Pure Python: 26.0ms ± 0.3ms 🚀 Speedup: 0.8x ============================================================ Average Grid Search speedup: 0.8x ============================================================
In [7]:
# pyright: reportArgumentType=false, reportUnusedImport=false, reportUnusedVariable=false, reportUnusedExpression=false, reportCallIssue=false, reportAttributeAccessIssue=false, reportOptionalMemberAccess=false, reportOperatorIssue=false, reportGeneralTypeIssues=false, reportReturnType=false, reportAssignmentType=false, reportIndexIssue=false, reportDeprecated=false, reportUndefinedVariable=false, reportPrivateImportUsage=false
def benchmark_information_theory(n_obs_list=(500, 1000, 2500, 5000), n_runs=3):
"""Benchmark Shannon Entropy and Mutual Information."""
results = []
for n_obs in n_obs_list:
print(f"\n📈 Testing Information Theory with {n_obs:,} observations...")
rng = np.random.default_rng(42)
x = rng.standard_normal(n_obs)
y = 0.7 * x + 0.3 * rng.standard_normal(n_obs)
# ----- Mutual Information: OptimizR -----
rust_mi_times = []
for _ in range(n_runs):
start = time.perf_counter()
_ = mutual_information(x, y)
rust_mi_times.append(time.perf_counter() - start)
rust_mi_mean = float(np.mean(rust_mi_times))
rust_mi_std = float(np.std(rust_mi_times))
# ----- Mutual Information: sklearn (with discretization cost) -----
python_mi_times = []
for _ in range(n_runs):
start = time.perf_counter()
x_d = np.digitize(x, bins=np.linspace(x.min(), x.max(), 20))
y_d = np.digitize(y, bins=np.linspace(y.min(), y.max(), 20))
_ = mutual_info_score(x_d, y_d)
python_mi_times.append(time.perf_counter() - start)
python_mi_mean = float(np.mean(python_mi_times))
python_mi_std = float(np.std(python_mi_times))
mi_speedup = python_mi_mean / rust_mi_mean
# ----- Shannon Entropy: OptimizR -----
rust_entropy_times = []
for _ in range(n_runs):
start = time.perf_counter()
_ = shannon_entropy(x)
rust_entropy_times.append(time.perf_counter() - start)
rust_entropy_mean = float(np.mean(rust_entropy_times))
rust_entropy_std = float(np.std(rust_entropy_times))
# ----- Shannon Entropy: Pure Python -----
def python_shannon_entropy(data, n_bins=20):
hist, _ = np.histogram(data, bins=n_bins, density=True)
hist = hist[hist > 0]
bin_width = (data.max() - data.min()) / n_bins
prob = hist * bin_width
prob = prob / prob.sum()
return -float(np.sum(prob * np.log2(prob)))
python_entropy_times = []
for _ in range(n_runs):
start = time.perf_counter()
_ = python_shannon_entropy(x)
python_entropy_times.append(time.perf_counter() - start)
python_entropy_mean = float(np.mean(python_entropy_times))
python_entropy_std = float(np.std(python_entropy_times))
entropy_speedup = python_entropy_mean / rust_entropy_mean
results.append({
'n_obs': n_obs,
'rust_mi_time': rust_mi_mean,
'python_mi_time': python_mi_mean,
'mi_speedup': mi_speedup,
'rust_entropy_time': rust_entropy_mean,
'python_entropy_time': python_entropy_mean,
'entropy_speedup': entropy_speedup,
})
print(f" Mutual Information:")
print(f" OptimizR: {rust_mi_mean*1000:.2f}ms ± {rust_mi_std*1000:.2f}ms")
print(f" sklearn: {python_mi_mean*1000:.2f}ms ± {python_mi_std*1000:.2f}ms")
print(f" 🚀 Speedup: {mi_speedup:.1f}x")
print(f" Shannon Entropy:")
print(f" OptimizR: {rust_entropy_mean*1000:.2f}ms ± {rust_entropy_std*1000:.2f}ms")
print(f" NumPy: {python_entropy_mean*1000:.2f}ms ± {python_entropy_std*1000:.2f}ms")
print(f" 🚀 Speedup: {entropy_speedup:.1f}x")
return pd.DataFrame(results)
info_results = benchmark_information_theory()
print("\n" + "="*60)
print(f"Average MI speedup: {info_results['mi_speedup'].mean():.1f}x")
print(f"Average Entropy speedup: {info_results['entropy_speedup'].mean():.1f}x")
print("="*60)
📈 Testing Information Theory with 500 observations...
Mutual Information:
OptimizR: 0.25ms ± 0.28ms
sklearn: 6.18ms ± 6.52ms
🚀 Speedup: 25.2x
Shannon Entropy:
OptimizR: 0.03ms ± 0.01ms
NumPy: 0.22ms ± 0.05ms
🚀 Speedup: 8.7x
📈 Testing Information Theory with 1,000 observations...
Mutual Information:
OptimizR: 0.12ms ± 0.04ms
sklearn: 1.61ms ± 0.04ms
🚀 Speedup: 13.8x
Shannon Entropy:
OptimizR: 0.04ms ± 0.01ms
NumPy: 0.22ms ± 0.03ms
🚀 Speedup: 5.0x
📈 Testing Information Theory with 2,500 observations...
Mutual Information:
OptimizR: 0.24ms ± 0.04ms
sklearn: 1.78ms ± 0.06ms
🚀 Speedup: 7.4x
Shannon Entropy:
OptimizR: 0.10ms ± 0.00ms
NumPy: 0.23ms ± 0.02ms
🚀 Speedup: 2.3x
📈 Testing Information Theory with 5,000 observations...
Mutual Information:
OptimizR: 0.49ms ± 0.07ms
sklearn: 2.21ms ± 0.15ms
🚀 Speedup: 4.5x
Shannon Entropy:
OptimizR: 0.20ms ± 0.01ms
NumPy: 0.26ms ± 0.02ms
🚀 Speedup: 1.3x
============================================================
Average MI speedup: 12.8x
Average Entropy speedup: 4.3x
============================================================
In [11]:
# pyright: reportArgumentType=false, reportUnusedImport=false, reportUnusedVariable=false, reportUnusedExpression=false, reportCallIssue=false, reportAttributeAccessIssue=false, reportOptionalMemberAccess=false, reportOperatorIssue=false, reportGeneralTypeIssues=false, reportReturnType=false, reportAssignmentType=false, reportIndexIssue=false, reportDeprecated=false, reportUndefinedVariable=false, reportPrivateImportUsage=false
import math
from optimizr import path_signature, simulate_hawkes, robust_drift
# ---------- Pure-Python references ----------
def python_path_signature(path, level):
"""Truncated path signature up to `level`, computed with a Chen-style
iterative tensor product. O((level * d) ** level)."""
path = np.asarray(path, dtype=float)
n, d = path.shape
increments = np.diff(path, axis=0) # (n-1, d)
# signature[k] is a flat tensor of shape (d,)*k
signature = [np.array([1.0])] # level 0
for k in range(1, level + 1):
signature.append(np.zeros((d,) * k))
for inc in increments:
# Chen's identity: S_{0,t+dt} = S_{0,t} ⊗ exp(dx)
# exp(dx) truncated at `level`: 1 + dx + dx⊗dx/2 + ...
new_sig = [np.array([1.0])]
for k in range(1, level + 1):
acc = np.zeros((d,) * k)
# S_{0,t}[j] ⊗ inc^{k-j} / (k-j)!
for j in range(0, k + 1):
left = signature[j]
# inc^{k-j} / (k-j)!
if k - j == 0:
right = np.array(1.0)
else:
right = inc.copy()
for _ in range(k - j - 1):
right = np.multiply.outer(right, inc)
right = right / math.factorial(k - j)
if j == 0:
acc = acc + right
elif k - j == 0:
acc = acc + left
else:
acc = acc + np.multiply.outer(left, right)
new_sig.append(acc)
signature = new_sig
# flatten to vector (drop level-0 = 1.0)
return np.concatenate([s.ravel() for s in signature[1:]])
def python_simulate_hawkes(baseline, alpha, beta, t_max, seed=42):
"""Ogata thinning algorithm for an exponential-kernel Hawkes process."""
rng = np.random.default_rng(seed)
events = []
t = 0.0
intensity = baseline
while t < t_max:
# upper bound on intensity
m = intensity
if m <= 0.0:
break
u = rng.random()
w = -np.log(u) / m
t = t + w
if t >= t_max:
break
# decay
intensity_decayed = baseline + (intensity - baseline) * np.exp(-beta * w)
d = rng.random()
if d * m <= intensity_decayed:
events.append(t)
intensity = intensity_decayed + alpha
else:
intensity = intensity_decayed
return np.asarray(events, dtype=float)
def python_robust_drift(observations, dt, huber_delta=1.345, max_iter=200, tol=1e-9):
"""IRLS Huber M-estimator for the drift of dX = mu dt + sigma dW."""
obs = np.asarray(observations, dtype=float)
inc = np.diff(obs) / dt
mu = float(np.median(inc))
for _ in range(max_iter):
r = inc - mu
s = 1.4826 * np.median(np.abs(r - np.median(r))) + 1e-12
z = r / s
# Huber weights
w = np.where(np.abs(z) <= huber_delta, 1.0, huber_delta / np.abs(z))
new_mu = float(np.sum(w * inc) / np.sum(w))
if abs(new_mu - mu) < tol:
mu = new_mu
break
mu = new_mu
return mu
# ---------- Benchmark driver ----------
def benchmark_v2_primitives(n_runs=3):
rows = []
# ----- Path signature -----
print("\n🔣 Path signature (level 3, 2-D Brownian path)")
rng = np.random.default_rng(0)
sig_sizes = (200, 400, 800)
sig_results = []
for n_pts in sig_sizes:
path = np.cumsum(rng.standard_normal((n_pts, 2)) * (1.0 / n_pts) ** 0.5, axis=0)
rust_t = []
for _ in range(n_runs):
t0 = time.perf_counter()
_ = path_signature(path, 3)
rust_t.append(time.perf_counter() - t0)
py_t = []
for _ in range(n_runs):
t0 = time.perf_counter()
_ = python_path_signature(path, 3)
py_t.append(time.perf_counter() - t0)
rmean, pmean = float(np.mean(rust_t)), float(np.mean(py_t))
sp = pmean / rmean
sig_results.append({'n_pts': n_pts, 'rust_time': rmean,
'python_time': pmean, 'speedup': sp})
print(f" n={n_pts:4d}: Rust {rmean*1000:7.2f}ms · Py {pmean*1000:8.2f}ms · 🚀 {sp:6.1f}x")
sig_df = pd.DataFrame(sig_results)
sig_avg = float(sig_df['speedup'].mean())
rows.append({'task': 'Path signature (lvl 3)', 'baseline': 'Pure NumPy',
'avg_speedup': sig_avg, 'max_speedup': float(sig_df['speedup'].max())})
# ----- Hawkes simulation -----
print("\n💥 Hawkes simulation (exponential kernel, baseline=1, α=0.5, β=1)")
hawk_horizons = (100.0, 500.0, 2000.0)
hawk_results = []
for t_max in hawk_horizons:
rust_t = []
for k in range(n_runs):
t0 = time.perf_counter()
_ = simulate_hawkes(1.0, 0.5, 1.0, t_max, kernel_type='exponential', seed=42 + k)
rust_t.append(time.perf_counter() - t0)
py_t = []
for k in range(n_runs):
t0 = time.perf_counter()
_ = python_simulate_hawkes(1.0, 0.5, 1.0, t_max, seed=42 + k)
py_t.append(time.perf_counter() - t0)
rmean, pmean = float(np.mean(rust_t)), float(np.mean(py_t))
sp = pmean / rmean
hawk_results.append({'t_max': t_max, 'rust_time': rmean,
'python_time': pmean, 'speedup': sp})
print(f" T={t_max:7.0f}: Rust {rmean*1000:7.2f}ms · Py {pmean*1000:8.2f}ms · 🚀 {sp:6.1f}x")
hawk_df = pd.DataFrame(hawk_results)
hawk_avg = float(hawk_df['speedup'].mean())
rows.append({'task': 'Hawkes simulation', 'baseline': 'Pure NumPy (Ogata)',
'avg_speedup': hawk_avg, 'max_speedup': float(hawk_df['speedup'].max())})
# ----- Robust drift -----
print("\n🛡️ Robust drift (Huber M-estimator, σ=0.2, μ_true=0.05)")
drift_sizes = (1000, 2500, 5000)
drift_results = []
for n_obs in drift_sizes:
dt = 1.0 / 252.0
mu_true, sigma = 0.05, 0.2
rng2 = np.random.default_rng(7)
inc = mu_true * dt + sigma * np.sqrt(dt) * rng2.standard_normal(n_obs)
# add 5 % heavy-tailed outliers
mask = rng2.random(n_obs) < 0.05
inc[mask] = inc[mask] + sigma * np.sqrt(dt) * 8.0 * rng2.standard_normal(int(mask.sum()))
obs = np.concatenate([[0.0], np.cumsum(inc)])
rust_t = []
for _ in range(n_runs):
t0 = time.perf_counter()
_ = robust_drift(obs, dt)
rust_t.append(time.perf_counter() - t0)
py_t = []
for _ in range(n_runs):
t0 = time.perf_counter()
_ = python_robust_drift(obs, dt)
py_t.append(time.perf_counter() - t0)
rmean, pmean = float(np.mean(rust_t)), float(np.mean(py_t))
sp = pmean / rmean
drift_results.append({'n_obs': n_obs, 'rust_time': rmean,
'python_time': pmean, 'speedup': sp})
print(f" n={n_obs:5d}: Rust {rmean*1000:7.2f}ms · Py {pmean*1000:8.2f}ms · 🚀 {sp:6.1f}x")
drift_df = pd.DataFrame(drift_results)
drift_avg = float(drift_df['speedup'].mean())
rows.append({'task': 'Robust drift (Huber)', 'baseline': 'Pure NumPy (IRLS)',
'avg_speedup': drift_avg, 'max_speedup': float(drift_df['speedup'].max())})
return pd.DataFrame(rows), sig_df, hawk_df, drift_df
v2_results, sig_df, hawk_df, drift_df = benchmark_v2_primitives()
print("\n" + "=" * 60)
print(v2_results.to_string(index=False))
print("=" * 60)
print(f"Average v2.0 primitive speedup: {v2_results['avg_speedup'].mean():.1f}x")
print("=" * 60)
🔣 Path signature (level 3, 2-D Brownian path) n= 200: Rust 0.95ms · Py 13.58ms · 🚀 14.3x n= 400: Rust 1.79ms · Py 22.73ms · 🚀 12.7x n= 800: Rust 2.93ms · Py 41.78ms · 🚀 14.3x 💥 Hawkes simulation (exponential kernel, baseline=1, α=0.5, β=1) T= 100: Rust 1.82ms · Py 0.71ms · 🚀 0.4x T= 500: Rust 20.74ms · Py 3.59ms · 🚀 0.2x T= 2000: Rust 422.57ms · Py 23.60ms · 🚀 0.1x 🛡️ Robust drift (Huber M-estimator, σ=0.2, μ_true=0.05) n= 1000: Rust 0.89ms · Py 2.61ms · 🚀 2.9x n= 2500: Rust 2.47ms · Py 4.05ms · 🚀 1.6x n= 5000: Rust 4.00ms · Py 3.12ms · 🚀 0.8x ============================================================ task baseline avg_speedup max_speedup Path signature (lvl 3) Pure NumPy 13.736466 14.255275 Hawkes simulation Pure NumPy (Ogata) 0.205974 0.388756 Robust drift (Huber) Pure NumPy (IRLS) 1.787452 2.939508 ============================================================ Average v2.0 primitive speedup: 5.2x ============================================================
In [12]:
# pyright: reportArgumentType=false, reportUnusedImport=false, reportUnusedVariable=false, reportUnusedExpression=false, reportCallIssue=false, reportAttributeAccessIssue=false, reportOptionalMemberAccess=false, reportOperatorIssue=false, reportGeneralTypeIssues=false, reportReturnType=false, reportAssignmentType=false, reportIndexIssue=false, reportDeprecated=false, reportUndefinedVariable=false, reportPrivateImportUsage=false
# Create summary table
summary = pd.DataFrame([
{
'Algorithm': 'Hidden Markov Model',
'Python Library': 'hmmlearn',
'Avg Speedup': hmm_results['speedup'].mean(),
'Max Speedup': hmm_results['speedup'].max(),
'Min Speedup': hmm_results['speedup'].min()
},
{
'Algorithm': 'MCMC Sampling',
'Python Library': 'Pure NumPy',
'Avg Speedup': mcmc_results['speedup'].mean(),
'Max Speedup': mcmc_results['speedup'].max(),
'Min Speedup': mcmc_results['speedup'].min()
},
{
'Algorithm': 'Differential Evolution',
'Python Library': 'scipy.optimize',
'Avg Speedup': de_results['speedup'].mean(),
'Max Speedup': de_results['speedup'].max(),
'Min Speedup': de_results['speedup'].min()
},
{
'Algorithm': 'Grid Search',
'Python Library': 'Pure NumPy',
'Avg Speedup': grid_results['speedup'].mean(),
'Max Speedup': grid_results['speedup'].max(),
'Min Speedup': grid_results['speedup'].min()
},
{
'Algorithm': 'Mutual Information',
'Python Library': 'sklearn.metrics',
'Avg Speedup': info_results['mi_speedup'].mean(),
'Max Speedup': info_results['mi_speedup'].max(),
'Min Speedup': info_results['mi_speedup'].min()
},
{
'Algorithm': 'Shannon Entropy',
'Python Library': 'Pure NumPy',
'Avg Speedup': info_results['entropy_speedup'].mean(),
'Max Speedup': info_results['entropy_speedup'].max(),
'Min Speedup': info_results['entropy_speedup'].min()
},
{
'Algorithm': 'Path Signature (lvl 3)',
'Python Library': 'Pure NumPy',
'Avg Speedup': sig_df['speedup'].mean(),
'Max Speedup': sig_df['speedup'].max(),
'Min Speedup': sig_df['speedup'].min()
},
{
'Algorithm': 'Hawkes Simulation',
'Python Library': 'Pure NumPy (Ogata)',
'Avg Speedup': hawk_df['speedup'].mean(),
'Max Speedup': hawk_df['speedup'].max(),
'Min Speedup': hawk_df['speedup'].min()
},
{
'Algorithm': 'Robust Drift (Huber)',
'Python Library': 'Pure NumPy (IRLS)',
'Avg Speedup': drift_df['speedup'].mean(),
'Max Speedup': drift_df['speedup'].max(),
'Min Speedup': drift_df['speedup'].min()
}
])
print("\n" + "="*80)
print(" FINAL BENCHMARK RESULTS")
print("="*80)
print(summary.to_string(index=False))
print("="*80)
overall_avg = summary['Avg Speedup'].mean()
overall_max = summary['Max Speedup'].max()
print(f"\n🎉 OVERALL AVERAGE SPEEDUP: {overall_avg:.1f}x")
print(f"🚀 MAXIMUM SPEEDUP ACHIEVED: {overall_max:.1f}x")
print(f"\n✅ Target of 50-100x improvement: {'ACHIEVED' if overall_avg >= 50 else 'PARTIAL'}")
================================================================================
FINAL BENCHMARK RESULTS
================================================================================
Algorithm Python Library Avg Speedup Max Speedup Min Speedup
Hidden Markov Model hmmlearn 12.903540 24.150751 8.034404
MCMC Sampling Pure NumPy 7.761596 9.669656 5.506439
Differential Evolution scipy.optimize 4.707406 6.150792 3.755996
Grid Search Pure NumPy 0.806245 0.906713 0.688598
Mutual Information sklearn.metrics 12.753020 25.216335 4.511097
Shannon Entropy Pure NumPy 4.311030 8.711705 1.280623
Path Signature (lvl 3) Pure NumPy 13.736466 14.255275 12.701160
Hawkes Simulation Pure NumPy (Ogata) 0.205974 0.388756 0.055857
Robust Drift (Huber) Pure NumPy (IRLS) 1.787452 2.939508 0.780180
================================================================================
🎉 OVERALL AVERAGE SPEEDUP: 6.6x
🚀 MAXIMUM SPEEDUP ACHIEVED: 25.2x
✅ Target of 50-100x improvement: PARTIAL
In [13]:
# pyright: reportArgumentType=false, reportUnusedImport=false, reportUnusedVariable=false, reportUnusedExpression=false, reportCallIssue=false, reportAttributeAccessIssue=false, reportOptionalMemberAccess=false, reportOperatorIssue=false, reportGeneralTypeIssues=false, reportReturnType=false, reportAssignmentType=false, reportIndexIssue=false, reportDeprecated=false, reportUndefinedVariable=false, reportPrivateImportUsage=false
import os
fig, axes = plt.subplots(2, 3, figsize=(18, 12))
# Plot 1: HMM scaling
axes[0, 0].plot(hmm_results['n_obs'], hmm_results['rust_time']*1000, 'o-', label='OptimizR (Rust)', linewidth=2)
axes[0, 0].plot(hmm_results['n_obs'], hmm_results['python_time']*1000, 's-', label='hmmlearn', linewidth=2)
axes[0, 0].set_xlabel('# Observations', fontsize=11)
axes[0, 0].set_ylabel('Time (ms)', fontsize=11)
axes[0, 0].set_title('HMM Performance', fontsize=13, fontweight='bold')
axes[0, 0].legend()
axes[0, 0].grid(alpha=0.3)
axes[0, 0].set_yscale('log')
# Plot 2: MCMC scaling
axes[0, 1].plot(mcmc_results['n_samples'], mcmc_results['rust_time']*1000, 'o-', label='OptimizR (Rust)', linewidth=2)
axes[0, 1].plot(mcmc_results['n_samples'], mcmc_results['python_time']*1000, 's-', label='Pure Python', linewidth=2)
axes[0, 1].set_xlabel('# MCMC Samples', fontsize=11)
axes[0, 1].set_ylabel('Time (ms)', fontsize=11)
axes[0, 1].set_title('MCMC Performance', fontsize=13, fontweight='bold')
axes[0, 1].legend()
axes[0, 1].grid(alpha=0.3)
axes[0, 1].set_yscale('log')
# Plot 3: DE scaling by dimension
axes[0, 2].plot(de_results['dimensions'], de_results['rust_time']*1000, 'o-', label='OptimizR (Rust)', linewidth=2)
axes[0, 2].plot(de_results['dimensions'], de_results['python_time']*1000, 's-', label='scipy.optimize', linewidth=2)
axes[0, 2].set_xlabel('Problem Dimension', fontsize=11)
axes[0, 2].set_ylabel('Time (ms)', fontsize=11)
axes[0, 2].set_title('Differential Evolution Performance', fontsize=13, fontweight='bold')
axes[0, 2].legend()
axes[0, 2].grid(alpha=0.3)
axes[0, 2].set_yscale('log')
# Plot 4: Grid Search scaling
axes[1, 0].plot(grid_results['total_evals'], grid_results['rust_time']*1000, 'o-', label='OptimizR (Rust)', linewidth=2)
axes[1, 0].plot(grid_results['total_evals'], grid_results['python_time']*1000, 's-', label='Pure Python', linewidth=2)
axes[1, 0].set_xlabel('Total Evaluations', fontsize=11)
axes[1, 0].set_ylabel('Time (ms)', fontsize=11)
axes[1, 0].set_title('Grid Search Performance', fontsize=13, fontweight='bold')
axes[1, 0].legend()
axes[1, 0].grid(alpha=0.3)
axes[1, 0].set_yscale('log')
axes[1, 0].set_xscale('log')
# Plot 5: Information Theory MI
axes[1, 1].plot(info_results['n_obs'], info_results['rust_mi_time']*1000, 'o-', label='OptimizR (Rust)', linewidth=2)
axes[1, 1].plot(info_results['n_obs'], info_results['python_mi_time']*1000, 's-', label='sklearn', linewidth=2)
axes[1, 1].set_xlabel('# Observations', fontsize=11)
axes[1, 1].set_ylabel('Time (ms)', fontsize=11)
axes[1, 1].set_title('Mutual Information Performance', fontsize=13, fontweight='bold')
axes[1, 1].legend()
axes[1, 1].grid(alpha=0.3)
axes[1, 1].set_yscale('log')
# Plot 6: Speedup comparison
algorithms = summary['Algorithm'].values
speedups = summary['Avg Speedup'].values
colors = plt.cm.RdYlGn(np.linspace(0.5, 1.0, len(algorithms)))
bars = axes[1, 2].barh(algorithms, speedups, color=colors, edgecolor='black', linewidth=1.5)
axes[1, 2].axvline(50, color='red', linestyle='--', linewidth=2, label='50x target', alpha=0.7)
axes[1, 2].axvline(100, color='darkred', linestyle='--', linewidth=2, label='100x target', alpha=0.7)
axes[1, 2].set_xlabel('Speedup Factor', fontsize=11)
axes[1, 2].set_title('Average Speedup by Algorithm', fontsize=13, fontweight='bold')
axes[1, 2].legend()
axes[1, 2].grid(alpha=0.3, axis='x')
for i, (bar, val) in enumerate(zip(bars, speedups)):
axes[1, 2].text(val + 2, bar.get_y() + bar.get_height()/2,
f'{val:.1f}x', va='center', fontweight='bold', fontsize=10)
plt.tight_layout()
out_dir = os.path.dirname(os.path.abspath('05_performance_benchmarks.ipynb'))
out_path = os.path.join(out_dir, 'outputs', 'benchmark_results.png')
os.makedirs(os.path.dirname(out_path), exist_ok=True)
plt.savefig(out_path, dpi=150, bbox_inches='tight')
plt.show()
print(f"\n✅ Benchmark visualization saved to: {out_path}")
✅ Benchmark visualization saved to: /Users/melvinalvarez/Documents/Workspace/optimiz-rs/examples/notebooks/outputs/benchmark_results.png