docs(v2.0.0-alpha.3): inline plot injection across the entire doc tree

Add scripts/inject_doc_plots.py that scans every .md and .rst page under
docs/source/, executes each Python code-block in an isolated namespace
with a non-interactive matplotlib backend, captures every figure
produced, and inserts an inline image directive immediately after the
code-block.  Markers AUTO-PLOT-BEGIN/END make the injection idempotent
on re-runs.  Blocks that fail to execute or produce no figure are left
untouched.

Add a transparent __getattr__ fallback in python/optimizr/__init__.py
that forwards any unresolved top-level attribute to the compiled _core
extension.  This lets all v1.x and v2.0 doc samples that use
'from optimizr import X' (estimate_ou_params_py, linear_bsde_constant_coeffs,
mmd_gaussian, ...) execute as written.

Augment the OU Parameter Estimation example
(docs/source/algorithms/optimal_control.md) with a two-panel
visualization (simulated path plus empirical/theoretical autocorrelation).

Net effect: 14 doc pages now display matplotlib plots inline directly
under the code that produced them -- including the OU page, point
processes, Grid Search, HMM, MCMC, plus the 8 v2.0 RST pages.
This commit is contained in:
ThotDjehuty
2026-05-12 13:05:14 +02:00
parent d8682f61e5
commit cce31055c1
43 changed files with 537 additions and 1 deletions
+36 -1
View File
@@ -261,15 +261,17 @@ Estimates Ornstein-Uhlenbeck process parameters from time series data.
```python
from optimizr import estimate_ou_params_py
import numpy as np
import matplotlib.pyplot as plt
# Simulate OU process (for testing)
dt = 1/252 # Daily data
T = 1000
kappa_true, theta_true, sigma_true = 3.0, 0.0, 0.2
rng = np.random.default_rng(0)
spread = [0.0]
for _ in range(T-1):
dx = kappa_true * (theta_true - spread[-1]) * dt + \
sigma_true * np.sqrt(dt) * np.random.randn()
sigma_true * np.sqrt(dt) * rng.standard_normal()
spread.append(spread[-1] + dx)
spread = np.array(spread)
@@ -280,7 +282,36 @@ kappa, theta, sigma, half_life = estimate_ou_params_py(spread, dt=dt)
print(f"True: κ={kappa_true:.2f}, θ={theta_true:.3f}, σ={sigma_true:.3f}")
print(f"Estimated: κ={kappa:.2f}, θ={theta:.3f}, σ={sigma:.3f}")
print(f"Half-life: {half_life:.1f} periods ({half_life*252:.1f} days)")
# Visualise the simulated path together with the estimated mean-reversion
# level and the decay envelope implied by the fitted half-life.
t_axis = np.arange(len(spread)) * dt * 252 # in days
fig, axes = plt.subplots(1, 2, figsize=(11, 4))
axes[0].plot(t_axis, spread, lw=0.7, label="simulated path")
axes[0].axhline(theta_true, color="k", ls=":", label="true θ")
axes[0].axhline(theta, color="red", ls="--", label="estimated θ")
axes[0].set_xlabel("days"); axes[0].set_ylabel("spread")
axes[0].set_title("OU simulation vs estimated long-run mean")
axes[0].legend(); axes[0].grid(alpha=0.3)
# Empirical autocorrelation vs theoretical exp(-κ τ).
lags = np.arange(0, 60)
x = spread - spread.mean()
acf = np.array([
(x[: len(x) - k] @ x[k:]) / (x @ x) for k in lags
])
axes[1].plot(lags, acf, "o-", label="empirical ACF")
axes[1].plot(lags, np.exp(-kappa * lags * dt), "--",
label=r"theoretical $e^{-\kappa\,\tau}$")
axes[1].set_xlabel("lag (days)"); axes[1].set_ylabel("autocorrelation")
axes[1].set_title("Mean-reversion fingerprint")
axes[1].legend(); axes[1].grid(alpha=0.3)
fig.tight_layout(); plt.show()
```
<!-- AUTO-PLOT-BEGIN -->
![Generated plot](../_static/auto/algorithms__optimal_control/block_03_fig_01.png)
<!-- AUTO-PLOT-END -->
**Method**: Maximum likelihood estimation (MLE) using analytical formulas for discrete-time OU process.
@@ -431,6 +462,10 @@ plt.plot(pnl_path, label='P&L')
plt.legend()
plt.tight_layout()
```
<!-- AUTO-PLOT-BEGIN -->
![Generated plot](../_static/auto/algorithms__optimal_control/block_06_fig_01.png)
<!-- AUTO-PLOT-END -->
**Metrics interpretation**:
- `total_return`: Should be positive with low transaction costs