52 Commits

Author SHA1 Message Date
ThotDjehuty 01bae1f060 feat(v1.1.x): PyO3 bindings + executed companion notebooks for 5 new groups
Adds Python bindings (behind feature='python-bindings') for graph,
risk_measures, topology, volterra, signatures.

Companion notebooks under examples/notebooks/:
- 05_graph.ipynb           (Laplacians + spectral clustering)
- 06_risk_measures.ipynb   (VaR / CVaR + simplex projection)
- 07_topology.ipynb        (Vietoris-Rips + persistent homology)
- 08_volterra.ipynb        (fractional ODE, Markovian lift, Volterra,
                            Fourier inversion)
- 09_signatures.ipynb      (path / log / random / kernel signatures)

All notebooks executed end-to-end against analytic ground truth
(closed-form solutions, Mittag-Leffler, exp(-t), unit-circle homology,
identical-path signature kernel).

Built and validated via: maturin develop --release --features python-bindings.

Workflow generated by 5 parallel optimizRs subagents (.github/agents/).
2026-05-12 11:46:24 +02:00
ThotDjehuty d780ed81d7 release(v1.1.0): additive CPU-only generic numerical primitives
Adds 9 new top-level / sub-modules to the Rust API only (no Python
bindings yet), with at least one analytic unit test per module.

New Rust modules:
- optimal_control::matrix_riccati  (RK4 backward solver)
- timeseries_utils::nonsync_covariance  (Hayashi-Yoshida)
- timeseries_utils::wavelet  (Haar / Daubechies DWT and MODWT)
- risk_measures  (VaR, CVaR, projected sub-gradient CVaR minimisation)
- graph::laplacian + graph::spectral_clustering  (Jacobi + k-means++)
- topology  (Vietoris-Rips persistent homology, bottleneck distance)
- volterra  (Caputo Adams, Markovian lift, second-kind Volterra,
             Fourier inversion of characteristic functions)
- signatures  (truncated tensor signature, log-sig, random reservoir,
               Salvi-Cass-Lyons signature kernel, shuffle product)

All previously stable APIs untouched; abi3-py38 ABI preserved.
New module tests: 29/29 passing. Pre-existing 5 unrelated failures
unchanged.
2026-05-12 10:59:09 +02:00
ThotDjehuty e67b0f8376 feat(portfolio): add CARA, convex, mean-variance & ERC portfolio optimization module
New Rust portfolio_optimization module with PyO3 bindings:
- CARA/CRRA utility maximization via projected gradient descent
- General-purpose convex objective solver on simplex (ProjectedGradientSolver)
- Mean-variance optimization (max Sharpe, target return, min variance)
- Equal Risk Contribution (ERC) portfolio allocation
- Python bindings: cara_optimal_weights, mean_variance_optimal_weights,
  min_variance_weights, erc_weights
- 6/6 unit tests passing

Convergence fix: removed gradient-norm criterion on simplex boundary
(projected gradient never vanishes at constrained optimum).
Default learning rate increased from 0.005 to 0.1.
2026-04-15 03:08:08 +02:00
ThotDjehuty 58c3793b66 fix(warnings): clean all compiler warnings across crate
- Remove unused imports (Array1, statrs, Uniform, VecDeque, PI, assert_abs_diff_eq)
- Prefix unused variables with underscore (log_likelihood, cash, position, positions, v100)
- Add #[allow(dead_code)] for intentionally unused utility functions and structs
2026-04-10 10:18:48 +02:00
ThotDjehuty df6b7f61f6 chore: clean root — move 7 doc files to docs/
- DOCUMENTATION_IMPROVEMENTS_NEEDED.md → docs/
- PUBLICATION_GUIDE_v1.0.0.md → docs/
- PUBLICATION_STATUS.md → docs/
- PYPI_PUBLISHING.md → docs/
- RELEASE_NOTES_v0.2.0.md, v0.3.0.md, v1.0.0.md → docs/
- Added build artifacts to .gitignore
2026-03-10 18:00:57 +01:00
ThotDjehuty 8a1571c83f fix(lint): fix escape sequences and matplotlib colormap API in docs scripts 2026-03-08 11:26:37 +01:00
ThotDjehuty 49be6c7bf7 docs(diagrams): add 4 remaining SVG figures for §8 HMM + §10 geometry
- fig_hmm_regime: 3-state Bull/Neutral/Bear HMM state machine + emission table
- fig_viterbi_trellis: Viterbi trellis K=3 T=4 with MAP path highlighted
- fig_std_vs_nat_gradient: standard vs natural gradient property comparison panels
- fig_lie_group_hierarchy: GL/SL/O/SO/Sp/H Lie group tree with finance annotations

All 27 SVGs regenerated; mathematical_foundations.md now 0 remaining ASCII blocks.
_fix_diagrams3.py added for reproducibility.
2026-03-07 13:39:43 +01:00
ThotDjehuty 5d06f0ab57 docs: replace all ASCII/Unicode diagram blocks with matplotlib SVG figures
Replace 23 ASCII/Unicode text diagram code-blocks in mathematical_foundations.md
with professionally rendered matplotlib SVG figures.

Changes:
- Add docs/source/_gen_diagrams.py: Python script generating all 23 SVG figures
  with consistent styling (white bg, blue/orange/green/red palette, scipy/numpy/
  matplotlib Agg backend)
- Add docs/source/_static/diagrams/*.svg: 23 rendered SVG figures covering:
  §1 DE/random-walk/BM/GBM, §2 Ito/Picard/FP/EM/OU, §3 Poisson/Merton/Levy,
  §6 Kalman, §7 MCMC, §9 KL/Fisher, §10 curvatures/natural-gradient
- Update mathematical_foundations.md: all ASCII art code-blocks replaced with
  MyST {figure} directives pointing to the generated SVGs
- Sphinx build: clean success, all 23 SVGs copied to build, 1 pre-existing warning

Resolves: user request for professional publication-quality figures instead of
  ASCII/Unicode art (which was too low-level for publication)
2026-03-07 11:29:14 +01:00
ThotDjehuty e80d717aa7 docs: upgrade all ASCII diagrams to Unicode box-drawing art
Replace ~20 ASCII diagrams across §1–§10 with polished Unicode art:
- Proper axes: ╰──→ ▲ ╌ with tick marks and labels
- Curves: ╭─╮ ╰─╯ ╲ ╱ for smooth paths and bells
- Steps: ─── ┐ └ for Poisson staircase
- Fills: ▓ ░ · ◦ ● for density and scatter plots
- Tables: ┌┬─┤├┼┘ Unicode box drawing for HMM state machine
- Remove duplicate BM fan + GBM diagrams (were repeated verbatim)
- Consistent 44-char width, title banners with ┄ separators
2026-03-07 10:38:38 +01:00
ThotDjehuty d24b8b1036 docs: enrich mathematical_foundations SDE sections + transparent logo 2026-03-07 09:44:08 +01:00
ThotDjehuty b41618c627 docs(theory): enrich Section 4 optimal control with 4 worked examples
- Add HJB/PMP/HJBI comparison overview table
- Add Merton 1969 portfolio allocation example (log-utility, constant fraction)
- Add Almgren-Chriss inventory liquidation example (LQR + TWAP-like schedule)
- Add PMP costate derivation for Merton problem
- Add American option as viscosity example (variational inequality, smooth-pasting)
- Explain jump integral term intuition in HJBI
- Add shooting method pseudocode for PMP
- Mirror all enrichments in LaTeX .tex source
- Regenerate PDF (13 pages, cross-refs resolved)
2026-03-06 20:22:35 +01:00
ThotDjehuty 7a964fd9ee fix(docs): convert mathematical_foundations.md to correct MyST Markdown syntax
Replace all RST directives with MyST equivalents so equations and
admonitions render correctly on ReadTheDocs:

- :math:`...` inline roles -> $...$ (dollarmath extension)
- .. math:: blocks -> $$...$$ display math
- .. admonition:: -> ::::{admonition} ... :::: colon-fence blocks
- .. list-table:: -> standard Markdown pipe tables
- ``code`` -> `code` backticks
- double-colon code blocks -> fenced ``` blocks

Build: clean (no new warnings).
2026-03-06 19:31:04 +01:00
ThotDjehuty 48220de0ce docs(theory): enrich mathematical foundations with 10 sections
Sections added/expanded:
- DE: operator table, jDE self-adaptive rules, convergence theorem
- Stochastic Processes: 4-axiom BM, Ito calculus+lemma, SDE table, OU MLE
- Jump Processes: Poisson, Merton formula, Levy-Khintchine theorem+table, SDE generator
- Optimal Control: HJB boxed PDE, LQR Riccati, PMP costate theorem, HJBI jumps, viscosity solutions
- Mean Field Games: HJB+KFP system, fixed-point pseudocode, Lasry-Lions convergence
- Kalman Filtering: predict/update recursion, KL-minimization view, Kalman-Bucy Riccati
- MCMC: MH accept-reject, optimal step h~2.38/sqrt(d), MALA+Langevin SDE
- HMM: full Baum-Welch E/M (alpha/beta/gamma/xi), Viterbi max-product
- Information Theory: Shannon entropy, KL+Gibbs, Fisher info+Cramer-Rao, mRMR, natural gradient preview
- Differential Geometry: Riemannian manifold+geodesic+Christoffel, Fisher-Rao metric,
  Amari natural gradient, dually-flat exp families, Lie groups, symplectic geometry+PMP, sectional curvature
- Quick Reference table (15 rows), 11 full references
- Sphinx build clean (1 unrelated chardet warning only)
2026-03-06 19:14:43 +01:00
ThotDjehuty f6a74716e2 docs: replace ASCII diagrams with Mermaid visuals + brand CSS
- Add sphinxcontrib-mermaid>=0.9.2 to requirements.txt
- Add custom.css with orange brand theme (admonitions, tables, mermaid containers)
- Replace Complete Algorithm ASCII pseudocode with Mermaid flowchart (differential_evolution.md)
- Add Mermaid stateDiagram-v2 for Bull/Normal/Bear regime transitions (hmm.md)
- Replace Rust module file tree with Mermaid graph TD (point_processes.md)
- Configure mermaid_version=10.9.0 and dark+orange themeVariables in conf.py
2026-03-06 17:39:03 +01:00
ThotDjehuty 6ab4976e7a docs(point_processes): add comprehensive ReadTheDocs documentation
- Algorithm guide: Hawkes processes, fBM, mixed fBM, Mittag-Leffler
- Full mathematical background with LaTeX equations
- API reference for all 8 Python-bound functions
- Usage examples, performance benchmarks, module architecture
- Updated index.rst toctree with new entries
2026-02-19 21:16:26 +01:00
ThotDjehuty 6c2ce3d238 refactor: remove finance-specific code from point_processes module
- Removed order_flow.rs (UnifiedTheoryParams, OrderFlowAnalyzer, MarketImpact)
- Removed analyze_order_flow, unified_theory_params, market_impact Python bindings
- Updated mod.rs documentation to be generic (no trading references)
- Kept general-purpose: Hawkes, fBM, mfBM, Mittag-Leffler, Hurst estimation

Finance-specific code moved to rust-hft-arbitrage-lab-internal
2026-02-19 19:35:04 +01:00
ThotDjehuty f7e62cbe6d feat: add point_processes module implementing unified theory framework 2026-02-19 19:01:29 +01:00
ThotDjehuty fc8a8d036e docs: Comprehensive enhancement of optimal control and HMM API documentation
Optimal Control: 94 to 610 lines with HJB theory, viscosity solutions, finite differences

HMM API: 16 to 571 lines with complete API reference and usage examples
2026-02-18 20:30:15 +01:00
ThotDjehuty 5f0c8b0d67 docs: Add comprehensive documentation improvement roadmap
- Identifies critical gaps in optimal control mathematical foundations
- Details needed HMM API expansion (currently only 16 lines)
- Lists all implemented algorithms requiring documentation
- Provides phased implementation plan with priorities
- References academic sources for mathematical content

User feedback: 'make it a lot more concise and detailed' + needs HJB equation theory, viscosity solutions, algorithm details
2026-02-17 19:59:56 +01:00
Melvin Avarez 78442d9e69 Delete FINAL_RELEASE_STATUS.md 2026-02-17 19:33:17 +01:00
ThotDjehuty efde8d19a5 fix: Update PyPI metadata with Optimiz-rs branding and logo (v1.0.1)
- Replace all OptimizR references with Optimiz-rs in README
- Fix logo URL to use GitHub raw link (displays on PyPI)
- Bump version to 1.0.1 for metadata update
- Published to PyPI: https://pypi.org/project/optimiz-rs/1.0.1/

Changes:
- README.md: 7 instances of OptimizR → Optimiz-rs
- README.md: Logo URL now uses raw.githubusercontent.com
- pyproject.toml: version 1.0.0 → 1.0.1
- Cargo.toml: version 1.0.0 → 1.0.1
2026-02-17 10:22:42 +01:00
ThotDjehuty b10263b4f2 docs: Rebrand OptimizR to Optimiz-rs throughout documentation
Updated Branding in ReadTheDocs:
-  All 'OptimizR' → 'Optimiz-rs' (17 files)
-  Project name in conf.py
-  HTML title and short title
-  All algorithm documentation
-  Getting started guide
-  Installation guide
-  Theory/mathematical foundations
-  Archive documentation

Documentation now consistently uses the new 'optimiz-rs' branding that
matches both PyPI and crates.io package names.

Note: Python module name 'optimizr' in import statements intentionally
unchanged (that's the actual module name).
2026-02-17 10:09:15 +01:00
ThotDjehuty bfd0d3c591 docs: Update all crates.io references to optimiz-rs
Updated Documentation:
-  README.md: cargo add optimiz-rs
-  RELEASE_NOTES_v1.0.0.md: Updated URLs and install commands
-  LINKEDIN_POST.md: Updated crates.io link
-  FINAL_RELEASE_STATUS.md: Updated all registry tables
-  PYPI_PUBLISHING.md: Updated crates.io URL
-  PUBLICATION_GUIDE_v1.0.0.md: Updated all references

Complete Naming Consistency Achieved:
- crates.io: optimiz-rs 
- PyPI: optimiz-rs 
- Old 'optimizr' package yanked on crates.io

Both registries now use identical naming: optimiz-rs
2026-02-17 10:02:29 +01:00
ThotDjehuty aa8b1f265d refactor: Rename crates.io package from optimizr to optimiz-rs
- Updated Cargo.toml: name = 'optimiz-rs'
- Maintains lib name as 'optimizr' for code compatibility
- Achieves naming consistency with PyPI (optimiz-rs)

Both registries now use the same base name:
- crates.io: optimiz-rs
- PyPI: optimiz-rs
2026-02-17 10:00:52 +01:00
ThotDjehuty fed03f5bdf feat: Update logo to optimiz-rs branding 🎨
Logo Updates:
-  New logo: logo_optimizrs.png (1024x1536 PNG)
-  README.md: Updated logo reference
-  docs/source/conf.py: Updated html_logo and html_favicon
-  Removed old logos: logo_optimizr_valid.png, logo_optimizr_valid.jpeg
-  Copied from: business/logo/logo_optimizrs.png

The logo now reflects the new optimiz-rs package name across:
- GitHub README
- ReadTheDocs documentation
- Sphinx HTML output
- favicon

All documentation will automatically use the updated branding.
2026-02-17 09:56:23 +01:00
ThotDjehuty 5f2d2d6187 feat: Successfully published optimiz-rs v1.0.0 to PyPI! 🎉
Publication Details:
-  Package name: optimiz-rs (cleaner than optimizr-rs)
-  PyPI URL: https://pypi.org/project/optimiz-rs/1.0.0/
-  Installation tested: pip install optimiz-rs works perfectly
-  Functionality verified: imports and DE algorithm working

Updated Documentation:
- README.md: Updated installation instructions
- RELEASE_NOTES_v1.0.0.md: Removed '(publishing in progress)'
- FINAL_RELEASE_STATUS.md: Updated to 100% complete status

Both Registries Now Live:
- crates.io: https://crates.io/crates/optimizr
- PyPI: https://pypi.org/project/optimiz-rs/

v1.0.0 is now 100% published and ready for the world! 🚀
2026-02-17 09:45:43 +01:00
ThotDjehuty 4714ce03e9 refactor: Rename PyPI package from optimizr-rs to optimiz-rs
- Shorter, cleaner package name
- Updated pyproject.toml: name = 'optimiz-rs'
- Updated all documentation (README, RELEASE_NOTES, LINKEDIN_POST, etc.)
- Rebuilt wheel: optimiz_rs-1.0.0-cp38-abi3-macosx_10_12_x86_64.whl
- Tested: Python import and DE algorithm working correctly

Package verified and ready for PyPI publication.
2026-02-17 09:43:04 +01:00
ThotDjehuty 2fa97c2eaa docs: Add final release status summary 2026-02-17 09:31:09 +01:00
ThotDjehuty e93d43f290 feat: Publish to crates.io and prepare PyPI publication
crates.io:

- Successfully published optimizr v1.0.0

- Available at: https://crates.io/crates/optimizr

PyPI Preparation:

- Renamed package to optimizr-rs (avoid name conflict)

- Updated pyproject.toml with new name

- Built wheel: optimizr_rs-1.0.0-cp38-abi3-macosx_10_12_x86_64.whl

- Ready for upload (need API token)

Documentation Updates:

- Updated RELEASE_NOTES with crates.io link

- Updated README.md installation instructions

- Updated LINKEDIN_POST.md with correct package names

- Created PYPI_PUBLISHING.md with token instructions

Next: Get PyPI API token and run twine upload
2026-02-17 09:30:20 +01:00
ThotDjehuty 5d4dff2165 docs: Add publication status tracking 2026-02-17 09:28:31 +01:00
ThotDjehuty ec9ae654cb docs: Add publication status report
Details publication blockers:

- crates.io: Email verification required

- PyPI: Package name conflict (optimizr already exists at v1.4.7)

Recommendations:

- Use optimizr-rs for PyPI (clear Rust variant naming)

- Verify email at crates.io/settings/profile

Current status: 99% ready, just need user actions
2026-02-17 09:07:27 +01:00
ThotDjehuty 5f44714261 feat: Fix remaining notebooks and prepare v1.0.0 publication
Notebook Improvements:

- Fixed 05_performance_benchmarks.ipynb: Reduced observation count from 50k to 10k max

- Fixed mean_field_games_tutorial.ipynb: Improved numerical stability

  - Reduced grid size (100x100 -> 50x50) for Python implementation

  - Added CFL condition checking and auto-adjustment

  - Implemented semi-implicit schemes for better stability

  - Added sub-stepping for Fokker-Planck solver

  - Enhanced error handling with NaN/Inf detection

  - Added graceful convergence handling

Marketing Materials:

- Created LINKEDIN_POST.md for v1.0.0 announcement

  - Compelling narrative with real benchmarks

  - Clear call-to-action for stars and contributions

  - Links to documentation and installation

Status:

- All 8 notebooks now functional (100% success rate)

- Ready for crates.io and PyPI publication

- Professional presentation for open source community
2026-02-17 09:05:43 +01:00
Melvin Alvarez 0c0c060bee remove useless informations 2026-02-16 21:42:29 +01:00
Melvin Alvarez 3e4390e462 chore(release): Prepare OptimizR v1.0.0 for production release
Production Release Preparation:

- 75% notebook success rate (6/8 fully functional)

- Comprehensive documentation and repository cleanup

Documentation:

- Created comprehensive examples/notebooks/README.md (200+ lines)

- Updated docs/source/index.rst version badge (0.3.0 to 1.0.0)

- Archived 17 temporary development markdown files to docs/archive/

- Added examples/notebooks/.gitignore for outputs/

Repository Cleanup:

- Removed test_release.py (temporary test script)

- Removed NOTEBOOK_EXECUTION_REPORT.md (development artifact)

- Organized development docs into docs/archive/

Working Notebooks (6/8):

1. 01_hmm_tutorial.ipynb - Market regime detection

2. 02_mcmc_tutorial.ipynb - Bayesian inference

3. 03_differential_evolution_tutorial.ipynb - Global optimization

4. 03_optimal_control_tutorial.ipynb - HJB equations

5. 04_kalman_filter_sensor_fusion.ipynb - Sensor fusion

6. 04_real_world_applications.ipynb - Portfolio optimization

Documented Limitations (2/8):

7. 05_performance_benchmarks.ipynb - Memory limits

8. mean_field_games_tutorial.ipynb - Numerical stability

Validation:

- All 11 core tests passing (0.73s)

- HMM, MCMC, Differential Evolution, Grid Search validated
2026-02-16 17:56:27 +01:00
Melvin Alvarez 6ebd1337fa fix: updated 3 more notebooks for v1.0.0 API - 75% success rate 2026-02-16 17:35:15 +01:00
Melvin Alvarez c18788160a fix: update MCMC and DE tutorials for v1.0.0 API - lambda closures, parameter renames, tuple unpacking 2026-02-16 17:15:45 +01:00
Melvin Alvarez 815efcb40d docs: add notebook execution report
Created comprehensive report documenting notebook validation results:
- 2/8 notebooks execute successfully (25%)
- 2/8 have API mismatches (parameter name changes)
- 1/8 has syntax errors (garbage characters)
- 3/8 require further investigation

The report includes:
- Detailed error analysis for each failed notebook
- Root cause analysis (API evolution without updates)
- Recommended fixes with priorities
- Impact assessment on documentation quality
- Proposed CI/CD workflow for automated validation

Priority: HIGH - Broken examples undermine v1.0.0 credibility.

Next steps: Fix high-priority notebooks (MCMC, DE, benchmarks, real-world)
2026-02-16 17:00:36 +01:00
Melvin Alvarez 6b084ae396 docs: add execution outputs to working notebooks
Successfully executed and saved outputs for:
- 01_hmm_tutorial.ipynb (HMM regime detection examples)
- 03_optimal_control_tutorial.ipynb (Optimal control and Kalman filtering)

These notebooks now demonstrate working code with real outputs,
validating documentation examples for v1.0.0 release.

Note: Several notebooks require API updates to match current library:
- 02_mcmc_tutorial.ipynb - mcmc_sample API changed
- 03_differential_evolution_tutorial.ipynb - parameter names changed
- 04_kalman_filter_sensor_fusion.ipynb - syntax errors
- 04_real_world_applications.ipynb - requires investigation
- 05_performance_benchmarks.ipynb - requires investigation
- mean_field_games_tutorial.ipynb - requires investigation

These will be fixed in follow-up commits.
2026-02-16 16:59:21 +01:00
Melvin Alvarez 50a4313d5a docs: enhance ReadTheDocs with visualization outputs and GitHub links 2026-02-16 16:42:24 +01:00
Melvin Alvarez 525015525b docs: add comprehensive publication guide for v1.0.0
- Step-by-step crates.io setup and publication
- Step-by-step PyPI setup and publication
- GitHub Actions automation template
- Post-publication checklist
- Marketing & announcement strategy
- Success metrics and tracking
- Known issues and limitations

Guide includes all commands needed to complete publication
after credentials are configured.
2026-02-16 16:29:55 +01:00
Melvin Alvarez 07d45297a7 docs: add v1.0.0 release notes
- First stable release announcement
- Complete feature list and performance benchmarks
- Breaking changes documentation
- Migration guide from v0.3.0
- Roadmap for v1.1.0, v1.2.0, v2.0.0
- Publication details for crates.io and PyPI
2026-02-16 16:27:50 +01:00
Melvin Alvarez a3117fe4a2 fix(build): enable python-bindings feature in maturin config
- Add python-bindings feature to maturin build configuration
- Ensures PyO3 extension module is built correctly for PyPI
- Fixes PyInit__core symbol warning during wheel build

This is required because we removed python-bindings from default features
to fix cargo publish linking issues. Maturin now explicitly enables it.
2026-02-16 16:26:49 +01:00
Melvin Alvarez 0dfb9b803e chore: add wheels/ to .gitignore 2026-02-16 16:24:37 +01:00
Melvin Alvarez f44280edd7 chore: prepare v1.0.0 release for crates.io and PyPI
BREAKING CHANGES:
- Version bumped to 1.0.0 (stable release)
- Default features: Removed python-bindings from default (fixes linking issues)
- python-bindings now opt-in feature for PyO3 builds

Metadata Updates:
- Authors: HFThot Research Lab <contact@hfthot-lab.eu>
- Repository: https://github.com/ThotDjehuty/optimiz-r
- Homepage: https://hfthot-lab.eu
- Documentation: https://optimiz-r.readthedocs.io

README Updates:
- Version badge: 0.3.0 → 1.0.0
- What's New section updated for v1.0.0 stable release
- Citation author updated
- Contact information updated

This prepares OptimizR for publication to:
- crates.io (Rust package registry)
- PyPI (Python package index)

API is now stable and follows semantic versioning from v1.0.0 forward.
2026-02-16 16:24:25 +01:00
Melvin Alvarez f43659c8e6 docs: add Documentation & Getting Started section with ReadTheDocs link 2026-02-16 16:05:23 +01:00
Melvin Alvarez ea2265cb49 Enhance documentation content 2026-02-09 18:58:45 +01:00
Melvin Alvarez 74fbbd369b Add make docs commands for html build 2026-02-09 18:31:14 +01:00
Melvin Alvarez 7a9ab20b89 docs: add mfg tutorial and enrich theory 2026-02-09 17:27:35 +01:00
Melvin Alvarez 6e3367c396 docs: deepen theory and benchmarks 2026-02-09 17:09:27 +01:00
Melvin Alvarez 50d31d54cc ci(rtd): install openblas for docs 2026-02-09 16:46:17 +01:00
Melvin Alvarez 43af949904 docs(optimizr): add logo and fix rtd deps 2026-02-09 16:15:41 +01:00
Melvin Alvarez c1f4c0bde7 docs: add ReadTheDocs configuration and Sphinx documentation structure 2026-02-08 17:16:41 +01:00
171 changed files with 105202 additions and 1049 deletions
+8
View File
@@ -22,3 +22,11 @@ Cargo.lock
*.swp
*.swo
*~
wheels/
# Build artifacts
docs/source/_static/*.aux
docs/source/_static/*.log
docs/source/_static/*.toc
docs/source/theory/*.html
docs/source/theory/*.pdf
+24
View File
@@ -0,0 +1,24 @@
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
version: 2
build:
os: ubuntu-22.04
tools:
python: "3.11"
apt_packages:
- libopenblas-dev
sphinx:
configuration: docs/source/conf.py
formats:
- pdf
- epub
python:
install:
- requirements: docs/requirements.txt
- method: pip
path: .
+60
View File
@@ -0,0 +1,60 @@
# Changelog
All notable changes to **optimiz-rs** are documented in this file. The format
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.1.0] - 2026-05-12
### Added — purely additive, no existing API changes
- `optimal_control::matrix_riccati` — RK4 backward solver for the
matrix Riccati differential equation
`dA/dt = -2 A M A + Q`, plus terminal-condition variants for the
associated affine and constant components.
- `timeseries_utils::nonsync_covariance` — Hayashi--Yoshida estimator
for asynchronous covariance, with parallel matrix variant.
- `timeseries_utils::wavelet` — discrete and maximum-overlap wavelet
transforms (Haar, Daubechies orders 2--10) with periodic boundaries.
- `risk_measures` — empirical and parametric Value-at-Risk and
Conditional Value-at-Risk estimators, plus a projected sub-gradient
solver for convex CVaR minimisation over the unit simplex.
- `graph::laplacian` — combinatorial, symmetric-normalised and
random-walk graph Laplacians.
- `graph::spectral_clustering` — spectral clustering via Jacobi
diagonalisation and Lloyd's algorithm with k-means++ initialisation.
- `topology::persistent_homology` — Vietoris--Rips persistent homology
by the standard `Z/2` matrix-reduction algorithm.
- `topology::bottleneck` — bottleneck distance between persistence
diagrams via Hopcroft--Karp matching with binary search.
- `volterra::fractional_riccati` — Adams predictor--corrector solver
for Caputo fractional ODEs (Diethelm--Ford--Freed 2002).
- `volterra::markovian_lift` — multi-exponential approximation of
convolution kernels by non-negative least squares on a geometric
grid.
- `volterra::volterra_solver` — generic second-kind Volterra integral
equation solver via product-trapezoidal quadrature.
- `volterra::fourier_inversion` — direct trapezoidal Fourier inversion
of a characteristic function on a uniform frequency grid.
- `signatures::path_signature` — truncated tensor signature of a
piecewise-linear path with truncated tensor exponential.
- `signatures::log_signature` — truncated tensor logarithm of a
signature.
- `signatures::random_signature` — Cuchiero--Schmocker--Teichmann
random reservoir projection of the signature.
- `signatures::signature_kernel` — Salvi--Cass--Lyons signature kernel
via the Goursat finite-difference scheme.
- `signatures::utils` — shuffle product and Chen-identity-driven
signature concatenation.
### Changed
- Bumped crate version from `1.0.1` to `1.1.0`. All previously stable
symbols remain untouched and binary-compatible at the Python ABI
level (`abi3-py38`).
### Notes
This release is **CPU-only and additive**. No Python bindings were added
in `1.1.0`; the new modules are exposed via the Rust API only and will
be wrapped behind the `python-bindings` feature in a follow-up release.
+5 -5
View File
@@ -1,11 +1,11 @@
[package]
name = "optimizr"
version = "0.3.0"
name = "optimiz-rs"
version = "1.1.0"
edition = "2021"
authors = ["Your Name <your.email@example.com>"]
authors = ["HFThot Research Lab <contact@hfthot-lab.eu>"]
description = "High-performance optimization algorithms in Rust with Python bindings"
license = "MIT"
repository = "https://github.com/yourusername/optimiz-r"
repository = "https://github.com/ThotDjehuty/optimiz-r"
keywords = ["optimization", "machine-learning", "statistics", "numerical", "scientific"]
categories = ["algorithms", "science", "mathematics"]
readme = "README.md"
@@ -28,7 +28,7 @@ ordered-float = "4.2"
statrs = "0.17"
[features]
default = ["python-bindings"]
default = []
python-bindings = ["pyo3", "numpy"]
parallel = []
+2 -2
View File
@@ -94,9 +94,9 @@ publish: ## Publish to PyPI
@echo "Publishing to PyPI..."
maturin publish
docs: ## Build documentation
docs: ## Build documentation (HTML)
@echo "Building documentation..."
@echo "Documentation build not yet implemented"
sphinx-build -b html docs/source docs/build/html
example: ## Run example script
@echo "Running HMM example..."
+62 -21
View File
@@ -1,23 +1,40 @@
# OptimizR 🚀
# Optimiz-rs 🚀
<p align="center">
<img src="https://raw.githubusercontent.com/ThotDjehuty/optimiz-r/main/docs/source/logo_optimizrs.png" alt="Optimiz-rs Logo" width="220" />
</p>
**High-performance optimization algorithms in Rust with Python bindings**
[![Version](https://img.shields.io/badge/version-0.3.0-blue.svg)](https://github.com/ThotDjehuty/optimiz-r/releases)
[![Version](https://img.shields.io/badge/version-1.1.0-blue.svg)](https://github.com/ThotDjehuty/optimiz-r/releases)
[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
[![Rust](https://img.shields.io/badge/rust-1.70+-orange.svg)](https://www.rust-lang.org/)
[![Python](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/)
OptimizR provides blazingly fast, production-ready implementations of advanced optimization and statistical inference algorithms. Built with Rust for maximum performance and exposed to Python through PyO3, it delivers 50-100× speedup over pure Python implementations.
Optimiz-rs provides blazingly fast, production-ready implementations of advanced optimization and statistical inference algorithms. Built with Rust for maximum performance and exposed to Python through PyO3, it delivers 50-100× speedup over pure Python implementations.
## ✨ What's New in v0.3.0
## ✨ What's New in v1.1.0
🎮 **Mean Field Games (MFG)** - Complete 1D solver for large population dynamics with HJB-Fokker-Planck coupling
📚 **Validated Tutorial Notebooks** - All 7 example notebooks tested and production-ready
🏗️ **Maturin Build System** - Reliable cross-platform builds (fixes macOS issues)
🐍 **Enhanced Python Wrappers** - Smart OOP interfaces with automatic Rust acceleration
📖 **Comprehensive Documentation** - New MFG tutorial with 3D visualizations and complete audit report
This release adds a broad collection of **CPU-only generic numerical primitives**, all purely additive:
[**→ See Full Release Notes**](RELEASE_NOTES_v0.3.0.md)
- **`optimal_control::matrix_riccati`** — backward RK4 solver for the matrix Riccati ODE.
- **`timeseries_utils::nonsync_covariance`** — Hayashi--Yoshida asynchronous covariance estimator.
- **`timeseries_utils::wavelet`** — DWT and MODWT (Haar, Daubechies 2--10).
- **`risk_measures`** — empirical / parametric VaR and CVaR, plus convex CVaR minimisation.
- **`graph::laplacian`** + **`graph::spectral_clustering`** — combinatorial / normalised / random-walk Laplacians and spectral clustering with Jacobi diagonalisation.
- **`topology`** — Vietoris--Rips persistent homology and bottleneck distance.
- **`volterra`** — fractional Caputo Adams solver, Markovian lift by NNLS on a geometric grid, second-kind Volterra solver, direct Fourier inversion of characteristic functions.
- **`signatures`** — truncated tensor signatures, log-signatures, random-reservoir projection (Cuchiero--Schmocker--Teichmann), Salvi--Cass--Lyons signature kernel, shuffle product / Chen concatenation.
All new modules are exposed via the **Rust API only** in this release; Python bindings will follow in a subsequent minor release. The previously stable Python API is unchanged.
## ✨ What's New in v1.0.0
🎉 **Production Ready** - First stable release with comprehensive documentation
📚 **ReadTheDocs** - Full documentation at https://optimiz-r.readthedocs.io
🏗️ **Published to crates.io** - Install with `cargo add optimiz-rs`
🐍 **Published to PyPI** - Install with `pip install optimiz-rs`
🔒 **Stable API** - Semantic versioning from v1.0.0 forward
## Features
@@ -47,10 +64,16 @@ OptimizR provides blazingly fast, production-ready implementations of advanced o
## Installation
### From PyPI (coming soon)
### From PyPI (Python)
```bash
pip install optimizr
pip install optimiz-rs
```
### From crates.io (Rust)
```bash
cargo add optimiz-rs
```
### From Source
@@ -425,8 +448,8 @@ Hamilton-Jacobi-Bellman equation solvers for stochastic control:
Comparison against pure Python/NumPy/SciPy implementations (v0.2.0):
| Algorithm | Problem Size | OptimizR (Rust) | NumPy/SciPy | Speedup |
|-----------|--------------|-----------------|-------------|---------|
| Algorithm | Problem Size | Optimiz-rs (Rust) | NumPy/SciPy | Speedup |
|-----------|--------------|-------------------|-------------|---------|
| **DE - rand/1** | 50D Rosenbrock | 285ms | 21.2s | **74×** |
| **DE - best/1** | 50D Rosenbrock | 270ms | 23.8s | **88×** |
| **DE - adaptive jDE** | 50D Rosenbrock | 310ms | 24.5s | **79×** |
@@ -469,7 +492,7 @@ Python script examples:
- [HMM Regime Detection](examples/hmm_regime_detection.py)
- [Parallel DE Benchmark](examples/parallel_de_benchmark.py)
- [Polaroid-Optimizr Integration](examples/polaroid_optimizr_integration.py)
- [Polarway-Optimizr Integration](examples/polarway_optimizr_integration.py)
- [Timeseries Integration](examples/timeseries_integration.py)
### Mathematical Background
@@ -481,6 +504,23 @@ Detailed mathematical descriptions and references:
- [Differential Evolution Theory](docs/theory/differential_evolution.md) - Updated for v0.2.0
- [Information Theory](docs/theory/information_theory.md)
## 📚 Documentation & Getting Started
**Comprehensive documentation is available on ReadTheDocs:**
👉 **[https://optimiz-r.readthedocs.io/en/latest/](https://optimiz-r.readthedocs.io/en/latest/)**
The documentation includes:
- 🚀 **Quick Start Guide** - Get up and running in minutes
- 📖 **Installation** - Detailed setup instructions for all platforms
- 🎓 **Tutorials** - Step-by-step guides for each algorithm
- 📚 **API Reference** - Complete function and class documentation
- 🔬 **Theory & Math** - Mathematical foundations and references
- 💡 **Examples** - Real-world use cases and code samples
-**Performance** - Benchmarks and optimization tips
**New to Optimiz-rs?** Start with the [Quick Start Guide](https://optimiz-r.readthedocs.io/en/latest/quickstart.html) or try the [Mean Field Games Tutorial](examples/notebooks/mean_field_games_tutorial.ipynb).
## Development
### Building from Source
@@ -541,14 +581,14 @@ MIT License - see [LICENSE](LICENSE) file for details.
## Citation
If you use OptimizR in your research, please cite:
If you use Optimiz-rs in your research, please cite:
```bibtex
@software{optimizr2024,
title = {OptimizR: High-Performance Optimization Algorithms in Rust},
author = {Your Name},
title = {Optimiz-rs: High-Performance Optimization Algorithms in Rust},
author = {HFThot Research Lab},
year = {2024},
version = {0.2.0},
version = {1.0.0},
url = {https://github.com/ThotDjehuty/optimiz-r}
}
```
@@ -571,8 +611,9 @@ Inspired by:
- Issues: [GitHub Issues](https://github.com/ThotDjehuty/optimiz-r/issues)
- Discussions: [GitHub Discussions](https://github.com/ThotDjehuty/optimiz-r/discussions)
- Email: your.email@example.com
- Website: [HFThot Research Lab](https://hfthot-lab.eu)
- Email: contact@hfthot-lab.eu
---
**OptimizR** - Fast optimization for data science and machine learning 🚀
**Optimiz-rs** - Fast optimization for data science and machine learning 🚀
+226
View File
@@ -0,0 +1,226 @@
# Documentation Improvements Needed
**Date:** 2026-02-17
**Version:** v1.0.1
## Summary
The user identified several critical gaps in the Optimiz-rs documentation that need to be addressed:
1. **Optimal Control Page (`docs/source/algorithms/optimal_control.md`)**
- Needs much more detail on HJB equations
- Needs explanation of viscosity solutions
- Needs to introduce what's actually in the code/module
- Needs more mathematical foundations
2. **HMM API Page (`docs/source/api/hmm.md`)**
- Currently almost empty (only ~16 lines)
- Needs explanation of what algorithms are implemented
- Needs details on how they work
- Needs guidance on when/how to use them
3. **General Documentation**
- Make more concise and detailed throughout
- Better balance of theory and practice
## Required Enhancements
### 1. Optimal Control Documentation
#### Mathematical Foundations Needed:
- **HJB Equation:** Full derivation and intuition
- General form for stochastic processes
- Specialization to Ornstein-Uhlenbeck process
- Connection to optimal stopping/switching problems
- **Viscosity Solutions:** Detailed explanation
- Why classical solutions don't exist (kinks at boundaries)
- Definition of viscosity solutions
- Numerical approximation via upwind schemes
- Monotonicity and convergence properties
- **Finite Difference Methods:**
- Grid discretization approach
- Upwind vs central differences
- Policy iteration algorithm
- Convergence criteria
#### Implementation Details Needed:
- **What's Actually in the Module:**
- HJB solver for OU process (src/optimal_control/hjb_solver.rs)
- Viscosity solution solver (src/optimal_control/viscosity.rs)
- Regime switching (src/optimal_control/regime_switching.rs)
- Jump diffusion (src/optimal_control/jump_diffusion.rs)
- MRSJD - Multi-Regime Switching Jump Diffusion (src/optimal_control/mrsjd.rs)
- OU parameter estimation (src/optimal_control/ou_estimator.rs)
- Kalman filters: Linear, EKF, UKF (src/optimal_control/kalman_filter.rs)
- Backtesting framework (src/optimal_control/backtest.rs)
#### Usage Guidance Needed:
- When to use each algorithm
- Parameter tuning guidelines
- Diagnostic plots and convergence monitoring
- Integration with other modules (HMM, Mean Field Games)
- Real-world trading examples
### 2. HMM API Documentation
#### Algorithms to Document:
- **Forward Algorithm:** Compute P(O|λ) efficiently
- Forward variable α_t(i)
- Recursive computation
- Numerical stability (scaling)
- **Backward Algorithm:** Alternative for completeness
- Backward variable β_t(i)
- Use in Baum-Welch
- **Viterbi Algorithm:** Most likely state sequence
- Dynamic programming approach
- Backtracking for path recovery
- **Baum-Welch (EM) Algorithm:** Parameter learning
- E-step: compute γ_t(i) and ξ_t(i,j)
- M-step: update π, A, B parameters
- Convergence properties
#### API Methods to Explain:
- **`HMM(n_states)`:** Constructor
- When to use 2 vs 3+ states
- Initialization strategy
- **`fit(X, n_iterations, tolerance)`:** Training
- What data X should look like
- How many iterations needed
- Convergence diagnostics
- Multiple random restarts
- **`predict(X)`:** Viterbi decoding
- Returns most likely state sequence
- Use cases: regime detection, trading signals
- **`score(X)`:** Log-likelihood
- Model comparison
- Convergence monitoring
- Anomaly detection
#### Usage Examples Needed:
- **Regime Detection:**
- Market regimes (bull/bear)
- Volatility regimes (high/low)
- Integration with optimal control
- **Parameter Estimation Per Regime:**
- Combine with OU parameter estimation
- Regime-specific HJB solving
- **Model Selection:**
- BIC/AIC for choosing number of states
- Cross-validation approaches
- **Numerical Best Practices:**
- Data requirements (minimum samples)
- Handling outliers
- Initialization sensitivity
- Convergence diagnostics
### 3. API Reference Page (`docs/source/api/optimal_control.md`)
Currently 117 lines but needs:
- Complete function signatures
- Parameter descriptions with types
- Return value specifications
- Detailed examples for each function
- Error handling documentation
## Implementation Plan
### Phase 1: Mathematical Foundations (High Priority)
1. Expand optimal_control.md with HJB equation derivations
2. Add viscosity solutions section with theory and numerics
3. Add finite difference methods explanation
### Phase 2: Algorithm Details (High Priority)
1. HMM API documentation expansion
2. Detail each algorithm (forward, backward, Viterbi, Baum-Welch)
3. Add mathematical formulas and intuition
### Phase 3: Usage Guidance (Medium Priority)
1. Add "When to Use" sections for each algorithm
2. Parameter tuning guidelines
3. Diagnostic procedures
4. Integration examples
### Phase 4: API Reference (Medium Priority)
1. Complete function signatures
2. Parameter and return types
3. Error documentation
4. Cross-references
### Phase 5: Examples and Tutorials (Low Priority)
1. Jupyter notebooks for common use cases
2. End-to-end workflows
3. Performance benchmarking examples
## Technical Notes
### Current Implementation Status:
**Optimal Control Module (`src/optimal_control/`):**
- ✅ HJB solver (hjb_solver.rs)
- ✅ Viscosity solutions (viscosity.rs)
- ✅ Regime switching (regime_switching.rs)
- ✅ Jump diffusion (jump_diffusion.rs)
- ✅ MRSJD (mrsjd.rs)
- ✅ OU estimation (ou_estimator.rs)
- ✅ Kalman filters (kalman_filter.rs, kalman_py_bindings.rs)
- ✅ Backtesting (backtest.rs)
**HMM Module (`src/hmm/`):**
- ✅ Gaussian emissions (emission.rs)
- ✅ Forward-Backward algorithm (model.rs)
- ✅ Viterbi decoding (viterbi.rs)
- ✅ Baum-Welch training (model.rs)
- ✅ Python bindings (python_bindings.rs)
### Documentation Files to Update:
1. `docs/source/algorithms/optimal_control.md` (currently 94 lines → target: 500+ lines)
2. `docs/source/api/hmm.md` (currently 16 lines → target: 300+ lines)
3. `docs/source/api/optimal_control.md` (currently 117 lines → target: 400+ lines)
4. `docs/source/algorithms/hmm.md` (currently 607 lines → verify completeness)
### Backup Files Created:
- `docs/source/algorithms/optimal_control.md.backup`
- `docs/source/api/hmm.md.backup`
- `docs/source/api/optimal_control.md.backup`
## Next Steps
1. **Immediate:** Write comprehensive optimal_control mathematical foundations
2. **Immediate:** Expand HMM API documentation with algorithm details
3. **Soon:** Add usage examples and integration guides
4. **Later:** Create Jupyter notebook tutorials
## References Needed
### Optimal Control:
- Fleming & Soner (2006): Controlled Markov Processes and Viscosity Solutions
- Øksendal (2003): Stochastic Differential Equations
- Pham (2009): Continuous-time Stochastic Control and Optimization
- Barles & Souganidis (1991): Convergence of approximation schemes
### HMM:
- Rabiner (1989): Tutorial on HMMs and selected applications
- Murphy (2012): Machine Learning: A Probabilistic Perspective
- Bishop (2006): Pattern Recognition and Machine Learning
### Kalman Filtering:
- Kalman (1960): A New Approach to Linear Filtering
- Julier & Uhlmann (1997): Unscented Kalman Filter
---
**Status:** Documentation gaps identified. Implementation in progress.
**Priority:** High - These are critical for user onboarding and proper usage.
+19
View File
@@ -0,0 +1,19 @@
# Minimal makefile for Sphinx documentation
SHELL := /bin/sh
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build
.PHONY: help clean html
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS)
clean:
rm -rf "$(BUILDDIR)"
html:
$(SPHINXBUILD) -b html "$(SOURCEDIR)" "$(BUILDDIR)/html" $(SPHINXOPTS)
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+395
View File
@@ -0,0 +1,395 @@
# OptimizR v1.0.0 Publication Guide
**Status:** ✅ READY FOR PUBLICATION
**Date:** February 16, 2026
**Repository:** https://github.com/ThotDjehuty/optimiz-r
**Tag:** v1.0.0
---
## ✅ Completed Preparation
### 1. Fixed Cargo.toml (Commit: f44280e)
- ✅ Removed `python-bindings` from default features
- ✅ Updated version: 0.3.0 → 1.0.0
- ✅ Updated authors: HFThot Research Lab <contact@hfthot-lab.eu>
- ✅ Updated repository URL: https://github.com/ThotDjehuty/optimiz-r
### 2. Fixed pyproject.toml (Commit: f44280e, a3117fe)
- ✅ Updated version: 0.3.0 → 1.0.0
- ✅ Updated authors: HFThot Research Lab
- ✅ Updated URLs (homepage, docs, repository)
- ✅ Fixed maturin configuration to use python-bindings feature
### 3. Updated README.md (Commit: f44280e)
- ✅ Version badge: 0.3.0 → 1.0.0
- ✅ What's New section updated for v1.0.0
- ✅ Citation author updated
- ✅ Contact information updated
### 4. Added .gitignore (Commit: 0dfb9b8)
- ✅ Excluded wheels/ directory from git
### 5. Created RELEASE_NOTES_v1.0.0.md (Commit: 07d4529)
- ✅ Comprehensive release notes
- ✅ Breaking changes documentation
- ✅ Migration guide
- ✅ Roadmap for future versions
### 6. Build Verification
-`cargo publish --dry-run` - SUCCESS
-`maturin build --release --features python-bindings` - SUCCESS
- ✅ Wheel built: `optimizr-1.0.0-cp38-abi3-macosx_10_12_x86_64.whl`
### 7. Git Tag & Push
- ✅ Created tag: v1.0.0
- ✅ Pushed to GitHub: main branch + v1.0.0 tag
---
## 📋 Next Steps: Actual Publication
### Step 1: Set Up crates.io Account
1. **Create Account** (if not already done)
- Visit: https://crates.io/
- Sign in with GitHub account
2. **Generate API Token**
- Go to: https://crates.io/settings/tokens
- Create new token: "OptimizR v1.0.0 Publication"
- Copy the token (you won't see it again)
3. **Configure Credentials**
```bash
cargo login <your-crates-io-token>
```
This creates `~/.cargo/credentials` with your token
### Step 2: Publish to crates.io
```bash
cd /Users/melvinalvarez/Documents/Workspace/optimiz-r
# Final verification (already tested ✅)
cargo publish --dry-run
# Actual publication
cargo publish
# Expected output:
# Updating crates.io index
# Packaging optimizr v1.0.0 (/Users/.../optimiz-r)
# Uploading optimizr v1.0.0
# Published optimizr v1.0.0
```
**Verification:**
- Visit: https://crates.io/crates/optimiz-rs
- Should show v1.0.0 within a few minutes
---
### Step 3: Set Up PyPI Account
1. **Create PyPI Account** (if not already done)
- Visit: https://pypi.org/account/register/
- Verify email
2. **Enable 2FA** (required for publishing)
- Settings → Account Security
- Set up 2FA with authenticator app
3. **Create API Token**
- Account settings → API tokens
- Scope: Entire account (or specific project after first upload)
- Copy the token (starts with `pypi-`)
4. **Configure Credentials**
```bash
# Create ~/.pypirc
cat > ~/.pypirc << 'EOF'
[distutils]
index-servers =
pypi
[pypi]
username = __token__
password = pypi-YOUR_TOKEN_HERE
EOF
# Secure the file
chmod 600 ~/.pypirc
```
### Step 4: Publish to PyPI
```bash
cd /Users/melvinalvarez/Documents/Workspace/optimiz-r
# Build wheels for multiple platforms (current: macOS only)
# Option 1: Build for current platform only
maturin build --release --features python-bindings
# Option 2: Use maturin publish which builds and uploads
maturin publish --username __token__ --password pypi-YOUR_TOKEN_HERE
# OR if ~/.pypirc is configured:
maturin publish
```
**Multi-Platform Wheels (Optional but Recommended):**
To publish wheels for Linux, Windows, and macOS:
```bash
# Use GitHub Actions (recommended)
# Already have .github/workflows/ci.yml - extend it with:
# - maturin publish on tag push
# - Build wheels for: Linux (x86_64, aarch64), Windows (x86_64), macOS (x86_64, aarch64)
# Manual alternative: Use cibuildwheel
pip install cibuildwheel
cibuildwheel --platform linux
cibuildwheel --platform windows
cibuildwheel --platform macos
# Upload all wheels
maturin upload target/wheels/*
```
**Verification:**
- Visit: https://pypi.org/project/optimizr/
- Should show v1.0.0 within a few minutes
- Test installation:
```bash
pip install optimizr==1.0.0
python -c "import optimizr; print(optimizr.__version__)"
```
---
## 🔄 GitHub Actions Automation (Recommended)
To automate future releases, update `.github/workflows/ci.yml`:
```yaml
name: Release
on:
push:
tags:
- 'v*'
jobs:
publish-crates:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Publish to crates.io
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_TOKEN }}
run: cargo publish --token $CARGO_REGISTRY_TOKEN
publish-pypi:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install maturin
run: pip install maturin
- name: Build wheels
run: maturin build --release --features python-bindings
- name: Publish to PyPI
env:
MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
run: maturin publish --username __token__ --password $MATURIN_PYPI_TOKEN
```
**Setup Secrets:**
1. GitHub repo → Settings → Secrets and variables → Actions
2. Add secrets:
- `CRATES_TOKEN`: Your crates.io API token
- `PYPI_TOKEN`: Your PyPI API token (starts with `pypi-`)
---
## 📊 Post-Publication Checklist
### Immediate (After Publishing)
- [ ] **Verify crates.io**: https://crates.io/crates/optimiz-rs/1.0.0
- [ ] **Verify PyPI**: https://pypi.org/project/optimizr/1.0.0
- [ ] **Test Rust installation**:
```bash
cargo new test-optimiz-rs
cd test-optimiz-rs
cargo add optimiz-rs
cargo build
```
- [ ] **Test Python installation**:
```bash
python -m venv test-env
source test-env/bin/activate
pip install optimizr==1.0.0
python -c "import optimizr; print(optimizr.__version__)"
```
### Documentation Updates
- [ ] **Update OPEN_SOURCE_STRATEGY.md**:
```markdown
#### 2. **Optimiz-R** - Portfolio Optimization Engine
- **Current Status:** ✅ v1.0.0 published (crates.io + PyPI)
- **Repository:** https://github.com/ThotDjehuty/optimiz-r
- **Documentation:** https://optimiz-r.readthedocs.io
- **Installation:** `cargo add optimiz-rs` or `pip install optimiz-rs`
```
- [ ] **Create GitHub Release**:
- Go to: https://github.com/ThotDjehuty/optimiz-r/releases/new
- Tag: v1.0.0
- Title: "OptimizR v1.0.0 - First Stable Release"
- Description: Copy from RELEASE_NOTES_v1.0.0.md
- Attach assets: wheels from target/wheels/
### Marketing & Announcements
- [ ] **Blog Post** (https://hfthot-lab.eu):
```markdown
Title: "OptimizR v1.0.0: High-Performance Optimization in Rust"
Sections:
1. What is OptimizR?
2. Performance benchmarks (50-100× speedup)
3. Key features & algorithms
4. Getting started (Rust & Python)
5. Why open source?
6. Roadmap & community
```
- [ ] **Social Media Announcements**:
- Twitter/X: "🚀 OptimizR v1.0.0 is live! High-performance optimization algorithms in Rust with Python bindings. 50-100× faster than pure Python. MIT licensed. #rustlang #python #optimization"
- LinkedIn: Professional announcement with benchmarks
- Reddit:
- r/rust: "OptimizR v1.0.0: Optimization algorithms with 50-100× speedup"
- r/Python: "Fast optimization library (Rust-powered) now on PyPI"
- r/algotrading: "Open-source optimization for quant finance"
- [ ] **Hacker News** (https://news.ycombinator.com/submit):
```
Title: "OptimizR v1.0.0 High-Performance Optimization Algorithms in Rust"
URL: https://github.com/ThotDjehuty/optimiz-r
```
- [ ] **Dev.to Article**:
- "Building a 100× Faster Optimization Library with Rust and PyO3"
- Include benchmarks, code examples, lessons learned
### Community Building
- [ ] **Update README badges**:
- Add crates.io badge: `[![Crates.io](https://img.shields.io/crates/v/optimiz-rs.svg)](https://crates.io/crates/optimiz-rs)`
- Add PyPI badge: `[![PyPI](https://img.shields.io/pypi/v/optimizr.svg)](https://pypi.org/project/optimizr/)`
- Add downloads badge
- [ ] **Create Discord/Discussions**:
- Enable GitHub Discussions for Q&A
- Or create Discord server for community
- [ ] **Contributing Guide** (CONTRIBUTING.md):
- How to contribute
- Development setup
- Code style guidelines
- PR process
---
## 🎯 Success Metrics (Month 1)
### Downloads
- **Target crates.io**: 100 downloads
- **Target PyPI**: 500 downloads
### Community
- **GitHub Stars**: 50+
- **Issues/Questions**: 5-10
- **Contributors**: 2-3
### Documentation
- **ReadTheDocs views**: 1000+
- **Tutorial completions**: 50+
---
## 🐛 Known Issues & Limitations
### Current Limitations
1. **Single-platform wheels**: Only macOS built locally
- **Solution**: Use GitHub Actions for multi-platform builds
2. **Compiler warnings**: 8 unused variable warnings
- **Solution**: Run `cargo fix --lib -p optimizr` and commit
3. **Documentation**: Some examples could be more comprehensive
- **Solution**: Add more real-world use cases to tutorials
### Not Yet Implemented
- GPU acceleration (roadmap: v1.2.0)
- Additional DE variants (JADE, SHADE) - roadmap: v1.1.0
- Multi-objective optimization - roadmap: v2.0.0
---
## 📞 Support
If publication issues occur:
**Crates.io Issues:**
- Check: https://crates.io/policies
- Email: help@crates.io
- Docs: https://doc.rust-lang.org/cargo/reference/publishing.html
**PyPI Issues:**
- Check: https://pypi.org/help/
- Docs: https://packaging.python.org/tutorials/packaging-projects/
- Forum: https://discuss.python.org/c/packaging/14
**General:**
- Email: contact@hfthot-lab.eu
- GitHub Issues: https://github.com/ThotDjehuty/optimiz-r/issues
---
## ✅ Summary
**OptimizR v1.0.0 is READY FOR PUBLICATION**
All code changes committed ✅
All tests passing ✅
Documentation complete ✅
Git tag created (v1.0.0) ✅
Release notes written ✅
Pushed to GitHub ✅
**Next Action:** Set up crates.io and PyPI credentials, then run:
```bash
cargo publish # For Rust users
maturin publish # For Python users
```
**Total Time Invested:** ~2 hours (as estimated)
**Publication Time:** 15-30 minutes (once credentials configured)
---
**Ready to publish! 🚀**
+199
View File
@@ -0,0 +1,199 @@
# Publication Status Report - OptimizR v1.0.0
**Date:** February 17, 2026
**Author:** ThotDjehuty
---
## ✅ Completed Tasks
### 1. Notebook Fixes (100% Success Rate)
- **05_performance_benchmarks.ipynb**: ✅ Fixed
- Reduced HMM observation count from 50k to 10k max
- Reduced Information Theory tests to 10k max
- Added memory stability documentation
- All benchmarks now run without kernel crashes
- **mean_field_games_tutorial.ipynb**: ✅ Fixed
- Reduced grid from 100×100 to 50×50 for Python stability
- Implemented CFL condition checking with auto-adjustment
- Added semi-implicit schemes for HJB solver
- Added sub-stepping for Fokker-Planck solver
- Enhanced error handling (NaN/Inf detection)
- Graceful convergence handling with detailed logging
**Result**: All 8 tutorial notebooks are now functional!
### 2. Documentation & Marketing
- **LINKEDIN_POST.md**: ✅ Created
- Compelling narrative with real benchmarks
- Clear value proposition (50-100× speedup)
- Call-to-action for GitHub stars and contributions
- Links to docs, crates.io, PyPI, GitHub
- **OPEN_SOURCE_STRATEGY.md**: ✅ Updated
- Added v1.0.0 release status
- Complete feature list
- Performance metrics
- Publication links (prepared for crates.io and PyPI)
### 3. Git Configuration
- ✅ Configured as ThotDjehuty (admin@hfthot-lab.eu)
- ✅ All commits properly attributed
### 4. Code Commits
- ✅ Committed notebook fixes with detailed changelog
- ✅ Pushed to GitHub remote (main branch)
---
## ⚠️ Publication Issues
### crates.io - Email Verification Required
**Status:** ❌ **Cannot publish yet**
**Error Message:**
```
the remote server responded with an error (status 400 Bad Request):
A verified email address is required to publish crates to crates.io.
Visit https://crates.io/settings/profile to set and verify your email address.
```
**Action Required:**
1. Visit https://crates.io/settings/profile
2. Add and verify email address: melvin.caradu@gmail.com (or admin@hfthot-lab.eu)
3. Re-run: `cargo publish`
**Package is ready:** All builds pass, just waiting for email verification.
---
### PyPI - Package Name Conflict
**Status:** ❌ **Cannot publish under "optimizr"**
**Issue:** Package name "optimizr" is already taken on PyPI (v1.4.7)
- URL: https://pypi.org/project/optimizr/
- Owner: Different maintainer
- Description: Different project (not our Rust library)
**Error Message:**
```
ERROR HTTPError: 403 Forbidden from https://upload.pypi.org/legacy/
```
**Solutions:**
1. **Option A: Use Different Package Name** (Recommended)
- `optimiz-rs` - Rust variant naming
- `optimizr-hft` - HFT-focused variant
- `hfthot-optimizr` - Branded name
- `rustimizr` - Play on "Rust optimization"
**Steps:**
```bash
# 1. Update pyproject.toml
[project]
name = "optimiz-rs" # New name
# 2. Rebuild wheel
maturin build --release
# 3. Upload to PyPI
twine upload target/wheels/optimiz_rs-1.0.0-*.whl -u ThotDjehuty -p "..."
```
2. **Option B: Contact Current Owner**
- Request name transfer
- Package appears abandoned (last update unclear)
- This could take weeks/months
**Recommendation:** Go with Option A (alternative name) to unblock release immediately.
---
## 📝 Next Steps
### Immediate (Today)
1. **crates.io:**
- [ ] Verify email at https://crates.io/settings/profile
- [ ] Run `cargo publish`
- [ ] Update RELEASE_NOTES with crates.io link
2. **PyPI:**
- [ ] Decide on alternative package name
- [ ] Update `pyproject.toml` with new name
- [ ] Rebuild wheel: `maturin build --release`
- [ ] Upload: `twine upload target/wheels/*.whl -u ThotDjehuty -p "G2p._468pfSH73G"`
- [ ] Update RELEASE_NOTES and LINKEDIN_POST with PyPI link
3. **Documentation:**
- [ ] Update installation instructions with correct package names
- [ ] Update README.md
- [ ] Update ReadTheDocs references
### This Week
- [ ] Post LinkedIn announcement (after both publications)
- [ ] Create GitHub release v1.0.0 with notes
- [ ] Announce on relevant subreddits (r/rust, r/algotrading)
- [ ] Share on Hacker News
- [ ] Reach out to Python/Rust communities
---
## 📊 Current Status Summary
| Task | Status | Notes |
|------|--------|-------|
| Fix notebooks | ✅ Complete | 8/8 working (100%) |
| Create LinkedIn post | ✅ Complete | Ready to publish |
| Update OPEN_SOURCE.md | ✅ Complete | v1.0.0 documented |
| Git configuration | ✅ Complete | ThotDjehuty identity |
| Commit changes | ✅ Complete | Pushed to GitHub |
| **crates.io** | ⏸️ **Blocked** | **Need email verification** |
| **PyPI** | ⏸️ **Blocked** | **Need alternative name** |
---
## 🎯 Recommended Package Name
**Suggested:** `optimiz-rs`
**Rationale:**
- Clear that it's the Rust implementation
- Follows Python packaging conventions for Rust bindings
- SEO-friendly (people searching "optimizr rust" will find it)
- Professional and descriptive
- Available on PyPI (checked)
**Update locations:**
1. `pyproject.toml` → `name = "optimiz-rs"`
2. `README.md` → `pip install optimiz-rs`
3. `docs/source/installation.rst` → Update pip command
4. `LINKEDIN_POST.md` → Update installation instructions
5. `RELEASE_NOTES_v1.0.0.md` → Update PyPI references
---
## 🔥 What We Achieved Today
**ALL notebooks now functional** (8/8 = 100%)
**Professional marketing materials** (LinkedIn post ready)
**Documentation updated** (OPEN_SOURCE_STRATEGY.md)
**Code properly committed** (as ThotDjehuty)
**Wheel built successfully** (ready for PyPI)
**crates.io ready** (just needs email verification)
🎉 **OptimizR v1.0.0 is 99% ready for public release!**
Just need to:
1. Verify email on crates.io (2 minutes)
2. Choose PyPI name and rebuild (5 minutes)
3. Publish both packages (2 minutes)
4. Post LinkedIn announcement (copy-paste ready)
**Total time to completion: ~10 minutes of user action required**
---
**Status:** Ready for user decisions on crates.io email and PyPI package name.
+88
View File
@@ -0,0 +1,88 @@
# PyPI Publishing Instructions
## Current Status
- ✅ Package built: `optimiz_rs-1.0.0-cp38-abi3-macosx_10_12_x86_64.whl`
- ✅ Package name: `optimiz-rs` (to avoid conflict with existing "optimizr")
- ❌ Need PyPI API token (username/password auth deprecated)
## Steps to Publish
### 1. Get PyPI API Token
1. Login to PyPI: https://pypi.org/account/login/
- Username: `ThotDjehuty`
- Password: `G2p._468pfSH73G`
2. Create API token: https://pypi.org/manage/account/token/
- Click "Add API token"
- Token name: `optimiz-rs-publishing`
- Scope: "Entire account" (can limit to project later)
- **IMPORTANT:** Copy the token immediately (starts with `pypi-`)
- Store securely (won't be shown again)
### 2. Upload to PyPI
```bash
cd /Users/melvinalvarez/Documents/Workspace/optimiz-r
# Upload with API token
twine upload target/wheels/optimiz_rs-1.0.0-cp38-abi3-macosx_10_12_x86_64.whl \
-u __token__ \
-p pypi-YOUR_TOKEN_HERE
```
**Note:** Username must be `__token__` (literal string) when using API tokens.
### 3. Verify Publication
After successful upload, verify at:
- Package page: https://pypi.org/project/optimiz-rs/
- Test install: `pip install optimiz-rs`
### 4. Update Documentation
Once published, update these files:
- `RELEASE_NOTES_v1.0.0.md` - Change "(publishing in progress)" to actual link
- `LINKEDIN_POST.md` - Update PyPI link
- Commit and push changes
## Alternative: Store Token in ~/.pypirc
For future uploads, store token securely:
```bash
# Create ~/.pypirc
cat > ~/.pypirc << 'EOF'
[pypi]
username = __token__
password = pypi-YOUR_TOKEN_HERE
EOF
# Secure the file
chmod 600 ~/.pypirc
# Then upload without credentials in command
twine upload target/wheels/optimiz_rs-1.0.0-*.whl
```
## Troubleshooting
**403 Forbidden:**
- PyPI deprecated username/password auth
- Must use API tokens
- Ensure username is `__token__` (not your actual username)
**Package name conflict:**
- Already handled - using `optimiz-rs`
- Cannot use `optimizr`, `optimiz-r`, or `optimizR` (all normalize to "optimizr")
**Token not working:**
- Verify token copied completely (very long string)
- Check token hasn't expired
- Ensure no extra spaces/newlines
---
**Current Publication Status:**
- ✅ crates.io: Published at https://crates.io/crates/optimiz-rs
- ⏳ PyPI: Ready to publish (just need API token)
+231
View File
@@ -0,0 +1,231 @@
# OptimizR v1.0.0 Release Notes
**Release Date:** February 16, 2026
**Status:** ✅ Stable Release
---
## 🎉 First Stable Release
OptimizR v1.0.0 marks the first production-ready stable release with a commitment to semantic versioning going forward. The API is now stable and breaking changes will only occur in major version bumps.
## 📦 Distribution
### crates.io (Rust)
```bash
cargo add optimiz-rs
```
🔗 https://crates.io/crates/optimiz-rs
### PyPI (Python)
```bash
pip install optimiz-rs
```
🔗 https://pypi.org/project/optimiz-rs/
## 🆕 What's New in v1.0.0
### Publication & Distribution
-**Published to crates.io** - Available in Rust package registry (Feb 17, 2026)
-**Published to PyPI** - Available as `optimiz-rs` via pip install (Feb 17, 2026)
-**Stable API** - Semantic versioning from v1.0.0 forward
-**Production Ready** - Comprehensive testing and validation
**Note:** PyPI package is named `optimiz-rs` (not `optimizr`) to distinguish the Rust implementation.
### Documentation
- 📚 **ReadTheDocs** - Full documentation at https://optimiz-r.readthedocs.io
- 📖 **Getting Started Guide** - Quick start for new users
- 📝 **API Reference** - Complete function and class documentation
- 🎓 **Tutorials** - Step-by-step guides for all algorithms
- 🔬 **Theory & Math** - Mathematical foundations and references
### Build System Improvements
- 🏗️ **Fixed Cargo.toml** - Removed python-bindings from default features
- Resolves linker errors when using as Rust library
- Python bindings now opt-in feature (automatically enabled by maturin)
- 🐍 **Maturin Configuration** - Explicit python-bindings feature in pyproject.toml
- Ensures correct PyO3 extension builds for PyPI
- Fixes cross-platform compatibility
### Metadata Updates
- 👥 **Authors**: HFThot Research Lab <admin@hfthot-lab.eu>
- 🔗 **Repository**: https://github.com/ThotDjehuty/optimiz-r
- 📚 **Documentation**: https://optimiz-r.readthedocs.io
## 🚀 Features (Stable)
### Optimization Algorithms
-**Differential Evolution** - 5 strategies (rand/1, best/1, current-to-best/1, rand/2, best/2)
-**Adaptive jDE** - Self-tuning mutation factor and crossover rate
-**Grid Search** - Exhaustive parameter space exploration
### Hidden Markov Models
-**Baum-Welch Training** - EM algorithm for parameter learning
-**Viterbi Decoding** - Most likely state sequence
-**Gaussian Emissions** - Continuous observation models
### MCMC Sampling
-**Metropolis-Hastings** - Bayesian parameter estimation
-**Adaptive Proposals** - Gaussian random walk
-**Convergence Diagnostics** - Acceptance rate tracking
### Mean Field Games (v0.3.0+)
-**1D MFG Solver** - Large population dynamics
-**HJB-Fokker-Planck Coupling** - Fixed-point iteration
-**Agent Population Dynamics** - Spatial-temporal evolution
### Mathematical Toolkit
-**Numerical Differentiation** - gradient(), hessian(), jacobian()
-**Statistics** - mean(), variance(), skewness(), kurtosis()
-**Linear Algebra** - norms, normalization, trace, outer product
-**Information Theory** - mutual_information(), shannon_entropy()
## ⚡ Performance
- **50-100× faster** than pure Python implementations
- **95% memory reduction** vs NumPy/SciPy
- **Parallel-ready** with Rayon infrastructure
- Production-tested on multi-dimensional problems
## 📊 Benchmarks
### Differential Evolution (Rosenbrock 10D)
- OptimizR (Rust): **0.12s**
- SciPy (Python): **8.9s**
- **Speedup: 74×**
### HMM Training (1000 observations, 3 states)
- OptimizR (Rust): **0.03s**
- hmmlearn (Python): **2.4s**
- **Speedup: 80×**
### Mean Field Games (100×100 grid)
- OptimizR (Rust): **0.4s**
- Pure Python: **45s**
- **Speedup: 112×**
## 🔧 Breaking Changes from v0.3.0
### Cargo Feature Flags
```toml
# OLD (v0.3.0):
[features]
default = ["python-bindings"] # Always included
# NEW (v1.0.0):
[features]
default = [] # No default features
python-bindings = ["pyo3", "numpy"] # Opt-in
```
**Impact:**
- Rust-only users: No breaking changes (python-bindings not needed)
- Python users: No impact (maturin automatically enables python-bindings)
If you're using OptimizR as a Rust library and explicitly depend on Python bindings:
```toml
# Update your Cargo.toml:
[dependencies]
optimizr = { version = "1.0", features = ["python-bindings"] }
```
## 📝 Migration Guide
### From v0.3.0 to v1.0.0
**For Rust Users:**
No code changes required. If you were using python-bindings explicitly, add it to features list.
**For Python Users:**
```bash
# Install via pip
pip install optimiz-rs
# Or specify version
pip install optimiz-rs==1.0.0
```
**Note:** Package name changed from `optimizr` to `optimiz-rs` to avoid PyPI naming conflict.
**API Compatibility:**
✅ All Python APIs remain unchanged
✅ All Rust APIs remain unchanged
✅ Function signatures are identical
✅ Return types are identical
✅ No deprecations or removals
## 🐛 Bug Fixes
- Fixed linking errors when using OptimizR as Rust-only library
- Fixed PyInit__core symbol warning in maturin builds
- Resolved flate2 yanked dependency warning
## 📚 Documentation
### New Documentation
- Complete ReadTheDocs site: https://optimiz-r.readthedocs.io
- Getting Started guide
- Installation instructions for all platforms
- Tutorial notebooks (7 validated examples)
- API reference with examples
- Theory and mathematical background
### Validated Tutorial Notebooks
1.**Hidden Markov Models** - Regime detection
2.**MCMC Sampling** - Bayesian inference
3.**Differential Evolution** - Global optimization
4.**Optimal Control** - HJB solver (theory)
5.**Real-World Applications** - Complete workflows
6.**Performance Benchmarks** - Rust vs Python
7.**Mean Field Games** - Population dynamics
## 🔮 Roadmap
### v1.1.0 (Q2 2026)
- [ ] Additional DE variants (JADE, SHADE, L-SHADE)
- [ ] Particle Swarm Optimization (PSO)
- [ ] CMA-ES algorithm
- [ ] More HMM emission distributions
### v1.2.0 (Q3 2026)
- [ ] GPU acceleration via CUDA/ROCm
- [ ] Additional language bindings (R, Julia, JavaScript)
- [ ] Distributed computing support
- [ ] Advanced parallel strategies
### v2.0.0 (2027)
- [ ] Neural Evolution Strategies (NES)
- [ ] Multi-objective optimization
- [ ] Constraint handling methods
- [ ] Advanced uncertainty quantification
## 🙏 Acknowledgments
Built with:
- [Rust](https://www.rust-lang.org/) - Systems programming language
- [PyO3](https://pyo3.rs/) - Rust bindings for Python
- [Maturin](https://www.maturin.rs/) - Build and publish Rust crates as Python packages
- [NumPy](https://numpy.org/) - Numerical computing in Python
Inspired by:
- scipy.optimize
- scikit-learn
- hmmlearn
- emcee
## 📞 Support & Community
- **Issues**: [GitHub Issues](https://github.com/ThotDjehuty/optimiz-r/issues)
- **Discussions**: [GitHub Discussions](https://github.com/ThotDjehuty/optimiz-r/discussions)
- **Email**: contact@hfthot-lab.eu
## 📄 License
MIT License - see [LICENSE](LICENSE) file for details.
---
**OptimizR v1.0.0** - Fast optimization for data science and machine learning 🚀
Thank you to all contributors and early adopters who helped make this release possible!
@@ -1,4 +1,4 @@
# OptimizR Refactoring - Completion Report
# Optimiz-rs Refactoring - Completion Report
## ✅ All Tasks Completed
@@ -263,7 +263,7 @@ pyproject.toml # ✅ Unchanged
✅ **All todo items completed successfully!**
The OptimizR codebase has been completely refactored with:
The Optimiz-rs codebase has been completely refactored with:
- ✅ Modular trait-based architecture
- ✅ Functional programming patterns
- ✅ Advanced design patterns (Strategy, Builder, Traits)
@@ -1,4 +1,4 @@
# OptimizR Development Guide
# Optimiz-rs Development Guide
## Quick Start
@@ -1,7 +1,7 @@
# OptimizR Enhancement Strategy
# Optimiz-rs Enhancement Strategy
**Date**: January 2, 2025
**Context**: Post-Polaroid Phase 4, exploring integration and improvements
**Context**: Post-Polarway Phase 4, exploring integration and improvements
**Based On**: v0.2.0 codebase review, roadmap analysis, synergy opportunities
## Current State Analysis
@@ -58,19 +58,19 @@
- Simulated Annealing
- Ant Colony Optimization
## Synergy Opportunities: Polaroid + OptimizR
## Synergy Opportunities: Polarway + Optimiz-rs
### 1. Time-Series Feature Engineering for HMM
**Description**: Use Polaroid's time-series operations to create features for regime detection
**Description**: Use Polarway's time-series operations to create features for regime detection
**Implementation**:
```python
# Polaroid: Fast feature creation
# Polarway: Fast feature creation
df = client.lag(['price'], periods=1) # Lagged prices
df = client.pct_change(['price'], periods=1) # Returns
df = client.diff(['price'], periods=1) # Price changes
# OptimizR: Regime detection on features
# Optimiz-rs: Regime detection on features
returns = df['price_pct_change'].to_numpy()
hmm = HMM(n_states=3) # Bull, Bear, Sideways
hmm.fit(returns, n_iterations=100)
@@ -78,8 +78,8 @@ states = hmm.predict(returns)
```
**Value**:
- Polaroid provides fast feature engineering (50-200× faster for large datasets)
- OptimizR provides statistical inference (HMM regime detection)
- Polarway provides fast feature engineering (50-200× faster for large datasets)
- Optimiz-rs provides statistical inference (HMM regime detection)
- Combined: Real-time regime switching for trading strategies
### 2. Risk Metrics on Time-Series Data
@@ -87,18 +87,18 @@ states = hmm.predict(returns)
**Implementation**:
```python
# Polaroid: Efficient return calculation
# Polarway: Efficient return calculation
df = client.pct_change(['price'], periods=1)
returns = df['price_pct_change'].to_numpy()
# OptimizR: Risk analysis
# Optimiz-rs: Risk analysis
hurst = compute_hurst_exponent(returns) # Mean-reversion detection
half_life = estimate_half_life(returns) # Reversion time
risk_metrics = compute_risk_metrics(returns) # Comprehensive suite
```
**Value**:
- Fast preprocessing (Polaroid) + sophisticated analysis (OptimizR)
- Fast preprocessing (Polarway) + sophisticated analysis (Optimiz-rs)
- Useful for pairs trading, mean-reversion strategies
- Real-time risk monitoring
@@ -107,11 +107,11 @@ risk_metrics = compute_risk_metrics(returns) # Comprehensive suite
**Implementation**:
```python
# Polaroid: Multi-asset feature creation
# Polarway: Multi-asset feature creation
df = client.lag(['spy_price', 'vix'], periods=[1, 5, 20])
df = client.pct_change(['spy_price'], periods=1)
# OptimizR: Solve optimal control problem
# Optimiz-rs: Solve optimal control problem
# State: [price, volatility regime]
# Control: portfolio weights
value_fn = solve_hjb_regime_switching(...)
@@ -127,13 +127,13 @@ value_fn = solve_hjb_regime_switching(...)
**Implementation**:
```python
# Polaroid: Backtest execution (fast data ops)
# Polarway: Backtest execution (fast data ops)
def backtest_strategy(params):
df = client.lag(['price'], periods=int(params[0]))
# ... strategy logic ...
return -sharpe_ratio # Minimize negative Sharpe
# OptimizR: Find optimal parameters
# Optimiz-rs: Find optimal parameters
result = differential_evolution(
objective_fn=backtest_strategy,
bounds=[(1, 50), (0.01, 0.5)], # [lag_period, threshold]
@@ -143,8 +143,8 @@ result = differential_evolution(
```
**Value**:
- Polaroid handles heavy data processing
- OptimizR finds optimal parameters
- Polarway handles heavy data processing
- Optimiz-rs finds optimal parameters
- 74-88× faster than SciPy DE
## High-Priority Enhancements
@@ -258,14 +258,14 @@ impl SHADEMemory {
### Priority 3: Time-Series Integration Helpers
**Problem**: Using Polaroid + OptimizR requires manual glue code
**Problem**: Using Polarway + Optimiz-rs requires manual glue code
**Solution**: Create helper functions for common time-series + optimization patterns
**Implementation Strategy**:
1. Add `timeseries_utils` module to OptimizR
1. Add `timeseries_utils` module to Optimiz-rs
2. Functions for common workflows
3. Optional Polaroid integration (via feature flag)
3. Optional Polarway integration (via feature flag)
**Code Outline**:
```rust
@@ -350,7 +350,7 @@ result = tsu.optimize_strategy_params(
1. **Session 1 (Current)**: Time-Series Integration Helpers (1-2 hours)
- Low effort, immediate value
- Makes Polaroid + OptimizR integration obvious
- Makes Polarway + Optimiz-rs integration obvious
- Creates examples for documentation
2. **Session 2**: Enable Rust-Native Parallelization (1-2 hours)
@@ -372,7 +372,7 @@ result = tsu.optimize_strategy_params(
For each enhancement:
1. **Unit tests**: Algorithm correctness (sphere function, Rosenbrock)
2. **Benchmarks**: Performance comparison (before/after)
3. **Integration tests**: Polaroid + OptimizR workflows
3. **Integration tests**: Polarway + Optimiz-rs workflows
4. **Documentation**: Usage examples, API docs
## Git Commit Strategy (per MANDATORY rules)
@@ -404,4 +404,4 @@ Each enhancement gets:
---
**Next Action**: Implement Priority 3 (Time-Series Integration Helpers) as it's lowest effort with immediate value for demonstrating Polaroid + OptimizR synergy.
**Next Action**: Implement Priority 3 (Time-Series Integration Helpers) as it's lowest effort with immediate value for demonstrating Polarway + Optimiz-rs synergy.
@@ -1,4 +1,4 @@
# OptimizR Enhancement Suite - Implementation Complete
# Optimiz-rs Enhancement Suite - Implementation Complete
**Date**: January 2, 2026
**Session Duration**: ~3 hours
@@ -8,13 +8,13 @@
## Overview
Completed comprehensive enhancement suite for OptimizR v0.2.0, implementing all 3 priorities from the Enhancement Strategy:
Completed comprehensive enhancement suite for Optimiz-rs v0.2.0, implementing all 3 priorities from the Enhancement Strategy:
1. ✅ **Time-Series Integration Helpers** (Priority 3)
2. ✅ **Rust Parallelization** (Priority 2)
3. ✅ **SHADE Algorithm** (Priority 1)
Additionally created integration examples combining Polaroid + OptimizR workflows.
Additionally created integration examples combining Polarway + Optimiz-rs workflows.
---
@@ -22,7 +22,7 @@ Additionally created integration examples combining Polaroid + OptimizR workflow
### 1. Time-Series Integration Helpers (Commit: 9a8032e, 7f77f29)
**Purpose**: Bridge OptimizR's optimization with time-series analysis for financial workflows.
**Purpose**: Bridge Optimiz-rs's optimization with time-series analysis for financial workflows.
**Implementation**:
- Created `src/timeseries_utils.rs` (400+ lines)
@@ -41,7 +41,7 @@ Additionally created integration examples combining Polaroid + OptimizR workflow
- All functions tested and working
**Impact**:
- Enables Polaroid → OptimizR workflows
- Enables Polarway → Optimiz-rs workflows
- Simplifies regime detection with HMM
- Streamlines pairs trading analysis
@@ -130,12 +130,12 @@ Additionally created integration examples combining Polaroid + OptimizR workflow
### 4. Integration Examples (Included with parallelization)
**Purpose**: Demonstrate Polaroid + OptimizR workflows.
**Purpose**: Demonstrate Polarway + Optimiz-rs workflows.
**Implementation**:
- `examples/polaroid_optimizr_integration.py` (500+ lines)
- `examples/polarway_optimizr_integration.py` (500+ lines)
- 4 comprehensive workflows:
1. **Regime Detection**: Polaroid features → HMM → regime classification
1. **Regime Detection**: Polarway features → HMM → regime classification
2. **Strategy Optimization**: Moving average crossover with DE
3. **Risk Analysis**: Portfolio with rolling metrics
4. **Pairs Trading**: Complete pipeline with cointegration check
@@ -148,11 +148,11 @@ Additionally created integration examples combining Polaroid + OptimizR workflow
**Impact**:
- End-to-end examples for financial analysis
- Demonstrates Polaroid + OptimizR synergy
- Demonstrates Polarway + Optimiz-rs synergy
- Ready for production adaptation
**Files**:
- `examples/polaroid_optimizr_integration.py`
- `examples/polarway_optimizr_integration.py`
- `examples/timeseries_integration.py`
- `examples/parallel_de_benchmark.py`
@@ -221,9 +221,9 @@ All commits pushed to origin/main ✅
## 🎯 Alignment with Roadmap
All enhancements align with OptimizR v0.3.0 roadmap:
All enhancements align with Optimiz-rs v0.3.0 roadmap:
- ✅ **Time-series integration**: Enable Polaroid workflows
- ✅ **Time-series integration**: Enable Polarway workflows
- ✅ **Parallelization**: Unlock Rayon infrastructure
- ✅ **SHADE**: State-of-the-art adaptive DE
@@ -325,4 +325,4 @@ Future (v0.3.0+):
**Status**: ✅ **ALL OBJECTIVES COMPLETE**
**Next**: Integrate SHADE into DE, performance testing
**Version**: OptimizR v0.2.0 → v0.3.0 prep
**Version**: Optimiz-rs v0.2.0 → v0.3.0 prep
@@ -9,7 +9,7 @@ Example notebooks in `examples/notebooks/` **ARE WORKING CORRECTLY**! They use P
- Automatic Rust backend when available
- Graceful fallback to pure Python
## Actual OptimizR Python API (from lib.rs)
## Actual Optimiz-rs Python API (from lib.rs)
### ✅ Available Functions/Classes:
@@ -155,7 +155,7 @@ from optimizr import (
```
**Features Demonstrated:**
- Direct comparison: OptimizR (Rust) vs Python libraries
- Direct comparison: Optimiz-rs (Rust) vs Python libraries
- Benchmarks against: hmmlearn, scipy, sklearn
- Performance metrics and speedup calculations
@@ -178,7 +178,7 @@ from optimizr import (
### Python Wrapper Design (Brilliant!)
OptimizR uses a **two-layer architecture**:
Optimiz-rs uses a **two-layer architecture**:
1. **Rust Core** (`src/` with PyO3):
- `HMMParams` class
@@ -237,7 +237,7 @@ This design is **excellent** because:
## Testing Summary
| Notebook | Status | OptimizR Features | Test Result |
| Notebook | Status | Optimiz-rs Features | Test Result |
|----------|--------|-------------------|-------------|
| 01_hmm_tutorial.ipynb | ✅ PASS | HMM (Rust) | All cells run |
| 02_mcmc_tutorial.ipynb | ✅ PASS | mcmc_sample | Imports OK |
@@ -277,7 +277,7 @@ This design is **excellent** because:
**Actual Status:** Notebooks use Python wrappers correctly
**What I Learned:**
1. OptimizR has excellent two-layer design
1. Optimiz-rs has excellent two-layer design
2. Python wrappers provide familiar OOP interface
3. Rust acceleration is transparent to users
4. Only 1 minor fix needed (random_state parameter)
@@ -1,8 +1,8 @@
# OptimizR Project Summary
# Optimiz-rs Project Summary
## What is OptimizR?
## What is Optimiz-rs?
OptimizR is a **general-purpose optimization library** that provides high-performance implementations of advanced algorithms in Rust with easy-to-use Python bindings. It's designed to be fast, reliable, and production-ready for open-source distribution.
Optimiz-rs is a **general-purpose optimization library** that provides high-performance implementations of advanced algorithms in Rust with easy-to-use Python bindings. It's designed to be fast, reliable, and production-ready for open-source distribution.
## Key Features
@@ -140,7 +140,7 @@ x_opt, f_min = differential_evolution(
## Differences from rust-hft-arbitrage-lab
| Aspect | rust-hft-arbitrage-lab | OptimizR |
| Aspect | rust-hft-arbitrage-lab | Optimiz-rs |
|--------|----------------------|----------|
| **Purpose** | HFT trading strategies | General optimization library |
| **Scope** | Trading-specific | Domain-agnostic |
@@ -165,7 +165,7 @@ x_opt, f_min = differential_evolution(
```bash
git init
git add .
git commit -m "Initial commit: OptimizR v0.1.0"
git commit -m "Initial commit: Optimiz-rs v0.1.0"
git remote add origin https://github.com/ThotDjehuty/optimiz-r.git
git push -u origin main
```
@@ -217,7 +217,7 @@ Based on benchmarks from rust-hft-arbitrage-lab:
## Marketing/Outreach
1. **Reddit**: r/rust, r/python, r/MachineLearning
2. **Hacker News**: "Show HN: OptimizR - Fast optimization algorithms in Rust"
2. **Hacker News**: "Show HN: Optimiz-rs - Fast optimization algorithms in Rust"
3. **Twitter/X**: Tweet with #rustlang #python
4. **PyPI**: Ensure good package description
5. **GitHub Topics**: optimization, rust, python, scientific-computing
@@ -1,8 +1,8 @@
# OptimizR Refactoring Summary
# Optimiz-rs Refactoring Summary
## Overview
This document summarizes the major refactoring applied to OptimizR to improve modularity, introduce functional programming patterns, implement design patterns, and add concurrency support.
This document summarizes the major refactoring applied to Optimiz-rs to improve modularity, introduce functional programming patterns, implement design patterns, and add concurrency support.
## Architecture Changes
@@ -369,7 +369,7 @@ impl ProposalStrategy for MyProposal {
## Conclusion
This refactoring significantly improves OptimizR's:
This refactoring significantly improves Optimiz-rs's:
- **Modularity**: Clear trait boundaries, easy to extend
- **Maintainability**: Builder patterns, functional utilities reduce boilerplate
- **Performance**: Parallel execution, memoization, lazy evaluation
@@ -1,4 +1,4 @@
# OptimizR Setup Complete! ✅
# Optimiz-rs Setup Complete! ✅
## What Was Done
@@ -136,7 +136,7 @@ pytest tests/ -v -k HMM # Run HMM tests only
## Performance
OptimizR provides **50-100x speedup** over pure Python for:
Optimiz-rs provides **50-100x speedup** over pure Python for:
- HMM fitting (71x faster)
- MCMC sampling (71x faster)
- Differential Evolution (53x faster)
@@ -144,7 +144,7 @@ OptimizR provides **50-100x speedup** over pure Python for:
## Summary
The OptimizR project is now **fully functional** with:
The Optimiz-rs project is now **fully functional** with:
- ✅ Zero compilation errors
- ✅ All tests passing
- ✅ Docker support
@@ -1,7 +1,7 @@
# Time-Series Integration Helpers Implementation Summary
## Overview
Completed Priority 3 from Enhancement Strategy: Time-series integration helpers for OptimizR v0.3.0. These 6 helper functions bridge OptimizR's optimization capabilities with time-series analysis, particularly useful for regime-switching models and pairs trading strategies.
Completed Priority 3 from Enhancement Strategy: Time-series integration helpers for Optimiz-rs v0.3.0. These 6 helper functions bridge Optimiz-rs's optimization capabilities with time-series analysis, particularly useful for regime-switching models and pairs trading strategies.
## Implementation Details
@@ -15,7 +15,7 @@ Completed Priority 3 from Enhancement Strategy: Time-series integration helpers
- Volatility proxy: squared returns
- Lagged returns for each lag period
- Returns: Feature matrix (N-max_lag rows × (3 + num_lags) columns)
- Use case: Prepare price data for OptimizR's HMM regime detection
- Use case: Prepare price data for Optimiz-rs's HMM regime detection
2. **`rolling_hurst_exponent(returns: &[f64], window_size: usize) -> Vec<f64>`**
- Purpose: Detect mean-reversion vs trending behavior
@@ -146,7 +146,7 @@ import optimizr
prices = [100.0, 101.5, 99.8, 102.3, 103.7]
features = optimizr.prepare_for_hmm_py(prices, [1, 2])
# Use with OptimizR's HMM for regime detection
# Use with Optimiz-rs's HMM for regime detection
```
### Mean-Reversion Check
View File
+7
View File
@@ -0,0 +1,7 @@
# Python dependencies for building documentation (pinned for RTD compatibility)
sphinx>=7.0.0,<8.0.0
furo==2023.9.10
myst-parser==2.0.0
sphinx-autodoc-typehints==1.24.0
charset-normalizer>=3.4.0
sphinxcontrib-mermaid>=0.9.2
+141
View File
@@ -0,0 +1,141 @@
"""Replace the 4 remaining ASCII diagram blocks in mathematical_foundations.md
with {figure} directives pointing to the new SVGs.
"""
import pathlib
MD = pathlib.Path(__file__).parent / "theory" / "mathematical_foundations.md"
text = MD.read_text(encoding="utf-8")
# ── 1. HMM regime state machine → fig_hmm_regime ──────────────────────────
old1 = '''\
```
HMM regime state machine (K = 3)
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
A₁₂ → A₂₃ →
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ State 1 │──────▶│ State 2 │──────▶│ State 3 │
│ Bull │◀──────│ Neutral │◀──────│ Bear │
└─────────────┘ └─────────────┘ └─────────────┘
← A₂₁ ← A₃₂
Emission B_k(y) = 𝒩(μ_k, σ_k²):
┌────────┬────────┬────────┬──────────────────┐
│ State │ μ │ σ │ Character │
├────────┼────────┼────────┼──────────────────┤
│ Bull │ +0.05 │ 0.12 │ high return, low vol │
│ Neutral│ 0.00 │ 0.18 │ flat, medium vol │
│ Bear │ -0.08 │ 0.35 │ crash, high vol │
└────────┴────────┴────────┴──────────────────┘
(self-transition: A₁₁=0.97, A₂₂=0.97, A₃₃=0.90)
```'''
new1 = '''\
```{figure} ../_static/diagrams/fig_hmm_regime.svg
:align: center
:width: 90%
HMM $K=3$ state machine with Bull / Neutral / Bear regimes and Gaussian emission
parameters. Self-transitions $A_{11}=A_{22}=0.97$, $A_{33}=0.90$.
```'''
# ── 2. Viterbi trellis → fig_viterbi_trellis ───────────────────────────────
old2 = '''\
```
Viterbi trellis (K=3, T=4)
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
State t=1 t=2 t=3 t=4
1 ○─────────▶○─────────▶○─────────▶○
╲ ╳
2 ○─────────▶●─────────▶●─────────▶○ ● = MAP path
╲ ╲ ╲
3 ○─────────▶○─────────▶○─────────▶○
δ_t(k) = max_j [δ_{t1}(j) · A_jk · B_k(y_t)]
ψ_t(k) = argmax_j ← backtrack pointer
Traceback: z_4★ ← z_3★ ← z_2★ ← z_1★ via ψ
```'''
new2 = r'''\
```{figure} ../_static/diagrams/fig_viterbi_trellis.svg
:align: center
:width: 82%
Viterbi trellis ($K=3$, $T=4$). Filled nodes mark the MAP (most probable) state
sequence; arrows show transition candidates. Backtracking via $\psi_t(k)$ recovers
$z_1^\star \to z_4^\star$.
```'''
# ── 3. Standard vs natural gradient (text comparison) → fig_std_vs_nat_gradient
old3 = '''\
```
Standard vs natural gradient
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
Standard: θ_{k+1} = θ_k η·∇ℒ Natural: θ_{k+1} = θ_k η·ℐ(θ)^{1}∇ℒ
────────────────────────────────────────────
┌────────────────────┐ ┌────────────────────┐
│ Flat ℝᵈ geometry │ │ Riemannian metric (θ) │
│ Ignores curvature │ │ Adapts to geometry │
│ Slow on ill-cond │ │ Reparam invariant │
│ O(κ()) iters │ │ O(1) on exp families │
└────────────────────┘ └────────────────────┘
On Gaussian / exponential family: ℐ⁻¹∇ℒ = MLE step → 1 iteration!
```'''
new3 = '''\
```{figure} ../_static/diagrams/fig_std_vs_nat_gradient.svg
:align: center
:width: 88%
Standard versus natural gradient: geometric properties. On exponential families
the natural gradient equals the MLE Newton step, achieving convergence in one
iteration.
```'''
# ── 4. Matrix Lie group hierarchy → fig_lie_group_hierarchy ─────────────────
old4 = '''\
```
Matrix Lie group hierarchy
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
GL(n,) ─ all invertible n×n real matrices
├──▶ SL(n,) det = 1
├──▶ O(n) RᵀR = I (orthogonal)
│ └─▶ SO(n) det = +1 (pure rotations)
│ ↳ portfolio factor rotation, PCA constraints
└──▶ Sp(2n,) preserves symplectic form ω
↳ Hamiltonian mechanics, PMP §4.2 / §10.4
H(n) Heisenberg ─ upper triangular, 1s on diagonal
↳ path-signature feature maps
```'''
new4 = r'''\
```{figure} ../_static/diagrams/fig_lie_group_hierarchy.svg
:align: center
:width: 90%
Matrix Lie group hierarchy: subgroup inclusions and their quantitative-finance
applications. $SO(n)$ underpins PCA factor rotation; $\mathrm{Sp}(2n,\mathbb{R})$
governs Hamiltonian mechanics (PMP §10.4); $H(n)$ drives path-signature features.
```'''
replacements = [(old1, new1), (old2, new2), (old3, new3), (old4, new4)]
for i, (old, new) in enumerate(replacements, 1):
if old in text:
text = text.replace(old, new, 1)
print(f" Block {i}: replaced OK")
else:
print(f" Block {i}: NOT FOUND — check encoding/whitespace")
MD.write_text(text, encoding="utf-8")
print("Done.")
+911
View File
@@ -0,0 +1,911 @@
#!/usr/bin/env python3
"""
Generate all matplotlib diagrams for mathematical_foundations.md.
Run from the docs/source directory (or workspace root):
python docs/source/_gen_diagrams.py
Outputs SVG files to docs/source/_static/diagrams/
"""
import os
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import matplotlib.ticker as mticker
from scipy.stats import norm
# ─── output dir ─────────────────────────────────────────────────────────────
OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "_static", "diagrams")
os.makedirs(OUT, exist_ok=True)
# ─── palette & defaults ─────────────────────────────────────────────────────
C0 = "#2E6BE5" # blue
C1 = "#E8850A" # orange
C2 = "#27AE60" # green
C3 = "#D62728" # red
GRAY = "#888888"
BAND = "#AACBE8"
matplotlib.rcParams.update({
"font.size" : 11,
"axes.titlesize" : 12,
"axes.labelsize" : 11,
"xtick.labelsize" : 9,
"ytick.labelsize" : 9,
"axes.spines.top" : False,
"axes.spines.right" : False,
"figure.dpi" : 150,
"savefig.bbox" : "tight",
"savefig.transparent" : False,
"figure.facecolor" : "white",
"axes.facecolor" : "white",
"lines.linewidth" : 1.8,
"text.usetex" : False,
})
def save(name):
plt.savefig(os.path.join(OUT, name + ".svg"))
plt.close()
# ════════════════════════════════════════════════════════════════════════════
# §1 DIFFERENTIAL EVOLUTION
# ════════════════════════════════════════════════════════════════════════════
def fig_de_mutation():
r1 = np.array([0.5, 0.3])
r2 = np.array([1.2, 1.4])
r3 = np.array([1.8, 0.6])
F = 0.7
vi = r1 + F * (r2 - r3)
fig, ax = plt.subplots(figsize=(6, 4.2))
# difference vector r3 → r2
ax.annotate("", r2, r3,
arrowprops=dict(arrowstyle="-|>", color=C2, lw=2.0, mutation_scale=14))
mid = (r2 + r3) / 2
ax.text(mid[0] - 0.05, mid[1] + 0.09,
r"$F(\mathbf{x}_{r_2}-\mathbf{x}_{r_3})$",
ha="center", fontsize=10, color=C2)
# mutation arrow r1 → vi (dashed)
ax.annotate("", vi, r1,
arrowprops=dict(arrowstyle="-|>", color=C1, lw=2.0,
mutation_scale=14, linestyle="dashed"))
ax.text((r1[0]+vi[0])/2, (r1[1]+vi[1])/2 - 0.1,
r"$+F(\cdots)$", ha="center", fontsize=9, color=C1)
pts = {
r"$\mathbf{x}_{r_1}$ (base)": (r1, C0),
r"$\mathbf{x}_{r_2}$": (r2, C0),
r"$\mathbf{x}_{r_3}$": (r3, C0),
r"$\mathbf{v}_i$ (mutant)": (vi, C1),
}
for lbl, (p, col) in pts.items():
ax.scatter(*p, s=90, color=col, zorder=6)
offset = (0.05, 0.07)
if "mutant" in lbl:
offset = (0.07, 0.05)
ax.text(p[0] + offset[0], p[1] + offset[1], lbl, fontsize=10, color=col)
ax.set_xlim(0.1, 2.5); ax.set_ylim(0.0, 1.85)
ax.set_xlabel(r"$x_1$"); ax.set_ylabel(r"$x_2$")
ax.set_title(r"DE Mutation: $\mathbf{v}_i = \mathbf{x}_{r_1} + F\,(\mathbf{x}_{r_2} - \mathbf{x}_{r_3})$")
ax.set_aspect("equal", adjustable="box")
save("fig_de_mutation")
def fig_rastrigin():
x = np.linspace(-2.5, 2.5, 800)
y = 10 + x**2 - 10 * np.cos(2 * np.pi * x)
fig, ax = plt.subplots(figsize=(7, 3.8))
ax.plot(x, y, color=C0, lw=2, label=r"$f(x) = 10 + x^2 - 10\cos(2\pi x)$")
ax.fill_between(x, y, alpha=0.07, color=C0)
ax.axhline(0, color=GRAY, lw=0.7, ls=":")
# global minimum
ax.scatter([0], [0], s=110, color=C1, zorder=6, label=r"global min $f^*=0$", marker="*")
# local minima
lm_x = np.array([-2.0, -1.0, 1.0, 2.0])
lm_y = 10 + lm_x**2 - 10 * np.cos(2 * np.pi * lm_x)
ax.scatter(lm_x, lm_y, s=55, color=C3, zorder=5, label="local minima", marker="o")
ax.annotate(r"$\approx 10^d$ local pits", (1.0, lm_y[2]),
(1.5, 12), fontsize=9, color=C3,
arrowprops=dict(arrowstyle="->", color=C3, lw=1.0))
ax.set_xlabel(r"$x$"); ax.set_ylabel(r"$f(x)$")
ax.set_title(r"Rastrigin function ($d = 1$) — many local minima")
ax.legend(fontsize=9, framealpha=0.6)
save("fig_rastrigin")
# ════════════════════════════════════════════════════════════════════════════
# §2.1 BROWNIAN MOTION
# ════════════════════════════════════════════════════════════════════════════
def fig_random_walk():
rng = np.random.default_rng(42)
n = 300
t = np.linspace(0, 1, n)
W = np.cumsum(rng.choice([-1, 1], size=n)) / np.sqrt(n)
fig, ax = plt.subplots(figsize=(7, 3.5))
ax.plot(t, W, color=C0, lw=1.4)
ax.axhline(0, color=GRAY, lw=0.8, ls="--", alpha=0.6)
ax.set_xlabel(r"$t$"); ax.set_ylabel(r"$W_t^{(n)}$")
ax.set_title(r"Coin-flip random walk ($n=300$) $\longrightarrow$ Brownian motion as $n\to\infty$")
save("fig_random_walk")
def fig_bm_fan():
rng = np.random.default_rng(0)
n, dt = 500, 0.002
npaths = 10
ts = np.linspace(0, 1, n)
paths = np.cumsum(rng.normal(0, np.sqrt(dt), (npaths, n)), axis=1)
paths[:, 0] = 0
fig, ax = plt.subplots(figsize=(7, 4.2))
lo, hi = -2 * np.sqrt(ts), 2 * np.sqrt(ts)
ax.fill_between(ts, lo, hi, alpha=0.13, color=C0, label=r"$\pm 2\sqrt{t}$ (95% band)")
ax.plot(ts, hi, color=C0, lw=1.2, ls="--", alpha=0.55)
ax.plot(ts, lo, color=C0, lw=1.2, ls="--", alpha=0.55)
colors_cycle = plt.colormaps["tab10"](np.linspace(0, 0.9, npaths))
for i, p in enumerate(paths):
ax.plot(ts, p, lw=0.9, alpha=0.75, color=colors_cycle[i])
ax.axhline(0, color=GRAY, lw=0.8, ls=":")
ax.set_xlabel(r"$t$"); ax.set_ylabel(r"$W_t$")
ax.set_title(r"Brownian motion — sample paths spread as $\sqrt{t}$ (trumpet fan)")
ax.legend(fontsize=9, framealpha=0.7)
save("fig_bm_fan")
def fig_gbm():
rng = np.random.default_rng(7)
T, n, dt = 1.0, 500, 0.002
mu, sigma, S0 = 0.10, 0.30, 1.0
ts = np.linspace(0, T, n)
fig, ax = plt.subplots(figsize=(7, 3.8))
ax.plot(ts, S0 * np.exp(mu * ts), color=C1, lw=1.8, ls="--",
label=r"$\mathbb{E}[S_t] = S_0 e^{\mu t}$")
ax.plot(ts, S0 * np.exp((mu - 0.5*sigma**2) * ts), color=C2, lw=1.5, ls=":",
label=r"median $\approx S_0 e^{(\mu-\sigma^2/2)t}$")
colors_cycle = plt.colormaps["Blues"](np.linspace(0.4, 0.85, 7))
for i in range(7):
W = np.cumsum(rng.normal(0, np.sqrt(dt), n))
S = S0 * np.exp((mu - 0.5*sigma**2) * ts + sigma * W)
ax.plot(ts, S, lw=0.9, alpha=0.7, color=colors_cycle[i])
ax.set_xlabel(r"$t$"); ax.set_ylabel(r"$S_t$")
ax.set_title(r"Geometric Brownian motion ($\mu=0.10,\;\sigma=0.30$)")
ax.legend(fontsize=9, framealpha=0.6)
save("fig_gbm")
# ════════════════════════════════════════════════════════════════════════════
# §2.2 ITŌ CALCULUS
# ════════════════════════════════════════════════════════════════════════════
def fig_ito_correction():
t = np.linspace(0, 2.2, 300)
mu, sigma = 0.12, 0.30
fig, ax = plt.subplots(figsize=(7, 3.8))
ax.plot(t, mu * t, color=C1, lw=2, ls="--",
label=r"Naïve slope $\mu t$ (wrong)")
ax.plot(t, (mu - 0.5*sigma**2) * t, color=C0, lw=2,
label=r"Itō slope $(\mu - \sigma^2/2)\,t$ (correct)")
# gap annotation at t = 1.8
g_x = 1.8
y_top = mu * g_x
y_bot = (mu - 0.5*sigma**2) * g_x
ax.annotate("", (g_x, y_bot), (g_x, y_top),
arrowprops=dict(arrowstyle="<->", color=C3, lw=1.6))
ax.text(g_x + 0.07, (y_top + y_bot) / 2,
r"gap $= \sigma^2 T/2$", fontsize=9, color=C3, va="center")
ax.axhline(0, color=GRAY, lw=0.6, ls=":")
ax.set_xlabel(r"$t$"); ax.set_ylabel(r"$\mathbb{E}[\log S_t] - \log S_0$")
ax.set_title(r"Itō correction: $\mathbb{E}[\log S_t]$ always below the naïve slope $\mu t$")
ax.legend(fontsize=9)
save("fig_ito_correction")
# ════════════════════════════════════════════════════════════════════════════
# §2.3 FOKKER-PLANCK
# ════════════════════════════════════════════════════════════════════════════
def fig_fokker_planck():
x = np.linspace(-0.5, 5.5, 600)
mu_drift, sigma_diff = 0.8, 0.3
times = [0.05, 0.5, 1.5]
colors = [C3, C2, C0]
labels = [r"$t = 0.05$ (narrow spike)",
r"$t = 0.50$",
r"$t = 1.50$ (wide, drifted)"]
fig, ax = plt.subplots(figsize=(7, 3.8))
for t, col, lbl in zip(times, colors, labels):
mean = mu_drift * t
std = sigma_diff * np.sqrt(t)
y = norm.pdf(x, mean, std)
ax.plot(x, y, color=col, lw=2, label=lbl)
ax.fill_between(x, y, alpha=0.10, color=col)
ax.set_xlabel(r"$x$"); ax.set_ylabel(r"$p(t, x)$")
ax.set_title(r"Fokker-Planck: density drifts $(\mu=0.8)$ and broadens $(\sigma=0.3)$")
ax.legend(fontsize=9)
save("fig_fokker_planck")
# ════════════════════════════════════════════════════════════════════════════
# §2.3 EULER-MARUYAMA vs MILSTEIN
# ════════════════════════════════════════════════════════════════════════════
def fig_em_milstein():
dts = np.array([0.1, 0.05, 0.02, 0.01, 0.005, 0.001])
em_err = 0.38 * dts**0.5
mil_err = 0.19 * dts**1.0
fig, ax = plt.subplots(figsize=(6, 4))
ax.loglog(dts, em_err, "o-", color=C0, lw=2, ms=7,
label=r"Euler-Maruyama (order $1/2$)")
ax.loglog(dts, mil_err, "s--", color=C1, lw=2, ms=7,
label=r"Milstein (order $1$)")
ax.set_xlabel(r"Step size $\Delta t$")
ax.set_ylabel(r"Strong error $\|X_T - \hat{X}_T\|$")
ax.set_title("SDE numerical schemes — strong convergence order")
ax.legend(fontsize=10); ax.grid(True, which="both", alpha=0.3)
save("fig_em_milstein")
# ════════════════════════════════════════════════════════════════════════════
# §2.4 ORNSTEIN-UHLENBECK
# ════════════════════════════════════════════════════════════════════════════
def fig_ou_path():
rng = np.random.default_rng(3)
T, n, dt = 5.0, 2000, 0.0025
kappa, theta, sigma = 3.0, 0.5, 0.4
X = np.zeros(n); X[0] = 2.0
for i in range(1, n):
X[i] = X[i-1] + kappa * (theta - X[i-1]) * dt + sigma * rng.normal(0, np.sqrt(dt))
ts = np.linspace(0, T, n)
sig_inf = sigma / np.sqrt(2 * kappa)
fig, ax = plt.subplots(figsize=(7, 3.8))
ax.plot(ts, X, color=C0, lw=1.0, alpha=0.9, label=r"$X_t$")
ax.axhline(theta, color=C1, lw=1.8, ls="--",
label=fr"$\theta = {theta}$ (long-run mean)")
ax.fill_between(ts,
theta - 2 * sig_inf,
theta + 2 * sig_inf,
alpha=0.10, color=GRAY, label=r"$\theta \pm 2\sigma_\infty$")
ax.set_xlabel(r"$t$"); ax.set_ylabel(r"$X_t$")
ax.set_title(fr"Ornstein-Uhlenbeck ($\kappa={kappa},\;\theta={theta},\;\sigma={sigma}$) — mean-reversion")
ax.legend(fontsize=9)
save("fig_ou_path")
def fig_ou_transition():
x = np.linspace(-0.3, 2.6, 500)
kappa, theta, sigma, x0 = 3.0, 0.5, 0.4, 2.0
taus = [0.1, 0.5, 2.0]
colors = [C3, C2, C0]
fig, ax = plt.subplots(figsize=(7, 3.8))
for tau, col in zip(taus, colors):
mean = theta + (x0 - theta) * np.exp(-kappa * tau)
var = sigma**2 / (2 * kappa) * (1 - np.exp(-2 * kappa * tau))
y = norm.pdf(x, mean, np.sqrt(var))
ax.plot(x, y, color=col, lw=2,
label=fr"$\tau = {tau:.1f}$ (mean $= {mean:.2f}$)")
ax.fill_between(x, y, alpha=0.09, color=col)
ax.axvline(theta, color=C1, lw=1.3, ls="--", label=fr"$\theta = {theta}$")
ax.set_xlabel(r"$x$"); ax.set_ylabel(r"$p(x_\tau \mid x_0)$")
ax.set_title(r"OU transition density: drifts toward $\theta$, widens over time")
ax.legend(fontsize=9)
save("fig_ou_transition")
def fig_ou_loglik():
kappa_v = np.linspace(10, 120, 80)
theta_v = np.linspace(-0.005, 0.011, 80)
K, T = np.meshgrid(kappa_v, theta_v)
Z = -(((K - 55) / 22)**2 + ((T - 0.003) / 0.003)**2)
fig, ax = plt.subplots(figsize=(6.2, 4.5))
cf = ax.contourf(theta_v * 1000, kappa_v, Z.T, levels=20, cmap="Blues")
ax.contour(theta_v * 1000, kappa_v, Z.T, levels=8,
colors="white", linewidths=0.7, alpha=0.55)
ax.plot(3, 55, "*", color=C1, ms=16, zorder=5,
label=r"MLE $\hat\theta, \hat\kappa$")
plt.colorbar(cf, ax=ax, label="Log-likelihood (normalised)")
ax.set_xlabel(r"$\theta \times 10^3$"); ax.set_ylabel(r"$\kappa$")
ax.set_title(r"OU log-likelihood surface $\ell(\kappa, \theta \mid \hat\sigma)$")
ax.legend(fontsize=10)
save("fig_ou_loglik")
def fig_ou_residuals():
rng = np.random.default_rng(9)
r = rng.normal(0, 1, 600)
x = np.linspace(-4, 4, 300)
fig, ax = plt.subplots(figsize=(6, 3.8))
ax.hist(r, bins=32, density=True, color=C0, alpha=0.50,
label="Standardised residuals")
ax.plot(x, norm.pdf(x), color=C1, lw=2.2,
label=r"$\mathcal{N}(0,1)$ theory")
ax.set_xlabel(r"$r_i$"); ax.set_ylabel("Density")
ax.set_title(r"OU residual diagnostic: $r_i = (X_{t_i} - \hat\mu_i)/\hat\sigma$")
ax.legend(fontsize=9)
save("fig_ou_residuals")
# ════════════════════════════════════════════════════════════════════════════
# §3 JUMP PROCESSES
# ════════════════════════════════════════════════════════════════════════════
def fig_poisson():
rng = np.random.default_rng(1)
lam, T = 2, 4.0
arrivals, t = [], 0.0
while True:
t += rng.exponential(1 / lam)
if t > T: break
arrivals.append(t)
ts = np.concatenate([[0.0], arrivals, [T]])
ns = np.arange(len(ts) - 1)
fig, ax = plt.subplots(figsize=(7, 3.5))
for i, (t0, t1, n) in enumerate(zip(ts[:-1], ts[1:], ns)):
ax.hlines(n, t0, t1, color=C0, lw=2.8)
if i < len(arrivals):
ax.vlines(t1, n, n + 1, color=C0, lw=2.0, linestyle=":")
ax.scatter([t1], [n], s=45, color="white", edgecolors=C0, zorder=5, lw=1.5)
ax.scatter([t1], [n + 1], s=45, color=C0, zorder=5)
ax.yaxis.set_major_locator(mticker.MaxNLocator(integer=True))
ax.set_xlabel(r"$t$"); ax.set_ylabel(r"$N_t$")
ax.set_title(fr"Poisson process ($\lambda = {lam}$ jumps/unit) — inter-arrivals $\sim \mathrm{{Exp}}(\lambda)$")
save("fig_poisson")
def fig_jump_diffusion():
rng = np.random.default_rng(11)
T, n, dt = 1.0, 1000, 0.001
mu, sigma, lam = 0.05, 0.18, 2.5
ts = np.linspace(0, T, n)
S = np.ones(n)
jump_times = np.sort(rng.uniform(0, T, rng.poisson(lam * T)))
for i in range(1, n):
dW = rng.normal(0, np.sqrt(dt))
S[i] = S[i-1] * np.exp((mu - 0.5 * sigma**2) * dt + sigma * dW)
if np.any((ts[i-1] < jump_times) & (jump_times <= ts[i])):
S[i] *= np.exp(rng.normal(0.0, 0.09))
fig, ax = plt.subplots(figsize=(7, 3.8))
ax.plot(ts, S, color=C0, lw=1.3, label=r"$S_t$ (jump-diffusion path)")
# mark jump locations
jt_idx = [np.searchsorted(ts, jt) for jt in jump_times if jt < T]
ax.scatter(ts[jt_idx], S[jt_idx], s=50, color=C3, zorder=5,
label=r"Poisson jump $\tau_k$", marker="v")
ax.set_xlabel(r"$t$"); ax.set_ylabel(r"$S_t$")
ax.set_title(r"Merton jump-diffusion ($\lambda = 2.5$/yr, $\sigma_J = 9\%$)")
ax.legend(fontsize=9)
save("fig_jump_diffusion")
def fig_levy_tails():
x = np.linspace(0.05, 5, 600)
gauss_tail = norm.pdf(x)
gauss_tail /= gauss_tail[0]
vg_tail = np.exp(-1.5 * x) / x
vg_tail /= vg_tail[0]
alpha_tail = x ** (-1.8)
alpha_tail /= alpha_tail[0]
fig, ax = plt.subplots(figsize=(6.5, 4))
ax.semilogy(x, gauss_tail, lw=2, color=C0,
label=r"Gaussian ($\nu \equiv 0$)")
ax.semilogy(x, vg_tail, lw=2, color=C2,
label=r"Variance Gamma ($\nu \propto e^{-c|z|}/|z|$)")
ax.semilogy(x, alpha_tail, lw=2, color=C1, ls="--",
label=r"$\alpha$-stable ($\nu \propto |z|^{-1-\alpha}$, heaviest)")
ax.set_xlabel(r"Jump size $|z|$")
ax.set_ylabel(r"Lévy density $\nu(dz)/dz$ (log scale)")
ax.set_title("Lévy measure tails — heavier tail = more frequent/larger jumps")
ax.legend(fontsize=9); ax.grid(True, which="both", alpha=0.25)
save("fig_levy_tails")
# ════════════════════════════════════════════════════════════════════════════
# §6 KALMAN FILTER
# ════════════════════════════════════════════════════════════════════════════
def fig_kalman_covariance():
t = np.linspace(0, 30, 300)
Pinf = 0.17
Pt = Pinf + (1.0 - Pinf) * np.exp(-0.35 * t)
fig, ax = plt.subplots(figsize=(7, 3.5))
ax.plot(t, Pt, color=C0, lw=2, label=r"$P_t$ (error covariance)")
ax.axhline(Pinf, color=C1, lw=1.6, ls="--",
label=fr"$P_\infty \approx {Pinf}$ (steady-state)")
ax.fill_between(t, Pt, Pinf, alpha=0.10, color=C0)
ax.set_xlabel(r"$t$"); ax.set_ylabel(r"$P_t$")
ax.set_title(r"Kalman filter: error covariance converges exponentially to $P_\infty$")
ax.legend(fontsize=9); ax.set_ylim(0, 1.05)
save("fig_kalman_covariance")
# ════════════════════════════════════════════════════════════════════════════
# §7 MCMC
# ════════════════════════════════════════════════════════════════════════════
def fig_mcmc_energy():
x = np.linspace(-5, 5, 600)
pi = 0.5 * norm.pdf(x, -1.5, 0.8) + 0.5 * norm.pdf(x, 1.5, 0.9)
U = -np.log(pi + 1e-12)
U -= U.min()
fig, ax = plt.subplots(figsize=(7, 3.8))
ax.plot(x, U, color=C0, lw=2)
ax.fill_between(x, U, alpha=0.08, color=C0)
ax.scatter([-1.5, 1.5], [U[np.abs(x + 1.5).argmin()],
U[np.abs(x - 1.5).argmin()]],
s=90, color=C2, zorder=5, label=r"modes of $\pi$")
saddle_i = np.abs(x).argmin()
ax.scatter([x[saddle_i]], [U[saddle_i]], s=90, color=C3,
zorder=5, marker="^", label="energy barrier")
ax.annotate(r"accept with $e^{-\Delta U}$",
(x[saddle_i] + 0.3, U[saddle_i] - 0.4),
(2.2, 1.2), fontsize=9, color=C3,
arrowprops=dict(arrowstyle="->", color=C3, lw=1.0))
ax.set_xlabel(r"$x$"); ax.set_ylabel(r"$U(x) = -\log\pi(x)$")
ax.set_title(r"MCMC energy landscape (bimodal target $\pi$)")
ax.legend(fontsize=9)
save("fig_mcmc_energy")
def fig_mcmc_trace():
rng = np.random.default_rng(42)
x_cur = -1.5
chain = [x_cur]
for _ in range(2999):
prop = x_cur + rng.normal(0, 0.8)
pi_cur = 0.5 * norm.pdf(x_cur, -1.5, 0.8) + 0.5 * norm.pdf(x_cur, 1.5, 0.9)
pi_prop = 0.5 * norm.pdf(prop, -1.5, 0.8) + 0.5 * norm.pdf(prop, 1.5, 0.9)
x_cur = prop if rng.random() < pi_prop / pi_cur else x_cur
chain.append(x_cur)
chain = np.array(chain)
fig, axes = plt.subplots(1, 2, figsize=(9, 3.8))
axes[0].plot(chain, lw=0.6, color=C0, alpha=0.8)
axes[0].axhline(0, color=GRAY, lw=0.7, ls=":")
axes[0].set_xlabel("Iteration"); axes[0].set_ylabel(r"$x_t$")
axes[0].set_title("Trace plot — chain mixes between both modes")
x = np.linspace(-5, 5, 400)
true_pi = 0.5 * norm.pdf(x, -1.5, 0.8) + 0.5 * norm.pdf(x, 1.5, 0.9)
axes[1].hist(chain, bins=50, density=True, color=C0, alpha=0.50,
label="MCMC samples")
axes[1].plot(x, true_pi, color=C1, lw=2.2, label=r"true $\pi(x)$")
axes[1].set_xlabel(r"$x$"); axes[1].set_ylabel("Density")
axes[1].set_title("Marginal distribution")
axes[1].legend(fontsize=9)
plt.tight_layout()
save("fig_mcmc_trace")
# ════════════════════════════════════════════════════════════════════════════
# §9 INFORMATION THEORY
# ════════════════════════════════════════════════════════════════════════════
def fig_kl_asymmetry():
x = np.linspace(-10, 10, 800)
p = norm.pdf(x, 0, 1)
q = norm.pdf(x, 0, 4)
fig, ax = plt.subplots(figsize=(7, 3.8))
ax.plot(x, p, color=C0, lw=2, label=r"$p = \mathcal{N}(0,1)$ (narrow)")
ax.plot(x, q, color=C1, lw=2, ls="--", label=r"$q = \mathcal{N}(0,4)$ (wide)")
ax.fill_between(x, p, alpha=0.12, color=C0)
ax.fill_between(x, q, alpha=0.08, color=C1)
dx = x[1] - x[0]
eps = 1e-12
kl_pq = float(np.sum(p * np.log((p + eps) / (q + eps))) * dx)
kl_qp = float(np.sum(q * np.log((q + eps) / (p + eps)) * dx))
ax.text(-9.5, 0.085,
fr"$D_{{KL}}(p\|q) \approx {kl_pq:.2f}$ (small: $q$ covers $p$)",
fontsize=9, color=C0)
ax.text(-9.5, 0.066,
fr"$D_{{KL}}(q\|p) \approx {kl_qp:.2f}$ (large: $p$ misses tails of $q$)",
fontsize=9, color=C1)
ax.set_xlabel(r"$x$"); ax.set_ylabel("Density")
ax.set_title(r"KL divergence asymmetry: $D_{KL}(p\|q) \neq D_{KL}(q\|p)$")
ax.legend(fontsize=9)
save("fig_kl_asymmetry")
def fig_fisher_curvature():
theta = np.linspace(-3, 3, 400)
sigma_vals = [0.5, 1.0, 2.0]
colors = [C0, C2, C1]
labels = [r"$\sigma=0.5$ (high $\mathcal{I}$, sharp peak)",
r"$\sigma=1.0$",
r"$\sigma=2.0$ (low $\mathcal{I}$, flat peak)"]
fig, ax = plt.subplots(figsize=(7, 3.8))
for s, col, lbl in zip(sigma_vals, colors, labels):
logL = -0.5 * (theta / s)**2 - np.log(s)
logL -= logL.max()
ax.plot(theta, logL, lw=2, color=col, label=lbl)
ax.axvline(0, color=GRAY, lw=0.8, ls=":")
ax.set_xlabel(r"$\theta$"); ax.set_ylabel(r"$\log\mathcal{L}(\theta \mid x_\mathrm{obs})$ (centred)")
ax.set_title(r"Fisher information = log-likelihood curvature at $\theta^*$")
ax.legend(fontsize=9); ax.set_ylim(-4.2, 0.3)
save("fig_fisher_curvature")
# ════════════════════════════════════════════════════════════════════════════
# §10 DIFFERENTIAL GEOMETRY
# ════════════════════════════════════════════════════════════════════════════
def fig_curvatures():
fig, axes = plt.subplots(1, 3, figsize=(10, 3.5))
# K > 0 — converging geodesics
ax = axes[0]
ax.set_aspect("equal"); ax.axis("off")
theta_arc = np.linspace(0, np.pi, 200)
ax.plot(np.cos(theta_arc), np.sin(theta_arc), color=GRAY, lw=1.5, ls="--", alpha=0.35)
for ang in np.linspace(-0.45, 0.45, 7):
r = np.linspace(0, 1, 60)
ax.plot(r * np.sin(ang), r * np.cos(ang), color=C0, lw=1.5, alpha=0.75)
ax.scatter([0], [0], s=70, color=C1, zorder=5)
ax.text(0, -0.12, "meet at N pole", ha="center", fontsize=8, color=GRAY)
ax.set_title(r"$K > 0$ (sphere $S^2$)" + "\ngeodesics converge", fontsize=10)
# K = 0 — parallel
ax = axes[1]; ax.axis("off")
for y in np.linspace(-0.8, 0.8, 7):
ax.plot([-1, 1], [y, y], color=C0, lw=1.5)
ax.set_xlim(-1.3, 1.3); ax.set_ylim(-1.2, 1.2)
ax.text(0, -1.1, "remain equidistant", ha="center", fontsize=8, color=GRAY)
ax.set_title(r"$K = 0$ (flat $\mathbb{R}^2$)" + "\nparallel geodesics", fontsize=10)
# K < 0 — diverging
ax = axes[2]; ax.axis("off")
for ang in np.linspace(-0.55, 0.55, 7):
r = np.linspace(0, 1.2, 60)
scale = 1 + 0.55 * r
ax.plot(r * np.sin(ang * scale), r * np.cos(ang * scale), color=C0, lw=1.5, alpha=0.75)
ax.scatter([0], [0], s=70, color=C1, zorder=5)
ax.set_xlim(-1.1, 1.1); ax.set_ylim(-0.15, 1.5)
ax.text(0, -0.12, "spread exponentially", ha="center", fontsize=8, color=GRAY)
ax.set_title(r"$K < 0$ (hyperbolic $H^2$)" + "\ngeodesics diverge", fontsize=10)
plt.suptitle("Sectional curvature determines geodesic behaviour", y=1.03, fontsize=12)
plt.tight_layout()
save("fig_curvatures")
def fig_natural_gradient():
fig, axes = plt.subplots(1, 2, figsize=(9, 3.8))
theta1 = np.linspace(-2, 2, 300)
theta2 = np.linspace(-2, 2, 300)
T1, T2 = np.meshgrid(theta1, theta2)
# Standard: elongated contours → zigzag
Z_std = 6 * T1**2 + T2**2
axes[0].contour(T1, T2, Z_std, levels=7, colors=GRAY, alpha=0.45, linewidths=0.9)
path_std = [(1.6, 1.6), (0.05, 1.1), (0.75, 0.15), (0.03, 0.06), (0, 0)]
xs, ys = zip(*path_std)
axes[0].plot(xs, ys, "o-", color=C0, lw=1.8, ms=5)
axes[0].scatter([0], [0], s=120, color=C1, zorder=5, marker="*")
axes[0].set_title("Standard gradient $\\nabla_\\theta \\mathcal{L}$\n(zigzag on ill-conditioned $\\mathcal{I}$)",
fontsize=10)
axes[0].set_xlabel(r"$\theta_1$"); axes[0].set_ylabel(r"$\theta_2$")
# Natural: circular contours → direct path
Z_nat = T1**2 + T2**2
axes[1].contour(T1, T2, Z_nat, levels=7, colors=GRAY, alpha=0.45, linewidths=0.9)
path_nat = [(1.6, 1.6), (0.8, 0.8), (0.3, 0.3), (0, 0)]
xs2, ys2 = zip(*path_nat)
axes[1].plot(xs2, ys2, "o-", color=C2, lw=1.8, ms=5)
axes[1].scatter([0], [0], s=120, color=C1, zorder=5, marker="*")
axes[1].set_title(r"Natural gradient $\mathcal{I}^{-1}\nabla_\theta\mathcal{L}$" + "\n(direct, reparametrisation-invariant)",
fontsize=10)
axes[1].set_xlabel(r"$\theta_1$"); axes[1].set_ylabel(r"$\theta_2$")
plt.tight_layout()
save("fig_natural_gradient")
# ════════════════════════════════════════════════════════════════════════════
# §2.3 PICARD ITERATION
# ════════════════════════════════════════════════════════════════════════════
def fig_picard():
t = np.linspace(0, 1.5, 300)
# True solution: dx = x dt → x(t) = e^t
x_true = np.exp(t)
# Picard iterates starting at x0 = 1
x0 = np.ones_like(t) # n=0: constant 1
x1 = 1 + t # n=1: linear
x2 = 1 + t + t**2 / 2 # n=2: quadratic
x3 = 1 + t + t**2/2 + t**3/6 # n=3
fig, ax = plt.subplots(figsize=(7, 3.8))
ax.plot(t, x0, color=GRAY, lw=1.5, ls=":", label=r"$X^{(0)}$: constant")
ax.plot(t, x1, color=C3, lw=1.5, ls="-.", label=r"$X^{(1)}$: linear")
ax.plot(t, x2, color=C2, lw=1.5, ls="--", label=r"$X^{(2)}$: quadratic")
ax.plot(t, x3, color=C1, lw=1.8, label=r"$X^{(3)}$")
ax.plot(t, x_true, color=C0, lw=2.2, label=r"$X^{(\infty)} = e^t$ (true)")
ax.set_xlabel(r"$t$"); ax.set_ylabel(r"$X^{(n)}_t$")
ax.set_title(r"Picard iteration ($dX = X\,dt$, $X_0 = 1$) — successive approximations")
ax.legend(fontsize=9); ax.set_ylim(0.8, 5.0)
save("fig_picard")
# ════════════════════════════════════════════════════════════════════════════
# §8 HMM REGIME STATE MACHINE (K = 3)
# ════════════════════════════════════════════════════════════════════════════
def fig_hmm_regime():
from matplotlib.patches import FancyBboxPatch, FancyArrowPatch
import matplotlib.patheffects as pe
fig, ax = plt.subplots(figsize=(9, 4.2))
ax.set_xlim(0, 9); ax.set_ylim(0, 4); ax.axis("off")
states = [
(1.5, 2.6, "State 1\nBull", C2),
(4.5, 2.6, "State 2\nNeutral", GRAY),
(7.5, 2.6, "State 3\nBear", C3),
]
box_w, box_h = 2.0, 1.1
for (cx, cy, label, col) in states:
fancy = FancyBboxPatch((cx - box_w/2, cy - box_h/2), box_w, box_h,
boxstyle="round,pad=0.08", linewidth=1.6,
edgecolor=col, facecolor=col + "22",
zorder=2)
ax.add_patch(fancy)
ax.text(cx, cy, label, ha="center", va="center", fontsize=10,
fontweight="bold", color=col, zorder=3)
# Forward arrows A₁₂, A₂₃
for x0, x1, label in [(2.5, 3.5, r"$A_{12}$"), (5.5, 6.5, r"$A_{23}$")]:
ax.annotate("", xy=(x1, 2.85), xytext=(x0, 2.85),
arrowprops=dict(arrowstyle="-|>", color=C0, lw=1.5))
ax.text((x0+x1)/2, 2.98, label, ha="center", fontsize=9, color=C0)
# Backward arrows A₂₁, A₃₂
for x0, x1, label in [(3.5, 2.5, r"$A_{21}$"), (6.5, 5.5, r"$A_{32}$")]:
ax.annotate("", xy=(x1, 2.35), xytext=(x0, 2.35),
arrowprops=dict(arrowstyle="-|>", color=C1, lw=1.5))
ax.text((x0+x1)/2, 2.22, label, ha="center", fontsize=9, color=C1)
# Emission table
col_labels = ["State", r"$\mu$", r"$\sigma$", "Character"]
rows = [
["Bull", "+0.05", "0.12", "high return, low vol"],
["Neutral", " 0.00", "0.18", "flat, medium vol"],
["Bear", "0.08", "0.35", "crash, high vol"],
]
row_colors = [[C2+"33", C2+"33", C2+"33", C2+"33"],
[GRAY+"33", GRAY+"33", GRAY+"33", GRAY+"33"],
[C3+"33", C3+"33", C3+"33", C3+"33"]]
tbl = ax.table(cellText=rows, colLabels=col_labels, loc="bottom",
cellColours=row_colors, bbox=[0.05, 0.0, 0.90, 0.42])
tbl.auto_set_font_size(False); tbl.set_fontsize(9)
for (r, c), cell in tbl.get_celld().items():
cell.set_edgecolor("#cccccc")
if r == 0:
cell.set_facecolor(C0 + "33")
cell.set_text_props(fontweight="bold")
ax.set_title(r"HMM Regime State Machine ($K=3$) — Emission $B_k(y)=\mathcal{N}(\mu_k,\sigma_k^2)$",
fontsize=11, pad=6)
plt.tight_layout()
save("fig_hmm_regime")
# ════════════════════════════════════════════════════════════════════════════
# §8.2 VITERBI TRELLIS (K=3, T=4)
# ════════════════════════════════════════════════════════════════════════════
def fig_viterbi_trellis():
from matplotlib.patches import Circle, FancyArrowPatch
K, T = 3, 4
state_labels = ["1 (Bull)", "2 (Neutral)", "3 (Bear)"]
map_path = {(1, 1), (1, 2)} # state index 1 = "2 (Neutral)" at t=2,3 (0-indexed t)
fig, ax = plt.subplots(figsize=(8, 3.8))
ax.set_xlim(-0.5, T + 0.5); ax.set_ylim(-0.5, K - 0.3); ax.axis("off")
# x-positions: t=1..4 → 0.5, 1.5, 2.5, 3.5
xs = [0.6 * (t + 1) for t in range(T)]
ys = [K - 1 - k for k in range(K)] # top = state 1
# Draw crossing / passing arrows (selective to show crossing)
arrow_kw = dict(arrowstyle="-|>", connectionstyle="arc3,rad=0.0",
color=GRAY, lw=1.1, alpha=0.55)
cross_kw = dict(arrowstyle="-|>", connectionstyle="arc3,rad=0.18",
color=GRAY, lw=1.1, alpha=0.45)
for t in range(T - 1):
for k in range(K):
for k2 in range(K):
rad = 0.0 if k == k2 else (0.18 if k2 > k else -0.18)
col = C0 if (k == 1 and k2 == 1 and t >= 1) else GRAY
alpha = 0.9 if col == C0 else 0.3
ax.annotate("", xy=(xs[t+1], ys[k2]), xytext=(xs[t], ys[k]),
arrowprops=dict(arrowstyle="-|>",
connectionstyle=f"arc3,rad={rad}",
color=col, lw=1.2 if col == C0 else 0.8,
alpha=alpha))
# Draw nodes
r = 0.14
for k in range(K):
for t in range(T):
is_map = (k == 1 and 1 <= t <= 2)
fc = C0 if is_map else "white"
ec = C0 if is_map else GRAY
circ = Circle((xs[t], ys[k]), r, facecolor=fc, edgecolor=ec, lw=1.8, zorder=4)
ax.add_patch(circ)
# Labels on left
for k in range(K):
ax.text(-0.1, ys[k], state_labels[k], ha="right", va="center",
fontsize=9, color=C0 if k == 1 else "black")
# x-axis ticks
for t in range(T):
ax.text(xs[t], -0.35, f"$t={t+1}$", ha="center", va="top", fontsize=9)
# Legend
ax.scatter([], [], color=C0, s=80, label="● MAP (Viterbi) path", zorder=5)
ax.scatter([], [], facecolor="white", edgecolors=GRAY, s=80, label="○ other nodes", zorder=5)
ax.legend(loc="upper right", fontsize=9, framealpha=0.9)
ax.set_title(r"Viterbi Trellis ($K=3$, $T=4$) — $\delta_t(k)=\max_j\,\delta_{t-1}(j)\,A_{jk}\,B_k(y_t)$",
fontsize=11)
plt.tight_layout()
save("fig_viterbi_trellis")
# ════════════════════════════════════════════════════════════════════════════
# §10.2 STANDARD VS NATURAL GRADIENT — PROPERTY COMPARISON
# ════════════════════════════════════════════════════════════════════════════
def fig_std_vs_nat_gradient():
from matplotlib.patches import FancyBboxPatch
fig, axes = plt.subplots(1, 2, figsize=(9, 3.0))
panels = [
("Standard Gradient\n" + r"$\theta_{k+1} = \theta_k - \eta\nabla\mathcal{L}$",
["Flat $\\mathbb{R}^d$ geometry",
"Ignores manifold curvature",
"Slow on ill-conditioned $\\mathcal{I}$",
"$O(\\kappa(\\mathcal{I}))$ iterations"],
C3, C3 + "18"),
("Natural Gradient\n" + r"$\theta_{k+1} = \theta_k - \eta\,\mathcal{I}(\theta)^{-1}\nabla\mathcal{L}$",
["Riemannian metric $\\mathcal{I}(\\theta)$",
"Adapts to manifold geometry",
"Reparametrisation-invariant",
"$O(1)$ on exp. families (MLE step)"],
C2, C2 + "18"),
]
for ax, (title, props, border, bg) in zip(axes, panels):
ax.set_xlim(0, 1); ax.set_ylim(0, 1); ax.axis("off")
fancy = FancyBboxPatch((0.03, 0.04), 0.94, 0.92,
boxstyle="round,pad=0.04", linewidth=2,
edgecolor=border, facecolor=bg)
ax.add_patch(fancy)
ax.text(0.5, 0.87, title, ha="center", va="top", fontsize=10,
fontweight="bold", color=border, transform=ax.transAxes,
multialignment="center")
y = 0.68
for prop in props:
ax.text(0.12, y, "" + prop, ha="left", va="top", fontsize=9.5,
transform=ax.transAxes, color="#222222")
y -= 0.17
fig.suptitle("Standard vs Natural Gradient — geometric properties", fontsize=11, y=1.02)
plt.tight_layout()
save("fig_std_vs_nat_gradient")
# ════════════════════════════════════════════════════════════════════════════
# §10.3 MATRIX LIE GROUP HIERARCHY
# ════════════════════════════════════════════════════════════════════════════
def fig_lie_group_hierarchy():
from matplotlib.patches import FancyBboxPatch, FancyArrowPatch
fig, ax = plt.subplots(figsize=(9, 4.6))
ax.set_xlim(0, 9); ax.set_ylim(0, 4.6); ax.axis("off")
nodes = {
"GL": (4.5, 4.1, r"$\mathrm{GL}(n,\mathbb{R})$" + "\nall invertible $n\times n$", C0),
"SL": (1.8, 2.85, r"$\mathrm{SL}(n,\mathbb{R})$" + "\n" + r"$\det=1$", C2),
"On": (4.5, 2.85, r"$O(n)$" + "\n$R^\top R=I$", C1),
"Sp": (7.2, 2.85, r"$\mathrm{Sp}(2n,\mathbb{R})$" + "\npreserves " + r"$\omega$", C2),
"SO": (4.5, 1.55, r"$\mathrm{SO}(n)$" + "\n" + r"$\det=+1$ (rotations)", C2),
"Hn": (1.8, 1.55, r"$H(n)$ Heisenberg" + "\nupper triangular", C3),
}
notes = {
"SO": "portfolio factor\nrotation, PCA",
"Sp": "Hamiltonian\nmechanics, PMP",
"Hn": "path-signature\nfeature maps",
}
edges = [("GL","SL"), ("GL","On"), ("GL","Sp"), ("On","SO")]
bw, bh = 2.2, 0.76
for key, (cx, cy, label, col) in nodes.items():
fbp = FancyBboxPatch((cx-bw/2, cy-bh/2), bw, bh,
boxstyle="round,pad=0.07", lw=1.6,
edgecolor=col, facecolor=col+"22", zorder=2)
ax.add_patch(fbp)
ax.text(cx, cy, label, ha="center", va="center", fontsize=8.5,
multialignment="center", color=col, fontweight="bold", zorder=3)
if key in notes:
ax.text(cx + bw/2 + 0.15, cy, notes[key], va="center",
fontsize=7.5, color="#555555", fontstyle="italic")
for src, dst in edges:
sx, sy = nodes[src][0], nodes[src][1]
dx, dy = nodes[dst][0], nodes[dst][1]
ax.annotate("", xy=(dx, dy + bh/2 + 0.04), xytext=(sx, sy - bh/2 - 0.04),
arrowprops=dict(arrowstyle="-|>", color=GRAY, lw=1.4))
ax.set_title("Matrix Lie Group Hierarchy — subgroup inclusions and finance applications",
fontsize=11, pad=5)
plt.tight_layout()
save("fig_lie_group_hierarchy")
# ════════════════════════════════════════════════════════════════════════════
# RUN ALL
# ════════════════════════════════════════════════════════════════════════════
if __name__ == "__main__":
funcs = [
fig_de_mutation, fig_rastrigin,
fig_random_walk, fig_bm_fan, fig_gbm,
fig_ito_correction,
fig_picard,
fig_fokker_planck, fig_em_milstein,
fig_ou_path, fig_ou_transition, fig_ou_loglik, fig_ou_residuals,
fig_poisson, fig_jump_diffusion, fig_levy_tails,
fig_kalman_covariance,
fig_mcmc_energy, fig_mcmc_trace,
fig_kl_asymmetry, fig_fisher_curvature,
fig_curvatures, fig_natural_gradient,
# new §8 & §10 diagrams
fig_hmm_regime, fig_viterbi_trellis,
fig_std_vs_nat_gradient, fig_lie_group_hierarchy,
]
for fn in funcs:
print(f" {fn.__name__} ... ", end="", flush=True)
fn()
print("ok")
print(f"\nDone — {len(funcs)} SVGs saved to {OUT}")
+138
View File
@@ -0,0 +1,138 @@
/* ─── Optimiz-rs Documentation Custom Styles ──────────────────────────────── */
/* Brand: orange (#f97316) on furo dark theme */
:root {
--brand-orange: #f97316;
--brand-orange-light: #fb923c;
--brand-orange-dim: rgba(249, 115, 22, 0.15);
--brand-orange-border: rgba(249, 115, 22, 0.35);
}
/* ── Mermaid diagram container ─────────────────────────────────────────────── */
.mermaid {
background: transparent !important;
padding: 1.5rem 0;
text-align: center;
overflow-x: auto;
}
.mermaid svg {
max-width: 100%;
border-radius: 12px;
filter: drop-shadow(0 4px 16px rgba(249, 115, 22, 0.12));
}
/* ── Better admonitions ─────────────────────────────────────────────────────── */
.admonition {
border-radius: 10px !important;
border-left-width: 4px !important;
margin: 1.5rem 0 !important;
}
.admonition.note {
border-left-color: var(--brand-orange) !important;
background: var(--brand-orange-dim) !important;
}
.admonition.tip {
border-left-color: #22c55e !important;
background: rgba(34, 197, 94, 0.1) !important;
}
.admonition.warning {
border-left-color: #f59e0b !important;
background: rgba(245, 158, 11, 0.1) !important;
}
.admonition.important {
border-left-color: #ef4444 !important;
background: rgba(239, 68, 68, 0.1) !important;
}
/* ── Section headers ────────────────────────────────────────────────────────── */
h1, h2, h3 {
scroll-margin-top: 4rem;
}
h2 {
border-bottom: 2px solid var(--brand-orange-border);
padding-bottom: 0.4rem;
}
/* ── Code blocks ────────────────────────────────────────────────────────────── */
.highlight {
border-radius: 8px !important;
border: 1px solid rgba(255, 255, 255, 0.08) !important;
}
/* Inline code */
code.literal {
background: rgba(249, 115, 22, 0.08) !important;
border: 1px solid var(--brand-orange-border) !important;
padding: 0.1em 0.4em !important;
border-radius: 4px !important;
color: var(--brand-orange-light) !important;
font-size: 0.88em !important;
}
/* ── Tables ─────────────────────────────────────────────────────────────────── */
table.docutils {
border-collapse: collapse;
width: 100%;
margin: 1.2rem 0;
border-radius: 8px;
overflow: hidden;
font-size: 0.9rem;
}
table.docutils th {
background: var(--brand-orange-dim) !important;
color: var(--brand-orange-light) !important;
font-weight: 700;
padding: 0.6rem 0.9rem;
border-bottom: 2px solid var(--brand-orange-border);
}
table.docutils td {
padding: 0.5rem 0.9rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
table.docutils tr:hover td {
background: rgba(249, 115, 22, 0.04);
}
/* ── Performance metric boxes (for benchmarks) ──────────────────────────────── */
.perf-box {
background: linear-gradient(135deg, rgba(249,115,22,0.12) 0%, rgba(249,115,22,0.04) 100%);
border: 1px solid var(--brand-orange-border);
border-radius: 10px;
padding: 1rem 1.5rem;
margin: 1rem 0;
display: inline-block;
min-width: 140px;
text-align: center;
}
.perf-box .val {
font-size: 1.8rem;
font-weight: 800;
color: var(--brand-orange);
}
.perf-box .lbl {
font-size: 0.75rem;
color: #94a3b8;
margin-top: 0.2rem;
}
/* ── Navigation sidebar ─────────────────────────────────────────────────────── */
.sidebar-tree .current > .reference {
color: var(--brand-orange) !important;
}
/* ── Mobile ─────────────────────────────────────────────────────────────────── */
@media (max-width: 768px) {
.mermaid svg { width: 100% !important; }
table.docutils { font-size: 0.78rem; }
}
File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 174 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 55 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 30 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 53 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 43 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 122 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 112 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 50 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 35 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 53 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 50 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 123 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 69 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 58 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 62 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 95 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 128 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 194 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 168 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 42 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 112 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 42 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 31 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 32 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 77 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 50 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,494 @@
# Differential Evolution
**Differential Evolution (DE)** is a population-based metaheuristic optimization algorithm
introduced by Storn and Price (1997). It is particularly effective for continuous, non-convex,
multimodal optimization problems where gradient information is unavailable or unreliable.
This module provides a high-performance Rust implementation with Python bindings, supporting
multiple mutation strategies, adaptive parameter control (jDE), and parallel evaluation.
---
## Mathematical Foundations
### Problem Formulation
DE solves unconstrained (or box-constrained) minimization problems:
$$
\min_{\mathbf{x} \in \mathbb{R}^D} f(\mathbf{x})
$$
subject to box constraints:
$$
x_j \in [l_j, u_j], \quad j = 1, \ldots, D
$$
**DE is well-suited when:**
- $f$ is continuous but non-differentiable
- Multiple local minima exist
- Gradient information is unavailable or expensive
- Problem dimension is moderate ($D < 100$)
---
### Population
DE maintains a population of $N_P$ candidate solutions:
$$
P_g = \{\mathbf{x}_{1,g}, \mathbf{x}_{2,g}, \ldots, \mathbf{x}_{N_P,g}\}
$$
where $g$ is the generation number and $\mathbf{x}_{i,g} \in \mathbb{R}^D$.
**Rule of thumb:** $N_P = 10 \times D$ where $D$ is the problem dimension.
---
### Main Loop
For each generation $g = 0, 1, 2, \ldots$:
1. **Mutation**: Create mutant vectors by combining existing solutions
2. **Crossover**: Mix mutant with target vector to form trial vector
3. **Selection**: Keep better solution (greedy selection)
---
## Mutation Strategies
The mutation operator creates a **mutant vector** $\mathbf{v}_{i,g+1}$ from existing
population members:
### DE/rand/1 (Classic Strategy)
$$
\mathbf{v}_{i,g+1} = \mathbf{x}_{r_1,g} + F \cdot (\mathbf{x}_{r_2,g} - \mathbf{x}_{r_3,g})
$$
where:
- $r_1, r_2, r_3 \in \{1, \ldots, N_P\}$ are randomly chosen, distinct, and $\neq i$
- $F \in (0, 2]$ is the **mutation factor** (typically 0.51.0)
**Interpretation:** Start from a random population member $\mathbf{x}_{r_1}$,
move in direction given by the difference $(\mathbf{x}_{r_2} - \mathbf{x}_{r_3})$,
scaled by $F$.
**Characteristics:** Most explorative, good for diverse populations.
### DE/best/1
$$
\mathbf{v}_{i,g+1} = \mathbf{x}_{\text{best},g} + F \cdot (\mathbf{x}_{r_1,g} - \mathbf{x}_{r_2,g})
$$
**Advantage:** Faster convergence toward the best-known solution.
**Disadvantage:** More likely to get stuck in local minima.
### DE/current-to-best/1
$$
\mathbf{v}_{i,g+1} = \mathbf{x}_{i,g} + F \cdot (\mathbf{x}_{\text{best},g} - \mathbf{x}_{i,g}) + F \cdot (\mathbf{x}_{r_1,g} - \mathbf{x}_{r_2,g})
$$
**Interpretation:** Move current solution toward the best while also exploring.
**Characteristics:** Balanced exploration/exploitation.
### DE/rand/2
$$
\mathbf{v}_{i,g+1} = \mathbf{x}_{r_1,g} + F \cdot (\mathbf{x}_{r_2,g} - \mathbf{x}_{r_3,g}) + F \cdot (\mathbf{x}_{r_4,g} - \mathbf{x}_{r_5,g})
$$
**Characteristics:** More disruptive, better for highly multimodal problems.
### DE/best/2
$$
\mathbf{v}_{i,g+1} = \mathbf{x}_{\text{best},g} + F \cdot (\mathbf{x}_{r_1,g} - \mathbf{x}_{r_2,g}) + F \cdot (\mathbf{x}_{r_3,g} - \mathbf{x}_{r_4,g})
$$
**Characteristics:** Aggressive convergence to the best solution.
---
## Crossover
After mutation, the **trial vector** $\mathbf{u}_{i,g+1}$ is formed by mixing
components from the mutant and the target vector.
### Binomial Crossover
For each component $j = 1, \ldots, D$:
$$
u_{i,j,g+1} = \begin{cases}
v_{i,j,g+1} & \text{if } \text{rand}(0,1) \leq CR \text{ or } j = j_{\text{rand}} \\
x_{i,j,g} & \text{otherwise}
\end{cases}
$$
where:
- $CR \in [0, 1]$ is the **crossover probability**
- $j_{\text{rand}} \in \{1, \ldots, D\}$ ensures at least one component comes from the mutant
**Effect:** $CR$ controls how much of the mutant vector is used.
| CR Value | Effect |
|----------|--------|
| Low (0.10.3) | Less information exchange, slower convergence. Better for separable problems |
| High (0.70.9) | More information exchange, faster convergence. Better for non-separable problems |
| 0.0 | Pure mutation (except $j_{\text{rand}}$) |
| 1.0 | Full crossover |
---
## Selection
Greedy selection (for minimization):
$$
\mathbf{x}_{i,g+1} = \begin{cases}
\mathbf{u}_{i,g+1} & \text{if } f(\mathbf{u}_{i,g+1}) \leq f(\mathbf{x}_{i,g}) \\
\mathbf{x}_{i,g} & \text{otherwise}
\end{cases}
$$
**Property:** Population quality never decreases:
$$
f(\mathbf{x}_{\text{best},g+1}) \leq f(\mathbf{x}_{\text{best},g})
$$
---
## Complete Algorithm
```{mermaid}
flowchart TD
A["🎲 Initialize Population\nx_i = l + rand · (u l)"] --> B["📊 Evaluate Fitness\nf_i = f(x_i) for all i"]
B --> C{{"g < max_iter?"}}
C -->|Yes| D["Mutation\nv = x_r1 + F · (x_r2 x_r3)"]
D --> E["Crossover\nu_j = v_j if rand ≤ CR or j = j_rand\nelse u_j = x_j"]
E --> F["Clip to bounds [l, u]"]
F --> G{{"f(trial) ≤ f(target)?"}}
G -->|"Yes — better"| H["✅ Accept trial\nx_i ← u_i"]
G -->|"No — worse"| I["Keep current\nx_i unchanged"]
H & I --> C
C -->|No| J["🏆 Return x_best, f(x_best)"]
```
---
## Parameter Selection Guidelines
### Population Size ($N_P$)
| Size Category | Range | Use Case |
|---------------|-------|----------|
| Small | < 4D | Faster convergence; risk premature convergence. Simple unimodal problems |
| Medium | 10D (default) | Good balance for most problems |
| Large | > 20D | Better exploration; slower convergence. Highly multimodal problems |
**Minimum:** $N_P \geq 4$ (needed for mutation with three distinct indices).
### Mutation Factor ($F$)
| F Value | Effect |
|---------|--------|
| Low (0.40.6) | Fine-tuning, local search. Safer, less disruptive |
| High (0.81.2) | Exploration, global search. Escape local minima |
**Typical range:** $F \in [0.4, 1.0]$, default 0.8.
### Crossover Probability ($CR$)
| CR Value | Effect |
|----------|--------|
| Low (0.10.3) | Best for separable problems |
| High (0.70.9) | Best for non-separable problems |
**Default:** 0.70.9 for most problems.
---
## Python API
### Basic Usage
```python
import numpy as np
from optimizr import differential_evolution
def rastrigin(x):
"""Multimodal benchmark function with many local minima."""
A = 10
return A * len(x) + sum(x**2 - A * np.cos(2 * np.pi * x))
best_x, best_fx = differential_evolution(
objective_fn=rastrigin,
bounds=[(-5.12, 5.12)] * 10, # 10-dimensional problem
strategy="best1",
popsize=20,
maxiter=500,
adaptive=True,
)
print(f"Best fitness: {best_fx:.6f}")
print(f"Best solution: {best_x}")
```
**Expected output:**
```
Best fitness: 0.000042
Best solution: [ 0.00012 -0.00023 0.00018 ... ]
```
### Configuration Options
```python
from optimizr import DifferentialEvolution
de = DifferentialEvolution(
bounds=[(-5, 5)] * 20,
strategy="rand1", # mutation strategy
popsize=200, # population size
maxiter=1000, # maximum generations
F=0.8, # mutation factor
CR=0.9, # crossover probability
tol=1e-8, # convergence tolerance
seed=42, # reproducibility
)
result = de.minimize(sphere_function)
print(f"Converged in {result.nit} iterations")
print(f"Function evaluations: {result.nfev}")
```
---
## Adaptive Control (jDE)
Optimiz-rs implements **jDE** (self-adaptive DE), where the parameters $F$ and $CR$
evolve with the population:
$$
F_{i,g+1} = \begin{cases}
F_l + \text{rand}(0,1) \cdot (F_u - F_l) & \text{if } \text{rand}(0,1) < \tau_1 \\
F_{i,g} & \text{otherwise}
\end{cases}
$$
$$
CR_{i,g+1} = \begin{cases}
\text{rand}(0,1) & \text{if } \text{rand}(0,1) < \tau_2 \\
CR_{i,g} & \text{otherwise}
\end{cases}
$$
**Enable jDE:**
```python
de = DifferentialEvolution(
bounds=[(-5, 5)] * 20,
adaptive=True, # enables jDE
tau_F=0.1, # probability of F mutation
tau_CR=0.1, # probability of CR mutation
)
```
**Advantages:**
- No need to manually tune $F$ and $CR$
- Adapts to problem landscape during optimization
- Generally robust across problem types
---
## Parallel Evaluation (Rust Backend)
For pure-Rust objectives or when Python callbacks are not needed, enable
data-parallel evaluation via Rayon:
```python
from optimizr import parallel_differential_evolution_rust
result = parallel_differential_evolution_rust(
objective="rastrigin", # built-in benchmark
dim=50,
bounds=(-5.12, 5.12),
popsize=500,
maxiter=2000,
n_threads=8,
)
print(f"Best fitness: {result.best_fitness:.8f}")
```
**Speedup:** Near-linear up to $N_P$ processors for expensive objectives.
---
## Convergence Analysis
### Theoretical Properties
**Global Convergence Theorem** (Lampinen, 2001):
Under these sufficient conditions:
- Population size $N_P > 3$
- Mutation factor $F > 0$
- At least one component crossed over ($j_{\text{rand}}$)
DE is a **global optimization method**: any point can be reached with positive probability.
### Diversity Measure
$$
D_g = \frac{1}{N_P D} \sum_{i=1}^{N_P} \sum_{j=1}^D |x_{i,j,g} - \bar{x}_{j,g}|
$$
| Diversity | Behavior |
|-----------|----------|
| High | Exploration (global search) |
| Low | Exploitation (local search) |
### Empirical Budget
**Rule of thumb:** Budget $10^4 \times D$ function evaluations for moderately difficult problems.
---
## Performance Comparison
| Algorithm | Gradient | Global | Constraints | Speed | Best For |
|-----------|----------|--------|-------------|-------|----------|
| **DE** | No | Yes | Box | Medium | Non-convex, continuous |
| Gradient Descent | Yes | No | Yes | Fast | Smooth, convex |
| Genetic Algorithm | No | Yes | Yes | Slow | Discrete, combinatorial |
| Particle Swarm | No | Yes | Box | Fast | Continuous, many dimensions |
| CMA-ES | No | Yes | Box | Fast | Continuous, noisy |
---
## Practical Tips
### 1. Start Simple
Use defaults: $N_P = 10D$, $F = 0.8$, $CR = 0.7$, `strategy="rand1"`.
### 2. Scale Variables
Normalize parameters to similar ranges for better performance.
### 3. Warm Start
If you have a good initial guess, seed the population around it.
### 4. Hybrid Approach
Use DE for global search, then a local optimizer for refinement:
```python
# Global search with DE
best_x, _ = differential_evolution(f, bounds, maxiter=200)
# Local refinement with L-BFGS-B
from scipy.optimize import minimize
result = minimize(f, best_x, method='L-BFGS-B', bounds=bounds)
```
### 5. Monitor Convergence
Plot:
- Best fitness vs. generation
- Average population fitness vs. generation
- Population diversity vs. generation
### 6. Restarts
If premature convergence detected, restart with new random population.
---
## Troubleshooting
| Symptom | Cause | Fix |
|---------|-------|-----|
| Slow convergence | $F$ or $CR$ too low | Increase $F$ to 0.8, $CR$ to 0.9 |
| Premature convergence | Population too small | Increase $N_P$ to 1520D |
| Oscillating fitness | $F$ too high | Decrease $F$ to 0.50.6 |
| Stuck in local minimum | Using `best1` strategy | Switch to `rand1` or `rand2` |
---
## Benchmark Results
Performance on standard test functions (D=30, $N_P=300$, 1000 generations):
| Function | Best Fitness | Iterations | Time (s) |
|----------|-------------|------------|----------|
| Sphere | 1.2e-28 | 412 | 0.8 |
| Rosenbrock | 2.4e-08 | 891 | 1.4 |
| Rastrigin | 4.1e-05 | 1000 | 2.1 |
| Ackley | 8.8e-15 | 623 | 1.2 |
| Griewank | 3.7e-12 | 548 | 1.0 |
---
## Advantages & Limitations
### Advantages
✅ No gradient information needed
✅ Handles non-convex, multimodal functions well
✅ Few parameters to tune
✅ Simple to implement and understand
✅ Robust across problem types
✅ Naturally handles box constraints
✅ Population maintains diversity
### Limitations
❌ Slower than gradient methods (when gradients are available)
❌ Scales poorly to high dimensions ($D > 100$)
❌ No convergence guarantees in finite time
❌ Requires many function evaluations
❌ Performance sensitive to parameter choices
---
## References
1. Storn, R. & Price, K. (1997). "Differential evolution A simple and efficient heuristic for global optimization over continuous spaces." *Journal of Global Optimization*, 11(4):341359.
2. Price, K., Storn, R.M. & Lampinen, J.A. (2005). *Differential Evolution: A Practical Approach to Global Optimization*. Springer.
3. Das, S. & Suganthan, P.N. (2011). "Differential evolution: A survey of the state-of-the-art." *IEEE Transactions on Evolutionary Computation*, 15(1):431.
4. Brest, J. et al. (2006). "Self-adapting control parameters in differential evolution." *IEEE Trans. Evolutionary Computation*, 10(6):646657. (jDE)
5. Tanabe, R. & Fukunaga, A. (2013). "Success-history based parameter adaptation for differential evolution." *IEEE CEC*, pp. 7178. (SHADE)
---
## Related Topics
- [Grid Search](grid_search.md) Exhaustive search for small parameter spaces
- [MCMC](mcmc.md) Sampling-based inference for Bayesian optimization
- [Mean Field Games](mean_field_games.md) Population dynamics optimization
+52
View File
@@ -0,0 +1,52 @@
Graph Laplacians and Spectral Clustering
========================================
The module :code:`graph` provides graph Laplacian operators and a
spectral clustering algorithm built on a Jacobi diagonaliser.
Laplacians
----------
For a weighted undirected graph with adjacency matrix :math:`W \in \mathbb{R}^{n\times n}_{\ge 0}`
and degree matrix :math:`D = \mathrm{diag}(W \mathbf{1})`:
- **Combinatorial**: :math:`L = D - W`.
- **Symmetric normalised**: :math:`L_{\mathrm{sym}} = I - D^{-1/2} W D^{-1/2}`.
- **Random-walk normalised**: :math:`L_{\mathrm{rw}} = I - D^{-1} W`.
Each operator is positive semidefinite and the multiplicity of the
zero eigenvalue equals the number of connected components.
Spectral Clustering
-------------------
Given :math:`W` and a target number of clusters :math:`k`:
1. Build :math:`L_{\mathrm{sym}}` (or another Laplacian).
2. Diagonalise via cyclic Jacobi rotations to obtain the eigenpairs
:math:`(\lambda_i, u_i)`.
3. Stack the :math:`k` eigenvectors associated with the smallest
eigenvalues as columns of :math:`U \in \mathbb{R}^{n \times k}`.
4. Normalise rows of :math:`U` and run Lloyd's algorithm with
k-means++ initialisation on the rows.
The Fiedler eigenvalue :math:`\lambda_2` is reported separately as a
proxy for the spectral gap.
API
---
.. code-block:: rust
pub enum LaplacianKind { Combinatorial, SymmetricNormalised, RandomWalk }
pub fn combinatorial_laplacian(w: ArrayView2<f64>) -> Result<Array2<f64>>;
pub fn normalised_laplacian(w: ArrayView2<f64>) -> Result<Array2<f64>>;
pub fn random_walk_laplacian(w: ArrayView2<f64>) -> Result<Array2<f64>>;
pub struct SpectralClusterResult {
pub labels: Vec<usize>,
pub eigenvalues: Vec<f64>,
pub fiedler_value: f64,
}
pub fn spectral_cluster(w: ArrayView2<f64>, k: usize, n_kmeans_iter: usize, seed: u64)
-> Result<SpectralClusterResult>;
+492
View File
@@ -0,0 +1,492 @@
# Grid Search
**Grid Search** (also called parameter sweep) is a deterministic hyperparameter optimization
method that exhaustively evaluates all combinations of parameter values from a predefined grid.
While simple, it provides guaranteed coverage of the search space and is ideal for
low-dimensional problems.
This module provides a fast implementation with optional Rust acceleration for
Cartesian product generation and parallel evaluation.
---
## Mathematical Foundations
### Problem Formulation
Given an objective function $f(\theta)$ and a discrete parameter grid:
$$
\Theta = \Theta_1 \times \Theta_2 \times \cdots \times \Theta_D
$$
where $\Theta_i = \{\theta_{i,1}, \theta_{i,2}, \ldots, \theta_{i,n_i}\}$ is the set of
candidate values for parameter $i$.
**Objective:** Find the optimal parameters:
$$
\theta^* = \arg\min_{\theta \in \Theta} f(\theta)
$$
### Total Evaluations
The number of function evaluations grows as the **Cartesian product**:
$$
|\Theta| = \prod_{i=1}^{D} n_i
$$
where $n_i = |\Theta_i|$ is the number of values for parameter $i$.
| Parameters | Values Each | Total Evaluations |
|------------|-------------|-------------------|
| 2 | 5 | 25 |
| 3 | 5 | 125 |
| 4 | 5 | 625 |
| 5 | 5 | 3,125 |
| 3 | 10 | 1,000 |
| 5 | 10 | 100,000 |
**Warning:** Grid search suffers from the **curse of dimensionality**. Use sparingly
for $D > 4$ or when function evaluations are expensive.
---
### Algorithm
```
Algorithm: Grid Search
──────────────────────
Input: objective f, parameter grid Θ = Θ₁ × Θ₂ × ... × Θ_D
1. Generate all combinations: C = Θ₁ × Θ₂ × ... × Θ_D
2. Initialize: best_score = ∞, best_params = None
3. For each θ in C:
a. Evaluate: score = f(θ)
b. If score < best_score:
best_score = score
best_params = θ
4. Return best_params, best_score
```
---
## When to Use Grid Search
### Good Use Cases
**Few parameters** (D ≤ 34)
**Coarse exploration** before fine-tuning
**Discrete parameters** (e.g., layer counts, categoricals)
**Reproducibility required** (deterministic)
**Parameter interactions** need full coverage
**Fast objectives** (< 1 second per evaluation)
### Poor Use Cases
**Many parameters** (D > 5) — exponential explosion
**Continuous parameters** — wastes evaluations between grid points
**Expensive objectives** — better to use adaptive methods
**High-resolution search** — consider random search or DE
---
## Python API
### Basic Usage
```python
from optimizr import grid_search
# Objective returns a scalar score (lower is better)
def objective(params):
lr = params["lr"]
dropout = params["dropout"]
# Simulate validation loss
return (lr - 0.02)**2 + (dropout - 0.1)**2
best_params, best_score = grid_search(
objective_fn=objective,
param_grid={
"lr": [0.005, 0.01, 0.02, 0.05, 0.1],
"dropout": [0.0, 0.05, 0.1, 0.2, 0.3],
},
)
print(f"Best parameters: {best_params}")
print(f"Best score: {best_score:.6f}")
```
**Expected output:**
```
Best parameters: {'lr': 0.02, 'dropout': 0.1}
Best score: 0.000000
```
### With Verbose Logging
```python
best_params, best_score = grid_search(
objective_fn=objective,
param_grid=param_grid,
verbose=True, # print progress
)
```
**Output:**
```
[1/25] lr=0.005, dropout=0.0 → score=0.0127
[2/25] lr=0.005, dropout=0.05 → score=0.0102
...
[15/25] lr=0.02, dropout=0.1 → score=0.0000 [BEST]
...
```
### Parallel Evaluation
For independent, thread-safe objectives:
```python
best_params, best_score = grid_search(
objective_fn=objective,
param_grid=param_grid,
n_jobs=4, # parallel workers
)
```
**Note:** Objective function must be thread-safe (no shared mutable state).
---
## Grid Construction Strategies
### Linear Grid
Evenly spaced values:
```python
param_grid = {
"lr": [0.001, 0.005, 0.01, 0.05, 0.1], # linear
}
```
### Logarithmic Grid
For parameters spanning orders of magnitude:
```python
import numpy as np
param_grid = {
"lr": list(np.logspace(-4, -1, 10)), # 1e-4 to 1e-1
"weight_decay": list(np.logspace(-5, -2, 8)),
}
```
### Mixed Types
```python
param_grid = {
"lr": [0.001, 0.01, 0.1], # continuous
"batch_size": [16, 32, 64, 128], # integer
"optimizer": ["adam", "sgd", "rmsprop"], # categorical
"use_bn": [True, False], # boolean
}
```
---
## Practical Example: Neural Network Tuning
```python
import numpy as np
from optimizr import grid_search
def train_and_evaluate(params):
"""
Train a neural network with given hyperparameters
and return validation loss.
"""
lr = params["lr"]
dropout = params["dropout"]
hidden_size = params["hidden_size"]
# Simulated training (replace with real training loop)
# In practice: build model, train, return val_loss
# Synthetic loss surface for demonstration
loss = (
(lr - 0.01)**2 / 0.001 +
(dropout - 0.15)**2 / 0.01 +
(hidden_size - 128)**2 / 10000
)
return loss + np.random.normal(0, 0.001) # add noise
param_grid = {
"lr": [0.001, 0.005, 0.01, 0.02, 0.05],
"dropout": [0.0, 0.1, 0.2, 0.3],
"hidden_size": [64, 128, 256],
}
print(f"Total combinations: {5 * 4 * 3} = 60")
best_params, best_score = grid_search(
objective_fn=train_and_evaluate,
param_grid=param_grid,
verbose=True,
)
print(f"\nBest configuration:")
print(f" Learning rate: {best_params['lr']}")
print(f" Dropout: {best_params['dropout']}")
print(f" Hidden size: {best_params['hidden_size']}")
print(f" Validation loss: {best_score:.4f}")
```
**Expected output:**
```
Total combinations: 60
[1/60] lr=0.001, dropout=0.0, hidden_size=64 → score=0.1892
...
[32/60] lr=0.01, dropout=0.2, hidden_size=128 → score=0.0027 [BEST]
...
Best configuration:
Learning rate: 0.01
Dropout: 0.2
Hidden size: 128
Validation loss: 0.0027
```
---
## Visualization
### Results Heatmap
```python
import matplotlib.pyplot as plt
import numpy as np
# Collect all results
results = {}
for lr in param_grid["lr"]:
for dropout in param_grid["dropout"]:
params = {"lr": lr, "dropout": dropout, "hidden_size": 128}
results[(lr, dropout)] = train_and_evaluate(params)
# Create heatmap
lrs = param_grid["lr"]
dropouts = param_grid["dropout"]
Z = np.array([[results[(lr, d)] for d in dropouts] for lr in lrs])
plt.figure(figsize=(8, 6))
plt.imshow(Z, origin='lower', aspect='auto', cmap='viridis_r')
plt.colorbar(label='Loss')
plt.xticks(range(len(dropouts)), dropouts)
plt.yticks(range(len(lrs)), lrs)
plt.xlabel('Dropout')
plt.ylabel('Learning Rate')
plt.title('Grid Search: Loss Surface')
plt.savefig('grid_search_heatmap.png', dpi=150)
```
**Output:**
Produces a 2D heatmap showing the loss landscape across different hyperparameter combinations:
- **X-axis**: Dropout rate values
- **Y-axis**: Learning rate values
- **Color intensity**: Loss values (darker = lower loss = better performance)
This visualization helps identify optimal parameter regions and understand parameter interactions.
📓 **Real-World Examples**: See [04_real_world_applications.ipynb](https://github.com/ThotDjehuty/optimiz-r/blob/main/examples/notebooks/04_real_world_applications.ipynb) for grid search applied to:
- Neural network hyperparameter tuning
- Portfolio optimization
- Trading strategy parameter selection
---
## Combining with Other Methods
### Coarse-to-Fine Search
Use grid search to identify promising regions, then refine:
```python
from optimizr import grid_search, differential_evolution
# Step 1: Coarse grid search
coarse_grid = {
"lr": [0.001, 0.01, 0.1],
"dropout": [0.0, 0.2, 0.4],
}
coarse_best, _ = grid_search(objective, coarse_grid)
print(f"Coarse best: {coarse_best}")
# Step 2: Fine grid around best
fine_grid = {
"lr": np.linspace(
coarse_best["lr"] * 0.5,
coarse_best["lr"] * 2,
10
).tolist(),
"dropout": np.linspace(
max(0, coarse_best["dropout"] - 0.1),
min(0.5, coarse_best["dropout"] + 0.1),
10
).tolist(),
}
final_best, final_score = grid_search(objective, fine_grid)
print(f"Final best: {final_best}, score: {final_score:.6f}")
```
### Warm-Start Differential Evolution
Use grid search results to seed DE:
```python
from optimizr import grid_search, differential_evolution
# Quick grid search for initial region
best_grid, _ = grid_search(objective, coarse_grid)
# Initialize DE population around grid search result
bounds = [
(best_grid["lr"] * 0.1, best_grid["lr"] * 10),
(max(0, best_grid["dropout"] - 0.2), min(0.5, best_grid["dropout"] + 0.2)),
]
final_x, final_fx = differential_evolution(
objective_fn=lambda x: objective({"lr": x[0], "dropout": x[1]}),
bounds=bounds,
maxiter=100,
)
```
---
## Performance Comparison
| Method | Evaluations | Coverage | Best For |
|--------|-------------|----------|----------|
| **Grid Search** | $\prod n_i$ (exponential) | Complete | Low-D, discrete |
| Random Search | Fixed budget | Probabilistic | High-D, continuous |
| Bayesian Optimization | Adaptive | Adaptive | Expensive objectives |
| Differential Evolution | $N_P \times \text{gens}$ | Adaptive | Complex landscapes |
### When to Switch Methods
| Scenario | Recommendation |
|----------|----------------|
| D ≤ 3, fast objective | Grid search |
| D = 410, moderate objective | Random search + grid refinement |
| D > 10 or expensive objective | Bayesian optimization or DE |
| Noisy objective | DE or ensemble methods |
---
## Advantages & Limitations
### Advantages
**Simple and deterministic** — reproducible results
**Complete coverage** — won't miss global optimum in grid
**No tuning required** — no algorithm-specific hyperparameters
**Parallel-friendly** — each evaluation is independent
**Good for discrete parameters** — natural fit
### Limitations
**Exponential scaling** — infeasible for many parameters
**Wasteful for continuous spaces** — evaluates between optimal values
**Uniform allocation** — doesn't focus on promising regions
**Resolution trade-off** — coarse grids miss optima, fine grids explode
---
## Tips
### 1. Start Coarse
Begin with 35 values per parameter. Refine after identifying good regions.
### 2. Use Log Scales
For parameters spanning orders of magnitude (learning rates, regularization):
```python
lrs = np.logspace(-4, -1, 10) # 1e-4 to 0.1
```
### 3. Limit Dimensions
Keep $D \leq 4$ for full grid search. Beyond that, use:
- Random search (same budget, better coverage)
- Successive halving
- Bayesian optimization
### 4. Cache Expensive Computations
If objective shares preprocessing:
```python
# Pre-compute shared data
X_train, y_train = load_and_preprocess()
def objective(params):
# Reuse X_train, y_train
return train_model(X_train, y_train, **params)
```
### 5. Save All Results
Store every evaluation for analysis:
```python
all_results = []
def logging_objective(params):
score = actual_objective(params)
all_results.append({"params": params, "score": score})
return score
grid_search(logging_objective, param_grid)
# Analyze all results afterward
import pandas as pd
df = pd.DataFrame(all_results)
print(df.sort_values("score").head(10))
```
---
## Related Topics
- [Differential Evolution](differential_evolution.md) Adaptive global optimization
- [MCMC](mcmc.md) Sampling-based exploration with uncertainty
- [HMM](hmm.md) Model selection with grid search over states
+624
View File
@@ -0,0 +1,624 @@
# Hidden Markov Models
Hidden Markov Models (HMMs) are probabilistic models for sequential data where observations
are generated by a system that transitions between **hidden (latent) states**. Rather than
observing the states directly, we see only outputs that depend probabilistically on each state.
This module provides a high-performance **Gaussian HMM** implementation with a Rust backend,
featuring the Forward-Backward algorithm, Viterbi decoding, and Baum-Welch parameter learning.
---
## Mathematical Foundations
### Model Definition
A Hidden Markov Model $\lambda$ is defined by three components:
#### 1. States
- **Number of states:** $N$
- **State at time $t$:** $q_t \in \{1, 2, \ldots, N\}$
- **State sequence:** $Q = q_1, q_2, \ldots, q_T$
#### 2. Observations
- **Observation at time $t$:** $o_t \in \mathbb{R}$ (continuous)
- **Observation sequence:** $O = o_1, o_2, \ldots, o_T$
#### 3. Parameters
**Initial State Distribution:**
$$
\pi_i = P(q_1 = i), \quad 1 \leq i \leq N
$$
$$
\sum_{i=1}^N \pi_i = 1
$$
**State Transition Matrix:**
$$
a_{ij} = P(q_{t+1} = j \mid q_t = i), \quad 1 \leq i,j \leq N
$$
$$
\sum_{j=1}^N a_{ij} = 1 \quad \text{for all } i
$$
**Emission Distribution** (Gaussian):
$$
b_j(o_t) = P(o_t \mid q_t = j) = \mathcal{N}(o_t; \mu_j, \sigma_j^2)
$$
$$
b_j(o_t) = \frac{1}{\sigma_j\sqrt{2\pi}} \exp\left(-\frac{(o_t - \mu_j)^2}{2\sigma_j^2}\right)
$$
**Complete model:** $\lambda = (\boldsymbol{\pi}, \mathbf{A}, \mathbf{B})$
---
### The Markov Property
#### First-Order Markov Assumption
The future state depends only on the current state, not the history:
$$
P(q_{t+1} \mid q_1, q_2, \ldots, q_t) = P(q_{t+1} \mid q_t)
$$
#### Output Independence
Observations are conditionally independent given the state:
$$
P(o_t \mid q_1, \ldots, q_T, o_1, \ldots, o_{t-1}, o_{t+1}, \ldots, o_T) = P(o_t \mid q_t)
$$
---
## The Three Fundamental Problems
HMMs are used to solve three fundamental problems:
| Problem | Given | Find | Solution |
|---------|-------|------|----------|
| **Evaluation** | Model $\lambda$, observations $O$ | $P(O \mid \lambda)$ | Forward Algorithm |
| **Decoding** | Model $\lambda$, observations $O$ | Most likely state sequence $Q^*$ | Viterbi Algorithm |
| **Learning** | Observations $O$ | Optimal parameters $\lambda^*$ | Baum-Welch Algorithm |
---
## Forward Algorithm
Computes $P(O \mid \lambda)$ efficiently using dynamic programming.
### Forward Variable
$$
\alpha_t(i) = P(o_1, o_2, \ldots, o_t, q_t = i \mid \lambda)
$$
The probability of observing the first $t$ observations AND being in state $i$ at time $t$.
### Algorithm
**Initialization** ($t = 1$):
$$
\alpha_1(i) = \pi_i \cdot b_i(o_1), \quad 1 \leq i \leq N
$$
**Recursion** ($1 \leq t < T$):
$$
\alpha_{t+1}(j) = \left[\sum_{i=1}^N \alpha_t(i) \cdot a_{ij}\right] \cdot b_j(o_{t+1})
$$
**Termination:**
$$
P(O \mid \lambda) = \sum_{i=1}^N \alpha_T(i)
$$
### Complexity
| Metric | Value |
|--------|-------|
| Time | $O(N^2 T)$ |
| Space | $O(N T)$ |
| Without DP | $O(N^T)$ — exponential! |
---
## Backward Algorithm
Alternative computation for completeness and use in Baum-Welch.
### Backward Variable
$$
\beta_t(i) = P(o_{t+1}, o_{t+2}, \ldots, o_T \mid q_t = i, \lambda)
$$
**Initialization** ($t = T$):
$$
\beta_T(i) = 1, \quad 1 \leq i \leq N
$$
**Recursion** ($t = T-1, T-2, \ldots, 1$):
$$
\beta_t(i) = \sum_{j=1}^N a_{ij} \cdot b_j(o_{t+1}) \cdot \beta_{t+1}(j)
$$
---
## Viterbi Algorithm
Finds the single **most likely state sequence** given observations.
### Objective
$$
Q^* = \arg\max_Q P(Q \mid O, \lambda) = \arg\max_Q P(Q, O \mid \lambda)
$$
### Viterbi Variable
$$
\delta_t(i) = \max_{q_1, \ldots, q_{t-1}} P(q_1, \ldots, q_{t-1}, q_t = i, o_1, \ldots, o_t \mid \lambda)
$$
The maximum probability of any path ending in state $i$ at time $t$.
### Algorithm
**Initialization** ($t = 1$):
$$
\delta_1(i) = \pi_i \cdot b_i(o_1)
$$
$$
\psi_1(i) = 0
$$
**Recursion** ($2 \leq t \leq T$):
$$
\delta_t(j) = \max_{1 \leq i \leq N} \left[\delta_{t-1}(i) \cdot a_{ij}\right] \cdot b_j(o_t)
$$
$$
\psi_t(j) = \arg\max_{1 \leq i \leq N} \left[\delta_{t-1}(i) \cdot a_{ij}\right]
$$
**Termination:**
$$
P^* = \max_{1 \leq i \leq N} \delta_T(i)
$$
$$
q_T^* = \arg\max_{1 \leq i \leq N} \delta_T(i)
$$
**Backtracking** ($t = T-1, T-2, \ldots, 1$):
$$
q_t^* = \psi_{t+1}(q_{t+1}^*)
$$
---
## Baum-Welch Algorithm
An **Expectation-Maximization (EM)** algorithm for learning HMM parameters from data.
### Auxiliary Variables
**State occupation probability:**
$$
\gamma_t(i) = P(q_t = i \mid O, \lambda) = \frac{\alpha_t(i) \cdot \beta_t(i)}{\sum_{j=1}^N \alpha_t(j) \cdot \beta_t(j)}
$$
**Transition probability:**
$$
\xi_t(i,j) = P(q_t = i, q_{t+1} = j \mid O, \lambda)
$$
$$
= \frac{\alpha_t(i) \cdot a_{ij} \cdot b_j(o_{t+1}) \cdot \beta_{t+1}(j)}{\sum_{i=1}^N \sum_{j=1}^N \alpha_t(i) \cdot a_{ij} \cdot b_j(o_{t+1}) \cdot \beta_{t+1}(j)}
$$
### E-Step
Compute $\gamma_t(i)$ and $\xi_t(i,j)$ for all $t$, $i$, $j$ using current parameters.
### M-Step
Update parameters to maximize expected log-likelihood:
**Initial state probabilities:**
$$
\bar{\pi}_i = \gamma_1(i)
$$
**Transition probabilities:**
$$
\bar{a}_{ij} = \frac{\sum_{t=1}^{T-1} \xi_t(i,j)}{\sum_{t=1}^{T-1} \gamma_t(i)}
$$
**Emission parameters (Gaussian):**
$$
\bar{\mu}_j = \frac{\sum_{t=1}^T \gamma_t(j) \cdot o_t}{\sum_{t=1}^T \gamma_t(j)}
$$
$$
\bar{\sigma}_j^2 = \frac{\sum_{t=1}^T \gamma_t(j) \cdot (o_t - \bar{\mu}_j)^2}{\sum_{t=1}^T \gamma_t(j)}
$$
### Convergence
Iterate E-step and M-step until:
$$
|L(\lambda^{(k+1)}) - L(\lambda^{(k)})| < \epsilon
$$
where $L(\lambda) = \log P(O \mid \lambda)$ is the log-likelihood.
**Properties:**
- Guaranteed to converge to a **local maximum**
- May converge to different solutions depending on initialization
- Multiple random restarts recommended
---
## Numerical Stability
### Scaling
Raw probabilities underflow for long sequences. Use **scaling factors**:
$$
c_t = \frac{1}{\sum_{i=1}^N \alpha_t(i)}
$$
Scaled forward variables:
$$
\hat{\alpha}_t(i) = c_t \cdot \alpha_t(i)
$$
### Log-Space Computation
For Viterbi, work in log-space:
$$
\log \delta_t(j) = \max_{1 \leq i \leq N} \left[\log \delta_{t-1}(i) + \log a_{ij}\right] + \log b_j(o_t)
$$
Use log-sum-exp for stable additions:
$$
\log(e^a + e^b) = \max(a,b) + \log(1 + e^{-|a-b|})
$$
---
## Model Selection
### Number of States
Choose the number of states $N$ using information criteria:
| Criterion | Formula | Description |
|-----------|---------|-------------|
| **AIC** | $-2\log L + 2k$ | Akaike Information Criterion |
| **BIC** | $-2\log L + k\log n$ | Bayesian Information Criterion |
where $k$ is the number of parameters and $n$ is the sample size.
**Lower values indicate better models** (penalized for complexity).
---
## Python API
### Basic Usage
```python
import numpy as np
from optimizr import HMM
# Generate synthetic regime data
np.random.seed(42)
returns = np.concatenate([
np.random.normal(0.01, 0.02, 500), # Bull regime
np.random.normal(-0.015, 0.03, 500), # Bear regime
])
# Fit a 2-state HMM
model = HMM(n_states=2)
model.fit(returns, n_iterations=100)
# Decode the most likely state sequence
states = model.predict(returns)
print("State counts:", np.unique(states, return_counts=True))
```
**Expected output:**
```
State counts: (array([0, 1]), array([498, 502]))
```
### Model Inspection
```python
# Examine learned parameters
print("Initial distribution:", model.pi)
print("Transition matrix:\n", model.A)
print("Emission means:", model.means)
print("Emission stds:", model.stds)
# Compute log-likelihood
ll = model.score(returns)
print(f"Log-likelihood: {ll:.2f}")
```
**Expected output:**
```
Initial distribution: [0.95 0.05]
Transition matrix:
[[0.992 0.008]
[0.010 0.990]]
Emission means: [ 0.0098 -0.0152]
Emission stds: [0.0198 0.0301]
Log-likelihood: 2847.31
```
### Feature Engineering for HMM
```python
from optimizr import prepare_for_hmm_py
# Prepare features from price data with rolling statistics
features = prepare_for_hmm_py(
prices,
lag_periods=[1, 5, 20], # multi-scale lookback
)
# Fit HMM with richer features
hmm = HMM(n_states=3).fit(features, n_iterations=120)
```
Use rolling statistics from `timeseries_utils` as additional features for richer
regime classification:
```python
from optimizr import rolling_hurst, rolling_halflife
hurst = rolling_hurst(prices, window=50)
halflife = rolling_halflife(prices, window=50)
features = np.column_stack([returns, hurst, halflife])
hmm = HMM(n_states=3).fit(features, n_iterations=100)
```
---
## Visualization
### State Sequence Plot
```python
import matplotlib.pyplot as plt
fig, axes = plt.subplots(2, 1, figsize=(12, 6), sharex=True)
# Price/returns
axes[0].plot(returns, alpha=0.7)
axes[0].set_ylabel('Returns')
axes[0].set_title('Returns Time Series')
# State sequence
axes[1].plot(states, drawstyle='steps-post', color='orange')
axes[1].set_ylabel('State')
axes[1].set_xlabel('Time')
axes[1].set_title('Decoded State Sequence')
axes[1].set_yticks([0, 1])
axes[1].set_yticklabels(['Bull', 'Bear'])
plt.tight_layout()
plt.savefig('hmm_states.png', dpi=150)
```
**Output:**
This visualization creates a two-panel plot:
- **Top panel**: Returns time series showing price movements
- **Bottom panel**: Decoded state sequence highlighting regime transitions (e.g., Bull/Bear markets)
For complete examples with regime detection on real market data, see the [HMM Tutorial notebook](https://github.com/ThotDjehuty/optimiz-r/blob/main/examples/notebooks/01_hmm_tutorial.ipynb).
### Transition Diagram
```{mermaid}
stateDiagram-v2
direction LR
[*] --> Normal
Bull : 📈 Bull Market
Normal : 📊 Normal Regime
Bear : 📉 Bear Market
Bull --> Bull : a₁₁ (self)
Bull --> Normal : a₁₂
Normal --> Bull : a₂₁
Normal --> Normal : a₂₂ (self)
Normal --> Bear : a₂₃
Bear --> Normal : a₃₂
Bear --> Bear : a₃₃ (self)
```
**Code example** — build and visualize the transition graph programmatically:
```python
import networkx as nx
G = nx.DiGraph()
for i in range(model.n_states):
for j in range(model.n_states):
if model.A[i, j] > 0.01: # threshold small probabilities
G.add_edge(f"State {i}", f"State {j}", weight=model.A[i, j])
pos = nx.spring_layout(G)
edge_labels = {(u, v): f"{d['weight']:.2f}" for u, v, d in G.edges(data=True)}
plt.figure(figsize=(8, 6))
nx.draw(G, pos, with_labels=True, node_size=2000, node_color='lightblue')
nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels)
plt.title('HMM Transition Diagram')
plt.savefig('hmm_transitions.png', dpi=150)
```
**Output:**
Generates a directed graph visualization showing:
- **Nodes**: Hidden states (e.g., State 0, State 1)
- **Edges**: Transition probabilities between states (labeled with probability values)
- Only transitions with probability > 0.01 are shown for clarity
📓 **Full Tutorial**: Explore [01_hmm_tutorial.ipynb](https://github.com/ThotDjehuty/optimiz-r/blob/main/examples/notebooks/01_hmm_tutorial.ipynb) for hands-on examples including:
- Multi-state regime detection
- Volatility clustering analysis
- Mean-reversion regime identification
- Viterbi decoding in financial time series
---
## Applications
### 1. Financial Regime Detection
HMMs identify market regimes (bull/bear/sideways) from returns:
```python
from optimizr import HMM
# Daily returns
hmm = HMM(n_states=3) # bull, bear, high-volatility
hmm.fit(daily_returns)
regimes = hmm.predict(daily_returns)
regime_labels = ['Bull', 'Bear', 'High Vol']
current_regime = regime_labels[regimes[-1]]
print(f"Current market regime: {current_regime}")
```
### 2. Mean Reversion Trading
Use regime as a filter for mean-reversion strategies:
```python
# Only trade mean-reversion when in low-volatility regime
regime = hmm.predict(recent_returns)[-1]
if regime == 0: # low-vol regime
# Execute mean-reversion strategy
pass
else:
# Hold or reduce position
pass
```
### 3. Risk Management
Adjust position sizing based on detected regime:
```python
regime_volatilities = {
0: 0.02, # low vol
1: 0.03, # medium vol
2: 0.05, # high vol
}
current_regime = hmm.predict(returns)[-1]
position_size = base_size * (target_vol / regime_volatilities[current_regime])
```
---
## Performance
Benchmarks on Apple M1 (1000 observations):
| States | Fit Time | Predict Time | Memory |
|--------|----------|--------------|--------|
| 2 | 45 ms | 2 ms | 0.5 MB |
| 3 | 68 ms | 3 ms | 0.8 MB |
| 5 | 124 ms | 6 ms | 1.4 MB |
| 10 | 412 ms | 18 ms | 4.2 MB |
**Complexity:** $O(N^2 T)$ for both forward-backward and Viterbi.
---
## Troubleshooting
| Symptom | Cause | Fix |
|---------|-------|-----|
| States not separating | Too few iterations | Increase `n_iterations` to 150200 |
| Converges to bad solution | Local optimum | Run multiple restarts with different seeds |
| Underflow errors | Long sequences | Enable log-space computation (automatic in Rust backend) |
| All observations in one state | Poor initialization | Try more states or normalize input data |
---
## Tips
- **Normalize inputs:** Scale features to similar ranges for better learning.
- **Multiple restarts:** Run 510 times with different seeds, keep best log-likelihood.
- **Feature engineering:** Include lagged returns, rolling volatility, and technical indicators.
- **State interpretation:** Higher $\sigma$ usually indicates volatile/bearish regimes.
---
## Implementation Notes
- Uses Rust backend when available; falls back to Python if unavailable.
- `fit` runs Baum-Welch; `predict` runs Viterbi.
- Call `score(X)` to compute log-likelihood for model comparison.
- Scaling is applied automatically to prevent numerical underflow.
---
## References
1. Rabiner, L.R. (1989). "A tutorial on hidden Markov models and selected applications in speech recognition." *Proceedings of the IEEE*, 77(2):257286.
2. Baum, L.E. & Petrie, T. (1966). "Statistical inference for probabilistic functions of finite state Markov chains." *Annals of Mathematical Statistics*, 37(6):15541563.
3. Viterbi, A. (1967). "Error bounds for convolutional codes and an asymptotically optimum decoding algorithm." *IEEE Trans. Info. Theory*, 13(2):260269.
4. Hamilton, J.D. (1989). "A new approach to the economic analysis of nonstationary time series and the business cycle." *Econometrica*, 57(2):357384.
---
## Related Topics
- [MCMC](mcmc.md) Alternative inference for more complex latent variable models
- [Differential Evolution](differential_evolution.md) Global optimization for HMM initialization
- [Mean Field Games](mean_field_games.md) Population dynamics with strategic interactions
+60
View File
@@ -0,0 +1,60 @@
Matrix Riccati Solver
=====================
The module :code:`optimal_control::matrix_riccati` integrates backward in time
the matrix Riccati differential equation
.. math::
\frac{dA(t)}{dt} = -2\,A(t)\,M\,A(t) + Q,
\qquad A(T) = A_T,
together with the affine and constant components
.. math::
\frac{dB(t)}{dt} = -2\,A(t)\,M\,B(t),
\qquad B(T) = B_T,
.. math::
\frac{dC(t)}{dt} = -B(t)^\top\,M\,B(t),
\qquad C(T) = C_T.
Discretisation
--------------
The grid :math:`\{t_n = T - n\,\Delta t\}_{n=0}^{N}` with
:math:`\Delta t = T / N` is traversed backward and a classical RK4 step is
applied to the joint vector field :math:`(A, B, C)`. Each macro step is
optionally subdivided into :math:`s` sub-steps for stability on stiff
problems.
Validation
----------
In the scalar case :math:`A, M, Q \in \mathbb{R}` with :math:`A(T) = 0`,
.. math::
A(t) \;=\; -\sqrt{\frac{Q}{2M}}\;\tanh\!\Big(\sqrt{2QM}\,(T - t)\Big),
a closed form used by the unit test :code:`scalar_riccati_matches_analytic`
to certify :math:`L^\infty` convergence below :math:`10^{-5}` on
:math:`[0, T]`.
API
---
.. code-block:: rust
pub fn solve_matrix_riccati(
m_matrix: ArrayView2<f64>,
q: ArrayView2<f64>,
n: ArrayView2<f64>,
a_terminal: ArrayView2<f64>,
b_terminal: ArrayView1<f64>,
c_terminal: f64,
t_horizon: f64,
config: RiccatiConfig,
) -> Result<RiccatiResult>;
+578
View File
@@ -0,0 +1,578 @@
# MCMC Sampling
**Markov Chain Monte Carlo (MCMC)** methods are a class of algorithms for sampling from
probability distributions by constructing a Markov chain whose stationary distribution
equals the target distribution. MCMC is fundamental to Bayesian inference, computational
statistics, and quantitative finance.
This module provides a high-performance **Metropolis-Hastings sampler** with Rust
acceleration, designed for Bayesian parameter estimation and posterior exploration.
---
## Mathematical Foundations
### The Monte Carlo Goal
Sample from a target distribution $\pi(\theta)$ where:
- Direct sampling is difficult or impossible
- We can evaluate $\pi(\theta)$ **up to a normalization constant**
Given samples $\theta^{(1)}, \ldots, \theta^{(N)} \sim \pi(\theta)$, we approximate:
**Expectations:**
$$
\mathbb{E}_\pi[f(\theta)] \approx \frac{1}{N}\sum_{i=1}^N f(\theta^{(i)})
$$
**Probabilities:**
$$
P(\theta \in A) \approx \frac{1}{N}\sum_{i=1}^N \mathbb{1}[\theta^{(i)} \in A]
$$
**Quantiles**, **posterior intervals**, and other distributional properties.
---
### Markov Chains
A sequence $\theta^{(0)}, \theta^{(1)}, \theta^{(2)}, \ldots$ is a **Markov chain** if:
$$
P(\theta^{(t+1)} \mid \theta^{(0)}, \ldots, \theta^{(t)}) = P(\theta^{(t+1)} \mid \theta^{(t)})
$$
The next state depends only on the current state.
### Transition Kernel
$$
K(\theta' \mid \theta) = P(\theta^{(t+1)} = \theta' \mid \theta^{(t)} = \theta)
$$
### Stationary Distribution
A distribution $\pi(\theta)$ is **stationary** if:
$$
\pi(\theta') = \int K(\theta' \mid \theta) \, \pi(\theta) \, d\theta
$$
If we start with $\theta^{(0)} \sim \pi$, then $\theta^{(t)} \sim \pi$ for all $t$.
### Ergodicity
A Markov chain is **ergodic** if:
1. **Irreducible:** Can reach any state from any state
2. **Aperiodic:** No cyclic behavior
For ergodic chains with stationary distribution $\pi$:
$$
\lim_{t \to \infty} P(\theta^{(t)} \in A) = \pi(A)
$$
regardless of initial state $\theta^{(0)}$.
### Detailed Balance
A sufficient condition for $\pi$ to be stationary:
$$
\pi(\theta) \, K(\theta' \mid \theta) = \pi(\theta') \, K(\theta \mid \theta')
$$
**Reversibility:** The probability of going $\theta \to \theta'$ equals that of $\theta' \to \theta$.
---
## Metropolis-Hastings Algorithm
The MH algorithm constructs a Markov chain whose stationary distribution is the target $\pi(\theta)$.
### Algorithm
**Input:** Target distribution $\pi(\theta)$, proposal distribution $q(\theta' \mid \theta)$
```
Algorithm: Metropolis-Hastings
──────────────────────────────
1. Initialize θ⁽⁰⁾
2. For t = 0, 1, 2, ..., N-1:
a. Propose: Draw θ* ~ q(θ* | θ⁽ᵗ⁾)
b. Compute acceptance probability:
α = min(1, [π(θ*) · q(θ⁽ᵗ⁾|θ*)] / [π(θ⁽ᵗ⁾) · q(θ*|θ⁽ᵗ⁾)])
c. Accept or reject:
u ~ Uniform(0, 1)
if u < α:
θ⁽ᵗ⁺¹⁾ = θ* # accept
else:
θ⁽ᵗ⁺¹⁾ = θ⁽ᵗ⁾ # reject
3. Return samples {θ⁽¹⁾, θ⁽²⁾, ..., θ⁽ᴺ⁾}
```
### Acceptance Probability
$$
\alpha = \min\left(1, \frac{\pi(\theta^*) \, q(\theta^{(t)} \mid \theta^*)}{\pi(\theta^{(t)}) \, q(\theta^* \mid \theta^{(t)})}\right)
$$
The ratio $\pi(\theta^*)/\pi(\theta^{(t)})$ compares likelihoods. The ratio
$q(\theta^{(t)} \mid \theta^*)/q(\theta^* \mid \theta^{(t)})$ corrects for asymmetric proposals.
### Why It Works
**Theorem:** The MH algorithm produces a Markov chain with stationary distribution $\pi(\theta)$.
The acceptance rule ensures **detailed balance** holds, guaranteeing convergence to $\pi$.
---
## Special Cases
### Metropolis Algorithm (Symmetric Proposal)
When the proposal is **symmetric:** $q(\theta' \mid \theta) = q(\theta \mid \theta')$
Acceptance probability simplifies to:
$$
\alpha = \min\left(1, \frac{\pi(\theta^*)}{\pi(\theta^{(t)})}\right)
$$
Always accept moves to higher probability; sometimes accept moves to lower probability.
### Random Walk Metropolis
Use a Gaussian proposal centered at the current state:
$$
q(\theta' \mid \theta) = \mathcal{N}(\theta' \mid \theta, \sigma^2 \mathbf{I})
$$
This is symmetric, so Metropolis acceptance applies.
**This is what Optimiz-rs implements.**
---
## Bayesian Inference with MCMC
### Bayes' Theorem
$$
p(\theta \mid D) = \frac{p(D \mid \theta) \, p(\theta)}{p(D)}
$$
| Term | Name | Description |
|------|------|-------------|
| $p(\theta \mid D)$ | Posterior | What we want |
| $p(D \mid \theta)$ | Likelihood | How well parameters explain data |
| $p(\theta)$ | Prior | Beliefs before seeing data |
| $p(D)$ | Evidence | Normalizing constant (often intractable) |
### MCMC for Posterior Sampling
The evidence $p(D)$ is often intractable, but we can evaluate:
$$
\pi(\theta) \propto p(D \mid \theta) \cdot p(\theta)
$$
MCMC only needs $\pi$ **up to a constant**, so we can sample from the posterior!
### Log-Posterior
In practice, work with log-probabilities to avoid underflow:
$$
\log \pi(\theta) = \log p(D \mid \theta) + \log p(\theta) + \text{const}
$$
---
## Python API
### Basic Usage
```python
import numpy as np
from optimizr import mcmc_sample
# Define log-likelihood for a Gaussian model
def log_likelihood(params, data):
mu, sigma = params
if sigma <= 0:
return -np.inf # invalid parameter
residuals = (data - mu) / sigma
return -0.5 * np.sum(residuals**2) - len(data) * np.log(sigma)
# Generate synthetic data: N(1.2, 1.0)
np.random.seed(42)
observations = np.random.randn(1000) + 1.2
# Run MCMC sampling
samples = mcmc_sample(
log_likelihood_fn=log_likelihood,
data=observations,
initial_params=np.array([0.0, 1.0]),
param_bounds=[(-5, 5), (0.1, 5.0)],
n_samples=8000,
burn_in=500,
proposal_std=0.2,
)
print("Posterior mean:", samples.mean(axis=0))
print("Posterior std:", samples.std(axis=0))
```
**Expected output:**
```
Posterior mean: [1.198 0.987]
Posterior std: [0.032 0.022]
```
The true values (1.2, 1.0) are recovered within posterior uncertainty.
### Configuration Options
```python
samples = mcmc_sample(
log_likelihood_fn=log_likelihood,
data=observations,
initial_params=np.array([0.0, 1.0]),
param_bounds=[(-5, 5), (0.1, 5.0)],
n_samples=10000, # total samples to generate
burn_in=1000, # discard initial samples
proposal_std=0.15, # step size for random walk
thin=2, # keep every 2nd sample
seed=42, # for reproducibility
)
```
### Posterior Analysis
```python
import matplotlib.pyplot as plt
# Trace plots
fig, axes = plt.subplots(2, 2, figsize=(12, 8))
# Mu trace
axes[0, 0].plot(samples[:, 0], alpha=0.7)
axes[0, 0].set_ylabel('μ')
axes[0, 0].set_title('Trace: μ')
axes[0, 0].axhline(1.2, color='r', linestyle='--', label='True')
# Sigma trace
axes[0, 1].plot(samples[:, 1], alpha=0.7)
axes[0, 1].set_ylabel('σ')
axes[0, 1].set_title('Trace: σ')
axes[0, 1].axhline(1.0, color='r', linestyle='--', label='True')
# Mu histogram
axes[1, 0].hist(samples[:, 0], bins=50, density=True, alpha=0.7)
axes[1, 0].axvline(1.2, color='r', linestyle='--', label='True')
axes[1, 0].set_xlabel('μ')
axes[1, 0].set_title('Posterior: μ')
# Sigma histogram
axes[1, 1].hist(samples[:, 1], bins=50, density=True, alpha=0.7)
axes[1, 1].axvline(1.0, color='r', linestyle='--', label='True')
axes[1, 1].set_xlabel('σ')
axes[1, 1].set_title('Posterior: σ')
plt.tight_layout()
plt.savefig('mcmc_posterior.png', dpi=150)
```
---
## Convergence Diagnostics
### Burn-in Period
Discard initial samples before the chain has converged to the stationary distribution.
**How to choose:**
- Plot trace plots and look for stabilization
- Typically 100010000 iterations
- Conservative: discard first 50% of samples
### Effective Sample Size (ESS)
Due to autocorrelation, MCMC samples are not independent:
$$
\text{ESS} = \frac{N}{1 + 2\sum_{k=1}^\infty \rho_k}
$$
where $\rho_k$ is the autocorrelation at lag $k$.
**Interpretation:** ESS ≈ number of independent samples.
**Goal:** ESS > 400 for reliable posterior estimates.
### Autocorrelation
$$
\rho_k = \frac{\text{Cov}(\theta^{(t)}, \theta^{(t+k)})}{\text{Var}(\theta^{(t)})}
$$
| Autocorrelation | Interpretation |
|-----------------|----------------|
| Low (< 0.1) | Fast mixing, efficient sampling |
| High (> 0.5) | Slow mixing, need more samples or better tuning |
### Gelman-Rubin Diagnostic ($\hat{R}$)
Run multiple chains with different starting points:
$$
\hat{R} = \sqrt{\frac{\text{Var}^+}{\text{Within-chain variance}}}
$$
| $\hat{R}$ Value | Interpretation |
|-----------------|----------------|
| ≈ 1.0 | Chains have converged |
| > 1.1 | Chains have NOT mixed — run longer |
---
## Proposal Tuning
### Acceptance Rate
**Optimal acceptance rate** (for random walk Metropolis):
| Dimension | Optimal Rate |
|-----------|--------------|
| 1D | 44% |
| High-D | 23.4% |
| Practical | 2040% |
**Tuning guidance:**
| Acceptance Rate | Problem | Fix |
|-----------------|---------|-----|
| Too high (> 50%) | Proposals too small | Increase `proposal_std` |
| Too low (< 10%) | Proposals too large | Decrease `proposal_std` |
### Adaptive Tuning
During burn-in, automatically adjust proposal variance:
```python
# Start with initial guess, let Rust backend tune
samples = mcmc_sample(
log_likelihood_fn=log_likelihood,
data=observations,
initial_params=initial,
param_bounds=bounds,
n_samples=10000,
burn_in=2000, # longer burn-in for adaptation
proposal_std=0.5, # initial value, will be adjusted
adaptive=True, # enable adaptive tuning
)
```
### Optimal Scaling
Roberts and Rosenthal (2001): For Gaussian targets in $d$ dimensions:
$$
\sigma^2_{\text{optimal}} = \frac{2.38^2}{d} \cdot \Sigma
$$
where $\Sigma$ is the posterior covariance.
---
## Applications
### 1. Bayesian Regression
```python
import numpy as np
from optimizr import mcmc_sample
def log_posterior(params, data):
X, y = data
beta = params[:-1]
sigma = params[-1]
if sigma <= 0:
return -np.inf
# Likelihood
y_pred = X @ beta
residuals = (y - y_pred) / sigma
ll = -0.5 * np.sum(residuals**2) - len(y) * np.log(sigma)
# Prior: N(0, 10) for beta, InvGamma for sigma
log_prior = -0.5 * np.sum(beta**2) / 100
return ll + log_prior
# Fit Bayesian linear regression
X = np.column_stack([np.ones(100), np.random.randn(100)])
y = 2 + 3 * X[:, 1] + np.random.randn(100) * 0.5
samples = mcmc_sample(
log_likelihood_fn=log_posterior,
data=(X, y),
initial_params=np.array([0.0, 0.0, 1.0]),
param_bounds=[(-10, 10), (-10, 10), (0.01, 5)],
n_samples=5000,
burn_in=500,
)
print("Intercept:", samples[:, 0].mean(), "±", samples[:, 0].std())
print("Slope:", samples[:, 1].mean(), "±", samples[:, 1].std())
print("Sigma:", samples[:, 2].mean(), "±", samples[:, 2].std())
```
### 2. Stochastic Volatility
```python
def log_posterior_sv(params, returns):
mu, phi, sigma_v = params
if not (0 < phi < 1) or sigma_v <= 0:
return -np.inf
# Autoregressive volatility model
T = len(returns)
log_var = np.zeros(T)
log_var[0] = mu / (1 - phi)
for t in range(1, T):
log_var[t] = mu + phi * (log_var[t-1] - mu)
# Likelihood
ll = -0.5 * np.sum(returns**2 / np.exp(log_var) + log_var)
return ll
samples = mcmc_sample(
log_likelihood_fn=log_posterior_sv,
data=daily_returns,
initial_params=np.array([-1.0, 0.9, 0.2]),
param_bounds=[(-5, 0), (0.01, 0.99), (0.01, 1.0)],
n_samples=10000,
burn_in=2000,
)
```
### 3. Portfolio Optimization with Uncertainty
```python
# Sample from posterior of expected returns
posterior_means = samples[:, :n_assets]
# For each posterior sample, compute optimal weights
optimal_weights = []
for mu_sample in posterior_means[::10]: # thin for speed
w = optimize_portfolio(mu_sample, cov_matrix)
optimal_weights.append(w)
# Report posterior distribution of weights
weights_mean = np.mean(optimal_weights, axis=0)
weights_std = np.std(optimal_weights, axis=0)
```
---
## Performance
Benchmarks on Apple M1:
| Parameters | Samples | Time | Samples/sec |
|------------|---------|------|-------------|
| 2 | 10,000 | 0.8 s | 12,500 |
| 5 | 10,000 | 1.2 s | 8,333 |
| 10 | 10,000 | 2.1 s | 4,762 |
| 20 | 10,000 | 4.8 s | 2,083 |
Performance scales approximately linearly with the number of parameters.
---
## Troubleshooting
| Symptom | Cause | Fix |
|---------|-------|-----|
| Acceptance rate ~0% | `proposal_std` too large | Decrease by 50% |
| Acceptance rate ~100% | `proposal_std` too small | Increase by 50100% |
| Chains stuck | Local mode | Use multiple chains, different starts |
| Poor mixing | Strong correlations | Reparameterize or increase samples |
| `log_likelihood` returns `-inf` | Invalid parameters | Check bounds, add guards |
---
## Tips
### 1. Keep `proposal_std` Modest
Start with 0.10.5 of the expected posterior standard deviation. Adjust to
achieve 2040% acceptance rate.
### 2. Use Adequate Burn-in
`burn_in` should be at least 510% of total samples for stable chains.
### 3. Provide Tight Bounds
Specify `param_bounds` to avoid exploring invalid regions (negative variances, etc.).
### 4. Monitor Convergence
Always check trace plots and autocorrelation before using posterior samples.
### 5. Multiple Chains
Run 24 chains from different starting points. Compare posteriors and compute $\hat{R}$.
---
## MCMC vs. Alternatives
| Method | Pros | Cons |
|--------|------|------|
| **MCMC** | General, exact (asymptotically) | Slow convergence, diagnostics needed |
| Variational Inference | Fast, scalable | Approximate, may be biased |
| Importance Sampling | Simple, independent samples | Requires good proposal |
| Grid/Quadrature | Deterministic | Exponential in dimension |
---
## References
1. Metropolis, N. et al. (1953). "Equation of state calculations by fast computing machines." *J. Chem. Phys.*, 21(6):10871092.
2. Hastings, W.K. (1970). "Monte Carlo sampling methods using Markov chains and their applications." *Biometrika*, 57(1):97109.
3. Gelfand, A.E. & Smith, A.F.M. (1990). "Sampling-based approaches to calculating marginal densities." *JASA*, 85(410):398409.
4. Roberts, G.O. & Rosenthal, J.S. (2001). "Optimal scaling for various Metropolis-Hastings algorithms." *Statistical Science*, 16(4):351367.
5. Brooks, S. et al. (2011). *Handbook of Markov Chain Monte Carlo*. CRC Press.
---
## Related Topics
- [HMM](hmm.md) Sequential latent variable models with EM learning
- [Differential Evolution](differential_evolution.md) Global optimization for finding MAP estimates
- [Mean Field Games](mean_field_games.md) Population dynamics with coupled PDEs
+378
View File
@@ -0,0 +1,378 @@
# Mean Field Games
Mean Field Games (MFG) provide a powerful framework for modeling strategic interactions
among a large number of rational agents. Rather than tracking every individual, MFG theory
replaces the population with a *distribution* and derives equilibrium conditions from
coupled partial differential equations.
This module implements a **1D Mean Field Games solver** with a high-performance Rust
backend exposed to Python via PyO3.
---
## Mathematical Foundations
### The State of a Representative Agent
Each agent's state $X_t$ evolves according to a controlled stochastic differential equation:
$$
dX_t = b(X_t, \alpha_t, m_t)\,dt + \sigma\,dW_t
$$
where:
- $\alpha_t$ is the agent's control (decision variable)
- $m_t$ is the population distribution at time $t$
- $W_t$ is standard Brownian motion
- $\sigma$ controls the diffusion intensity (related to `nu` in the solver)
The agent seeks to minimize expected cumulative cost:
$$
J(\alpha) = \mathbb{E}\left[\int_0^T L(X_t, \alpha_t, m_t)\,dt + g(X_T)\right]
$$
---
### The MFG System: Two Coupled PDEs
The MFG equilibrium is characterized by **two coupled PDEs**:
#### 1. Hamilton-Jacobi-Bellman (HJB) Equation — Backward in Time
The value function $u(x,t)$ represents the optimal cost-to-go and satisfies:
$$
-\frac{\partial u}{\partial t} - \nu \frac{\partial^2 u}{\partial x^2} + H\left(x, \frac{\partial u}{\partial x}, m\right) = 0
$$
**Terminal condition:** $u(x, T) = g(x)$ (terminal cost)
The Hamiltonian $H$ captures the running cost. For quadratic control costs:
$$
H(x, p, m) = \frac{|p|^2}{2} - f(x, m)
$$
where $f(x, m)$ is the congestion cost (penalizes crowded regions).
#### 2. Fokker-Planck (FP) Equation — Forward in Time
The population density $m(x,t)$ evolves according to:
$$
\frac{\partial m}{\partial t} - \nu \frac{\partial^2 m}{\partial x^2} - \frac{\partial}{\partial x}\left(m \frac{\partial u}{\partial x}\right) = 0
$$
**Initial condition:** $m(x, 0) = m_0(x)$ (initial population distribution)
This equation propagates the density forward given the optimal velocity field
$v^*(x,t) = -\partial u / \partial x$ from the HJB solution.
---
### The Fixed-Point Loop
The solver uses an iterative scheme to find the coupled equilibrium:
```
Algorithm: MFG Fixed-Point Iteration
─────────────────────────────────────
1. Initialize: m⁽⁰⁾(x,t) = m₀(x) for all t
2. For k = 0, 1, 2, ... until convergence:
a. Solve HJB backward: u⁽ᵏ⁺¹⁾ given m⁽ᵏ⁾
b. Solve FP forward: m̃⁽ᵏ⁺¹⁾ given u⁽ᵏ⁺¹⁾
c. Relax: m⁽ᵏ⁺¹⁾ = α·m̃⁽ᵏ⁺¹⁾ + (1-α)·m⁽ᵏ⁾
d. Check: ||m⁽ᵏ⁺¹⁾ - m⁽ᵏ⁾|| < tol ?
3. Return: (u*, m*, iterations)
```
The relaxation parameter `alpha` (typically 0.30.7) stabilizes convergence by
damping oscillations between iterations.
---
## Numerical Methods
### Discretization
The solver uses a finite-difference scheme on a uniform grid:
| Parameter | Notation | Description |
|-----------|----------|-------------|
| `nx` | $N_x$ | Number of spatial grid points |
| `nt` | $N_t$ | Number of time steps |
| `dx` | $\Delta x = (x_{max} - x_{min}) / (N_x - 1)$ | Spatial step |
| `dt` | $\Delta t = T / N_t$ | Time step |
### Stability: The CFL Condition
For numerical stability, the scheme requires:
$$
\frac{\nu \cdot \Delta t}{(\Delta x)^2} \leq \frac{1}{2}
$$
**Practical rule**: If you see oscillations or blow-up, either:
- Increase `nt` (smaller $\Delta t$)
- Increase `nu` (more diffusion smooths the solution)
- Decrease `nx` (larger $\Delta x$)
### Transport: Upwind Differencing
The advection term $\partial(m \cdot v)/\partial x$ uses **upwind differencing**
to ensure stability:
- If $v > 0$: use backward difference
- If $v < 0$: use forward difference
This prevents numerical oscillations in steep density gradients.
### Diffusion: Implicit Scheme
The diffusion term $\nu \partial^2 m / \partial x^2$ is solved **implicitly**
using a tridiagonal system (Thomas algorithm), making the scheme unconditionally
stable for diffusion.
### Mass Conservation
After each Fokker-Planck step, the density is renormalized:
$$
m^{(k+1)} \leftarrow \frac{m^{(k+1)}}{\int m^{(k+1)} dx}
$$
This ensures $\int m(x,t)\,dx = 1$ is preserved throughout the simulation.
---
## Python API
### Configuration
```python
from optimizr import MFGConfig
config = MFGConfig(
nx=100, # spatial grid points
nt=100, # time steps
x_min=0.0, # left boundary
x_max=1.0, # right boundary
T=1.0, # terminal time
nu=0.01, # diffusion coefficient (viscosity)
max_iter=50, # maximum fixed-point iterations
tol=1e-5, # convergence tolerance
alpha=0.5, # relaxation parameter
)
```
### Solving the MFG System
```python
import numpy as np
from optimizr import MFGConfig, solve_mfg_1d_rust
# Define spatial grid
x = np.linspace(0, 1, 100)
# Initial population: Gaussian centered at x=0.3
m0 = np.exp(-50 * (x - 0.3) ** 2)
m0 /= np.trapz(m0, x) # normalize to unit mass
# Terminal cost: quadratic penalty away from x=0.7
u_terminal = 0.5 * (x - 0.7) ** 2
# Create configuration
config = MFGConfig(
nx=100, nt=100,
x_min=0.0, x_max=1.0, T=1.0,
nu=0.01, max_iter=50, tol=1e-5, alpha=0.5,
)
# Solve the MFG system
u, m, iterations = solve_mfg_1d_rust(m0, u_terminal, config, lambda_congestion=0.5)
print(f"Converged in {iterations} iterations")
print(f"Value function shape: {u.shape}")
print(f"Density shape: {m.shape}")
```
**Expected output:**
```
Converged in 34 iterations
Value function shape: (100, 101)
Density shape: (100, 101)
```
---
## Visualization
### Density Evolution Heatmap
```python
import matplotlib.pyplot as plt
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
# Density heatmap
im0 = axes[0].imshow(m.T, origin='lower', aspect='auto',
extent=[0, 1, 0, 1], cmap='viridis')
axes[0].set_xlabel('Position x')
axes[0].set_ylabel('Time t')
axes[0].set_title('Population Density m(x,t)')
plt.colorbar(im0, ax=axes[0])
# Value function heatmap
im1 = axes[1].imshow(u.T, origin='lower', aspect='auto',
extent=[0, 1, 0, 1], cmap='plasma')
axes[1].set_xlabel('Position x')
axes[1].set_ylabel('Time t')
axes[1].set_title('Value Function u(x,t)')
plt.colorbar(im1, ax=axes[1])
plt.tight_layout()
plt.savefig('mfg_heatmaps.png', dpi=150)
```
**Output:**
This code generates two side-by-side heatmaps:
- **Left plot**: Population density `m(x,t)` evolution over space and time
- **Right plot**: Value function `u(x,t)` showing optimal value at each position and time
For interactive visualization with complete outputs, see the [Mean Field Games Tutorial notebook](https://github.com/ThotDjehuty/optimiz-r/blob/main/examples/notebooks/mean_field_games_tutorial.ipynb) on GitHub.
### Time Slices
```python
t_indices = [0, 25, 50, 75, 100]
colors = plt.cm.viridis(np.linspace(0, 1, len(t_indices)))
plt.figure(figsize=(8, 5))
for i, t_idx in enumerate(t_indices):
t_val = t_idx / 100.0
plt.plot(x, m[:, t_idx], color=colors[i], label=f't={t_val:.2f}')
plt.xlabel('Position x')
plt.ylabel('Density m(x,t)')
plt.title('Population Density at Different Times')
plt.legend()
plt.grid(True, alpha=0.3)
plt.savefig('mfg_time_slices.png', dpi=150)
```
**Output:**
This produces a line plot showing population density profiles at 5 different time points (t=0.0, 0.25, 0.5, 0.75, 1.0), illustrating how the population distribution evolves from initial to terminal conditions.
📓 **Complete Examples**: See the [Mean Field Games Tutorial](https://github.com/ThotDjehuty/optimiz-r/blob/main/examples/notebooks/mean_field_games_tutorial.ipynb) for interactive visualizations with real numerical solutions.
---
## Performance
Benchmarks on laptop-class CPU (Apple M1):
| Grid Size | Iterations | Time |
|-----------|------------|------|
| 64×40 | 28 | 0.08 s |
| 100×100 | 34 | 0.37 s |
| 200×200 | 41 | 2.1 s |
| 500×500 | 52 | 18.4 s |
Memory usage scales as $O(N_x \times N_t)$ for storing both arrays.
---
## Convergence Diagnostics
### What to Monitor
1. **Density residual**: $\|m^{(k+1)} - m^{(k)}\|_1$ should decrease monotonically
2. **Value residual**: $\|u^{(k+1)} - u^{(k)}\|_\infty$ should decrease
3. **Mass conservation**: $\int m(x,t)\,dx \approx 1.0$ at all times
4. **No oscillations**: Smooth density profiles without wiggles
### Troubleshooting
| Symptom | Cause | Fix |
|---------|-------|-----|
| Slow convergence | `alpha` too small | Increase to 0.60.7 |
| Oscillating residuals | `alpha` too large | Decrease to 0.30.4 |
| Numerical blow-up | CFL violation | Increase `nt` or `nu` |
| Density spikes | Weak diffusion | Increase `nu` or `lambda_congestion` |
| Negative densities | Upwind instability | Increase `nu` |
---
## The Congestion Term
The parameter `lambda_congestion` controls crowd aversion:
$$
f(x, m) = \lambda \cdot m(x)^{\gamma}
$$
| `lambda_congestion` | Effect |
|---------------------|--------|
| 0.0 | No interaction; agents ignore each other |
| 0.10.5 | Mild spreading; prefer less crowded regions |
| 1.0+ | Strong dispersion; density stays nearly uniform |
Higher values prevent density spikes but may slow convergence.
---
## Practical Tips
### Grid Resolution
- **Prototyping**: `nx=64, nt=40` — fast iteration, rough results
- **Publication**: `nx=100, nt=100` — good balance of speed and quality
- **High-fidelity**: `nx=200, nt=200` — smooth gradients, longer runtime
### Parameter Tuning
1. Start with `nu=0.01, alpha=0.5, lambda_congestion=0.5`
2. If convergence is slow, try `alpha=0.7`
3. If density has spikes, increase `lambda_congestion` to 1.0
4. If numerical issues appear, increase `nu` to 0.020.05
### Initial Conditions
Good choices for `m0`:
- **Gaussian**: `np.exp(-50 * (x - x0)**2)` — localized starting distribution
- **Uniform**: `np.ones(nx) / nx` — spread-out initial population
- **Bimodal**: Sum of two Gaussians — models two subpopulations
---
## References
1. Lasry, J.-M. and Lions, P.-L. (2007). "Mean field games." *Japanese Journal of Mathematics*, 2(1):229260.
2. Cardaliaguet, P. (2013). "Notes on Mean Field Games." Lecture notes, Collège de France.
3. Achdou, Y. and Capuzzo-Dolcetta, I. (2010). "Mean field games: numerical methods." *SIAM Journal on Numerical Analysis*, 48(3):11361162.
4. Huang, M., Malhamé, R., and Caines, P. (2006). "Large population stochastic dynamic games: closed-loop McKean-Vlasov systems and the Nash certainty equivalence principle." *Communications in Information and Systems*, 6(3):221252.
---
## Notebook Tutorial
For a complete walkthrough with validated outputs and visualizations, see the
[Mean Field Games Tutorial notebook](https://github.com/ThotDjehuty/optimiz-r/blob/main/examples/notebooks/mean_field_games_tutorial.ipynb) on GitHub.
The notebook demonstrates:
- Setting up initial distributions
- Running the solver with different parameters
- Visualizing density evolution as 3D surfaces and heatmaps
- Interpreting convergence diagnostics
- Comparing congestion levels
Audit documentation is available at [`docs/MFG_TUTORIAL_COMPLETE.md`](https://github.com/ThotDjehuty/optimiz-r/blob/main/docs/MFG_TUTORIAL_COMPLETE.md).
@@ -0,0 +1,48 @@
Asynchronous Covariance (Hayashi--Yoshida)
==========================================
The module :code:`timeseries_utils::nonsync_covariance` implements the
Hayashi--Yoshida estimator of the integrated covariance between two
asynchronously sampled processes :math:`X` and :math:`Y` observed at
distinct, non-overlapping observation grids
:math:`\{t^X_i\}` and :math:`\{t^Y_j\}`.
Estimator
---------
Let :math:`I_i = (t^X_{i-1}, t^X_i]` and :math:`J_j = (t^Y_{j-1}, t^Y_j]`.
The Hayashi--Yoshida estimator is
.. math::
\widehat{\langle X, Y\rangle}_{[0,T]}
\;=\;
\sum_{i, j}\,
\big(X_{t^X_i} - X_{t^X_{i-1}}\big)\,
\big(Y_{t^Y_j} - Y_{t^Y_{j-1}}\big)\,
\mathbf{1}\!\big[I_i \cap J_j \neq \emptyset\big].
It is consistent under non-synchronicity and avoids the *Epps effect*
that plagues naive grid interpolation.
Implementation
--------------
* A two-pointer scan in :math:`O(n_X + n_Y)` collects all overlapping
pairs.
* For matrices of size :math:`d \times d` with large per-asset sample
counts, off-diagonal entries are computed in parallel with Rayon.
API
---
.. code-block:: rust
pub fn hayashi_yoshida_covariance(
t1: &[f64], v1: &[f64],
t2: &[f64], v2: &[f64],
) -> Result<f64>;
pub fn hayashi_yoshida_matrix(
series: &[(Vec<f64>, Vec<f64>)],
) -> Result<Vec<Vec<f64>>>;
+610
View File
@@ -0,0 +1,610 @@
# Optimal Control
This module provides advanced optimal control algorithms for financial applications, including Hamilton-Jacobi-Bellman (HJB) equation solvers, regime-switching models, parameter estimation, and state-space filtering. All algorithms are implemented in high-performance Rust with Python bindings.
## Mathematical Foundations
### Hamilton-Jacobi-Bellman (HJB) Equation
The HJB equation is a fundamental result in optimal control theory that provides the necessary and sufficient conditions for optimality of a control policy. For a stochastic control problem:
$$
V(x) = \sup_{\alpha \in \mathcal{A}} \mathbb{E}\left[\int_0^\infty e^{-\rho t} L(X_t, \alpha_t) dt \mid X_0 = x\right]
$$
where $V(x)$ is the value function, $\rho$ is the discount rate, $L$ is the running cost, and $X_t$ follows a controlled stochastic process. The HJB equation is:
$$
\rho V(x) = \sup_{\alpha \in \mathcal{A}} \left\{ \mathcal{L}^\alpha V(x) + L(x, \alpha) \right\}
$$
where $\mathcal{L}^\alpha$ is the infinitesimal generator of the controlled process.
#### Application to Mean-Reverting Spreads
For pairs trading with an Ornstein-Uhlenbeck (OU) spread process:
$$
dX_t = \kappa(\theta - X_t)dt + \sigma dW_t
$$
with transaction costs $c > 0$, the HJB equation becomes:
$$
\rho V(x) = \kappa(\theta - x)V'(x) + \frac{\sigma^2}{2}V''(x) + \sup_{\alpha \in \{-1, 0, 1\}} \{ -c|\alpha| + \alpha x \}
$$
The optimal control is a threshold policy: buy when $x < x_L$, sell when $x > x_U$, hold otherwise.
### Viscosity Solutions
Classical solutions to HJB equations rarely exist due to:
1. **Non-smoothness at boundaries**: The value function $V(x)$ has kinks where the optimal control switches
2. **Lack of regularity**: Second derivatives $V''(x)$ may not exist everywhere
3. **Free boundary problems**: The optimal switching thresholds $(x_L, x_U)$ are unknown
**Viscosity solutions** generalize the notion of solution to allow for non-smooth value functions. A function $V$ is a viscosity solution if:
1. **Subsolution property**: For any smooth test function $\phi$ such that $V - \phi$ has a local maximum at $x_0$:
$$\rho V(x_0) \leq \mathcal{H}(x_0, V(x_0), D\phi(x_0), D^2\phi(x_0))$$
2. **Supersolution property**: For any smooth test function $\psi$ such that $V - \psi$ has a local minimum at $x_0$:
$$\rho V(x_0) \geq \mathcal{H}(x_0, V(x_0), D\psi(x_0), D^2\psi(x_0))$$
where $\mathcal{H}$ is the Hamiltonian.
**Key properties:**
- **Uniqueness**: Under suitable conditions (coercivity, proper discount), the viscosity solution is unique
- **Stability**: Viscosity solutions are stable under uniform convergence
- **Numerical convergence**: Monotone finite difference schemes converge to the viscosity solution
### Finite Difference Methods
We discretize the HJB equation on a spatial grid $x_i = x_{\min} + ih$, $i = 0, \ldots, N$, with grid spacing $h$.
#### Upwind Schemes
For the OU drift term $\kappa(\theta - x)V'(x)$, we use **upwind finite differences** to ensure monotonicity and stability:
- If $\kappa(\theta - x_i) > 0$ (rightward drift): use forward difference
$$V'(x_i) \approx \frac{V_{i+1} - V_i}{h}$$
- If $\kappa(\theta - x_i) < 0$ (leftward drift): use backward difference
$$V'(x_i) \approx \frac{V_i - V_{i-1}}{h}$$
The diffusion term uses centered differences:
$$V''(x_i) \approx \frac{V_{i+1} - 2V_i + V_{i-1}}{h^2}$$
#### Policy Iteration Algorithm
The HJB equation with control is solved via **policy iteration**:
1. **Initialize**: Start with policy $\alpha^{(0)}$ (e.g., always hold)
2. **Policy evaluation**: Solve the linear system for value function $V^{(k)}$:
$$\rho V^{(k)}_i = \mathcal{L}^{\alpha^{(k)}} V^{(k)}_i + L(x_i, \alpha^{(k)}_i)$$
3. **Policy improvement**: Update policy by maximizing Hamiltonian:
$$\alpha^{(k+1)}_i = \arg\max_{\alpha} \{ \mathcal{L}^\alpha V^{(k)}_i + L(x_i, \alpha) \}$$
4. **Convergence check**: If $\|\alpha^{(k+1)} - \alpha^{(k)}\|_\infty < \epsilon$, stop; otherwise return to step 2
**Convergence properties:**
- Typically 10-50 iterations for practical problems
- Geometric convergence rate
- Numerical solution converges to viscosity solution as $h \to 0$
## Implemented Algorithms
### 1. HJB Solver for OU Process
Solves the optimal switching problem for mean-reverting spreads with transaction costs.
**Implementation**: `src/optimal_control/hjb_solver.rs`
**Python API**:
```python
from optimizr import solve_hjb_py, solve_hjb_full_py
# Basic solver - returns optimal thresholds
lower, upper, residual, iters = solve_hjb_py(
kappa=3.0, # Mean reversion speed
theta=0.0, # Long-run mean
sigma=0.2, # Volatility
rho=0.04, # Discount rate
transaction_cost=0.001, # Transaction cost per trade
n_points=400, # Number of grid points
max_iter=4000, # Maximum policy iterations
tolerance=1e-7, # Convergence tolerance
n_std=5.0, # Grid extent in standard deviations
)
print(f"Optimal bounds: ({lower:.3f}, {upper:.3f})")
print(f"Residual: {residual:.2e}, Iterations: {iters}")
# Full solver - also returns value function and derivatives
lower, upper, residual, iters, V, V_x, V_xx = solve_hjb_full_py(
kappa=3.0, theta=0.0, sigma=0.2, rho=0.04,
transaction_cost=0.001, n_points=400
)
# Plot value function derivatives for diagnostics
import matplotlib.pyplot as plt
plt.plot(V_x)
plt.axvline(lower, color='r', linestyle='--', label='Lower bound')
plt.axvline(upper, color='g', linestyle='--', label='Upper bound')
plt.legend()
plt.title("Value function derivative V'(x)")
```
**Parameters**:
- `kappa`: Mean reversion speed (typical range: 0.1-10). Higher values → faster reversion → narrower bands
- `theta`: Long-run mean (typically 0 for normalized spreads)
- `sigma`: Volatility (typical range: 0.1-1.0). Higher values → wider bands
- `rho`: Discount rate (typical: 0.01-0.1). Higher values → more myopic strategy
- `transaction_cost`: Per-trade cost (typical: 0.0001-0.01). Higher values → wider bands, fewer trades
- `n_points`: Grid resolution (recommended: 200-500). Higher → more accurate but slower
- `n_std`: Grid extent (recommended: 3-6). Should cover 99%+ of spread distribution
**Returns**:
- `lower`: Optimal buy threshold (negative value)
- `upper`: Optimal sell threshold (positive value)
- `residual`: Maximum policy change in last iteration (should be < tolerance)
- `iters`: Number of policy iterations (typically 10-50)
- `V`, `V_x`, `V_xx`: (full solver only) Value function and derivatives on grid
**When to use**:
- Pairs trading with mean-reverting spreads
- Statistical arbitrage with transaction costs
- Optimal entry/exit for mean-reverting assets
- Requires reliable OU parameter estimates (see OU estimation below)
**Diagnostics**:
- Plot $V'(x)$ to check smoothness near thresholds
- Verify `residual < tolerance` for convergence
- Check that thresholds are within grid bounds
- If not converged: increase `max_iter` or adjust grid parameters
### 2. Viscosity Solution Solver
General-purpose viscosity solution solver for HJB equations with arbitrary Hamiltonians.
**Implementation**: `src/optimal_control/viscosity.rs`
**Usage**: Advanced users can extend this for custom control problems beyond OU switching.
### 3. Regime Switching Models
Optimal control with multiple market regimes, each with different dynamics.
**Implementation**: `src/optimal_control/regime_switching.rs`
**Approach**:
1. Use HMM to identify hidden regimes (see HMM section)
2. Estimate OU parameters per regime
3. Solve HJB per regime to get regime-specific thresholds
4. Switch control policy based on decoded regime
**Example workflow**:
```python
from optimizr import HMM, estimate_ou_params_py, solve_hjb_py
import numpy as np
# Step 1: Train HMM on spread returns
returns = np.diff(spread)
hmm = HMM(n_states=2)
hmm.fit(returns.reshape(-1, 1), n_iterations=100)
regimes = hmm.predict(returns.reshape(-1, 1))
# Step 2: Estimate OU parameters per regime
params = []
for regime_id in range(2):
mask = (regimes == regime_id)
spread_regime = spread[1:][mask] # Align with returns
kappa, theta, sigma, half_life = estimate_ou_params_py(
spread_regime, dt=1/252
)
params.append((kappa, theta, sigma))
print(f"Regime {regime_id}: κ={kappa:.2f}, θ={theta:.3f}, σ={sigma:.3f}")
# Step 3: Solve HJB per regime
thresholds = []
for kappa, theta, sigma in params:
lower, upper, _, _ = solve_hjb_py(
kappa=kappa, theta=theta, sigma=sigma,
rho=0.04, transaction_cost=0.001
)
thresholds.append((lower, upper))
print(f"Thresholds: ({lower:.3f}, {upper:.3f})")
# Step 4: Apply regime-specific control
current_regime = regimes[-1]
lower, upper = thresholds[current_regime]
if spread[-1] < lower:
action = "BUY"
elif spread[-1] > upper:
action = "SELL"
else:
action = "HOLD"
print(f"Current regime: {current_regime}, Action: {action}")
```
### 4. Jump Diffusion Models
Extension of OU process with Poisson jumps for modeling sudden price shocks.
**Implementation**: `src/optimal_control/jump_diffusion.rs`
**Model**:
$$
dX_t = \kappa(\theta - X_t)dt + \sigma dW_t + J_t dN_t
$$
where $N_t$ is a Poisson process with intensity $\lambda$, and $J_t \sim \mathcal{N}(\mu_J, \sigma_J^2)$ are jump sizes.
**Use case**: Markets with flash crashes, earnings announcements, or other discontinuous events.
### 5. Multi-Regime Switching Jump Diffusion (MRSJD)
Combines regime switching with jump diffusion for maximum flexibility.
**Implementation**: `src/optimal_control/mrsjd.rs`
**Model**: Each regime has its own OU parameters AND jump process parameters.
**Use case**: Complex markets with both regime changes and sudden shocks (e.g., crypto, emerging markets).
### 6. OU Parameter Estimation
Estimates Ornstein-Uhlenbeck process parameters from time series data.
**Implementation**: `src/optimal_control/ou_estimator.rs`
**Python API**:
```python
from optimizr import estimate_ou_params_py
import numpy as np
# Simulate OU process (for testing)
dt = 1/252 # Daily data
T = 1000
kappa_true, theta_true, sigma_true = 3.0, 0.0, 0.2
spread = [0.0]
for _ in range(T-1):
dx = kappa_true * (theta_true - spread[-1]) * dt + \
sigma_true * np.sqrt(dt) * np.random.randn()
spread.append(spread[-1] + dx)
spread = np.array(spread)
# Estimate parameters
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)")
```
**Method**: Maximum likelihood estimation (MLE) using analytical formulas for discrete-time OU process.
**Parameters**:
- `spread`: Time series of spread values (1D numpy array)
- `dt`: Time step in years (e.g., 1/252 for daily data, 1/52 for weekly)
**Returns**:
- `kappa`: Mean reversion speed (annualized)
- `theta`: Long-run mean
- `sigma`: Volatility (annualized)
- `half_life`: Half-life in time step units ($\ln(2)/\kappa \cdot dt^{-1}$)
**Practical tips**:
- Use at least 500-1000 observations for stable estimates
- Check half-life: typical pairs have half-life 5-60 days
- Winsorize extreme outliers (e.g., clip at ±5σ) if needed
- For rolling estimates, use expanding or rolling windows of 250-500 periods
### 7. Kalman Filtering
State-space filtering for latent variable estimation and forecasting.
**Implementation**: `src/optimal_control/kalman_filter.rs`, `src/optimal_control/kalman_py_bindings.rs`
#### Linear Kalman Filter
For linear Gaussian state-space models:
$$
\begin{aligned}
x_{t+1} &= F x_t + B u_t + w_t, \quad w_t \sim \mathcal{N}(0, Q) \\
y_t &= H x_t + v_t, \quad v_t \sim \mathcal{N}(0, R)
\end{aligned}
$$
**Python API**:
```python
from optimizr import LinearKalmanFilter
import numpy as np
# Define system matrices
F = [[1.0, 1.0], [0.0, 1.0]] # State transition (2×2)
H = [[1.0, 0.0]] # Observation matrix (1×2)
Q = [[1e-4, 0.0], [0.0, 1e-4]] # Process noise covariance
R = [[1e-2]] # Measurement noise covariance
# Initialize filter
kf = LinearKalmanFilter(
f_matrix=F,
h_matrix=H,
q_matrix=Q,
r_matrix=R,
initial_state=[0.0, 0.0],
initial_covariance=[[1.0, 0.0], [0.0, 1.0]],
)
# Online filtering loop
observations = np.random.randn(100)
states = []
for obs in observations:
kf.predict(control=[0.0, 0.0]) # Prediction step
kf.update(observation=[obs]) # Correction step
state = kf.get_state()
states.append(state)
states = np.array(states)
print(f"Final state estimate: {states[-1]}")
```
**Use cases**:
- Tracking latent spread dynamics with noise
- State estimation for control (e.g., estimate velocity from noisy position)
- Online parameter adaptation
#### Extended Kalman Filter (EKF)
For nonlinear systems with local linearization.
**Use case**: Nonlinear spread dynamics, regime probabilities as states.
#### Unscented Kalman Filter (UKF)
For highly nonlinear systems using sigma-point approximation.
**Python API**:
```python
from optimizr import UnscentedKalmanFilter
ukf = UnscentedKalmanFilter(
state_dim=2,
obs_dim=1,
q_matrix=Q,
r_matrix=R,
initial_state=[0.0, 0.0],
initial_covariance=[[1.0, 0.0], [0.0, 1.0]],
)
# Similar predict/update interface
```
**Use case**: Jump diffusion models, volatility estimation, option pricing.
### 8. Backtesting Framework
Backtests optimal switching strategies on historical data.
**Implementation**: `src/optimal_control/backtest.rs`
**Python API**:
```python
from optimizr import backtest_optimal_switching_py
# First, get optimal thresholds
lower, upper, _, _ = solve_hjb_py(
kappa=3.0, theta=0.0, sigma=0.2,
rho=0.04, transaction_cost=0.001
)
# Backtest on historical spread
metrics = backtest_optimal_switching_py(
spread=spread, # Historical spread data
lower_bound=lower, # Optimal buy threshold
upper_bound=upper, # Optimal sell threshold
transaction_cost=0.001, # Must match HJB solver
)
(
total_return, # Cumulative return
sharpe, # Annualized Sharpe ratio
max_dd, # Maximum drawdown
n_trades, # Number of round-trip trades
win_rate, # Fraction of profitable trades
pnl_path, # P&L time series
) = metrics
print(f"Return: {total_return:.2%}, Sharpe: {sharpe:.2f}")
print(f"Max DD: {max_dd:.2%}, Trades: {n_trades}, Win rate: {win_rate:.2%}")
# Plot P&L path
import matplotlib.pyplot as plt
plt.figure(figsize=(12, 6))
plt.subplot(2, 1, 1)
plt.plot(spread, label='Spread')
plt.axhline(lower, color='r', linestyle='--', label='Lower')
plt.axhline(upper, color='g', linestyle='--', label='Upper')
plt.legend()
plt.subplot(2, 1, 2)
plt.plot(pnl_path, label='P&L')
plt.legend()
plt.tight_layout()
```
**Metrics interpretation**:
- `total_return`: Should be positive with low transaction costs
- `sharpe`: Good values > 1.0, excellent > 2.0
- `max_dd`: Risk metric, compare to expected return
- `n_trades`: Too many → excessive costs; too few → missing opportunities
- `win_rate`: Typically 40-60% for mean-reversion strategies
**Parameter tuning**:
- If `win_rate` low but `max_dd` high → bands too narrow, increase `transaction_cost` or `rho`
- If `n_trades` low → bands too wide, decrease `transaction_cost` or `rho`
- Compare Sharpe ratios across different parameter settings
## Complete Workflow Example
Here's a complete optimal control pipeline for pairs trading:
```python
import numpy as np
import pandas as pd
from optimizr import (
estimate_ou_params_py,
solve_hjb_py,
backtest_optimal_switching_py,
HMM
)
# 1. Load price data (example with simulated data)
np.random.seed(42)
T = 5000
dt = 1/252
# Simulate cointegrated pair
price_A = 100 * np.exp(np.cumsum(0.0001 + 0.01*np.sqrt(dt)*np.random.randn(T)))
price_B = 100 * np.exp(np.cumsum(0.0001 + 0.01*np.sqrt(dt)*np.random.randn(T)))
spread = np.log(price_A) - np.log(price_B)
# Split into train/test
train_spread = spread[:3000]
test_spread = spread[3000:]
# 2. Estimate OU parameters
kappa, theta, sigma, half_life = estimate_ou_params_py(train_spread, dt=dt)
print(f"OU parameters: κ={kappa:.2f}, θ={theta:.3f}, σ={sigma:.3f}")
print(f"Half-life: {half_life:.1f} days")
# 3. Solve HJB for optimal thresholds
lower, upper, residual, iters = solve_hjb_py(
kappa=kappa,
theta=theta,
sigma=sigma,
rho=0.04,
transaction_cost=0.001,
n_points=400,
max_iter=2000,
tolerance=1e-7,
)
print(f"Optimal thresholds: ({lower:.3f}, {upper:.3f})")
print(f"Converged in {iters} iterations, residual={residual:.2e}")
# 4. Backtest on out-of-sample data
metrics = backtest_optimal_switching_py(
spread=test_spread,
lower_bound=lower,
upper_bound=upper,
transaction_cost=0.001,
)
total_return, sharpe, max_dd, n_trades, win_rate, pnl_path = metrics
print(f"\nBacktest Results:")
print(f" Total Return: {total_return:.2%}")
print(f" Sharpe Ratio: {sharpe:.2f}")
print(f" Max Drawdown: {max_dd:.2%}")
print(f" # Trades: {n_trades}")
print(f" Win Rate: {win_rate:.2%}")
# 5. Optional: Regime-aware control with HMM
returns = np.diff(train_spread)
hmm = HMM(n_states=2)
hmm.fit(returns.reshape(-1, 1), n_iterations=100)
regimes = hmm.predict(returns.reshape(-1, 1))
# Estimate OU per regime and get regime-specific thresholds
for regime_id in range(2):
mask = (regimes == regime_id)
spread_regime = train_spread[1:][mask]
k, t, s, _ = estimate_ou_params_py(spread_regime, dt=dt)
l, u, _, _ = solve_hjb_py(k, t, s, 0.04, 0.001)
print(f"Regime {regime_id}: κ={k:.2f}, thresholds=({l:.3f}, {u:.3f})")
```
## Performance Characteristics
### Computational Complexity
- **HJB Solver**: $O(N \cdot K)$ where $N$ is `n_points`, $K$ is policy iterations (~10-50)
- **OU Estimation**: $O(T)$ where $T$ is time series length (closed-form MLE)
- **Kalman Filter**: $O(T \cdot d^3)$ where $d$ is state dimension (matrix inversion per step)
- **Backtesting**: $O(T)$ single pass through data
### Typical Runtimes (on modern CPU)
- HJB solve (400 points): ~10-50ms
- OU estimation (5000 samples): ~1ms
- Kalman filter (1000 steps, 2D state): ~10ms
- Backtest (5000 samples): ~5ms
### Memory Requirements
- HJB solver: $O(N)$ for grid storage (~few KB)
- Kalman filter: $O(d^2)$ for covariance matrices (~few KB for small $d$)
- Backtesting: $O(T)$ for P&L path storage (~few MB for long histories)
## Integration with Other Modules
### With HMM (Hidden Markov Models)
- Use HMM to detect market regimes
- Estimate OU parameters per regime
- Apply regime-specific optimal controls
- See `api/hmm.md` for HMM documentation
### With Mean Field Games
- Use optimal control as individual agent strategy
- Aggregate across population for mean-field dynamics
- See `algorithms/mean_field_games.md` for MFG theory
### With Sparse Optimization
- Use Kalman-filtered states as inputs to sparse controllers
- Combine L1-regularized control with HJB thresholds
- See `algorithms/sparse_optimization.md`
## Troubleshooting
### HJB solver not converging
- **Symptom**: `residual > tolerance` after `max_iter`
- **Fix**: Increase `max_iter` (try 5000-10000); reduce `tolerance` requirement; check that OU parameters are reasonable
### Thresholds outside grid bounds
- **Symptom**: Optimal thresholds at grid edges
- **Fix**: Increase `n_std` (try 6-8); check OU parameter estimates (very high σ needs wider grid)
### OU estimates unstable
- **Symptom**: Negative `kappa` or extreme `half_life`
- **Fix**: Use more data (>1000 samples); check for non-stationarity; consider winsorizing outliers
### Backtest Sharpe ratio low
- **Symptom**: Sharpe < 0.5 despite positive thresholds
- **Fix**: Check for regime changes (use HMM); verify spread is actually mean-reverting; adjust `transaction_cost` in HJB solver
### Kalman filter diverging
- **Symptom**: State estimates exploding
- **Fix**: Check process noise `Q` is not too large; verify observations are scaled properly; use UKF for strong nonlinearity
## References
### Optimal Control Theory
- **Fleming, W. H., & Soner, H. M.** (2006). *Controlled Markov Processes and Viscosity Solutions*. Springer.
- **Øksendal, B.** (2003). *Stochastic Differential Equations: An Introduction with Applications* (6th ed.). Springer.
- **Pham, H.** (2009). *Continuous-time Stochastic Control and Optimization with Financial Applications*. Springer.
### Viscosity Solutions
- **Barles, G., & Souganidis, P. E.** (1991). Convergence of approximation schemes for fully nonlinear second order equations. *Asymptotic Analysis*, 4(3), 271-283.
- **Crandall, M. G., Ishii, H., & Lions, P.-L.** (1992). User's guide to viscosity solutions of second order partial differential equations. *Bulletin of the American Mathematical Society*, 27(1), 1-67.
### Kalman Filtering
- **Kalman, R. E.** (1960). A new approach to linear filtering and prediction problems. *Journal of Basic Engineering*, 82(1), 35-45.
- **Julier, S. J., & Uhlmann, J. K.** (1997). New extension of the Kalman filter to nonlinear systems. *Signal Processing, Sensor Fusion, and Target Recognition VI*, 3068, 182-193.
### Financial Applications
- **Avellaneda, M., & Lee, J.-H.** (2010). Statistical arbitrage in the US equities market. *Quantitative Finance*, 10(7), 761-782.
- **Gatev, E., Goetzmann, W. N., & Rouwenhorst, K. G.** (2006). Pairs trading: Performance of a relative-value arbitrage rule. *The Review of Financial Studies*, 19(3), 797-827.
## See Also
- [HMM API Reference](../api/hmm.md) - Hidden Markov Models for regime detection
- [Mean Field Games](mean_field_games.md) - Population-level optimal control
- [Optimal Control API](../api/optimal_control.md) - Complete function signatures and types
@@ -0,0 +1,94 @@
# Optimal Control
HamiltonJacobiBellman (HJB) solvers, regime-switching thresholds, OU parameter estimation, and Kalman filtering utilities backed by Rust.
## HJB switching boundaries (OU process)
```python
from optimizr import solve_hjb_py, solve_hjb_full_py
lower, upper, residual, iters = solve_hjb_py(
kappa=3.0,
theta=0.0,
sigma=0.2,
rho=0.04,
transaction_cost=0.001,
n_points=400,
max_iter=4000,
tolerance=1e-7,
n_std=5.0,
)
print(f"bounds=({lower:.3f}, {upper:.3f}), residual={residual:.2e}, iters={iters}")
```
- Model: $dX_t = \kappa(\theta - X_t)\,dt + \sigma\,dW_t$ with quadratic transaction costs.
- Output: optimal buy/sell thresholds; `solve_hjb_full_py` also returns $V, V_x, V_{xx}$ for diagnostics.
- Diagnostics: plot $V_x$ for smoothness near thresholds; monitor `residual` and increase `max_iter` if not converged.
## Backtesting optimal switching
```python
from optimizr import backtest_optimal_switching_py
metrics = backtest_optimal_switching_py(
spread=spread,
lower_bound=lower,
upper_bound=upper,
transaction_cost=0.001,
)
(
total_return,
sharpe,
max_dd,
n_trades,
win_rate,
pnl_path,
) = metrics
```
Inspect `win_rate` vs `max_dd` to tune aggressiveness; combine with HMM regimes for state-aware controls.
## OU parameter estimation
```python
import numpy as np
from optimizr import estimate_ou_params_py
spread = np.random.randn(10_000)
kappa, theta, sigma, half_life = estimate_ou_params_py(spread, dt=1/252)
```
Method-of-moments / MLE fit returns $(\kappa, \theta, \sigma, \text{half-life})$. Use a few thousand samples for stability; winsorize heavy tails if needed.
## Kalman filtering (linear, EKF, UKF)
```python
import numpy as np
from optimizr import LinearKalmanFilter
F = [[1.0, 1.0], [0.0, 1.0]]
H = [[1.0, 0.0]]
Q = [[1e-4, 0.0], [0.0, 1e-4]]
R = [[1e-2]]
kf = LinearKalmanFilter(
f_matrix=F,
h_matrix=H,
q_matrix=Q,
r_matrix=R,
initial_state=[0.0, 0.0],
initial_covariance=[[1.0, 0.0], [0.0, 1.0]],
)
kf.predict(control=[0.0, 0.0])
kf.update(observation=[1.2])
state = kf.get_state()
```
- Interfaces: `LinearKalmanFilter`, `UnscentedKalmanFilter`, and `KalmanState` for batch `filter` and smoothing.
- Concept: prediction (dynamics prior) + correction (measurement residual); RTS smoother refines past states.
## Practical notes
- Rust backend (`optimizr._core`) must be present for control utilities; install from source if wheels are unavailable.
- Grids: for HJB, `n_points≈400` is stable; widen `n_std` for volatile spreads.
- Combine with Mean Field Games: see `mean_field_games.md` for population dynamics; use Kalman estimates as control inputs if needed.
+538
View File
@@ -0,0 +1,538 @@
# Point Processes & Fractional Brownian Motion
This module implements the mathematical framework from **Muhle-Karbe, Jusselin & Rosenbaum** (2022) for modeling order flow microstructure through self-exciting point processes and fractional dynamics.
It provides high-performance Rust implementations of:
- **Hawkes Processes** with flexible excitation kernels
- **Fractional Brownian Motion (fBM)** with exact simulation
- **Mixed Fractional Brownian Motion (mfBM)** for aggregate flow
- **Mittag-Leffler Functions** for scaling limit analysis
---
## Mathematical Foundations
### The Unified Theory of Order Flow
The key insight from the unified theory is that a **single parameter** — the Hurst exponent $H_0 \approx 3/4$ — governs all market microstructure quantities:
$$
\boxed{H_0 \approx \frac{3}{4}}
$$
This parameter determines:
| Quantity | Formula | Value at $H_0 = 3/4$ |
|----------|---------|----------------------|
| Price roughness | $H_{\text{price}} = H_0 - \tfrac{1}{2}$ | $1/4$ |
| Volatility roughness | $H_{\text{vol}} \approx H_0 - \tfrac{1}{2}$ | $\approx 0.1$ |
| Market impact exponent | $\delta = 1 - \tfrac{1}{2H_0}$ | $1/3$ |
| Kyle's lambda | $\Lambda \sim n^{-\delta}$ | $\sim n^{-1/3}$ |
| Kernel tail exponent | $\alpha_0 = H_0/2$ | $3/8$ |
The model structure is:
$$
N = F + R
$$
where:
- $N$ = total order flow (observable)
- $F$ = core (fundamental) order flow
- $R$ = reaction (self-exciting) order flow modeled by Hawkes processes
---
## Hawkes Processes
### Definition
A (univariate) Hawkes process $N(t)$ has conditional intensity:
$$
\lambda(t) = \nu + \int_0^{t^-} \phi(t - s) \, dN(s) = \nu + \sum_{t_i < t} \phi(t - t_i)
$$
where:
- $\nu > 0$ is the **baseline intensity** (exogenous arrival rate)
- $\phi: \mathbb{R}_+ \to \mathbb{R}_+$ is the **excitation kernel** (self-exciting memory)
- $t_i$ are past event times
The process is **stable** (stationary) when the **branching ratio** satisfies:
$$
\|\phi\|_{L^1} = \int_0^\infty \phi(t) \, dt < 1
$$
The expected number of events per unit time in stationarity is:
$$
\mathbb{E}[\lambda] = \frac{\nu}{1 - \|\phi\|_{L^1}}
$$
### Excitation Kernels
#### Exponential Kernel (Short Memory)
$$
\phi(t) = \alpha \, e^{-\beta t}, \quad \alpha, \beta > 0
$$
Properties:
- **L¹ norm**: $\|\phi\|_{L^1} = \alpha / \beta$
- **Stability**: $\alpha < \beta$
- **Half-life**: $t_{1/2} = \ln 2 / \beta$
- **Tail**: exponential decay (no long memory)
- **Characteristic timescale**: $\tau = 1/\beta$
The exponential kernel leads to an intensity process that is Markovian — the full history can be summarized by the current intensity level. The integrated kernel is:
$$
\int_0^t \phi(s) \, ds = \frac{\alpha}{\beta} \left(1 - e^{-\beta t}\right)
$$
#### Power-Law Kernel (Long Memory)
$$
\phi(t) = K_0 \, (1 + t)^{-(1 + \alpha_0)}, \quad K_0 > 0, \; \alpha_0 \in (0, 1)
$$
Properties:
- **L¹ norm**: $\|\phi\|_{L^1} = K_0 / \alpha_0$
- **Stability**: $K_0 < \alpha_0$
- **Tail exponent**: $\alpha_0$ controls memory persistence
- **Hurst connection**: $H_0 = 2\alpha_0$ (from the unified theory)
- **Long memory**: polynomial decay produces clustering at all timescales
The integrated kernel is:
$$
\int_0^t \phi(s) \, ds = \frac{K_0}{\alpha_0} \left[1 - (1 + t)^{-\alpha_0}\right]
$$
The **critical** regime ($\|\phi\|_{L^1} = 1$) corresponds to $K_0 = \alpha_0$, and the **nearly-critical** regime ($\|\phi\|_{L^1} = 1 - \varepsilon$) is relevant for real market data where the branching ratio is very close to 1.
#### Completely Monotone Kernel (Assumption A)
From the unified theory paper's **Assumption A**, the most general kernel satisfying the scaling limit theorems:
$$
\phi(t) = K_0 \, t^{-\alpha_0} \, E_{1-\alpha_0}\!\left(-\lambda \, t^{1-\alpha_0}\right)
$$
where $E_\alpha$ is the Mittag-Leffler function. This kernel:
- Is **completely monotone** on $(0, \infty)$
- Interpolates between power-law and exponential behavior
- Satisfies all conditions for the scaling limit theorems
### Simulation: Ogata's Thinning Algorithm
The Hawkes process is simulated using **Ogata's thinning algorithm**:
1. Compute upper bound $\lambda_{\max} \geq \lambda(t)$ for the current intensity
2. Generate candidate inter-arrival time $\tau \sim \text{Exp}(\lambda_{\max})$
3. Accept with probability $\lambda(t + \tau) / \lambda_{\max}$
4. If rejected, advance time to $t + \tau$ and repeat
The algorithm has expected time complexity $O(n \log n)$ where $n$ is the number of events.
### Maximum Likelihood Estimation
The log-likelihood of a Hawkes process on $[0, T]$ with event times $\{t_1, \ldots, t_n\}$:
$$
\ell(\boldsymbol{\theta}) = \sum_{i=1}^n \log \lambda(t_i) - \int_0^T \lambda(t) \, dt
$$
The compensator (integrated intensity) decomposes as:
$$
\int_0^T \lambda(t) \, dt = \nu T + \sum_{i=1}^n \int_0^{T - t_i} \phi(s) \, ds
$$
### Bivariate Hawkes Process
For order flow modeling, buy and sell reaction orders follow a **bivariate Hawkes process** $\mathbf{N} = (N^+, N^-)$ with intensity:
$$
\begin{aligned}
\lambda^+(t) &= \mu^+(t) + \int \left[\phi_1(t-s) \, dN^+(s) + \phi_2(t-s) \, dN^-(s)\right] \\
\lambda^-(t) &= \mu^-(t) + \int \left[\phi_2(t-s) \, dN^+(s) + \phi_1(t-s) \, dN^-(s)\right]
\end{aligned}
$$
where:
- $\phi_1$: **self-excitation** kernel (buy $\to$ buy, sell $\to$ sell)
- $\phi_2$: **cross-excitation** kernel (buy $\to$ sell, sell $\to$ buy)
- $\mu^\pm(t)$: baselines driven by core order flow
The **stability condition** requires the spectral radius of the kernel matrix:
$$
\rho\!\left(\begin{pmatrix} \|\phi_1\|_1 & \|\phi_2\|_1 \\ \|\phi_2\|_1 & \|\phi_1\|_1 \end{pmatrix}\right) = \|\phi_1\|_1 + \|\phi_2\|_1 < 1
$$
The **signed flow** $N^+(t) - N^-(t)$ captures the net order imbalance driving price changes, while the **unsigned volume** $N^+(t) + N^-(t)$ measures total reaction activity.
---
## Fractional Brownian Motion
### Definition
Fractional Brownian motion (fBM) $B^H_t$ with **Hurst parameter** $H \in (0, 1)$ is the unique centered Gaussian process with:
$$
\text{Cov}(B^H_s, B^H_t) = \frac{1}{2}\left(|t|^{2H} + |s|^{2H} - |t-s|^{2H}\right)
$$
Key properties:
- **Self-similarity**: $B^H_{ct} \overset{d}{=} c^H B^H_t$ for all $c > 0$
- **Stationary increments**: $B^H_t - B^H_s \overset{d}{=} B^H_{t-s}$
- **Variance**: $\text{Var}(B^H_t) = t^{2H}$
The three regimes are:
| Range | Behavior | Autocorrelation | Financial Interpretation |
|-------|----------|----------------|------------------------|
| $H < 1/2$ | **Anti-persistent** (mean-reverting) | Negative | Price reversals dominate |
| $H = 1/2$ | **Standard BM** (no memory) | Zero | Random walk |
| $H > 1/2$ | **Persistent** (trending) | Positive | Trends persist |
### Fractional Gaussian Noise (fGn)
The increments of fBM form **fractional Gaussian noise** with autocovariance:
$$
\gamma(k) = \frac{1}{2}\left(|k-1|^{2H} - 2|k|^{2H} + |k+1|^{2H}\right)
$$
For $H > 1/2$, $\gamma(k) > 0$ for all $k$, indicating **long-range dependence**:
$$
\sum_{k=0}^\infty \gamma(k) = \infty
$$
### Simulation Methods
#### Cholesky Method
Exact simulation by forming the covariance matrix $\Sigma$ and computing its Cholesky decomposition:
$$
\Sigma = L L^\top, \quad \mathbf{B}^H = L \mathbf{Z}, \quad \mathbf{Z} \sim \mathcal{N}(\mathbf{0}, I_n)
$$
Complexity: $O(n^3)$ for decomposition, $O(n^2)$ for simulation.
#### Hosking's Method (Durbin-Levinson)
For regular time grids, uses the **Durbin-Levinson algorithm** to compute prediction coefficients for the fGn, then reconstructs fBM by cumulative summation:
1. Compute autocovariance sequence $\gamma(0), \gamma(1), \ldots, \gamma(n-1)$
2. Recursively compute Levinson coefficients $\phi_{i,j}$ and prediction variances $v_i$
3. Generate fGn: $X_i = \sum_{j=0}^{i-1} \phi_{i,j} X_{i-1-j} + \sqrt{v_i} Z_i$
4. Cumulate: $B^H_k = \sum_{i=0}^{k-1} X_i \cdot (\Delta t)^H$
Complexity: $O(n^2)$ — more efficient than Cholesky for large $n$.
### Hurst Exponent Estimation
#### Rescaled Range (R/S) Analysis
The R/S statistic for a subseries of length $n$:
$$
(R/S)_n = \frac{\max_{1 \leq k \leq n} W_k - \min_{1 \leq k \leq n} W_k}{S_n}
$$
where $W_k = \sum_{i=1}^k (X_i - \bar{X})$ is the cumulative deviation and $S_n$ is the standard deviation.
For fBM/fGn:
$$
\mathbb{E}[(R/S)_n] \sim c \cdot n^H \quad \text{as } n \to \infty
$$
The Hurst exponent is estimated by linear regression of $\log(R/S)$ against $\log n$:
$$
\hat{H} = \frac{\sum_i (\log n_i - \overline{\log n})(\log(R/S)_i - \overline{\log(R/S)})}{\sum_i (\log n_i - \overline{\log n})^2}
$$
---
## Mixed Fractional Brownian Motion
### Definition
The mixed fBM (mfBM) combines a standard BM with an independent fBM:
$$
M^H(t) = a \cdot B(t) + b \cdot B^H(t)
$$
where:
- $B(t)$: standard Brownian motion (diffusive component)
- $B^H(t)$: fractional BM with Hurst index $H$
- $a, b$: mixing coefficients
### Covariance Structure
$$
\text{Cov}(M^H_s, M^H_t) = a^2 \min(s,t) + \frac{b^2}{2}\left(|t|^{2H} + |s|^{2H} - |t-s|^{2H}\right)
$$
### Role in the Unified Theory
In the scaling limit of the Hawkes-based order flow model, the aggregate order flow converges to:
$$
\frac{1}{\sqrt{n}} \sum_{i=1}^{\lfloor nt \rfloor} (N^+_i - N^-_i) \xrightarrow{d} \sigma_F \cdot M^{H_0}(t)
$$
where the Hurst exponent $H_0 = 2\alpha_0$ is determined by the kernel tail.
### Semimartingale Property
The mfBM is a **semimartingale** if and only if $H > 3/4$. This has pricing implications:
- For $H > 3/4$: classical stochastic calculus applies, no arbitrage
- For $H \leq 3/4$: not a semimartingale, requires fractional calculus
### Scale-Dependent Hurst Analysis
To identify a mfBM (vs pure fBM or BM), examine the **scale-dependent Hurst exponent**:
$$
H(\Delta) = \frac{1}{2} \cdot \frac{\log \text{Var}[X(t+2\Delta) - X(t)]}{\log \text{Var}[X(t+\Delta) - X(t)]} \cdot \frac{1}{\log 2}
$$
For pure fBM, $H(\Delta) \approx H$ at all scales. For mfBM:
- **Short timescales**: $H(\Delta) \to 1/2$ (BM dominates)
- **Long timescales**: $H(\Delta) \to H$ (fBM dominates)
This crossover behavior is a hallmark of the mixed process and matches empirical observations in order flow data.
---
## Mittag-Leffler Functions
### Definition
The generalized Mittag-Leffler function:
$$
E_{\alpha,\beta}(z) = \sum_{k=0}^\infty \frac{z^k}{\Gamma(\alpha k + \beta)}, \quad \alpha > 0, \; \beta > 0
$$
Special cases:
- $E_{1,1}(z) = e^z$ (exponential function)
- $E_{2,1}(z^2) = \cosh(z)$ (hyperbolic cosine)
- $E_{1,2}(z) = (e^z - 1)/z$ (exponential integral)
### Asymptotic Behavior
For $0 < \alpha < 1$ and large $|z|$:
$$
E_{\alpha,\beta}(z) \sim \begin{cases}
\frac{1}{\alpha} z^{(1-\beta)/\alpha} \exp\!\left(z^{1/\alpha}\right) & z \to +\infty \\[6pt]
-\sum_{k=1}^{p} \frac{z^{-k}}{\Gamma(\beta - \alpha k)} + O(|z|^{-p-1}) & z \to -\infty
\end{cases}
$$
### The $f_{\alpha_0, \lambda_0}$ Function
From **Theorem 3.1** of Muhle-Karbe et al., the key scaling function:
$$
f_{\alpha_0, \lambda_0}(x) = \lambda_0 \, x^{\alpha_0 - 1} \, E_{\alpha_0, \alpha_0}\!\left(-\lambda_0 \, x^{\alpha_0}\right)
$$
This function controls how the Hawkes process's self-excitation structure manifests in the scaling limit. Its integral satisfies:
$$
\int_0^t f_{\alpha_0, \lambda_0}(s) \, ds = t^{\alpha_0} \, E_{\alpha_0, \alpha_0 + 1}\!\left(-\lambda_0 \, t^{\alpha_0}\right)
$$
The function $f_{\alpha_0, \lambda_0}$ interpolates between:
- **Short times**: $f(x) \sim \lambda_0 x^{\alpha_0 - 1}$ (power-law singularity)
- **Long times**: $f(x) \sim x^{-1-\alpha_0}$ (power-law decay like the kernel)
---
## Usage Examples
### Simulating a Hawkes Process
```python
import optimizr
import numpy as np
import matplotlib.pyplot as plt
# Simulate with exponential kernel
events_exp = optimizr.simulate_hawkes(
baseline=1.0, # ν = 1.0
alpha=0.5, # α = 0.5
beta=1.0, # β = 1.0
t_max=100.0,
kernel_type="exponential",
seed=42
)
# Simulate with power-law kernel (H₀ ≈ 0.75)
events_pl = optimizr.simulate_hawkes(
baseline=0.1,
alpha=0.35, # K₀ = 0.35
beta=0.375, # α₀ = 0.375 → H₀ = 2 × 0.375 = 0.75
t_max=100.0,
kernel_type="power_law",
seed=42
)
print(f"Exponential kernel: {len(events_exp)} events")
print(f"Power-law kernel: {len(events_pl)} events")
```
### Bivariate Buy/Sell Reaction Flow
```python
import optimizr
import numpy as np
# Generate core order flow (Poisson driver)
rng = np.random.default_rng(42)
core_buys = np.sort(rng.uniform(0, 100, 200))
core_sells = np.sort(rng.uniform(0, 100, 180))
# Simulate bivariate Hawkes reaction flow
buy_times, sell_times = optimizr.simulate_bivariate_hawkes(
core_buy_times=core_buys,
core_sell_times=core_sells,
phi1_alpha=0.3, # Self-excitation (buy→buy, sell→sell)
phi1_beta=1.0,
phi2_alpha=0.2, # Cross-excitation (buy→sell, sell→buy)
phi2_beta=1.0,
t_max=100.0,
seed=42
)
print(f"Reaction buys: {len(buy_times)}, Reaction sells: {len(sell_times)}")
print(f"Net order imbalance: {len(buy_times) - len(sell_times)}")
# Check stability
l1_phi1 = 0.3 / 1.0 # L¹ norm of self-excitation
l1_phi2 = 0.2 / 1.0 # L¹ norm of cross-excitation
spectral_radius = l1_phi1 + l1_phi2
print(f"Spectral radius: {spectral_radius:.2f} ({'stable' if spectral_radius < 1 else 'UNSTABLE'})")
```
### Simulating Fractional Brownian Motion
```python
import optimizr
import numpy as np
import matplotlib.pyplot as plt
# Simulate fBM paths with different Hurst exponents
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
for i, h in enumerate([0.3, 0.5, 0.8]):
path = optimizr.simulate_fbm(hurst=h, n=1000, dt=0.01, seed=42)
# Estimate Hurst exponent from the path
h_est = optimizr.estimate_hurst(path)
axes[i].plot(path, linewidth=0.5)
axes[i].set_title(f"H = {h:.1f} (estimated: {h_est:.3f})")
axes[i].set_xlabel("Time step")
plt.suptitle("Fractional Brownian Motion Paths")
plt.tight_layout()
plt.show()
```
### Mixed fBM for Aggregate Order Flow
```python
import optimizr
import numpy as np
# Simulate mixed fBM (BM + fBM with H₀ = 0.75)
path = optimizr.simulate_mixed_fbm(
a=1.0, # BM coefficient
b=1.0, # fBM coefficient
hurst=0.75, # H₀ from unified theory
n=5000,
dt=0.01,
seed=42
)
# Scale-dependent Hurst analysis (identifies mfBM vs pure fBM)
scales = [10, 50, 100, 500, 1000, 2000]
hurst_by_scale = optimizr.scale_dependent_hurst(
data=path,
scales=scales
)
print("Scale-Dependent Hurst Exponents:")
print("-" * 35)
for scale, h in sorted(hurst_by_scale.items()):
print(f" Scale {scale:>5d}: H = {h:.4f}")
```
### Mittag-Leffler and Scaling Functions
```python
import optimizr
import numpy as np
import matplotlib.pyplot as plt
# Verify E_{1,1}(z) = exp(z)
z = 2.0
ml_value = optimizr.mittag_leffler_py(
alpha=1.0, beta=1.0, z=z
)
print(f"E_{{1,1}}({z}) = {ml_value:.6f}")
print(f"exp({z}) = {np.exp(z):.6f}")
# Plot the scaling function f_{α₀,λ₀}(x)
x = np.linspace(0.01, 10, 500)
alpha_0 = 0.375 # From H₀ = 0.75
lambda_0 = 1.0
f_values = [optimizr.f_alpha_lambda_py(alpha_0, lambda_0, xi) for xi in x]
plt.figure(figsize=(10, 5))
plt.subplot(1, 2, 1)
plt.plot(x, f_values)
plt.xlabel('x')
plt.ylabel(r'$f_{\alpha_0, \lambda_0}(x)$')
plt.title(f'Scaling Function (α₀={alpha_0}, λ₀={lambda_0})')
plt.subplot(1, 2, 2)
plt.loglog(x, np.abs(f_values))
plt.xlabel('x (log)')
plt.ylabel(r'$|f_{\alpha_0, \lambda_0}(x)|$ (log)')
plt.title('Power-law decay in scaling limit')
plt.tight_layout()
plt.show()
```
---
## Theoretical References
1. **Muhle-Karbe, Jusselin & Rosenbaum** (2022). *A unified approach to the analysis of high-frequency financial markets and limit order books.* Annals of Applied Probability.
2. **Jaisson & Rosenbaum** (2015). *Limit theorems for nearly unstable Hawkes processes.* Annals of Applied Probability, 25(2), 600-631.
3. **Bacry, Mastromatteo & Muzy** (2015). *Hawkes processes in finance.* Market Microstructure and Liquidity, 1(01), 1550005.
4. **Mandelbrot & Van Ness** (1968). *Fractional Brownian motions, fractional noises and applications.* SIAM Review, 10(4), 422-437.
5. **Gatheral, Jaisson & Rosenbaum** (2018). *Volatility is rough.* Quantitative Finance, 18(6), 933-949.
6. **Ogata** (1981). *On Lewis' simulation method for point processes.* IEEE Transactions on Information Theory, 27(1), 23-31.
7. **Hosking** (1984). *Modeling persistence in hydrological time series using fractional differencing.* Water Resources Research, 20(12), 1898-1908.
+73
View File
@@ -0,0 +1,73 @@
Risk Measures: VaR and CVaR
============================
The module :code:`risk_measures` provides Value-at-Risk and Conditional
Value-at-Risk estimators together with a convex CVaR minimisation
solver over the unit simplex.
Definitions
-----------
For a real random variable :math:`L` (a *loss*), the Value-at-Risk at
confidence level :math:`\alpha \in (0, 1)` is the lower :math:`\alpha`-
quantile
.. math::
\mathrm{VaR}_\alpha(L)
\;=\;
\inf\!\big\{ \ell \in \mathbb{R} : \mathbb{P}(L \le \ell) \ge \alpha \big\}.
The Conditional Value-at-Risk (also called Average Value-at-Risk) is
.. math::
\mathrm{CVaR}_\alpha(L)
\;=\;
\frac{1}{1-\alpha}\,
\int_\alpha^1 \mathrm{VaR}_u(L)\,du.
For a sample :math:`L_1, \dots, L_n` of i.i.d. losses sorted in increasing
order, the empirical CVaR at level :math:`\alpha` is
.. math::
\widehat{\mathrm{CVaR}}_\alpha
\;=\;
\frac{1}{n - k}\, \sum_{i = k+1}^{n} L_{(i)},
\qquad k = \lfloor \alpha\, n \rfloor.
Convex minimisation
-------------------
Rockafellar--Uryasev (2000) showed that
.. math::
\mathrm{CVaR}_\alpha(L)
\;=\;
\min_{\zeta \in \mathbb{R}}\;
\zeta + \frac{1}{1 - \alpha}\,\mathbb{E}\!\big[(L - \zeta)_+\big].
Given samples of a vector :math:`r^{(s)} \in \mathbb{R}^d`,
:code:`minimize_cvar` solves
.. math::
\min_{w \in \Delta_d,\;\zeta \in \mathbb{R}}\;
\zeta + \frac{1}{(1 - \alpha)\, S}\, \sum_{s=1}^S
\big(\zeta - \langle r^{(s)}, w\rangle\big)_+,
over the unit simplex :math:`\Delta_d`, by a projected sub-gradient
method using the Held--Wolfe--Crowder simplex projection.
API
---
.. code-block:: rust
pub fn historical_var(losses: &[f64], alpha: f64) -> Result<f64>;
pub fn parametric_var(mu: f64, sigma: f64, alpha: f64) -> Result<f64>;
pub fn cvar_value(losses: &[f64], alpha: f64) -> Result<f64>;
pub fn minimize_cvar(returns: ArrayView2<f64>, cfg: &CVaRConfig)
-> Result<CVaRResult>;
+36
View File
@@ -0,0 +1,36 @@
# Risk Metrics
Time-series utilities for risk analysis, mean-reversion detection, and bootstrapped P&L distributions.
## Quick start
```python
import numpy as np
from optimizr import (
hurst_exponent_py,
estimate_half_life_py,
bootstrap_returns_py,
compute_risk_metrics_py,
)
returns = np.random.randn(2000) * 0.01
print("Hurst:", hurst_exponent_py(returns))
print("Half-life:", estimate_half_life_py(returns))
metrics = compute_risk_metrics_py(returns)
print(metrics) # mean, std, skew, kurtosis, sharpe
bootstrapped = bootstrap_returns_py(returns, n_samples=1000)
print("Bootstrap samples:", len(bootstrapped))
```
## Rolling and integration helpers
- Use `rolling_hurst_exponent_py` and `rolling_half_life_py` (from `timeseries_utils`) for sliding-window diagnostics on trading pairs.
- Combine with HMM: feed rolling statistics as features for regime detection.
- Pair with DE/Grid search: optimize strategy thresholds while computing half-life inside the objective.
## Practical guidance
- Input should be 1D NumPy arrays of returns; winsorize extreme tails before estimating Hurst/half-life for stability.
- Half-life helps size holding periods for mean-reversion trades; revisit whenever volatility regime changes.
- Bootstrap outputs can feed VaR/ES estimates; increase `n_samples` for tighter confidence bands.
+119
View File
@@ -0,0 +1,119 @@
Path Signatures
===============
The module :code:`signatures` provides truncated tensor signatures
(Lyons 1998), log-signatures, random reservoir projections, and the
Salvi--Cass--Lyons signature kernel.
Truncated Signature
-------------------
For a continuous path :math:`X : [0, T] \to \mathbb{R}^d` of bounded
variation, the *signature* is the formal series
.. math::
S(X)_{0,T}
\;=\;
1 + \sum_{k \ge 1} \sum_{i_1, \dots, i_k}
S^{i_1, \dots, i_k}_{0, T}\,
e_{i_1} \otimes \dots \otimes e_{i_k},
with iterated Stieltjes integrals
.. math::
S^{i_1, \dots, i_k}_{0, T}
\;=\;
\int_{0 < u_1 < \dots < u_k < T}
dX^{i_1}_{u_1}\, \dots\, dX^{i_k}_{u_k}.
For piecewise-linear input with increments :math:`\Delta_n`, the
truncated signature obeys the multiplicative recursion
.. math::
S^{(M)}_{0, t_n}
\;=\;
S^{(M)}_{0, t_{n-1}}\,\otimes_M\,\exp_M(\Delta_n),
where :math:`\exp_M(\Delta) = \sum_{k=0}^M \Delta^{\otimes k} / k!`.
Log-Signature
-------------
The truncated tensor logarithm
.. math::
\log(S)
\;=\;
\sum_{n \ge 1} \frac{(-1)^{n+1}}{n}\,(S - 1)^{\otimes n}
lives in the truncated free Lie algebra and provides a more
parsimonious representation.
Random Signature
----------------
Following Cuchiero--Schmocker--Teichmann (2023), one drives a random
reservoir on :math:`\mathbb{R}^N`,
.. math::
dZ_t = A_0 Z_t\, dt + \sum_{i=1}^d A_i Z_t\, dX^i_t,
with random matrices :math:`A_i \in \mathbb{R}^{N \times N}` whose
entries are i.i.d. Gaussian with variance :math:`1/N`. The map
:math:`X \mapsto Z_T` is a finite-dimensional random projection of
:math:`S(X)`.
Signature Kernel (Salvi--Cass--Lyons)
-------------------------------------
The signature inner product
.. math::
K(s, t) \;=\; \langle S(X)_{0, s},\; S(Y)_{0, t}\rangle
solves the linear hyperbolic PDE
.. math::
\frac{\partial^2 K}{\partial s\,\partial t}
\;=\;
\langle \dot X_s, \dot Y_t \rangle\, K(s, t),
\qquad
K(s, 0) = K(0, t) = 1.
It is integrated on a uniform grid via the Goursat scheme
.. math::
K_{i+1, j+1}
= K_{i+1, j} + K_{i, j+1} - K_{i, j}
+ \langle \Delta x_i, \Delta y_j\rangle\,
\tfrac{1}{2}(K_{i+1, j} + K_{i, j+1}).
API
---
.. code-block:: rust
pub struct TruncatedSignature {
pub channels: usize,
pub level: usize,
pub tensors: Vec<Vec<f64>>,
}
pub fn path_signature(path: &[Vec<f64>], level: usize) -> Result<TruncatedSignature>;
pub fn log_signature(sig: &TruncatedSignature) -> Result<TruncatedLogSignature>;
pub struct RandomSignatureConfig {
pub reservoir_dim: usize, pub seed: u64, pub variance: f64,
}
pub fn random_signature(path: &[Vec<f64>], cfg: &RandomSignatureConfig)
-> Result<RandomSignatureResult>;
pub fn signature_kernel(x: &[Vec<f64>], y: &[Vec<f64>])
-> Result<SignatureKernelResult>;
@@ -0,0 +1,46 @@
# Sparse Optimization
Sparse PCA, Elastic Net, and BoxTao decomposition with Rust speed.
## Sparse PCA
```python
import numpy as np
from optimizr import sparse_pca_py
X = np.random.randn(500, 20)
components = sparse_pca_py(X, n_components=5, l1_ratio=0.15)
print(components.shape) # (5, 20)
```
- Output: component matrix `(n_components, n_features)`; rows are sparse loadings.
- Tuning: increase `l1_ratio` for harder sparsity; decrease to retain variance.
## Elastic Net
```python
import numpy as np
from optimizr import elastic_net_py
X = np.random.randn(200, 8)
y = np.random.randn(200)
coeffs = elastic_net_py(X, y, l1_ratio=0.3, alpha=0.01)
print(coeffs)
```
- Handles collinearity better than pure Lasso; use for factor shrinkage.
- Sweep `alpha` on a log scale (e.g., $10^{-3}$ to $10^{-1}$) and pick via validation.
## BoxTao decomposition
```python
from optimizr import box_tao_decomposition_py
solution = box_tao_decomposition_py(X)
```
Useful for constrained sparse decomposition problems; the Rust backend keeps iterations fast.
## Practical notes
- Inputs must be NumPy arrays; standardize features for stable conditioning.
- For high dimensional data, start with fewer components/features to avoid over-regularization.
- Combine with risk metrics: use sparse loadings to build interpretable factors, then evaluate with `compute_risk_metrics_py`.
+67
View File
@@ -0,0 +1,67 @@
Topological Data Analysis
=========================
The module :code:`topology` implements Vietoris--Rips persistent
homology and the bottleneck distance between persistence diagrams.
Vietoris--Rips Filtration
-------------------------
For a finite point cloud :math:`X = \{x_1, \dots, x_n\} \subset \mathbb{R}^d`
and scale :math:`\varepsilon \ge 0`, the Vietoris--Rips complex is
.. math::
\mathrm{VR}_\varepsilon(X)
\;=\;
\big\{ \sigma \subseteq X : \mathrm{diam}(\sigma) \le \varepsilon \big\}.
Increasing :math:`\varepsilon` yields a filtration; the persistent
homology of this filtration produces, for each homological degree
:math:`k`, a multiset of birth/death pairs
.. math::
D_k(X) = \big\{ (b_i, d_i) : 0 \le b_i < d_i \le \infty \big\}.
Persistence Algorithm
---------------------
The boundary matrix :math:`\partial` is built over :math:`\mathbb{Z}/2`
and reduced left-to-right: for each column :math:`j` we cancel its
lowest entry by adding any earlier column with the same low. Pairs
:math:`(\mathrm{low}(j), j)` give birth/death pairs.
Bottleneck Distance
-------------------
For two diagrams :math:`D` and :math:`D'`,
.. math::
d_B(D, D')
\;=\;
\inf_{\eta : D \to D'}\;
\sup_{x \in D}\, \|x - \eta(x)\|_\infty,
where matchings may pair points with the diagonal
:math:`\Delta = \{(t, t) : t \ge 0\}` at cost :math:`(d - b)/2`.
The implementation binary-searches the threshold :math:`\varepsilon`
and certifies a perfect matching by Hopcroft--Karp on the bipartite
graph of admissible edges.
API
---
.. code-block:: rust
pub struct PersistencePair { pub dim: usize, pub birth: f64, pub death: f64 }
pub struct PersistenceDiagram { pub pairs: Vec<PersistencePair> }
pub fn vietoris_rips_filtration(points: &[Vec<f64>], max_dim: usize, max_eps: f64)
-> Result<Vec<Simplex>>;
pub fn persistent_homology(points: &[Vec<f64>], max_dim: usize, max_eps: f64)
-> Result<PersistenceDiagram>;
pub fn bottleneck_distance(d1: &[PersistencePair], d2: &[PersistencePair])
-> Result<f64>;
+122
View File
@@ -0,0 +1,122 @@
Volterra and Fractional Solvers
================================
The module :code:`volterra` collects four CPU-only generic numerical
primitives for Volterra integral equations and related transforms.
Fractional Caputo Adams Solver
------------------------------
For :math:`\alpha \in (0, 1)`, solve
.. math::
D^\alpha h(t) = F(t, h(t)),
\qquad h(0) = h_0,
with the Diethelm--Ford--Freed (2002) fractional Adams predictor--
corrector. Predictor:
.. math::
h^P_{n+1}
= h_0
+ \frac{\Delta t^\alpha}{\alpha\,\Gamma(\alpha)}
\sum_{k=0}^{n}
\big[(n+1-k)^\alpha - (n-k)^\alpha\big]\, F(t_k, h_k).
Corrector:
.. math::
h_{n+1}
= h_0
+ \frac{\Delta t^\alpha}{\Gamma(\alpha + 2)}
\Big[ F(t_{n+1}, h^P_{n+1}) + \sum_{k=0}^{n} a_{n+1, k}\, F(t_k, h_k) \Big],
with
.. math::
a_{n+1, 0} = n^{\alpha + 1} - (n - \alpha)\,(n+1)^\alpha,
a_{n+1, k} = (n - k + 2)^{\alpha + 1} + (n - k)^{\alpha + 1}
- 2\,(n - k + 1)^{\alpha + 1},
\qquad 1 \le k \le n.
Markovian Lift
--------------
A convolution kernel :math:`K(t)` admitting
.. math::
K(t) = \int_0^\infty e^{-\gamma t}\, \nu(d\gamma)
is approximated by
.. math::
K(t) \;\approx\; \sum_{j=1}^N c_j\, e^{-\gamma_j t},
\qquad c_j \ge 0,
with rates :math:`\gamma_j` on a geometric grid and weights fitted by
non-negative least squares.
Generic Volterra Equation
-------------------------
For
.. math::
y(t) = g(t) + \int_0^t K(t - s, y(s))\, ds,
the trapezoidal product-integration scheme reads
.. math::
y_n = g_n + \Delta t\,\Big[
\tfrac{1}{2} K(t_n, y_0)
+ \sum_{k=1}^{n-1} K(t_n - t_k, y_k)
+ \tfrac{1}{2} K(0, y_n) \Big],
solved implicitly by fixed-point iteration on :math:`y_n`.
Fourier Inversion
-----------------
Recover a density from a characteristic function :math:`\varphi(u)` via
.. math::
f(x) \;\approx\;
\frac{\Delta u}{\pi}\,
\sum_{k=0}^{N_u - 1}
w_k \big[\,\Re \varphi(u_k)\,\cos(u_k x)
+ \Im \varphi(u_k)\,\sin(u_k x)\,\big],
with trapezoidal weights :math:`w_k`.
API
---
.. code-block:: rust
pub fn solve_fractional_ode<F: Fn(f64, f64) -> f64>(
h0: f64, alpha: f64, t_horizon: f64, n_steps: usize, rhs: F,
) -> Result<FractionalOdeResult>;
pub fn geometric_grid_lift<K: Fn(f64) -> f64>(
kernel: K, t_samples: &[f64],
n_factors: usize, gamma_min: f64, gamma_max: f64, nnls_iter: usize,
) -> Result<MarkovianLift>;
pub fn solve_volterra<G, K>(
g: G, kernel: K, t_horizon: f64, n_steps: usize,
fixed_point_iter: usize, fixed_point_tol: f64,
) -> Result<VolterraResult>;
pub fn fourier_invert<P: Fn(f64) -> (f64, f64)>(
phi: P, x_grid: &[f64], u_max: f64, n_u: usize,
) -> Result<DensityResult>;
+51
View File
@@ -0,0 +1,51 @@
Discrete and Maximum-Overlap Wavelet Transforms
================================================
The module :code:`timeseries_utils::wavelet` provides Haar and Daubechies
wavelet transforms with periodic boundary handling.
Filter banks
------------
For an orthogonal scaling filter :math:`\{h_k\}_{k=0}^{L-1}` the quadrature
mirror filter (QMF) is
.. math::
g_k = (-1)^k\, h_{L - 1 - k},
so that :math:`\sum_k h_k = \sqrt{2}` and :math:`\sum_k g_k = 0`.
DWT (one level, periodic)
-------------------------
For an input vector :math:`x \in \mathbb{R}^N` with :math:`N` even,
.. math::
a_n = \sum_{k=0}^{L-1} h_k\, x_{(2n + k)\bmod N},
\qquad
d_n = \sum_{k=0}^{L-1} g_k\, x_{(2n + k)\bmod N},
\qquad n = 0, \dots, N/2 - 1.
Successive levels apply the same filter to the previous approximation
:math:`a^{(j)}`.
MODWT (Maximum Overlap)
-----------------------
The MODWT does not downsample: at level :math:`j`, the filter is dilated
by inserting :math:`2^{j-1} - 1` zeros between successive taps and applied
in a periodic convolution. The result is shift-invariant.
API
---
.. code-block:: rust
pub enum WaveletFamily { Haar, Daubechies(u8) }
pub fn scaling_filter(family: WaveletFamily) -> Result<Vec<f64>>;
pub fn qmf(h: &[f64]) -> Vec<f64>;
pub fn dwt_step(x: &[f64], h: &[f64], g: &[f64]) -> (Vec<f64>, Vec<f64>);
pub fn dwt(x: &[f64], family: WaveletFamily, levels: usize) -> Result<Vec<Vec<f64>>>;
pub fn modwt_step(x: &[f64], h: &[f64], g: &[f64], level: usize) -> (Vec<f64>, Vec<f64>);
+45
View File
@@ -0,0 +1,45 @@
# API: differential_evolution
```python
from optimizr import differential_evolution
best_x, best_fx = differential_evolution(
objective_fn,
bounds,
popsize=15,
maxiter=1000,
f=None, # mutation factor (auto if None)
cr=None, # crossover rate (auto if None)
strategy="rand1", # rand1, best1, currenttobest1, rand2, best2
seed=None,
tol=1e-6,
atol=1e-8,
track_history=False, # keep per-iter best
parallel=False, # Python callbacks stay sequential; see Rust path below
adaptive=False, # jDE when True
constraint_penalty=1000.0,
)
```
- `objective_fn`: callable `f(x: np.ndarray) -> float`
- `bounds`: list of `(min, max)` tuples
- Returns `(best_x: np.ndarray, best_fx: float)`
## Parallel Rust entry point
For built-in benchmark objectives (no Python callbacks), use the Rust-native path with Rayon:
```python
from optimizr import parallel_differential_evolution_rust
result = parallel_differential_evolution_rust(
objective_name="rastrigin", # sphere, rosenbrock, ackley, griewank
bounds=[(-5, 5)] * 20,
maxiter=500,
parallel=True,
)
```
## Notes
- Adaptive control uses jDE in the current Python API; SHADE/L-SHADE live in Rust and will surface in a future release.
- Use `track_history=True` to export convergence curves for benchmarking.
+14
View File
@@ -0,0 +1,14 @@
# API: grid_search
```python
from optimizr import grid_search
best_params, best_score = grid_search(
objective_fn,
param_grid,
)
```
- `objective_fn`: callable receiving a dict of parameters and returning a scalar loss
- `param_grid`: dict of name -> list of values to enumerate
- Returns `(best_params: dict, best_score: float)`
+571
View File
@@ -0,0 +1,571 @@
# API Reference: Hidden Markov Model (HMM)
The `HMM` class provides a complete implementation of Hidden Markov Models with Gaussian emissions for regime detection, time series modeling, and state inference.
## Quick Start
```python
from optimizr import HMM
import numpy as np
# Create model with 2 hidden states (e.g., bull/bear market)
model = HMM(n_states=2)
# Train on returns data
returns = np.random.randn(1000, 1) # Should be 2D: (n_samples, n_features)
model.fit(returns, n_iterations=100, tolerance=1e-6)
# Decode most likely state sequence (Viterbi)
states = model.predict(returns)
# Compute log-likelihood (for model comparison)
logp = model.score(returns)
print(f"Log-likelihood: {logp:.2f}")
print(f"Decoded states: {states[:10]}")
```
## Constructor
### `HMM(n_states: int)`
Creates a new Hidden Markov Model with Gaussian emissions.
**Parameters:**
- `n_states` (int): Number of hidden states/regimes. Common choices:
- `n_states=2`: Binary regime (e.g., bull/bear, high/low volatility)
- `n_states=3`: Three-regime model (e.g., bull/sideways/bear)
- `n_states>3`: Fine-grained regime detection (requires more data)
**Returns:**
- `HMM` object with random initialization
**Initialization:**
- Transition matrix $A$: Uniform with slight self-transition bias
- Initial state distribution $\pi$: Uniform
- Emission parameters (means $\mu_i$, covariances $\Sigma_i$): From K-means clustering
**Example:**
```python
# Binary regime model
hmm_2 = HMM(n_states=2)
# Three-regime model for more nuanced detection
hmm_3 = HMM(n_states=3)
```
**When to use:**
- `n_states=2`: Most common, sufficient for many applications
- Higher `n_states`: When you have strong prior belief in multiple regimes and sufficient data (>1000 samples per state)
## Methods
### `fit(X, n_iterations=100, tolerance=1e-6, n_init=1, random_state=None)`
Trains the HMM on observed data using the Baum-Welch (Expectation-Maximization) algorithm.
**Parameters:**
- `X` (np.ndarray): Training data of shape `(n_samples, n_features)`
- For univariate time series: reshape to `(n, 1)` with `X.reshape(-1, 1)`
- For multivariate: pass directly as `(n, d)` where `d` is feature dimension
- `n_iterations` (int, default=100): Maximum number of EM iterations
- Typical range: 50-200
- More iterations → better convergence but slower training
- `tolerance` (float, default=1e-6): Convergence threshold
- Algorithm stops when log-likelihood improvement < `tolerance`
- Typical range: 1e-8 to 1e-4
- Smaller values → tighter convergence but more iterations
- `n_init` (int, default=1): Number of random initializations
- The best model (highest log-likelihood) is kept
- Recommended: 5-10 for production models (helps avoid local minima)
- `random_state` (int, optional): Random seed for reproducibility
**Returns:**
- `self`: The fitted HMM object (for method chaining)
**Algorithm: Baum-Welch (EM for HMMs)**
The Baum-Welch algorithm iteratively refines model parameters:
1. **E-step**: Compute state occupation probabilities
- Forward pass: $\alpha_t(i) = P(O_1, \ldots, O_t, S_t = i \mid \lambda)$
- Backward pass: $\beta_t(i) = P(O_{t+1}, \ldots, O_T \mid S_t = i, \lambda)$
- State probabilities: $\gamma_t(i) = \frac{\alpha_t(i)\beta_t(i)}{\sum_j \alpha_t(j)\beta_t(j)}$
- Transition probabilities: $\xi_t(i,j) = \frac{\alpha_t(i)a_{ij}b_j(O_{t+1})\beta_{t+1}(j)}{\sum_{i,j}\alpha_t(i)a_{ij}b_j(O_{t+1})\beta_{t+1}(j)}$
2. **M-step**: Update model parameters
- Initial probabilities: $\pi_i = \gamma_1(i)$
- Transition matrix: $a_{ij} = \frac{\sum_{t=1}^{T-1}\xi_t(i,j)}{\sum_{t=1}^{T-1}\gamma_t(i)}$
- Emission means: $\mu_i = \frac{\sum_{t=1}^T \gamma_t(i) O_t}{\sum_{t=1}^T \gamma_t(i)}$
- Emission covariances: $\Sigma_i = \frac{\sum_{t=1}^T \gamma_t(i)(O_t - \mu_i)(O_t - \mu_i)^T}{\sum_{t=1}^T \gamma_t(i)}$
3. **Convergence**: Repeat until log-likelihood change < tolerance
**Example:**
```python
import numpy as np
from optimizr import HMM
# Simulate two-regime data
np.random.seed(42)
n = 2000
# Regime 1: low volatility (first 1000 samples)
regime1 = np.random.normal(0.0, 0.5, 1000)
# Regime 2: high volatility (last 1000 samples)
regime2 = np.random.normal(0.0, 2.0, 1000)
data = np.concatenate([regime1, regime2]).reshape(-1, 1)
# Train HMM
hmm = HMM(n_states=2)
hmm.fit(data, n_iterations=200, tolerance=1e-6, n_init=5)
print("Training complete")
```
**Convergence diagnostics:**
```python
# Plot log-likelihood over iterations (requires storing history)
# Check if converged before max_iter
# Verify parameters make sense (e.g., distinct means for each state)
```
**Typical training time:**
- 1000 samples, 2 states, 100 iterations: ~50-100ms
- 10000 samples, 3 states, 200 iterations: ~500ms-1s
### `predict(X)`
Decodes the most likely sequence of hidden states using the Viterbi algorithm.
**Parameters:**
- `X` (np.ndarray): Observation sequence of shape `(n_samples, n_features)`
- Must match feature dimension used in `fit()`
**Returns:**
- `states` (np.ndarray): Most likely state sequence of shape `(n_samples,)`
- Values are integers in range `[0, n_states-1]`
**Algorithm: Viterbi**
The Viterbi algorithm finds the globally optimal state sequence:
1. **Initialization**: $\delta_1(i) = \pi_i \cdot b_i(O_1)$
2. **Recursion**: $\delta_t(j) = \max_i[\delta_{t-1}(i) \cdot a_{ij}] \cdot b_j(O_t)$
3. **Termination**: $P^* = \max_i[\delta_T(i)]$
4. **Backtracking**: Trace back from $\arg\max_i[\delta_T(i)]$ to recover state sequence
**Complexity:** $O(T \cdot K^2)$ where $T$ is sequence length, $K$ is number of states
**Example:**
```python
# After training (see fit() example)
states = hmm.predict(data)
# Analyze regime distribution
unique, counts = np.unique(states, return_counts=True)
for state, count in zip(unique, counts):
print(f"State {state}: {count} samples ({count/len(states)*100:.1f}%)")
# Identify regime switches
switches = np.where(np.diff(states) != 0)[0]
print(f"Number of regime switches: {len(switches)}")
# Use for trading: buy in regime 0, sell in regime 1
current_state = states[-1]
if current_state == 0:
print("Signal: BUY (low volatility regime)")
else:
print("Signal: SELL (high volatility regime)")
```
**Use cases:**
- **Regime detection**: Identify market states (bull/bear, high/low vol)
- **Trading signals**: Generate buy/sell signals based on regime
- **Risk management**: Adjust position size based on estimated regime
- **Anomaly detection**: Flag unusual regime transitions
### `score(X)`
Computes the log-likelihood of the observation sequence under the fitted model.
**Parameters:**
- `X` (np.ndarray): Observation sequence of shape `(n_samples, n_features)`
**Returns:**
- `logp` (float): Log-likelihood $\log P(O \mid \lambda)$
**Algorithm: Forward Algorithm**
The forward algorithm efficiently computes the likelihood:
1. **Initialization**: $\alpha_1(i) = \pi_i \cdot b_i(O_1)$
2. **Induction**: $\alpha_t(j) = \left[\sum_{i=1}^K \alpha_{t-1}(i) \cdot a_{ij}\right] \cdot b_j(O_t)$
3. **Termination**: $P(O \mid \lambda) = \sum_{i=1}^K \alpha_T(i)$
**Numerical stability:** Uses log-space computation with scaling to avoid underflow.
**Example:**
```python
# Model comparison: which number of states fits best?
logp_scores = {}
for n_states in [2, 3, 4]:
hmm = HMM(n_states=n_states)
hmm.fit(data, n_iterations=100)
logp = hmm.score(data)
logp_scores[n_states] = logp
print(f"{n_states} states: log-likelihood = {logp:.2f}")
# Higher log-likelihood is better (but watch for overfitting)
best_k = max(logp_scores, key=logp_scores.get)
print(f"Best model: {best_k} states")
# Use BIC for model selection (penalizes complexity)
def bic(logp, n_params, n_samples):
return -2 * logp + n_params * np.log(n_samples)
n_samples = len(data)
for n_states in [2, 3, 4]:
n_params = n_states**2 + 2*n_states # Approx: A, pi, means, variances
bic_score = bic(logp_scores[n_states], n_params, n_samples)
print(f"{n_states} states: BIC = {bic_score:.2f}")
```
**Use cases:**
- **Model selection**: Compare models with different `n_states` using BIC/AIC
- **Convergence monitoring**: Track log-likelihood during training
- **Outlier detection**: Low likelihood → data doesn't match model
- **Model reliability**: Higher likelihood → better fit (but watch overfitting)
## Complete Example: Market Regime Detection
Here's a complete workflow for detecting market regimes in financial data:
```python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from optimizr import HMM
# 1. Load financial data (example: S&P 500 returns)
# In practice, load from your data source
np.random.seed(42)
n_samples = 2000
# Simulate returns with regime changes
returns = []
for i in range(n_samples):
if i < 500: # Bull market
returns.append(np.random.normal(0.001, 0.01))
elif i < 1000: # Correction
returns.append(np.random.normal(-0.002, 0.02))
elif i < 1500: # Recovery
returns.append(np.random.normal(0.001, 0.015))
else: # Bear market
returns.append(np.random.normal(-0.001, 0.025))
returns = np.array(returns).reshape(-1, 1)
# 2. Train HMM with multiple initializations
print("Training HMM...")
hmm = HMM(n_states=3) # 3 regimes: bull, neutral, bear
hmm.fit(returns, n_iterations=200, tolerance=1e-6, n_init=10)
# 3. Decode regimes
states = hmm.predict(returns)
# 4. Analyze regimes
print("\nRegime Statistics:")
for state_id in range(3):
mask = (states == state_id)
state_returns = returns[mask]
mean_ret = np.mean(state_returns)
std_ret = np.std(state_returns)
count = np.sum(mask)
print(f"State {state_id}:")
print(f" Count: {count} ({count/len(returns)*100:.1f}%)")
print(f" Mean return: {mean_ret:.4f}")
print(f" Volatility: {std_ret:.4f}")
print(f" Sharpe (annualized): {mean_ret/std_ret * np.sqrt(252):.2f}")
# 5. Identify regime switches
switches = np.where(np.diff(states) != 0)[0] + 1
print(f"\nRegime switches: {len(switches)}")
print(f"Average regime duration: {len(returns)/len(switches):.1f} days")
# 6. Visualize regimes
plt.figure(figsize=(14, 8))
# Plot returns with regime colors
plt.subplot(3, 1, 1)
colors = ['green', 'yellow', 'red']
for state_id in range(3):
mask = (states == state_id)
plt.scatter(np.where(mask)[0], returns[mask],
c=colors[state_id], alpha=0.5, s=10,
label=f'State {state_id}')
plt.ylabel('Returns')
plt.title('Returns colored by HMM regime')
plt.legend()
plt.grid(True, alpha=0.3)
# Plot cumulative returns per regime
plt.subplot(3, 1, 2)
cumulative = np.cumsum(returns.flatten())
plt.plot(cumulative, color='black', linewidth=1)
for switch in switches:
plt.axvline(switch, color='red', alpha=0.3, linestyle='--')
plt.ylabel('Cumulative Returns')
plt.title('Cumulative returns with regime switches')
plt.grid(True, alpha=0.3)
# Plot state sequence
plt.subplot(3, 1, 3)
plt.plot(states, linewidth=0.5)
plt.ylabel('State')
plt.xlabel('Time')
plt.title('Decoded state sequence')
plt.yticks(range(3))
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('hmm_regime_detection.png', dpi=150)
print("\nPlot saved to hmm_regime_detection.png")
# 7. Generate trading signals
current_state = states[-1]
state_returns = returns[states == current_state]
expected_return = np.mean(state_returns)
expected_vol = np.std(state_returns)
print(f"\nCurrent regime: State {current_state}")
print(f"Expected return: {expected_return:.4f}")
print(f"Expected volatility: {expected_vol:.4f}")
if expected_return > 0.0005:
signal = "BUY"
position_size = 1.0
elif expected_return < -0.0005:
signal = "SELL"
position_size = 0.0
else:
signal = "HOLD"
position_size = 0.5
print(f"Trading signal: {signal}")
print(f"Recommended position size: {position_size*100:.0f}%")
```
## Advanced Usage
### Model Selection with BIC
Choose the optimal number of states using Bayesian Information Criterion:
```python
from optimizr import HMM
import numpy as np
def bic_score(hmm, X):
"""Compute BIC for HMM: BIC = -2*log(L) + k*log(n)"""
logp = hmm.score(X)
n_states = hmm.n_states # Assuming this attribute exists
n_features = X.shape[1]
# Parameters: transition matrix + initial prob + means + covariances
k = n_states**2 + n_states + n_states*n_features + n_states*n_features**2
n = X.shape[0]
return -2*logp + k*np.log(n)
# Test different numbers of states
results = []
for n_states in range(2, 6):
hmm = HMM(n_states=n_states)
hmm.fit(data, n_iterations=100, n_init=5)
bic = bic_score(hmm, data)
logp = hmm.score(data)
results.append((n_states, logp, bic))
print(f"{n_states} states: log-likelihood={logp:.2f}, BIC={bic:.2f}")
# Best model has lowest BIC
best_n_states = min(results, key=lambda x: x[2])[0]
print(f"\nBest model: {best_n_states} states")
```
### Integration with Optimal Control
Combine HMM regime detection with regime-specific optimal control:
```python
from optimizr import HMM, estimate_ou_params_py, solve_hjb_py
# 1. Detect regimes with HMM
returns = np.diff(spread)
hmm = HMM(n_states=2)
hmm.fit(returns.reshape(-1, 1), n_iterations=100)
regimes = hmm.predict(returns.reshape(-1, 1))
# 2. Estimate OU parameters per regime
thresholds = {}
for regime_id in range(2):
mask = (regimes == regime_id)
spread_regime = spread[1:][mask] # Align with returns
# Estimate OU parameters
kappa, theta, sigma, half_life = estimate_ou_params_py(
spread_regime, dt=1/252
)
# Solve HJB for regime-specific thresholds
lower, upper, _, _ = solve_hjb_py(
kappa=kappa, theta=theta, sigma=sigma,
rho=0.04, transaction_cost=0.001
)
thresholds[regime_id] = (lower, upper)
print(f"Regime {regime_id}: κ={kappa:.2f}, thresholds=({lower:.3f}, {upper:.3f})")
# 3. Apply regime-aware trading
current_regime = regimes[-1]
lower, upper = thresholds[current_regime]
current_spread = spread[-1]
if current_spread < lower:
action = "BUY"
elif current_spread > upper:
action = "SELL"
else:
action = "HOLD"
print(f"\nCurrent regime: {current_regime}")
print(f"Current spread: {current_spread:.3f}")
print(f"Thresholds: ({lower:.3f}, {upper:.3f})")
print(f"Action: {action}")
```
### Multivariate HMM
For multiple features (e.g., returns + volume + volatility):
```python
# Prepare multivariate data
returns = np.random.randn(1000, 1)
volume = np.random.randn(1000, 1)
volatility = np.random.randn(1000, 1)
# Stack features
X = np.hstack([returns, volume, volatility]) # Shape: (1000, 3)
# Train multivariate HMM
hmm = HMM(n_states=3)
hmm.fit(X, n_iterations=150)
# Decode regimes based on all features
states = hmm.predict(X)
# Each state now captures joint patterns in returns, volume, and volatility
```
## Best Practices
### Data Preparation
1. **Scaling**: Standardize features to similar scales
```python
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
```
2. **Stationarity**: Ensure time series is stationary (use returns, not prices)
```python
returns = np.diff(np.log(prices)) # Log returns
```
3. **Outlier handling**: Winsorize extreme values
```python
from scipy.stats import mstats
X_winsorized = mstats.winsorize(X, limits=[0.01, 0.01])
```
### Model Training
1. **Multiple initializations**: Use `n_init=5-10` to avoid local minima
2. **Convergence**: Monitor log-likelihood, ensure convergence before `max_iter`
3. **Validation**: Use held-out data to verify generalization
### Parameter Selection
1. **Number of states**: Start with 2-3, increase if necessary
2. **Iterations**: 100-200 typically sufficient
3. **Tolerance**: 1e-6 for production, 1e-4 for quick experimentation
### Practical Tips
1. **Minimum data**: Use at least 500 samples per state (1000+ for 2-state model)
2. **Regime persistence**: Check average regime duration is meaningful (not too short)
3. **Physical interpretation**: Verify decoded regimes make sense (e.g., high-vol state has higher variance)
4. **Robustness**: Test on multiple time periods, verify stability
## Troubleshooting
### Model not converging
- **Symptom**: Log-likelihood oscillating or not improving
- **Fix**: Increase `n_iterations`; try different `n_init`; check data scaling
### All samples assigned to one state
- **Symptom**: `predict()` returns all 0s or all 1s
- **Fix**: Reduce `n_states`; check data has sufficient variation; verify stationarity
### Unrealistic regime switches
- **Symptom**: State changes every few samples
- **Fix**: Add transition probability constraints (requires model extension); increase minimum regime duration
### Poor out-of-sample performance
- **Symptom**: High in-sample log-likelihood but poor predictions on new data
- **Fix**: Reduce `n_states` (overfitting); use cross-validation; add regularization
## Performance Characteristics
### Computational Complexity
- **Training (Baum-Welch)**: $O(I \cdot T \cdot K^2)$
- $I$: number of iterations (~100-200)
- $T$: sequence length
- $K$: number of states
- **Prediction (Viterbi)**: $O(T \cdot K^2)$
- **Scoring (Forward)**: $O(T \cdot K^2)$
### Memory Requirements
- Model parameters: $O(K^2 + K \cdot d^2)$ where $d$ is feature dimension
- Forward/backward matrices: $O(T \cdot K)$
### Typical Runtimes (on modern CPU)
- Train (1000 samples, 2 states, 100 iter): ~50-100ms
- Train (10000 samples, 3 states, 200 iter): ~500ms-1s
- Predict (1000 samples, 2 states): ~5-10ms
- Score (1000 samples, 2 states): ~5-10ms
## References
### Hidden Markov Models
- **Rabiner, L. R.** (1989). A tutorial on hidden Markov models and selected applications in speech recognition. *Proceedings of the IEEE*, 77(2), 257-286.
- **Murphy, K. P.** (2012). *Machine Learning: A Probabilistic Perspective*. MIT Press. (Chapter 17: Markov and hidden Markov models)
### Financial Applications
- **Guidolin, M., & Timmermann, A.** (2008). International asset allocation under regime switching, skew, and kurtosis preferences. *The Review of Financial Studies*, 21(2), 889-935.
- **Nystrup, P., Madsen, H., & Lindström, E.** (2015). Stylised facts of financial time series and hidden Markov models in continuous time. *Quantitative Finance*, 15(9), 1531-1541.
- **Ang, A., & Bekaert, G.** (2002). Regime switches in interest rates. *Journal of Business & Economic Statistics*, 20(2), 163-182.
### Algorithms
- **Forney, G. D.** (1973). The Viterbi algorithm. *Proceedings of the IEEE*, 61(3), 268-278.
- **Baum, L. E., Petrie, T., Soules, G., & Weiss, N.** (1970). A maximization technique occurring in the statistical analysis of probabilistic functions of Markov chains. *The Annals of Mathematical Statistics*, 41(1), 164-171.
## See Also
- [HMM Algorithms](../algorithms/hmm.md) - Detailed mathematical foundations (Forward-Backward, Viterbi, Baum-Welch)
- [Optimal Control](../algorithms/optimal_control.md) - Integrate HMM regimes with optimal control
- [Optimal Control API](optimal_control.md) - API reference for control algorithms
+16
View File
@@ -0,0 +1,16 @@
# API: HMM
```python
from optimizr import HMM
model = HMM(n_states=2)
model.fit(X, n_iterations=100, tolerance=1e-6)
states = model.predict(X)
logp = model.score(X)
```
Parameters
- `n_states`: number of hidden regimes
- `fit(X, n_iterations=100, tolerance=1e-6)`: train with Baum-Welch
- `predict(X)`: Viterbi decoding → `np.ndarray`
- `score(X)`: log-likelihood
+21
View File
@@ -0,0 +1,21 @@
# API: mcmc_sample
```python
from optimizr import mcmc_sample
samples = mcmc_sample(
log_likelihood_fn,
data,
initial_params,
param_bounds,
n_samples=10000,
burn_in=1000,
proposal_std=0.1,
)
```
- `log_likelihood_fn(params, data) -> float`
- `data`: np.ndarray passed through to the likelihood
- `initial_params`: np.ndarray starting point
- `param_bounds`: list of `(min, max)` tuples
- Returns `samples: np.ndarray` of shape `(n_samples, n_params)`
+117
View File
@@ -0,0 +1,117 @@
# API: Optimal Control / Kalman
High-level bindings exposed by the `optimizr` Python package. All functions require the Rust extension (`optimizr._core`).
**When to use this module**
- Threshold trading / switching problems solved via HJB (with and without frictions)
- State estimation and smoothing (Kalman, EKF, UKF)
- Parameter inference for mean-reverting spreads (OU) feeding into control logic
## HamiltonJacobiBellman (HJB) solvers
```python
from optimizr import solve_hjb_py, solve_hjb_full_py
# Switching boundaries for a mean-reverting spread (OU process)
lower, upper, residual, iters = solve_hjb_py(
kappa=3.0, theta=0.0, sigma=0.2, rho=0.04,
transaction_cost=0.001, n_points=400, max_iter=4000,
tolerance=1e-7, n_std=5.0,
)
# Full state (grid + derivatives) for research/visualization
(lower, upper, residual, iters, x_grid, value, grad, hess) = solve_hjb_full_py(
kappa=3.0, theta=0.0, sigma=0.2, rho=0.04,
transaction_cost=0.001, n_points=400,
)
```
### Model
We assume an OrnsteinUhlenbeck process $dX_t = \kappa(\theta - X_t)\,dt + \sigma\,dW_t$ with quadratic transaction costs. The HJB on grid $x \in [-n_{std}\,\sigma/\sqrt{\kappa},\; n_{std}\,\sigma/\sqrt{\kappa}]$ solves
$$
\rho V(x) = \min\Big\{ \tfrac12 \sigma^2 V_{xx}(x) + \kappa(\theta - x) V_x(x),\; V(x) + c_{\text{buy}},\; V(x) + c_{\text{sell}} \Big\}.
$$
`solve_hjb_py` returns optimal buy/sell thresholds; `solve_hjb_full_py` also returns $V$, $V_x$, and $V_{xx}$ for diagnostics.
**Diagnostic tips:**
- Plot $V_x$ to verify smoothness near the boundaries; kinks often signal insufficient grid resolution.
- Track `residual` and `iterations` to spot non-convergence; loosen `tolerance` or increase `max_iter` if needed.
## OU parameter estimation
```python
import numpy as np
from optimizr import estimate_ou_params_py
spread = np.random.randn(10_000)
kappa, theta, sigma, half_life = estimate_ou_params_py(spread, dt=1/252)
```
Method-of-moments / MLE estimation for
$$
X_{t+1} = X_t e^{-\kappa \Delta t} + \theta(1-e^{-\kappa \Delta t}) + \eta_t, \quad \eta_t \sim \mathcal{N}\Big(0,\; \tfrac{\sigma^2}{2\kappa}(1-e^{-2\kappa \Delta t})\Big).
$$
Returns $(\kappa, \theta, \sigma, \text{half\_life})$.
**Practical guidance:** Use at least a few thousand samples for stable estimates; heavy-tailed series benefit from pre-whitening or winsorizing before fitting.
## Backtesting optimal switching
```python
from optimizr import backtest_optimal_switching_py
metrics = backtest_optimal_switching_py(
spread=spread,
lower_bound=lower,
upper_bound=upper,
transaction_cost=0.001,
)
(total_return, sharpe, max_dd, n_trades, win_rate, pnl_path) = metrics
```
Applies HJB thresholds to historical spreads and reports return, Sharpe ratio, drawdown, trade count, win rate, and PnL path.
**What to inspect:**
- `win_rate` alongside `max_drawdown` to balance aggressiveness
- PnL path for regime shifts; combine with HMM states if you need regime-aware controls
## Kalman filtering (linear, EKF, UKF)
```python
import numpy as np
from optimizr import LinearKalmanFilter, KalmanState
F = [[1.0, 1.0], [0.0, 1.0]] # constant-velocity model
H = [[1.0, 0.0]] # observe position only
Q = [[1e-4, 0.0], [0.0, 1e-4]]
R = [[1e-2]]
kf = LinearKalmanFilter(
f_matrix=F,
h_matrix=H,
q_matrix=Q,
r_matrix=R,
initial_state=[0.0, 0.0],
initial_covariance=[[1.0, 0.0], [0.0, 1.0]],
)
kf.predict(control=[0.0, 0.0]) # optional control input via B matrix
kf.update(observation=[1.2])
state = kf.get_state() # KalmanState with getters for mean/cov
# Batch filtering
result = kf.filter(observations=[[1.0], [1.4], [1.9]], controls=None)
states = result.get_states()
log_likelihoods = result.get_log_likelihoods()
```
### Notes
- `LinearKalmanFilter` implements `predict`, `update`, and batch `filter`.
- `KalmanState` exposes `get_state()`, `get_covariance()`, and `get_log_likelihood()`.
- Extended/Unscented Kalman filters share the same interface (see `UnscentedKalmanFilter` in the Rust module) and are exported through the same bindings.
- For smoothing, use the RauchTungStriebel smoother (`RTSSmoother`) available in the bindings.
**Conceptual picture:** Kalman filtering = prediction (dynamics prior) + correction (measurement residual). EKF linearizes $f, h$; UKF propagates sigma points for better nonlinear fidelity. RTS smoothing runs backward in time to refine all past states.
See [`examples/notebooks/03_optimal_control_tutorial.ipynb`](https://github.com/ThotDjehuty/optimiz-r/blob/main/examples/notebooks/03_optimal_control_tutorial.ipynb) for end-to-end usage combining HJB thresholds, OU estimation, and filtering.
+117
View File
@@ -0,0 +1,117 @@
# API: Optimal Control / Kalman
High-level bindings exposed by the `optimizr` Python package. All functions require the Rust extension (`optimizr._core`).
**When to use this module**
- Threshold trading / switching problems solved via HJB (with and without frictions)
- State estimation and smoothing (Kalman, EKF, UKF)
- Parameter inference for mean-reverting spreads (OU) feeding into control logic
## HamiltonJacobiBellman (HJB) solvers
```python
from optimizr import solve_hjb_py, solve_hjb_full_py
# Switching boundaries for a mean-reverting spread (OU process)
lower, upper, residual, iters = solve_hjb_py(
kappa=3.0, theta=0.0, sigma=0.2, rho=0.04,
transaction_cost=0.001, n_points=400, max_iter=4000,
tolerance=1e-7, n_std=5.0,
)
# Full state (grid + derivatives) for research/visualization
(lower, upper, residual, iters, x_grid, value, grad, hess) = solve_hjb_full_py(
kappa=3.0, theta=0.0, sigma=0.2, rho=0.04,
transaction_cost=0.001, n_points=400,
)
```
### Model
We assume an OrnsteinUhlenbeck process $dX_t = \kappa(\theta - X_t)\,dt + \sigma\,dW_t$ with quadratic transaction costs. The HJB on grid $x \in [-n_{std}\,\sigma/\sqrt{\kappa},\; n_{std}\,\sigma/\sqrt{\kappa}]$ solves
$$
\rho V(x) = \min\Big\{ \tfrac12 \sigma^2 V_{xx}(x) + \kappa(\theta - x) V_x(x),\; V(x) + c_{\text{buy}},\; V(x) + c_{\text{sell}} \Big\}.
$$
`solve_hjb_py` returns optimal buy/sell thresholds; `solve_hjb_full_py` also returns $V$, $V_x$, and $V_{xx}$ for diagnostics.
**Diagnostic tips:**
- Plot $V_x$ to verify smoothness near the boundaries; kinks often signal insufficient grid resolution.
- Track `residual` and `iterations` to spot non-convergence; loosen `tolerance` or increase `max_iter` if needed.
## OU parameter estimation
```python
import numpy as np
from optimizr import estimate_ou_params_py
spread = np.random.randn(10_000)
kappa, theta, sigma, half_life = estimate_ou_params_py(spread, dt=1/252)
```
Method-of-moments / MLE estimation for
$$
X_{t+1} = X_t e^{-\kappa \Delta t} + \theta(1-e^{-\kappa \Delta t}) + \eta_t, \quad \eta_t \sim \mathcal{N}\Big(0,\; \tfrac{\sigma^2}{2\kappa}(1-e^{-2\kappa \Delta t})\Big).
$$
Returns $(\kappa, \theta, \sigma, \text{half\_life})$.
**Practical guidance:** Use at least a few thousand samples for stable estimates; heavy-tailed series benefit from pre-whitening or winsorizing before fitting.
## Backtesting optimal switching
```python
from optimizr import backtest_optimal_switching_py
metrics = backtest_optimal_switching_py(
spread=spread,
lower_bound=lower,
upper_bound=upper,
transaction_cost=0.001,
)
(total_return, sharpe, max_dd, n_trades, win_rate, pnl_path) = metrics
```
Applies HJB thresholds to historical spreads and reports return, Sharpe ratio, drawdown, trade count, win rate, and PnL path.
**What to inspect:**
- `win_rate` alongside `max_drawdown` to balance aggressiveness
- PnL path for regime shifts; combine with HMM states if you need regime-aware controls
## Kalman filtering (linear, EKF, UKF)
```python
import numpy as np
from optimizr import LinearKalmanFilter, KalmanState
F = [[1.0, 1.0], [0.0, 1.0]] # constant-velocity model
H = [[1.0, 0.0]] # observe position only
Q = [[1e-4, 0.0], [0.0, 1e-4]]
R = [[1e-2]]
kf = LinearKalmanFilter(
f_matrix=F,
h_matrix=H,
q_matrix=Q,
r_matrix=R,
initial_state=[0.0, 0.0],
initial_covariance=[[1.0, 0.0], [0.0, 1.0]],
)
kf.predict(control=[0.0, 0.0]) # optional control input via B matrix
kf.update(observation=[1.2])
state = kf.get_state() # KalmanState with getters for mean/cov
# Batch filtering
result = kf.filter(observations=[[1.0], [1.4], [1.9]], controls=None)
states = result.get_states()
log_likelihoods = result.get_log_likelihoods()
```
### Notes
- `LinearKalmanFilter` implements `predict`, `update`, and batch `filter`.
- `KalmanState` exposes `get_state()`, `get_covariance()`, and `get_log_likelihood()`.
- Extended/Unscented Kalman filters share the same interface (see `UnscentedKalmanFilter` in the Rust module) and are exported through the same bindings.
- For smoothing, use the RauchTungStriebel smoother (`RTSSmoother`) available in the bindings.
**Conceptual picture:** Kalman filtering = prediction (dynamics prior) + correction (measurement residual). EKF linearizes $f, h$; UKF propagates sigma points for better nonlinear fidelity. RTS smoothing runs backward in time to refine all past states.
See [`examples/notebooks/03_optimal_control_tutorial.ipynb`](https://github.com/ThotDjehuty/optimiz-r/blob/main/examples/notebooks/03_optimal_control_tutorial.ipynb) for end-to-end usage combining HJB thresholds, OU estimation, and filtering.
+450
View File
@@ -0,0 +1,450 @@
# API Reference: Point Processes
The point processes module provides Rust-accelerated functions for Hawkes process simulation, fractional Brownian motion, and related special functions — all accessible from Python via PyO3.
## Quick Start
```python
import optimizr
import numpy as np
# Simulate a Hawkes process with power-law kernel
events = optimizr.simulate_hawkes(
baseline=0.1,
alpha=0.35,
beta=0.375,
t_max=100.0,
kernel_type="power_law",
seed=42
)
# Simulate fractional Brownian motion
path = optimizr.simulate_fbm(hurst=0.75, n=1000, dt=0.01, seed=42)
# Estimate Hurst exponent
h_est = optimizr.estimate_hurst(path)
print(f"Estimated H: {h_est:.3f}")
```
---
## Hawkes Process Functions
### `simulate_hawkes(baseline, alpha, beta, t_max, kernel_type="exponential", seed=None)`
Simulate a univariate Hawkes process using Ogata's thinning algorithm.
**Parameters:**
- `baseline` (float): Baseline intensity $\nu > 0$. This is the exogenous event rate in the absence of self-excitation. Higher values produce more events even without clustering.
- `alpha` (float): Kernel amplitude parameter.
- For `"exponential"`: peak excitation rate $\alpha$ in $\phi(t) = \alpha e^{-\beta t}$
- For `"power_law"`: scaling constant $K_0$ in $\phi(t) = K_0 (1+t)^{-(1+\alpha_0)}$
- `beta` (float): Kernel decay parameter.
- For `"exponential"`: decay rate $\beta$ (inverse timescale)
- For `"power_law"`: tail exponent $\alpha_0 \in (0, 1)$. Connected to Hurst parameter by $H_0 = 2\alpha_0$.
- `t_max` (float): Maximum simulation time $T$. The process runs on $[0, T]$.
- `kernel_type` (str, default=`"exponential"`): Type of excitation kernel.
- `"exponential"`: Short-memory kernel with exponential decay
- `"power_law"`: Long-memory kernel with power-law tail
- `seed` (int, optional): Random seed for reproducibility.
**Returns:**
- `np.ndarray`: Array of event times $\{t_1, t_2, \ldots, t_n\}$ sorted in ascending order.
**Stability:**
- Exponential: stable when $\alpha / \beta < 1$
- Power-law: stable when $K_0 / \alpha_0 < 1$
**Examples:**
```python
import optimizr
# Markovian self-exciting process (exponential kernel)
events = optimizr.simulate_hawkes(
baseline=1.0,
alpha=0.5, # branching ratio = 0.5/1.0 = 0.5
beta=1.0,
t_max=100.0,
kernel_type="exponential",
seed=42
)
print(f"{len(events)} events, expected ≈ {1.0 / (1 - 0.5) * 100:.0f}")
# Long-memory process (power-law kernel, H₀ = 0.75)
events_pl = optimizr.simulate_hawkes(
baseline=0.1,
alpha=0.35, # K₀
beta=0.375, # α₀ → H₀ = 0.75
t_max=1000.0,
kernel_type="power_law",
seed=42
)
print(f"{len(events_pl)} events with long-memory clustering")
```
**When to use which kernel:**
| Scenario | Kernel | Typical Parameters |
|----------|--------|--------------------|
| High-frequency order arrivals | Exponential | $\alpha=0.5$, $\beta=2.0$ |
| Market microstructure (unified theory) | Power-law | $\alpha_0=0.375$, $K_0 \leq \alpha_0$ |
| Neural spike trains | Exponential | $\alpha=0.3$, $\beta=5.0$ |
| Seismology (aftershocks) | Power-law | $\alpha_0=0.5$, $K_0=0.4$ |
---
### `simulate_bivariate_hawkes(core_buy_times, core_sell_times, phi1_alpha, phi1_beta, phi2_alpha, phi2_beta, t_max, seed=None)`
Simulate a bivariate Hawkes process modeling buy/sell reaction order flow driven by core order flow.
The model captures how buy orders excite more buy orders (**self-excitation**) and sell orders (**cross-excitation**), and vice versa.
**Parameters:**
- `core_buy_times` (np.ndarray): Core buy order arrival times (driver process $F^+$).
- `core_sell_times` (np.ndarray): Core sell order arrival times (driver process $F^-$).
- `phi1_alpha` (float): Self-excitation kernel amplitude (buy→buy, sell→sell).
- `phi1_beta` (float): Self-excitation kernel decay rate.
- `phi2_alpha` (float): Cross-excitation kernel amplitude (buy→sell, sell→buy).
- `phi2_beta` (float): Cross-excitation kernel decay rate.
- `t_max` (float): Maximum simulation time.
- `seed` (int, optional): Random seed.
**Returns:**
- `tuple[np.ndarray, np.ndarray]`: `(buy_times, sell_times)` — arrays of reaction buy and sell event times.
**Stability condition:**
$$
\frac{\phi_{1,\alpha}}{\phi_{1,\beta}} + \frac{\phi_{2,\alpha}}{\phi_{2,\beta}} < 1
$$
**Example:**
```python
import optimizr
import numpy as np
# Core flow: Poisson arrivals
rng = np.random.default_rng(42)
core_buys = np.sort(rng.uniform(0, 100, 200))
core_sells = np.sort(rng.uniform(0, 100, 180))
# Symmetric reaction with moderate cross-excitation
buys, sells = optimizr.simulate_bivariate_hawkes(
core_buy_times=core_buys,
core_sell_times=core_sells,
phi1_alpha=0.3, # Self: L¹ = 0.3
phi1_beta=1.0,
phi2_alpha=0.15, # Cross: L¹ = 0.15
phi2_beta=1.0,
t_max=100.0,
seed=42
)
# Net order imbalance (price signal)
imbalance = len(buys) - len(sells)
print(f"Reaction buys: {len(buys)}, sells: {len(sells)}")
print(f"Order imbalance: {imbalance:+d}")
print(f"Spectral radius: {0.3 + 0.15:.2f}") # 0.45 < 1 → stable
```
---
## Fractional Brownian Motion Functions
### `simulate_fbm(hurst, n, dt=1.0, seed=None)`
Simulate a fractional Brownian motion sample path using Hosking's method (Durbin-Levinson algorithm).
**Parameters:**
- `hurst` (float): Hurst parameter $H \in (0, 1)$.
- $H < 0.5$: Anti-persistent (mean-reverting)
- $H = 0.5$: Standard Brownian motion
- $H > 0.5$: Persistent (trending)
- `n` (int): Number of time steps. The output has $n + 1$ values (including $B^H_0 = 0$).
- `dt` (float, default=1.0): Time step size $\Delta t$. Increments are scaled by $(\Delta t)^H$.
- `seed` (int, optional): Random seed.
**Returns:**
- `np.ndarray`: Array of length $n + 1$ representing the fBM path $\{B^H_0, B^H_{\Delta t}, B^H_{2\Delta t}, \ldots, B^H_{n\Delta t}\}$.
**Complexity:** $O(n^2)$ using Hosking's method (vs $O(n^3)$ for Cholesky).
**Example:**
```python
import optimizr
import numpy as np
import matplotlib.pyplot as plt
# Compare three regimes
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
for i, (h, label) in enumerate([
(0.3, "Anti-persistent"),
(0.5, "Standard BM"),
(0.8, "Persistent")
]):
path = optimizr.simulate_fbm(hurst=h, n=2000, dt=0.001, seed=42)
axes[i].plot(path, linewidth=0.5, color=['red', 'black', 'blue'][i])
axes[i].set_title(f"H = {h} ({label})")
axes[i].set_xlabel("Time step")
axes[i].set_ylabel("B^H(t)")
plt.suptitle("Fractional Brownian Motion: Three Regimes")
plt.tight_layout()
plt.show()
```
---
### `simulate_mixed_fbm(a, b, hurst, n, dt=1.0, seed=None)`
Simulate a mixed fractional Brownian motion $M^H(t) = a \cdot B(t) + b \cdot B^H(t)$.
**Parameters:**
- `a` (float): Coefficient for the standard BM component (diffusive).
- `b` (float): Coefficient for the fBM component (persistent).
- `hurst` (float): Hurst parameter $H$ of the fBM component. Typically $H \in (0.5, 1)$ for persistent flow.
- `n` (int): Number of time steps.
- `dt` (float, default=1.0): Time step size.
- `seed` (int, optional): Random seed.
**Returns:**
- `np.ndarray`: Array of length $n + 1$ representing the mixed fBM path.
**Financial interpretation:**
- The BM component captures short-term noise (market making, latency)
- The fBM component captures long-term persistence (informed trading, herding)
- At short timescales, $H_{\text{eff}} \to 1/2$ (BM dominates)
- At long timescales, $H_{\text{eff}} \to H$ (fBM dominates)
**Example:**
```python
import optimizr
import numpy as np
# Unified theory: aggregate order flow as mfBM
path = optimizr.simulate_mixed_fbm(
a=1.0, # BM weight
b=0.5, # fBM weight
hurst=0.75, # H₀ from unified theory
n=5000,
dt=0.01,
seed=42
)
# Semimartingale check: H > 3/4 allows classical stochastic calculus
is_semimartingale = 0.75 > 0.75 # Borderline case
print(f"Path length: {len(path)}")
print(f"Semimartingale: {is_semimartingale}")
```
---
### `estimate_hurst(data)`
Estimate the Hurst exponent from data using Rescaled Range (R/S) analysis.
**Parameters:**
- `data` (np.ndarray): 1-D time series data (path values, not increments).
**Returns:**
- `float`: Estimated Hurst exponent $\hat{H} \in [0.01, 0.99]$.
**Algorithm:**
1. Partition data into subseries of varying lengths $n_1, n_2, \ldots$
2. For each length, compute the average R/S statistic across subseries
3. Fit $\log(R/S) = H \log(n) + c$ by least-squares regression
**Note:** R/S analysis provides a rough estimate. For more precise estimation, consider DFA (Detrended Fluctuation Analysis) or wavelet methods. The method requires at least 20 data points.
**Example:**
```python
import optimizr
import numpy as np
# Verify estimation accuracy
for h_true in [0.3, 0.5, 0.7, 0.9]:
path = optimizr.simulate_fbm(hurst=h_true, n=5000, dt=1.0, seed=42)
h_est = optimizr.estimate_hurst(path)
print(f"H_true = {h_true:.1f}, H_est = {h_est:.3f}, error = {abs(h_true - h_est):.3f}")
```
---
### `scale_dependent_hurst(data, scales=None)`
Compute scale-dependent Hurst exponents using variance ratios at different time scales.
This function identifies whether data follows a pure fBM or a mixed fBM by examining how the effective Hurst exponent varies across scales.
**Parameters:**
- `data` (np.ndarray): 1-D time series data.
- `scales` (list of int, optional): Time scales to analyze. Default: `[10, 50, 100, 500, 1000, 2000, 5000]`.
**Returns:**
- `dict[int, float]`: Mapping from scale to estimated Hurst exponent at that scale.
**Interpretation:**
- **Constant $H(\Delta)$** across scales → pure fBM
- **$H(\Delta)$ increasing from $\sim 0.5$ to $H$** → mixed fBM (BM at short scales, fBM at long scales)
- **$H(\Delta) \approx 0.5$** at all scales → standard BM (no long memory)
**Example:**
```python
import optimizr
import numpy as np
# Generate mixed fBM (should show scale-dependent H)
path = optimizr.simulate_mixed_fbm(a=1.0, b=1.0, hurst=0.8, n=10000, dt=1.0, seed=42)
hurst_scales = optimizr.scale_dependent_hurst(
data=path,
scales=[10, 25, 50, 100, 250, 500, 1000, 2500]
)
print("Scale | H_effective")
print("-" * 25)
for s, h in sorted(hurst_scales.items()):
indicator = "← BM regime" if h < 0.55 else ("← fBM regime" if h > 0.65 else "← transition")
print(f"{s:>5d} | {h:.4f} {indicator}")
```
---
## Special Functions
### `mittag_leffler_py(alpha, beta, z)`
Compute the generalized Mittag-Leffler function $E_{\alpha,\beta}(z)$.
**Parameters:**
- `alpha` (float): First parameter $\alpha > 0$.
- `beta` (float): Second parameter $\beta > 0$.
- `z` (float): Real argument.
**Returns:**
- `float`: $E_{\alpha,\beta}(z)$
**Algorithm:**
- $|z| < 10$: Taylor series expansion with 100 terms
- $|z| \geq 10$: Asymptotic expansion
**Example:**
```python
import optimizr
import numpy as np
# Verify: E_{1,1}(z) = exp(z)
for z in [0.5, 1.0, 2.0]:
ml = optimizr.mittag_leffler_py(1.0, 1.0, z)
print(f"E_{{1,1}}({z}) = {ml:.8f}, exp({z}) = {np.exp(z):.8f}")
# Compute E_{0.5, 1}(z) (related to complementary error function)
z = -1.0
ml_half = optimizr.mittag_leffler_py(0.5, 1.0, z)
print(f"E_{{0.5,1}}({z}) = {ml_half:.8f}")
```
---
### `f_alpha_lambda_py(alpha0, lambda0, x)`
Compute the scaling function $f_{\alpha_0, \lambda_0}(x)$ from Theorem 3.1.
$$
f_{\alpha_0, \lambda_0}(x) = \lambda_0 \, x^{\alpha_0 - 1} \, E_{\alpha_0, \alpha_0}\!\left(-\lambda_0 \, x^{\alpha_0}\right)
$$
**Parameters:**
- `alpha0` (float): Tail exponent $\alpha_0 \in (0, 1)$.
- `lambda0` (float): Scaling parameter $\lambda_0 > 0$.
- `x` (float): Evaluation point $x > 0$.
**Returns:**
- `float`: $f_{\alpha_0, \lambda_0}(x)$
**Example:**
```python
import optimizr
import numpy as np
import matplotlib.pyplot as plt
# Plot for H₀ = 0.75 → α₀ = 0.375
x = np.linspace(0.01, 20, 500)
f = [optimizr.f_alpha_lambda_py(0.375, 1.0, xi) for xi in x]
plt.figure(figsize=(8, 4))
plt.plot(x, f)
plt.xlabel('x')
plt.ylabel(r'$f_{0.375, 1.0}(x)$')
plt.title('Scaling Function (Theorem 3.1)')
plt.grid(True, alpha=0.3)
plt.show()
```
---
## Module Architecture
```{mermaid}
graph TD
MOD["📦 mod.rs\nPublic API re-exports"]
K["🔧 kernels.rs\nExcitationKernel trait"]
H["⚡ hawkes.rs\nSimulation & fitting"]
ML["🔢 mittag_leffler.rs\nSpecial functions"]
FBM["〰️ mixed_fbm.rs\nFractional Brownian motion"]
PY["🐍 python_bindings.rs\nPyO3 bindings"]
MOD --> K
MOD --> H
MOD --> ML
MOD --> FBM
MOD --> PY
K --> EK["ExponentialKernel\nφ = α·e^−βt"]
K --> PLK["PowerLawKernel\nφ = K₀(1+t)^1−α"]
K --> CMK["CompletelyMonotoneKernel\nMittag-Leffler"]
H --> HP["HawkesProcess K\nunivariate · Ogata thinning"]
H --> BH["BivariateHawkes K\nbuy/sell reaction flow"]
ML --> mf["mittag_leffler · f_alpha_lambda\ngamma · incomplete_gamma"]
FBM --> fbm1["FractionalBM\nCholesky & Hosking"]
FBM --> fbm2["MixedFractionalBM\na·B + b·B^H"]
```
---
## Performance
All computations run in Rust, providing significant speedups over pure Python:
| Operation | n | Rust (optimizr) | Python (pure) | Speedup |
|-----------|---|-----------------|---------------|---------|
| Hawkes simulation (exp) | 10K events | ~2ms | ~150ms | **75×** |
| fBM (Hosking) | 5000 steps | ~15ms | ~800ms | **53×** |
| Hurst estimation (R/S) | 10K points | ~1ms | ~60ms | **60×** |
| Mittag-Leffler | 100 terms | ~5μs | ~300μs | **60×** |
Benchmarks on Apple M1 Pro, single thread. The Hawkes simulation uses Ogata's thinning which depends on the branching ratio — higher branching ratios (closer to 1) produce more events and take longer.
+19
View File
@@ -0,0 +1,19 @@
# API: Risk Metrics
```python
from optimizr import (
hurst_exponent_py,
compute_risk_metrics_py,
estimate_half_life_py,
bootstrap_returns_py,
)
h = hurst_exponent_py(returns)
hl = estimate_half_life_py(returns)
metrics = compute_risk_metrics_py(returns.tolist())
boot = bootstrap_returns_py(returns, n_samples=1000)
```
- `returns`: 1D NumPy array of returns
- `compute_risk_metrics_py` returns a dict with volatility, Sharpe, and drawdown estimates
- `bootstrap_returns_py` resamples the series for uncertainty estimation
+26
View File
@@ -0,0 +1,26 @@
# API: Sparse Optimization
## sparse_pca_py
```python
from optimizr import sparse_pca_py
components = sparse_pca_py(
X,
n_components=3,
l1_ratio=0.2,
)
```
- `X`: 2D NumPy array
- Returns component matrix `(n_components, n_features)`
## box_tao_decomposition_py
```python
from optimizr import box_tao_decomposition_py
solution = box_tao_decomposition_py(X)
```
## elastic_net_py
```python
from optimizr import elastic_net_py
coeffs = elastic_net_py(X, y, l1_ratio=0.3, alpha=0.01)
```
+36
View File
@@ -0,0 +1,36 @@
# Benchmarks
These results come from the Rust backends (release build) versus SciPys `differential_evolution` on the standard 10D test suite. Each row aggregates 10 runs (different seeds) with 500 iterations, population = $10\times$dim, self-adaptive jDE enabled.
| Function | Dim | Iterations | Success Rate | Avg Time (Rust) | Best Fitness | Speedup vs SciPy |
|----------|-----|------------|--------------|-----------------|--------------|------------------|
| Sphere | 10 | 500 | 100% | 12 ms | $1\times10^{-12}$ | 70× |
| Rosenbrock | 10 | 500 | 98% | 18 ms | $3\times10^{-6}$ | 65× |
| Rastrigin | 10 | 500 | 87% | 22 ms | $2\times10^{-2}$ | 72× |
| Ackley | 10 | 500 | 95% | 15 ms | $2\times10^{-8}$ | 58× |
**How to reproduce**
- Run [`examples/notebooks/05_performance_benchmarks.ipynb`](https://github.com/ThotDjehuty/optimiz-r/blob/main/examples/notebooks/05_performance_benchmarks.ipynb) (validated in CI) to regenerate figures and raw CSV metrics.
- Or from the [repo root](https://github.com/ThotDjehuty/optimiz-r), run `make benchmark` for the Rust-side microbenchmarks (no Python overhead).
- To compare against SciPy, set `SCIPY_BASELINE=1` in the notebook; it records wall-clock times and success percentages side by side.
**What the notebook plots**
- Convergence trajectories (best fitness vs iterations) for each function
- Histograms of self-adapted $(F, CR)$ values mid-run
- Speedup bars and success-rate bars vs SciPy on the same seeds
- Residuals heatmap for a sweep over population sizes (optional cell)
**Notes on methodology**
- Rust builds are compiled with `--release` and link against OpenBLAS.
- Success rate counts convergences within the target tolerance for each function.
- Times are per-run medians over 10 seeds; expect variance based on CPU/memory. The ratios (last column) are more stable than absolute milliseconds.
- Population sizing matters: for rough landscapes, increasing to `15×dim` improves the Rosenbrock success rate by ~23% at the cost of ~20% more time.
**Additional workloads (see notebook cells):**
- High-dimension stress test: Rastrigin 50D, population 800, 700 iterations (shows scaling trend)
- HMM forward-backward throughput: synthetic 3-state Gaussian emissions (Rust vs pure Python)
- MFG solver timing: 100×100 grid vs 150×150 grid (observed ~1.8× runtime increase, stable memory)
+9
View File
@@ -0,0 +1,9 @@
# Changelog
- **0.3.0**
- Added Mean Field Games Rust bindings
- Expanded risk metrics utilities
- Improved Python API consistency and documentation
- **0.2.x**
- Initial public release with DE, HMM, MCMC, sparse optimization
+86
View File
@@ -0,0 +1,86 @@
# Configuration file for the Sphinx documentation builder.
# https://www.sphinx-doc.org/en/master/usage/configuration.html
import os
import sys
sys.path.insert(0, os.path.abspath('../../python'))
# -- Project information -----------------------------------------------------
project = 'Optimiz-rs'
copyright = '2026, HFThot Research Lab'
author = 'HFThot Research Lab'
release = '0.3.0'
version = '0.3.0'
# -- General configuration ---------------------------------------------------
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.napoleon',
'sphinx.ext.viewcode',
'sphinx.ext.intersphinx',
'sphinx.ext.mathjax',
'myst_parser',
'sphinxcontrib.mermaid',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = []
# -- Options for HTML output -------------------------------------------------
html_theme = 'furo' # Modern, clean theme
html_static_path = ['_static']
html_title = 'Optimiz-rs Documentation'
html_short_title = 'Optimiz-rs'
html_logo = 'logo_optimizrs_transparent.png'
html_favicon = 'logo_optimizrs_transparent.png'
html_theme_options = {
"light_css_variables": {
"color-brand-primary": "#f97316",
"color-brand-content": "#f97316",
},
"dark_css_variables": {
"color-brand-primary": "#fb923c",
"color-brand-content": "#fb923c",
},
}
# Napoleon settings for Google/NumPy docstring parsing
napoleon_google_docstring = True
napoleon_numpy_docstring = True
napoleon_include_init_with_doc = False
napoleon_include_private_with_doc = False
napoleon_include_special_with_doc = True
napoleon_use_admonition_for_examples = False
napoleon_use_admonition_for_notes = False
napoleon_use_admonition_for_references = False
napoleon_use_ivar = False
napoleon_use_param = True
napoleon_use_rtype = True
napoleon_preprocess_types = False
napoleon_type_aliases = None
napoleon_attr_annotations = True
# Intersphinx configuration
intersphinx_mapping = {
'python': ('https://docs.python.org/3', None),
'numpy': ('https://numpy.org/doc/stable/', None),
}
# MyST parser configuration for markdown support
myst_enable_extensions = [
"colon_fence",
"deflist",
"dollarmath",
]
# Custom CSS
html_css_files = ["custom.css"]
# Mermaid configuration
mermaid_version = "10.9.0"
mermaid_init_js = "mermaid.initialize({startOnLoad:true, theme:'dark', themeVariables:{primaryColor:'#f97316',primaryTextColor:'#fff',primaryBorderColor:'#ea6a0a',lineColor:'#fb923c',secondaryColor:'#1e293b',tertiaryColor:'#0f172a'}});"
+7
View File
@@ -0,0 +1,7 @@
# Contributing
- Open an issue describing the feature or bugfix.
- Keep dependencies minimal; prefer NumPy/Matplotlib for examples.
- Add or update tests under `tests/` when changing behavior.
- Run `pytest` and `make html` in `docs/` before submitting a PR.
- Follow Rust fmt and Python formatting conventions in the repo.
+122
View File
@@ -0,0 +1,122 @@
# Examples
Practical snippets for every Optimiz-rs component.
## Differential Evolution (global optimization)
```python
import numpy as np
from optimizr import differential_evolution
def sphere(x):
return np.sum(x**2)
best_x, best_fx = differential_evolution(
objective_fn=sphere,
bounds=[(-10, 10)] * 5,
strategy="rand1",
maxiter=300,
adaptive=True,
)
print(best_fx)
```
## Grid Search (hyper-parameter sweep)
```python
from optimizr import grid_search
def objective(params):
lr, momentum = params["lr"], params["momentum"]
return (lr - 0.05)**2 + (momentum - 0.9)**2
best_params, best_score = grid_search(
objective_fn=objective,
param_grid={"lr": [0.01, 0.05, 0.1], "momentum": [0.8, 0.9, 0.95]},
)
print(best_params, best_score)
```
## Hidden Markov Models (regime detection)
```python
import numpy as np
from optimizr import HMM
returns = np.random.randn(800) * 0.02 + 0.005
returns[400:] -= 0.015 # regime shift
model = HMM(n_states=2).fit(returns)
states = model.predict(returns)
print(np.bincount(states))
```
## MCMC (posterior sampling)
```python
import numpy as np
from optimizr import mcmc_sample
def log_likelihood(params, data):
mu, sigma = params
residuals = (data - mu) / sigma
return -0.5 * np.sum(residuals**2) - len(data) * np.log(sigma)
data = np.random.randn(500) + 1.0
samples = mcmc_sample(
log_likelihood_fn=log_likelihood,
data=data,
initial_params=np.array([0.0, 1.0]),
param_bounds=[(-5, 5), (0.1, 5.0)],
)
print(samples.mean(axis=0))
```
## Mean Field Games (1D solver)
```python
from optimizr import MFGConfig, solve_mfg_1d_rust
config = MFGConfig(nx=64, nt=32, x_min=-2.0, x_max=2.0, T=1.0, epsilon=0.1, kappa=1.0)
solution = solve_mfg_1d_rust(config)
print(solution.converged)
```
## Sparse Optimization (Sparse PCA)
```python
import numpy as np
from optimizr import sparse_pca_py
X = np.random.randn(200, 10)
components = sparse_pca_py(X, n_components=3, l1_ratio=0.2)
print(components.shape)
```
## Risk Metrics (time series)
```python
import numpy as np
from optimizr import hurst_exponent_py, estimate_half_life_py
returns = np.random.randn(1000) * 0.01
print("Hurst:", hurst_exponent_py(returns))
print("Half-life:", estimate_half_life_py(returns))
```
## Notebooks
- Differential Evolution: `examples/notebooks/03_differential_evolution_tutorial.ipynb`
- Mean Field Games: `examples/notebooks/mean_field_games_tutorial.ipynb`
- HMM: `examples/notebooks/01_hmm_tutorial.ipynb`
- MCMC: `examples/notebooks/02_mcmc_tutorial.ipynb`
- Optimal Control & Kalman: `examples/notebooks/03_optimal_control_tutorial.ipynb`
- Performance benchmarks: `examples/notebooks/05_performance_benchmarks.ipynb`
## Contribute Examples
1. Fork the repository and add notebooks under `examples/notebooks/`
2. Keep dependencies minimal (NumPy/Matplotlib preferred)
3. Ensure the notebook runs end-to-end before submitting a PR
+53
View File
@@ -0,0 +1,53 @@
# Getting Started
This guide prepares a fresh environment, builds the Rust extension, and validates the install.
## 1. Install dependencies
```bash
# Python deps for docs and benchmarks
python -m venv .venv
source .venv/bin/activate
pip install -r docs/requirements.txt
pip install maturin numpy
```
## 2. Build and install Optimiz-rs locally
```bash
pip install .
# or editable mode for development
maturin develop --release
```
## 3. Quick verification
```bash
python - <<'PY'
import optimizr
from optimizr import differential_evolution, HMM
print("Optimiz-rs version:", optimizr.__version__)
# Simple objective
f = lambda x: sum(v * v for v in x)
pt, val = differential_evolution(f, bounds=[(-2, 2)] * 3, maxiter=50)
print("DE ok →", round(float(val), 4))
model = HMM(n_states=2).fit([0.01, -0.02, 0.0, 0.03])
print("HMM states →", model.predict([0.01, -0.02, 0.0, 0.03]))
PY
```
## 4. Build docs locally
```bash
cd docs
make html # or: sphinx-build -b html source build/html
open build/html/index.html
```
## 5. Troubleshooting
- If the Rust extension fails to compile, ensure `rustc --version` ≥ 1.70 and `maturin` is installed.
- On Apple Silicon, set `export MACOSX_DEPLOYMENT_TARGET=12.0` before building if you hit ABI errors.
- Delete stale builds with `rm -rf build dist target *.egg-info` then reinstall.
+149
View File
@@ -0,0 +1,149 @@
.. Optimiz-rs documentation master file
Optimiz-rs Documentation
======================
**High-performance optimization algorithms in Rust with Python bindings**
.. image:: https://img.shields.io/badge/version-1.0.0-blue.svg
:target: https://github.com/ThotDjehuty/optimiz-r/releases
:alt: Version
.. image:: https://img.shields.io/badge/license-MIT-green.svg
:target: https://github.com/ThotDjehuty/optimiz-r/blob/main/LICENSE
:alt: License
Optimiz-rs provides blazingly fast, production-ready implementations of advanced optimization and statistical inference algorithms. Built with Rust for maximum performance and exposed to Python through PyO3, it delivers **50-100× speedup** over pure Python implementations.
.. toctree::
:maxdepth: 2
:caption: Getting Started
getting-started
installation
quickstart
examples
.. toctree::
:maxdepth: 2
:caption: Algorithms
algorithms/differential_evolution
algorithms/mean_field_games
algorithms/hmm
algorithms/mcmc
algorithms/sparse_optimization
algorithms/optimal_control
algorithms/risk_metrics
algorithms/grid_search
algorithms/point_processes
.. toctree::
:maxdepth: 2
:caption: v1.1 Numerical Primitives
algorithms/matrix_riccati
algorithms/nonsync_covariance
algorithms/wavelet
algorithms/risk_measures
algorithms/graph_spectral
algorithms/topology
algorithms/volterra
algorithms/signatures
.. toctree::
:maxdepth: 2
:caption: API Reference
api/differential_evolution
api/grid_search
api/hmm
api/mcmc
api/sparse
api/optimal_control
api/risk_metrics
api/point_processes
.. toctree::
:maxdepth: 1
:caption: Advanced
theory/mathematical_foundations
mfg_tutorial
benchmarks
contributing
changelog
Features
--------
**Algorithms Included:**
- **Mean Field Games**: 1D MFG solver, HJB-Fokker-Planck coupling, agent population dynamics
- **Differential Evolution**: 5 strategies (rand/1, best/1, current-to-best/1, rand/2, best/2), adaptive jDE
- **Optimal Control**: HJB solvers, regime switching, jump diffusion, MRSJD framework
- **Hidden Markov Models**: Baum-Welch training, Viterbi decoding, Gaussian emissions
- **MCMC Sampling**: Metropolis-Hastings, adaptive proposals, Bayesian inference
- **Sparse Optimization**: Sparse PCA, Box-Tao decomposition, Elastic Net, ADMM
- **Risk Metrics**: Hurst exponent, half-life estimation, time series analysis
- **Information Theory**: Mutual information, Shannon entropy, feature selection
- **Time-Series Helpers**: Rolling Hurst/half-life, feature prep for HMM, lagged feature builders
- **Parallelization**: Rust-native population evaluation with Rayon for built-in objectives
🚀 **Performance:**
- **50-100× faster** than pure Python implementations
- **95% memory reduction** vs NumPy/SciPy
- **Parallel-ready** with Rayon infrastructure
- Production-tested on multi-dimensional problems
Quick Example
-------------
.. code-block:: python
import numpy as np
from optimizr import DifferentialEvolution
# Define objective function
def sphere(x):
return np.sum(x**2)
# Optimize
de = DifferentialEvolution(
bounds=[(-5, 5)] * 10,
strategy="best/1/bin",
population_size=50
)
result = de.optimize(sphere, max_iterations=100)
print(f"Best fitness: {result.best_fitness:.6f}")
print(f"Best solution: {result.best_solution}")
Installation
------------
From PyPI (coming soon):
.. code-block:: bash
pip install optimizr
From source:
.. code-block:: bash
# Clone repository
git clone https://github.com/ThotDjehuty/optimiz-r.git
cd optimiz-r
# Build and install
pip install maturin
maturin develop --release
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
+118
View File
@@ -0,0 +1,118 @@
# Installation Guide
## Requirements
- Python 3.8 or higher
- Rust 1.70 or higher (for building from source)
- pip
## Install from PyPI
**Coming soon**: Optimiz-rs will be available on PyPI.
```bash
pip install optimizr
```
## Install from Source
### Step 1: Clone Repository
```bash
git clone https://github.com/ThotDjehuty/optimiz-r.git
cd optimiz-r
```
### Step 2: Install Maturin
[Maturin](https://github.com/PyO3/maturin) is required to build Rust-Python bindings:
```bash
pip install maturin
```
### Step 3: Build and Install
**Development mode** (editable install, useful for development):
```bash
maturin develop --release
```
**Production install** (creates wheel and installs):
```bash
maturin build --release
pip install target/wheels/optimizr-*.whl
```
### Step 4: Verify Installation
```python
import optimizr
print(optimizr.__version__) # Should print "0.3.0"
```
## Platform-Specific Notes
### macOS
If you encounter build errors on macOS:
1. Ensure Xcode Command Line Tools are installed:
```bash
xcode-select --install
```
2. Install Rust via rustup:
```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```
### Windows
1. Install Visual Studio Build Tools (2019 or later)
2. Install Rust via [rustup-init.exe](https://rustup.rs/)
3. Follow standard installation steps
### Linux
Requires GCC or Clang:
```bash
# Ubuntu/Debian
sudo apt-get install build-essential
# If you see OpenBLAS link errors during wheels/docs build
sudo apt-get install libopenblas-dev
# Fedora/RHEL
sudo dnf install gcc gcc-c++
```
## Troubleshooting
**Issue**: `maturin: command not found`
**Solution**: Ensure pip bin directory is in PATH:
```bash
export PATH="$HOME/.local/bin:$PATH" # Linux/macOS
```
**Issue**: Rust compiler errors
**Solution**: Update Rust to latest stable:
```bash
rustup update stable
```
**Issue**: ImportError when importing optimizr
**Solution**: Rebuild with correct Python version:
```bash
maturin develop --release -i python3.10 # Replace with your Python version
```
**Issue**: BLAS/LAPACK linkage errors on Linux
**Solution**: Install OpenBLAS headers (see Linux section above) and rebuild with `maturin develop --release`.

Some files were not shown because too many files have changed in this diff Show More