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.
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 27 KiB |
@@ -40,6 +40,16 @@ Generic interacting-agent simulator (`consensus_dynamics`) — linear bounded-co
|
||||
ax.set_title('Bounded-confidence consensus, α = 0.3')
|
||||
fig.tight_layout(); plt.show()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.. AUTO-PLOT-BEGIN
|
||||
.. image:: ../_static/auto/algorithms__agent_based/block_03_fig_01.png
|
||||
:align: center
|
||||
:width: 80%
|
||||
|
||||
.. AUTO-PLOT-END
|
||||
.. image:: ../_static/v2/agent_based/plot_01.png
|
||||
:align: center
|
||||
:width: 80%
|
||||
@@ -56,6 +66,16 @@ Generic interacting-agent simulator (`consensus_dynamics`) — linear bounded-co
|
||||
ax.set_title('Convergence rate vs averaging weight α'); ax.legend(); ax.grid(alpha=0.3)
|
||||
fig.tight_layout(); plt.show()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.. AUTO-PLOT-BEGIN
|
||||
.. image:: ../_static/auto/algorithms__agent_based/block_04_fig_01.png
|
||||
:align: center
|
||||
:width: 80%
|
||||
|
||||
.. AUTO-PLOT-END
|
||||
.. image:: ../_static/v2/agent_based/plot_02.png
|
||||
:align: center
|
||||
:width: 80%
|
||||
|
||||
@@ -48,6 +48,16 @@ With $a(t) \equiv -\rho$, $b = c = 0$ and $Y_T = 1$ the analytic deterministic s
|
||||
ax.legend(); ax.grid(alpha=0.3)
|
||||
fig.tight_layout(); plt.show()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.. AUTO-PLOT-BEGIN
|
||||
.. image:: ../_static/auto/algorithms__bsde/block_03_fig_01.png
|
||||
:align: center
|
||||
:width: 80%
|
||||
|
||||
.. AUTO-PLOT-END
|
||||
.. image:: ../_static/v2/bsde/plot_01.png
|
||||
:align: center
|
||||
:width: 80%
|
||||
@@ -76,6 +86,16 @@ Crank–Nicolson is second-order in `Δt`.
|
||||
ax.set_title('Crank–Nicolson convergence'); ax.grid(which='both', alpha=0.3); ax.legend()
|
||||
fig.tight_layout(); plt.show()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.. AUTO-PLOT-BEGIN
|
||||
.. image:: ../_static/auto/algorithms__bsde/block_05_fig_01.png
|
||||
:align: center
|
||||
:width: 80%
|
||||
|
||||
.. AUTO-PLOT-END
|
||||
.. image:: ../_static/v2/bsde/plot_02.png
|
||||
:align: center
|
||||
:width: 80%
|
||||
|
||||
@@ -32,6 +32,16 @@ Maximum-Mean-Discrepancy distance with Gaussian kernel (`mmd_gaussian`). Self-d
|
||||
ax.set_title('Gaussian-kernel MMD vs translation (σ = 1)')
|
||||
ax.grid(alpha=0.3); fig.tight_layout(); plt.show()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.. AUTO-PLOT-BEGIN
|
||||
.. image:: ../_static/auto/algorithms__generative_calibration_hooks/block_03_fig_01.png
|
||||
:align: center
|
||||
:width: 80%
|
||||
|
||||
.. AUTO-PLOT-END
|
||||
.. image:: ../_static/v2/generative_calibration_hooks/plot_01.png
|
||||
:align: center
|
||||
:width: 80%
|
||||
@@ -49,6 +59,16 @@ Bandwidth dependence
|
||||
ax.set_title('MMD as a function of kernel bandwidth')
|
||||
fig.tight_layout(); plt.show()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.. AUTO-PLOT-BEGIN
|
||||
.. image:: ../_static/auto/algorithms__generative_calibration_hooks/block_04_fig_01.png
|
||||
:align: center
|
||||
:width: 80%
|
||||
|
||||
.. AUTO-PLOT-END
|
||||
.. image:: ../_static/v2/generative_calibration_hooks/plot_02.png
|
||||
:align: center
|
||||
:width: 80%
|
||||
|
||||
@@ -305,6 +305,13 @@ plt.ylabel('Learning Rate')
|
||||
plt.title('Grid Search: Loss Surface')
|
||||
plt.savefig('grid_search_heatmap.png', dpi=150)
|
||||
```
|
||||
<!-- AUTO-PLOT-BEGIN -->
|
||||

|
||||
<!-- AUTO-PLOT-END -->
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
**Output:**
|
||||
|
||||
|
||||
@@ -447,6 +447,13 @@ axes[1].set_yticklabels(['Bull', 'Bear'])
|
||||
plt.tight_layout()
|
||||
plt.savefig('hmm_states.png', dpi=150)
|
||||
```
|
||||
<!-- AUTO-PLOT-BEGIN -->
|
||||

|
||||
<!-- AUTO-PLOT-END -->
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
**Output:**
|
||||
|
||||
|
||||
@@ -41,6 +41,16 @@ Interacting-particle Euler scheme for $dX_t = θ(\bar X_t - X_t) dt + σ dW_t$ (
|
||||
ax.set_title('Mean-reverting McKean–Vlasov — 200 particles')
|
||||
fig.tight_layout(); plt.show()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.. AUTO-PLOT-BEGIN
|
||||
.. image:: ../_static/auto/algorithms__mckean_vlasov/block_03_fig_01.png
|
||||
:align: center
|
||||
:width: 80%
|
||||
|
||||
.. AUTO-PLOT-END
|
||||
.. image:: ../_static/v2/mckean_vlasov/plot_01.png
|
||||
:align: center
|
||||
:width: 80%
|
||||
@@ -54,6 +64,16 @@ Interacting-particle Euler scheme for $dX_t = θ(\bar X_t - X_t) dt + σ dW_t$ (
|
||||
ax.set_title('Marginal density at t = 0 and t = T')
|
||||
fig.tight_layout(); plt.show()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.. AUTO-PLOT-BEGIN
|
||||
.. image:: ../_static/auto/algorithms__mckean_vlasov/block_04_fig_01.png
|
||||
:align: center
|
||||
:width: 80%
|
||||
|
||||
.. AUTO-PLOT-END
|
||||
.. image:: ../_static/v2/mckean_vlasov/plot_02.png
|
||||
:align: center
|
||||
:width: 80%
|
||||
|
||||
@@ -295,6 +295,13 @@ axes[1, 1].set_title('Posterior: σ')
|
||||
plt.tight_layout()
|
||||
plt.savefig('mcmc_posterior.png', dpi=150)
|
||||
```
|
||||
<!-- AUTO-PLOT-BEGIN -->
|
||||

|
||||
<!-- AUTO-PLOT-END -->
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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 -->
|
||||

|
||||
<!-- 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 -->
|
||||

|
||||
<!-- AUTO-PLOT-END -->
|
||||
|
||||
|
||||
**Metrics interpretation**:
|
||||
- `total_return`: Should be positive with low transaction costs
|
||||
|
||||
@@ -47,6 +47,16 @@ $\partial_t m = \tfrac12 \partial_{xx} m$ with Gaussian initial density should r
|
||||
ax.set_title('Pure-diffusion Fokker–Planck'); ax.grid(alpha=0.3); ax.legend()
|
||||
fig.tight_layout(); plt.show()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.. AUTO-PLOT-BEGIN
|
||||
.. image:: ../_static/auto/algorithms__pde/block_03_fig_01.png
|
||||
:align: center
|
||||
:width: 80%
|
||||
|
||||
.. AUTO-PLOT-END
|
||||
.. image:: ../_static/v2/pde/plot_01.png
|
||||
:align: center
|
||||
:width: 80%
|
||||
@@ -78,6 +88,16 @@ $-\Delta u = 2\pi^2 \sin(\pi x)\sin(\pi y)$ on the unit square with zero Dirichl
|
||||
axes[1].set_title('error vs analytic'); plt.colorbar(im1, ax=axes[1])
|
||||
fig.tight_layout(); plt.show()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.. AUTO-PLOT-BEGIN
|
||||
.. image:: ../_static/auto/algorithms__pde/block_05_fig_01.png
|
||||
:align: center
|
||||
:width: 80%
|
||||
|
||||
.. AUTO-PLOT-END
|
||||
.. image:: ../_static/v2/pde/plot_02.png
|
||||
:align: center
|
||||
:width: 80%
|
||||
@@ -104,6 +124,16 @@ Heat-only relaxation ($H = 0$, σ² > 0) preserves a constant value, while a qua
|
||||
plt.colorbar(im, ax=ax)
|
||||
fig.tight_layout(); plt.show()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.. AUTO-PLOT-BEGIN
|
||||
.. image:: ../_static/auto/algorithms__pde/block_07_fig_01.png
|
||||
:align: center
|
||||
:width: 80%
|
||||
|
||||
.. AUTO-PLOT-END
|
||||
.. image:: ../_static/v2/pde/plot_03.png
|
||||
:align: center
|
||||
:width: 80%
|
||||
|
||||
@@ -451,6 +451,10 @@ plt.suptitle("Fractional Brownian Motion Paths")
|
||||
plt.tight_layout()
|
||||
plt.show()
|
||||
```
|
||||
<!-- AUTO-PLOT-BEGIN -->
|
||||

|
||||
<!-- AUTO-PLOT-END -->
|
||||
|
||||
|
||||
### Mixed fBM for Aggregate Order Flow
|
||||
|
||||
@@ -518,6 +522,10 @@ plt.title('Power-law decay in scaling limit')
|
||||
plt.tight_layout()
|
||||
plt.show()
|
||||
```
|
||||
<!-- AUTO-PLOT-BEGIN -->
|
||||

|
||||
<!-- AUTO-PLOT-END -->
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -41,6 +41,16 @@ $h'(t) = h(t)^2/γ - φ$ with $h(T) = A$. When $γ = φ = A = 1$ the right-hand
|
||||
ax.set_title('Riccati fixed point γ=φ=A=1')
|
||||
fig.tight_layout(); plt.show()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.. AUTO-PLOT-BEGIN
|
||||
.. image:: ../_static/auto/algorithms__quadratic_impact_control/block_03_fig_01.png
|
||||
:align: center
|
||||
:width: 80%
|
||||
|
||||
.. AUTO-PLOT-END
|
||||
.. image:: ../_static/v2/quadratic_impact_control/plot_01.png
|
||||
:align: center
|
||||
:width: 80%
|
||||
@@ -60,6 +70,16 @@ Vary $A$, fix $γ = 1$, $φ = 0.25$, $T = 1$.
|
||||
ax.set_title('Riccati sensitivity to terminal weight')
|
||||
fig.tight_layout(); plt.show()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.. AUTO-PLOT-BEGIN
|
||||
.. image:: ../_static/auto/algorithms__quadratic_impact_control/block_04_fig_01.png
|
||||
:align: center
|
||||
:width: 80%
|
||||
|
||||
.. AUTO-PLOT-END
|
||||
.. image:: ../_static/v2/quadratic_impact_control/plot_02.png
|
||||
:align: center
|
||||
:width: 80%
|
||||
|
||||
@@ -43,6 +43,16 @@ Synthetic stationary process with 5 % outliers
|
||||
ax.set_title('Synthetic series with heavy-tailed innovations')
|
||||
fig.tight_layout(); plt.show()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.. AUTO-PLOT-BEGIN
|
||||
.. image:: ../_static/auto/algorithms__robust_drift/block_03_fig_01.png
|
||||
:align: center
|
||||
:width: 80%
|
||||
|
||||
.. AUTO-PLOT-END
|
||||
.. image:: ../_static/v2/robust_drift/plot_01.png
|
||||
:align: center
|
||||
:width: 80%
|
||||
@@ -71,6 +81,16 @@ Synthetic stationary process with 5 % outliers
|
||||
ax.legend(); ax.grid(alpha=0.3); ax.set_title('Robust vs OLS drift estimate')
|
||||
fig.tight_layout(); plt.show()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.. AUTO-PLOT-BEGIN
|
||||
.. image:: ../_static/auto/algorithms__robust_drift/block_05_fig_01.png
|
||||
:align: center
|
||||
:width: 80%
|
||||
|
||||
.. AUTO-PLOT-END
|
||||
.. image:: ../_static/v2/robust_drift/plot_02.png
|
||||
:align: center
|
||||
:width: 80%
|
||||
|
||||
@@ -43,6 +43,15 @@ Two modes; only mode 1 pays a unit reward. Free switching should give `V_0(0) =
|
||||
ax.set_title('Snell envelope — free switching')
|
||||
fig.tight_layout(); plt.show()
|
||||
|
||||
|
||||
|
||||
|
||||
.. AUTO-PLOT-BEGIN
|
||||
.. image:: ../_static/auto/algorithms__stochastic_control/block_03_fig_01.png
|
||||
:align: center
|
||||
:width: 80%
|
||||
|
||||
.. AUTO-PLOT-END
|
||||
.. image:: ../_static/v2/stochastic_control/plot_01.png
|
||||
:align: center
|
||||
:width: 80%
|
||||
@@ -73,6 +82,15 @@ Closed-form Riccati for $a=q=0$, $b=r=s_T=1$, $T=1$ is $P(t) = 1/(1 + (T - t))$,
|
||||
axes[2].plot(tg[:-1], u); axes[2].set_title('feedback u(t) = -(b/r) P(t) x(t)'); axes[2].set_xlabel('t'); axes[2].grid(alpha=0.3)
|
||||
fig.tight_layout(); plt.show()
|
||||
|
||||
|
||||
|
||||
|
||||
.. AUTO-PLOT-BEGIN
|
||||
.. image:: ../_static/auto/algorithms__stochastic_control/block_05_fig_01.png
|
||||
:align: center
|
||||
:width: 80%
|
||||
|
||||
.. AUTO-PLOT-END
|
||||
.. image:: ../_static/v2/stochastic_control/plot_02.png
|
||||
:align: center
|
||||
:width: 80%
|
||||
@@ -96,6 +114,15 @@ Affine premium $δ_±(λ) = α_± + κ_± λ$. First-order condition: $\lambda^
|
||||
ax.set_title('Optimal upward intensity vs value-function gradient')
|
||||
ax.grid(alpha=0.3); fig.tight_layout(); plt.show()
|
||||
|
||||
|
||||
|
||||
|
||||
.. AUTO-PLOT-BEGIN
|
||||
.. image:: ../_static/auto/algorithms__stochastic_control/block_06_fig_01.png
|
||||
:align: center
|
||||
:width: 80%
|
||||
|
||||
.. AUTO-PLOT-END
|
||||
.. image:: ../_static/v2/stochastic_control/plot_03.png
|
||||
:align: center
|
||||
:width: 80%
|
||||
|
||||
@@ -207,6 +207,10 @@ plt.suptitle("Fractional Brownian Motion: Three Regimes")
|
||||
plt.tight_layout()
|
||||
plt.show()
|
||||
```
|
||||
<!-- AUTO-PLOT-BEGIN -->
|
||||

|
||||
<!-- AUTO-PLOT-END -->
|
||||
|
||||
|
||||
---
|
||||
|
||||
@@ -401,6 +405,10 @@ plt.title('Scaling Function (Theorem 3.1)')
|
||||
plt.grid(True, alpha=0.3)
|
||||
plt.show()
|
||||
```
|
||||
<!-- AUTO-PLOT-BEGIN -->
|
||||

|
||||
<!-- AUTO-PLOT-END -->
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -103,3 +103,20 @@ __all__ = [
|
||||
"min_variance_weights",
|
||||
"erc_weights",
|
||||
]
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
"""Transparent fallback: forward any unresolved top-level attribute access
|
||||
to the compiled `_core` extension. This keeps `from optimizr import X`
|
||||
working for every Rust-backed function (v1.x and v2.0 primitives) without
|
||||
having to enumerate the full list above."""
|
||||
try:
|
||||
from optimizr import _core as _ext
|
||||
except ImportError as exc: # pragma: no cover
|
||||
raise AttributeError(
|
||||
f"module 'optimizr' has no attribute {name!r} "
|
||||
f"(_core extension is not built: {exc})"
|
||||
) from exc
|
||||
if hasattr(_ext, name):
|
||||
return getattr(_ext, name)
|
||||
raise AttributeError(f"module 'optimizr' has no attribute {name!r}")
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
"""Inject matplotlib plots inline below every executable Python code-block
|
||||
across the entire `docs/source/` tree.
|
||||
|
||||
For each `.md` and `.rst` file:
|
||||
|
||||
1. Parse out the Python code-blocks (fenced ```python``` for Markdown,
|
||||
`.. code-block:: python` for reStructuredText).
|
||||
2. Execute each block in its own namespace with a non-interactive matplotlib
|
||||
backend. Every figure produced by the block is saved as a PNG under
|
||||
`docs/source/_static/auto/<page-slug>/<idx>.png`.
|
||||
3. Rewrite the page so that, immediately after the executable code-block, a
|
||||
markdown image (``) or RST `.. image::` directive points
|
||||
to the captured PNG.
|
||||
4. Blocks that fail to execute (missing imports, illustrative pseudo-code,
|
||||
external data dependencies …) are left untouched — no image is inserted
|
||||
and the existing source is preserved.
|
||||
|
||||
Idempotent: previously injected image directives are detected and refreshed
|
||||
in place rather than duplicated.
|
||||
|
||||
The script is purely additive on the v2.0.0 branch — it touches no Rust or
|
||||
binding code, only the doc tree and the auto-generated `_static/auto`
|
||||
folder.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import traceback
|
||||
from contextlib import redirect_stderr, redirect_stdout
|
||||
from pathlib import Path
|
||||
|
||||
import matplotlib
|
||||
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt # noqa: E402
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
DOC_SRC = ROOT / "docs" / "source"
|
||||
STATIC_DIR = DOC_SRC / "_static" / "auto"
|
||||
SKIP_DIRS = {"_static", "_build", "_templates"}
|
||||
|
||||
# A small allow-list of pages we KNOW exercise the bindings in a way that is
|
||||
# self-contained. Every other page is still scanned, but if a code block
|
||||
# fails we silently leave it alone.
|
||||
|
||||
# Auto-injected marker so that re-runs are idempotent.
|
||||
MD_BEGIN = "<!-- AUTO-PLOT-BEGIN -->"
|
||||
MD_END = "<!-- AUTO-PLOT-END -->"
|
||||
RST_BEGIN = ".. AUTO-PLOT-BEGIN"
|
||||
RST_END = ".. AUTO-PLOT-END"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def slugify(path: Path) -> str:
|
||||
rel = path.relative_to(DOC_SRC).with_suffix("")
|
||||
return str(rel).replace(os.sep, "__")
|
||||
|
||||
|
||||
def execute_block(code: str, page_ns: dict) -> list[Path] | None:
|
||||
"""Run `code` in `page_ns`. Return a list of PNG paths for the figures
|
||||
it created, or `None` if the block raised."""
|
||||
plt.close("all")
|
||||
buf_out, buf_err = io.StringIO(), io.StringIO()
|
||||
try:
|
||||
with redirect_stdout(buf_out), redirect_stderr(buf_err):
|
||||
exec(compile(code, "<doc-block>", "exec"), page_ns)
|
||||
except BaseException: # noqa: BLE001 — also catch pyo3 PanicException
|
||||
return None
|
||||
return list(plt.get_fignums())
|
||||
|
||||
|
||||
def save_figs(page_slug: str, block_idx: int) -> list[str]:
|
||||
out_dir = STATIC_DIR / page_slug
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
written: list[str] = []
|
||||
for i, num in enumerate(plt.get_fignums(), start=1):
|
||||
fig = plt.figure(num)
|
||||
fname = f"block_{block_idx:02d}_fig_{i:02d}.png"
|
||||
fpath = out_dir / fname
|
||||
fig.savefig(fpath, bbox_inches="tight", dpi=110)
|
||||
written.append(fpath.as_posix())
|
||||
plt.close("all")
|
||||
return written
|
||||
|
||||
|
||||
def relative_to_doc(target: Path, page_path: Path) -> str:
|
||||
return os.path.relpath(target, page_path.parent).replace(os.sep, "/")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Markdown
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
MD_BLOCK_RE = re.compile(
|
||||
r"(?P<head>```(?:python|py)\s*\n)(?P<body>.*?)(?P<tail>```)",
|
||||
re.DOTALL,
|
||||
)
|
||||
MD_AUTO_BLOCK_RE = re.compile(
|
||||
re.escape(MD_BEGIN) + r".*?" + re.escape(MD_END) + r"\n?",
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
|
||||
def process_markdown(path: Path) -> bool:
|
||||
text = path.read_text()
|
||||
# Strip previously-injected auto blocks so we re-run from a clean slate.
|
||||
text = MD_AUTO_BLOCK_RE.sub("", text)
|
||||
|
||||
page_slug = slugify(path)
|
||||
page_ns: dict = {"__name__": "__doc__"}
|
||||
out_chunks: list[str] = []
|
||||
last = 0
|
||||
block_idx = 0
|
||||
n_injected = 0
|
||||
|
||||
for m in MD_BLOCK_RE.finditer(text):
|
||||
block_idx += 1
|
||||
out_chunks.append(text[last:m.end()])
|
||||
last = m.end()
|
||||
|
||||
code = m.group("body")
|
||||
nums = execute_block(code, page_ns)
|
||||
if not nums:
|
||||
continue
|
||||
pngs = save_figs(page_slug, block_idx)
|
||||
if not pngs:
|
||||
continue
|
||||
|
||||
block = ["", MD_BEGIN]
|
||||
for png in pngs:
|
||||
rel = relative_to_doc(Path(png), path)
|
||||
block.append(f"")
|
||||
block.append(MD_END)
|
||||
block.append("")
|
||||
out_chunks.append("\n".join(block))
|
||||
n_injected += len(pngs)
|
||||
|
||||
out_chunks.append(text[last:])
|
||||
new_text = "".join(out_chunks)
|
||||
if new_text != path.read_text():
|
||||
path.write_text(new_text)
|
||||
return n_injected > 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# reStructuredText
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
RST_BLOCK_RE = re.compile(
|
||||
r"(?P<indent>[ \t]*)\.\. code-block::[ \t]+python\s*\n"
|
||||
r"(?:[ \t]*:.*\n)*"
|
||||
r"\n?"
|
||||
r"(?P<body>(?:(?:\1[ \t]+.*|[ \t]*)\n)+)",
|
||||
)
|
||||
RST_AUTO_BLOCK_RE = re.compile(
|
||||
re.escape(RST_BEGIN) + r".*?" + re.escape(RST_END) + r"\n?",
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
|
||||
def dedent_rst_body(body: str, indent: str) -> str:
|
||||
inner_indent = indent + " "
|
||||
out = []
|
||||
for line in body.splitlines():
|
||||
if line.startswith(inner_indent):
|
||||
out.append(line[len(inner_indent):])
|
||||
elif line.strip() == "":
|
||||
out.append("")
|
||||
else:
|
||||
# End of code-block (different indentation): stop early.
|
||||
break
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def process_rst(path: Path) -> bool:
|
||||
text = path.read_text()
|
||||
text = RST_AUTO_BLOCK_RE.sub("", text)
|
||||
|
||||
page_slug = slugify(path)
|
||||
page_ns: dict = {"__name__": "__doc__"}
|
||||
out_chunks: list[str] = []
|
||||
last = 0
|
||||
block_idx = 0
|
||||
n_injected = 0
|
||||
|
||||
for m in RST_BLOCK_RE.finditer(text):
|
||||
block_idx += 1
|
||||
out_chunks.append(text[last:m.end()])
|
||||
last = m.end()
|
||||
|
||||
indent = m.group("indent")
|
||||
code = dedent_rst_body(m.group("body"), indent)
|
||||
nums = execute_block(code, page_ns)
|
||||
if not nums:
|
||||
continue
|
||||
pngs = save_figs(page_slug, block_idx)
|
||||
if not pngs:
|
||||
continue
|
||||
|
||||
lines = ["", f"{indent}{RST_BEGIN}"]
|
||||
for png in pngs:
|
||||
rel = relative_to_doc(Path(png), path)
|
||||
lines.append(f"{indent}.. image:: {rel}")
|
||||
lines.append(f"{indent} :align: center")
|
||||
lines.append(f"{indent} :width: 80%")
|
||||
lines.append("")
|
||||
lines.append(f"{indent}{RST_END}")
|
||||
lines.append("")
|
||||
out_chunks.append("\n".join(lines))
|
||||
n_injected += len(pngs)
|
||||
|
||||
out_chunks.append(text[last:])
|
||||
new_text = "".join(out_chunks)
|
||||
if new_text != path.read_text():
|
||||
path.write_text(new_text)
|
||||
return n_injected > 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def iter_doc_pages():
|
||||
for dirpath, dirnames, filenames in os.walk(DOC_SRC):
|
||||
# Prune
|
||||
rel = Path(dirpath).relative_to(DOC_SRC)
|
||||
parts = set(rel.parts)
|
||||
if parts & SKIP_DIRS:
|
||||
dirnames.clear()
|
||||
continue
|
||||
for fn in filenames:
|
||||
if fn.endswith((".md", ".rst")) and not fn.endswith(".backup"):
|
||||
yield Path(dirpath) / fn
|
||||
|
||||
|
||||
def main() -> None:
|
||||
STATIC_DIR.mkdir(parents=True, exist_ok=True)
|
||||
n_pages = 0
|
||||
n_with_plots = 0
|
||||
total_imgs = 0
|
||||
for page in sorted(iter_doc_pages()):
|
||||
n_pages += 1
|
||||
try:
|
||||
if page.suffix == ".md":
|
||||
injected = process_markdown(page)
|
||||
else:
|
||||
injected = process_rst(page)
|
||||
except Exception: # noqa: BLE001
|
||||
print(f"!! {page.relative_to(DOC_SRC)} crashed:")
|
||||
traceback.print_exc()
|
||||
continue
|
||||
if injected:
|
||||
n_with_plots += 1
|
||||
# Count images in _static/auto/<page_slug>
|
||||
img_dir = STATIC_DIR / slugify(page)
|
||||
if img_dir.exists():
|
||||
n_imgs = len(list(img_dir.glob("*.png")))
|
||||
total_imgs += n_imgs
|
||||
print(f" {page.relative_to(DOC_SRC)} -> {n_imgs} plot(s)")
|
||||
else:
|
||||
print(f" {page.relative_to(DOC_SRC)} -> no plots")
|
||||
print()
|
||||
print(f"Scanned {n_pages} pages, {n_with_plots} got new images, "
|
||||
f"{total_imgs} PNGs total.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||