diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 1ef20fd..e065f28 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -49,7 +49,7 @@ jobs: uv venv .test-venv source .test-venv/bin/activate uv pip install dist/*.whl --reinstall - uv pip install --group dev --group examples + uv pip install --group dev --group docs --group examples pytest -v - uses: actions/upload-artifact@v5 @@ -104,7 +104,7 @@ jobs: uv pip install dist/*.whl --reinstall # Install dev dependencies from pyproject.toml using pip interface - uv pip install --group dev --group examples + uv pip install --group dev --group docs --group examples # Run tests directly pytest -v @@ -132,7 +132,7 @@ jobs: sudo apt-get install -y llvm-18-dev libclang-18-dev libzstd-dev libpolly-18-dev patchelf - name: Generate stubs - run: cargo run -p chronopt-py --no-default-features --features stubgen --bin generate_stubs + run: cargo run -p diffid-py --no-default-features --features stubgen --bin generate_stubs env: LLVM_SYS_181_PREFIX: /usr/lib/llvm-18 LIBCLANG_PATH: /usr/lib/llvm-18/lib diff --git a/.github/workflows/docs-checks.yml b/.github/workflows/docs-checks.yml deleted file mode 100644 index 7c051a2..0000000 --- a/.github/workflows/docs-checks.yml +++ /dev/null @@ -1,202 +0,0 @@ -name: Documentation Checks - -on: - schedule: - # Run weekly on Monday at 00:00 UTC - - cron: '0 0 * * 1' - workflow_dispatch: # Allow manual triggering - pull_request: - paths: - - 'docs/**' - - 'mkdocs.yml' - - '.github/workflows/docs-checks.yml' - -permissions: - contents: read - -jobs: - link-check: - name: Check Links - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - - name: Install dependencies - run: | - pip install mkdocs mkdocs-material mkdocstrings[python] \ - mkdocs-git-revision-date-localized-plugin \ - mkdocs-minify-plugin mkdocs-jupyter pymdown-extensions - - - name: Build documentation - run: mkdocs build --strict - - - name: Link Checker - uses: lycheeverse/lychee-action@v2 - with: - # Check all markdown files - args: --verbose --no-progress 'docs/**/*.md' 'site/**/*.html' --exclude-path 'site/tutorials/notebooks' --max-concurrency 10 - # Don't fail on broken links in draft mode - fail: ${{ github.event_name != 'pull_request' }} - env: - GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} - - - name: Create Issue on Failure - if: failure() && github.event_name == 'schedule' - uses: actions/github-script@v7 - with: - script: | - github.rest.issues.create({ - owner: context.repo.owner, - repo: context.repo.repo, - title: 'šŸ“‹ Documentation link check failed', - body: 'Scheduled link check found broken links in documentation.\n\nSee workflow run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}', - labels: ['documentation', 'automated'] - }) - - test-docs: - name: Documentation Tests - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - - name: Install uv - uses: astral-sh/setup-uv@v5 - - - name: Install chronopt and dependencies - run: | - uv pip install --system pytest - uv pip install --system mkdocs mkdocs-material mkdocstrings[python] \ - mkdocs-git-revision-date-localized-plugin \ - mkdocs-minify-plugin mkdocs-jupyter pymdown-extensions - # Install chronopt if possible (may fail if rust build needed) - uv pip install --system -e python/ || echo "āš ļø Could not install chronopt (rust build required)" - - - name: Run documentation tests - run: pytest tests/test_docs.py -v - continue-on-error: true # Don't fail if chronopt not installed - - notebook-validation: - name: Validate Notebooks - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - - name: Install dependencies - run: | - pip install jupyter nbformat nbconvert - - - name: Validate notebook format - run: | - for notebook in docs/tutorials/notebooks/*.ipynb; do - echo "Validating $notebook" - python -m nbformat.validator "$notebook" || echo "āš ļø $notebook validation failed" - done - - - name: Check for execution errors - run: | - # Check notebooks don't have error outputs - for notebook in docs/tutorials/notebooks/*.ipynb; do - if grep -q '"output_type": "error"' "$notebook"; then - echo "āŒ $notebook contains error outputs" - exit 1 - fi - done - echo "āœ… No error outputs found in notebooks" - - spelling-check: - name: Spelling Check - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Check British English spelling - uses: reviewdog/action-misspell@v1 - with: - github_token: ${{ secrets.github_token }} - locale: "UK" - reporter: github-pr-review - level: warning - filter_mode: diff_context - # Exclude code blocks and technical terms - exclude: | - *.ipynb - *.py - *.rs - - build-preview: - name: Build Preview - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - - name: Install dependencies - run: | - pip install mkdocs mkdocs-material mkdocstrings[python] \ - mkdocs-git-revision-date-localized-plugin \ - mkdocs-minify-plugin mkdocs-jupyter pymdown-extensions - - - name: Build documentation - run: mkdocs build --strict - - - name: Upload preview artifact - uses: actions/upload-artifact@v4 - with: - name: docs-preview-pr-${{ github.event.pull_request.number }} - path: site/ - retention-days: 7 - - - name: Comment PR with preview info - uses: actions/github-script@v7 - with: - script: | - const comment = `## šŸ“š Documentation Preview - - Documentation built successfully for this PR! - - **Download Preview**: Check the artifacts section of [this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) - - **Build Details**: - - Commit: \`${{ github.sha }}\` - - Built at: ${new Date().toISOString()} - - To view locally: - 1. Download the \`docs-preview-pr-${{ github.event.pull_request.number }}\` artifact - 2. Extract and open \`index.html\` in a browser - - _This preview will be available for 7 days._ - `; - - github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: comment - }) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index a7b8239..d2399a7 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -2,19 +2,26 @@ name: Documentation on: push: - branches: - - main + branches: ["main"] + tags: + - '*' pull_request: - branches: - - main workflow_dispatch: +# Prevent concurrent deployments +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +# Default permissions: read-only permissions: - contents: write + contents: read jobs: build-and-deploy: runs-on: ubuntu-latest + permissions: + contents: write steps: - name: Checkout repository uses: actions/checkout@v4 @@ -29,30 +36,24 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v5 + - name: Cache uv dependencies + uses: actions/cache@v4 + with: + path: ~/.cache/uv + key: ${{ runner.os }}-uv-docs-deploy-${{ hashFiles('.github/workflows/docs.yml') }} + restore-keys: | + ${{ runner.os }}-uv-docs-deploy- + - name: Install dependencies - run: | - uv pip install --system mkdocs - uv pip install --system mkdocs-material - uv pip install --system mkdocstrings[python] - uv pip install --system mkdocs-git-revision-date-localized-plugin - uv pip install --system mkdocs-minify-plugin - uv pip install --system mkdocs-jupyter - uv pip install --system pymdown-extensions + run: uv pip install --system --group docs - name: Build documentation - run: mkdocs build + run: mkdocs build --strict - name: Deploy to GitHub Pages - if: github.event_name == 'push' && github.ref == 'refs/heads/main' + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./site cname: false # Set to your custom domain if you have one - - - name: Upload artifact for PR preview - if: github.event_name == 'pull_request' - uses: actions/upload-artifact@v4 - with: - name: docs-preview - path: ./site diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f91c182..2bad492 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -50,7 +50,7 @@ jobs: uv venv .test-venv source .test-venv/bin/activate uv pip install dist/*.whl --reinstall - uv pip install --group dev --group examples + uv pip install --group dev --group docs --group examples pytest -v - uses: actions/upload-artifact@v5 @@ -106,7 +106,7 @@ jobs: uv pip install dist/*.whl --reinstall # Install dev dependencies from pyproject.toml using pip interface - uv pip install --group dev --group examples + uv pip install --group dev --group docs --group examples # Run tests directly pytest -v @@ -134,7 +134,7 @@ jobs: sudo apt-get install -y llvm-18-dev libclang-18-dev libzstd-dev libpolly-18-dev patchelf - name: Generate stubs - run: cargo run -p chronopt-py --no-default-features --features stubgen --bin generate_stubs + run: cargo run -p diffid-py --no-default-features --features stubgen --bin generate_stubs env: LLVM_SYS_181_PREFIX: /usr/lib/llvm-18 LIBCLANG_PATH: /usr/lib/llvm-18/lib diff --git a/Cargo.lock b/Cargo.lock index 426e78a..142e7d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -268,33 +268,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "chronopt" -version = "0.2.0" -dependencies = [ - "criterion", - "diffsol", - "nalgebra", - "proptest", - "rand 0.9.2", - "rand_distr", - "rayon", -] - -[[package]] -name = "chronopt-py" -version = "0.2.0" -dependencies = [ - "chronopt", - "clap 4.5.54", - "nalgebra", - "numpy", - "pyo3", - "pyo3-build-config", - "pyo3-stub-gen", - "pyo3-stub-gen-derive", -] - [[package]] name = "clang-sys" version = "1.8.1" @@ -728,6 +701,33 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "930c7171c8df9fb1782bdf9b918ed9ed2d33d1d22300abb754f9085bc48bf8e8" +[[package]] +name = "diffid" +version = "0.2.0" +dependencies = [ + "criterion", + "diffsol", + "nalgebra", + "proptest", + "rand 0.9.2", + "rand_distr", + "rayon", +] + +[[package]] +name = "diffid-py" +version = "0.2.0" +dependencies = [ + "clap 4.5.54", + "diffid", + "nalgebra", + "numpy", + "pyo3", + "pyo3-build-config", + "pyo3-stub-gen", + "pyo3-stub-gen-derive", +] + [[package]] name = "diffsl" version = "0.8.3" diff --git a/README.md b/README.md index baecdea..759e859 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@
-# chronopt -[![Python Versions from PEP 621 TOML](https://img.shields.io/python/required-version-toml?tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2Fbradyplanden%2Fchronopt%2Fmain%2Fpyproject.toml&label=Python)](https://pypi.org/project/chronopt/) -[![License](https://img.shields.io/github/license/bradyplanden/chronopt?color=blue)](https://github.com/bradyplanden/chronopt/blob/main/LICENSE) -[![Releases](https://img.shields.io/github/v/release/bradyplanden/chronopt?color=gold)](https://github.com/bradyplanden/chronopt/releases) +# diffid +[![Python Versions from PEP 621 TOML](https://img.shields.io/python/required-version-toml?tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2Fbradyplanden%2Fdiffid%2Fmain%2Fpyproject.toml&label=Python)](https://pypi.org/project/diffid/) +[![License](https://img.shields.io/github/license/bradyplanden/diffid?color=blue)](https://github.com/bradyplanden/diffid/blob/main/LICENSE) +[![Releases](https://img.shields.io/github/v/release/bradyplanden/diffid?color=gold)](https://github.com/bradyplanden/diffid/releases) -**chron**os-**opt**imum is a Rust-first toolkit for time-series inference and optimisation with ergonomic Python bindings. It couples high-performance solvers with a highly customisable builder API for identification and optimisation of differential systems. +**diff**erential **id**entification is a Rust-first toolkit for time-series inference and optimisation with ergonomic Python bindings. It couples high-performance solvers with a highly customisable builder API for identification and optimisation of differential systems.
@@ -21,14 +21,14 @@ - Flexible integration with state-of-the-art differential solvers, such as [Diffrax](https://github.com/patrick-kidger/diffrax), [DifferentialEquations.jl](https://github.com/SciML/diffeqpy) -## Why Chronopt? +## Why Diffid? - Optimisation based workflow run the forward simulation thousands of times, a performance improvement on the process can produce results hour or days earlier - The Rust core provides a high-performance inference loop with fewer runtime errors. - Quickly integrated into Python workflows, and later use the rust crate directly for even higher performance. ## Documentation -šŸ“š **[Full Documentation](https://bradyplanden.github.io/chronopt/)** +**[Full Documentation](https://bradyplanden.github.io/diffid/)** Visit the comprehensive documentation for: - Getting started guides and tutorials @@ -38,16 +38,16 @@ Visit the comprehensive documentation for: ## Installation -Chronopt targets Python >= 3.11. Windows builds are currently marked experimental. +Diffid targets Python >= 3.11. Windows builds are currently marked experimental. ```bash -pip install chronopt +pip install diffid # Or with uv -uv pip install chronopt +uv pip install diffid # Optional extras -pip install "chronopt[plotting]" +pip install "diffid[plotting]" ``` ## Quickstart @@ -55,7 +55,7 @@ pip install "chronopt[plotting]" ### ScalarProblem ```python import numpy as np -import chronopt as chron +import diffid def rosenbrock(x): @@ -64,7 +64,7 @@ def rosenbrock(x): builder = ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_objective(rosenbrock) .with_parameter("x", 1.5) .with_parameter("y", -1.5) @@ -81,7 +81,7 @@ print(f"Success: {result.success}") ```python import numpy as np -import chronopt as chron +import diffid # Example diffsol ODE (logistic growth) @@ -96,7 +96,7 @@ observations = np.exp(-1.3 * t) data = np.column_stack((t, observations)) builder = ( - chron.DiffsolBuilder() + diffid.DiffsolBuilder() .with_diffsl(dsl) .with_data(data) .with_parameter("k", 1.0) @@ -104,7 +104,7 @@ builder = ( ) problem = builder.build() -optimiser = chron.CMAES().with_max_iter(1000) +optimiser = diffid.CMAES().with_max_iter(1000) result = optimiser.run(problem, [0.5,0.5]) print(result.x) @@ -127,13 +127,13 @@ uv run maturin develop Regenerate `.pyi` stubs after changing the bindings: ```bash -uv run cargo run -p chronopt-py --no-default-features --features stubgen --bin generate_stubs +uv run cargo run -p diffid-py --no-default-features --features stubgen --bin generate_stubs ``` Without `uv`, invoke the generator directly: ```bash -cargo run -p chronopt-py --no-default-features --features stubgen --bin generate_stubs +cargo run -p diffid-py --no-default-features --features stubgen --bin generate_stubs ``` ### Pre-commit hooks diff --git a/docs/algorithms/index.md b/docs/algorithms/index.md index bb25b39..9510652 100644 --- a/docs/algorithms/index.md +++ b/docs/algorithms/index.md @@ -1,6 +1,6 @@ # Algorithms -Detailed documentation for each optimisation and sampling algorithm in Chronopt. +Detailed documentation for each optimisation and sampling algorithm in Diffid. ## Optimisers @@ -114,7 +114,7 @@ All algorithms are implemented in Rust for performance: - **Numerically stable**: Careful floating-point handling - **Well-tested**: Comprehensive test suite -Source code: [rust/src/optimisers/](https://github.com/bradyplanden/chronopt/tree/main/rust/src/optimisers) +Source code: [rust/src/optimisers/](https://github.com/bradyplanden/diffid/tree/main/rust/src/optimisers) ## References diff --git a/docs/algorithms/optimizers/adam.md b/docs/algorithms/optimizers/adam.md index c5e4218..3b79cf0 100644 --- a/docs/algorithms/optimizers/adam.md +++ b/docs/algorithms/optimizers/adam.md @@ -111,10 +111,10 @@ The algorithm terminates when any condition is met: ## Example ```python -import chronopt as chron +import diffid as chron optimiser = ( - chron.Adam() + diffid.Adam() .with_max_iter(5000) .with_step_size(0.001) .with_betas(0.9, 0.999) diff --git a/docs/algorithms/optimizers/cmaes.md b/docs/algorithms/optimizers/cmaes.md index b8d7b8f..5d868ec 100644 --- a/docs/algorithms/optimizers/cmaes.md +++ b/docs/algorithms/optimizers/cmaes.md @@ -108,10 +108,10 @@ The algorithm terminates when any condition is met: ## Example ```python -import chronopt as chron +import diffid as chron optimiser = ( - chron.CMAES() + diffid.CMAES() .with_max_iter(500) .with_step_size(0.3) .with_population_size(20) diff --git a/docs/algorithms/optimizers/nelder-mead.md b/docs/algorithms/optimizers/nelder-mead.md index 51badaa..c2623fa 100644 --- a/docs/algorithms/optimizers/nelder-mead.md +++ b/docs/algorithms/optimizers/nelder-mead.md @@ -88,10 +88,10 @@ The algorithm terminates when any of the following conditions is met: ## Example ```python -import chronopt as chron +import diffid as chron optimiser = ( - chron.NelderMead() + diffid.NelderMead() .with_max_iter(5000) .with_step_size(0.1) .with_threshold(1e-8) diff --git a/docs/algorithms/samplers/dynamic-nested-sampling.md b/docs/algorithms/samplers/dynamic-nested-sampling.md index f3db770..95e171d 100644 --- a/docs/algorithms/samplers/dynamic-nested-sampling.md +++ b/docs/algorithms/samplers/dynamic-nested-sampling.md @@ -120,9 +120,9 @@ Nested sampling requires a **negative log-likelihood** cost metric: ```python problem = ( - chron.ScalarProblemBuilder() + diffid.ScalarProblemBuilder() .with_objective(model_fn) - .with_cost_metric(chron.CostMetric.GaussianNLL) # Required + .with_cost_metric(diffid.CostMetric.GaussianNLL) # Required .build() ) ``` @@ -132,10 +132,10 @@ The objective function should return negative log-likelihood; the algorithm inte ## Example ```python -import chronopt as chron +import diffid as chron sampler = ( - chron.DynamicNestedSampling() + diffid.DynamicNestedSampling() .with_live_points(128) .with_termination_tol(1e-3) .with_seed(42) diff --git a/docs/algorithms/samplers/metropolis-hastings.md b/docs/algorithms/samplers/metropolis-hastings.md index 6e961df..594cd18 100644 --- a/docs/algorithms/samplers/metropolis-hastings.md +++ b/docs/algorithms/samplers/metropolis-hastings.md @@ -104,9 +104,9 @@ Metropolis-Hastings requires a **negative log-likelihood** cost metric: ```python problem = ( - chron.ScalarProblemBuilder() + diffid.ScalarProblemBuilder() .with_objective(model_fn) - .with_cost_metric(chron.CostMetric.GaussianNLL) # Required + .with_cost_metric(diffid.CostMetric.GaussianNLL) # Required .build() ) ``` @@ -114,10 +114,10 @@ problem = ( ## Example ```python -import chronopt as chron +import diffid as chron sampler = ( - chron.MetropolisHastings() + diffid.MetropolisHastings() .with_iterations(5000) .with_num_chains(4) .with_step_size(0.05) diff --git a/docs/api-reference/index.md b/docs/api-reference/index.md index 5383f0a..99997c2 100644 --- a/docs/api-reference/index.md +++ b/docs/api-reference/index.md @@ -1,10 +1,10 @@ # API Reference -Complete API documentation for Chronopt's Python and Rust interfaces. +Complete API documentation for Diffid's Python and Rust interfaces. ## Python API -Chronopt provides a comprehensive Python API with full type hints and automatic documentation. +Diffid provides a comprehensive Python API with full type hints and automatic documentation.
@@ -54,12 +54,12 @@ Chronopt provides a comprehensive Python API with full type hints and automatic The Rust core provides high-performance implementations of all algorithms. -[:octicons-arrow-right-24: Rust Documentation on docs.rs](https://docs.rs/chronopt/latest/chronopt/) +[:octicons-arrow-right-24: Rust Documentation on docs.rs](https://docs.rs/diffid/latest/diffid/) ## Module Structure ``` -chronopt/ +diffid/ ā”œā”€ā”€ ScalarBuilder # Direct function optimisation ā”œā”€ā”€ DiffsolBuilder # ODE fitting with DiffSL/Diffsol ā”œā”€ā”€ VectorBuilder # Custom solver integration @@ -80,7 +80,7 @@ chronopt/ All Python functions include comprehensive type hints: ```python -from chronopt import ScalarBuilder, CMAES, OptimisationResults +from diffid import ScalarBuilder, CMAES, OptimisationResults import numpy.typing as npt def optimize_function( @@ -106,7 +106,7 @@ All builders maintain parameter order based on the sequence of `.with_parameter( ```python builder = ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_parameter("x", 1.0) # Index 0 .with_parameter("y", 2.0) # Index 1 ) diff --git a/docs/api-reference/python/builders.md b/docs/api-reference/python/builders.md index c0a7833..80a78db 100644 --- a/docs/api-reference/python/builders.md +++ b/docs/api-reference/python/builders.md @@ -12,7 +12,7 @@ Builders provide a fluent API for constructing optimisation problems. Choose the ## ScalarBuilder -::: chronopt.ScalarBuilder +::: diffid.ScalarBuilder options: show_root_heading: true show_source: false @@ -27,13 +27,13 @@ Builders provide a fluent API for constructing optimisation problems. Choose the ```python import numpy as np -import chronopt as chron +import diffid as chron def rosenbrock(x): return np.asarray([(1 - x[0])**2 + 100*(x[1] - x[0]**2)**2]) builder = ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_objective(rosenbrock) .with_parameter("x", 1.5) .with_parameter("y", -1.5) @@ -52,7 +52,7 @@ result = problem.optimise() ## DiffsolBuilder -::: chronopt.DiffsolBuilder +::: diffid.DiffsolBuilder options: show_root_heading: true show_source: false @@ -70,7 +70,7 @@ result = problem.optimise() ```python import numpy as np -import chronopt as chron +import diffid as chron dsl = """ in { r = 1, k = 1 } @@ -83,14 +83,14 @@ observations = np.exp(-1.3 * t) data = np.column_stack((t, observations)) builder = ( - chron.DiffsolBuilder() + diffid.DiffsolBuilder() .with_diffsl(dsl) .with_data(data) .with_parameter("k", 1.0) .with_backend("dense") ) problem = builder.build() -optimiser = chron.CMAES().with_max_iter(1000) +optimiser = diffid.CMAES().with_max_iter(1000) result = optimiser.run(problem, [0.5, 0.5]) ``` @@ -113,14 +113,14 @@ out_i { state1, state2 } # Optional: output variables ### When to Use - Fitting ODE parameters to time-series data -- Using Chronopt's built-in high-performance solver +- Using Diffid's built-in high-performance solver - Models expressible in DiffSL syntax --- ## VectorBuilder -::: chronopt.VectorBuilder +::: diffid.VectorBuilder options: show_root_heading: true show_source: false @@ -136,7 +136,7 @@ out_i { state1, state2 } # Optional: output variables ```python import numpy as np -import chronopt as chron +import diffid as chron def custom_solver(params): """Your custom ODE solver (e.g., using JAX/Diffrax).""" @@ -150,7 +150,7 @@ observations = ... # Your experimental data data = np.column_stack((t, observations)) builder = ( - chron.VectorBuilder() + diffid.VectorBuilder() .with_objective(custom_solver) .with_data(data) .with_parameter("alpha", 1.0) @@ -181,7 +181,7 @@ See the [Custom Solvers Guide](../../guides/custom-solvers.md) for examples with ## Problem -::: chronopt.Problem +::: diffid.Problem options: show_root_heading: true show_source: false @@ -208,11 +208,11 @@ Builders use a fluent interface - chain methods in any order: ```python builder = ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_objective(func) .with_parameter("x", 1.0) .with_parameter("y", 2.0) - .with_cost_metric(chron.RMSE()) + .with_cost_metric(diffid.RMSE()) ) ``` @@ -221,7 +221,7 @@ builder = ( Builders are immutable - each method returns a new builder: ```python -base = chron.ScalarBuilder().with_objective(func) +base = diffid.ScalarBuilder().with_objective(func) problem1 = base.with_parameter("x", 1.0).build() problem2 = base.with_parameter("x", 2.0).build() # Different initial guess @@ -233,7 +233,7 @@ Parameters are indexed in the order they're added: ```python builder = ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_parameter("y", 2.0) # Index 0 .with_parameter("x", 1.0) # Index 1 ) diff --git a/docs/api-reference/python/cost-metrics.md b/docs/api-reference/python/cost-metrics.md index fdebf87..bda1cae 100644 --- a/docs/api-reference/python/cost-metrics.md +++ b/docs/api-reference/python/cost-metrics.md @@ -12,7 +12,7 @@ Cost metrics define how model predictions are compared to observations. They det ## CostMetric -::: chronopt.CostMetric +::: diffid.CostMetric options: show_root_heading: true show_source: false @@ -28,15 +28,15 @@ $$\text{SSE} = \sum_{i=1}^{n} (y_i - \hat{y}_i)^2$$ ### Example Usage ```python -import chronopt as chron +import diffid as chron builder = ( - chron.DiffsolBuilder() + diffid.DiffsolBuilder() .with_diffsl(dsl) .with_data(data) .with_parameter("k", 1.0) # SSE is used by default, but can be explicit: - # .with_cost_metric(chron.SSE()) + # .with_cost_metric(diffid.SSE()) ) ``` @@ -71,14 +71,14 @@ $$\text{RMSE} = \sqrt{\frac{1}{n} \sum_{i=1}^{n} (y_i - \hat{y}_i)^2}$$ ### Example Usage ```python -import chronopt as chron +import diffid as chron builder = ( - chron.DiffsolBuilder() + diffid.DiffsolBuilder() .with_diffsl(dsl) .with_data(data) .with_parameter("k", 1.0) - .with_cost_metric(chron.RMSE()) + .with_cost_metric(diffid.RMSE()) ) ``` @@ -114,18 +114,18 @@ where $\sigma^2$ is estimated from residuals. ### Example Usage ```python -import chronopt as chron +import diffid as chron builder = ( - chron.DiffsolBuilder() + diffid.DiffsolBuilder() .with_diffsl(dsl) .with_data(data) .with_parameter("k", 1.0) - .with_cost_metric(chron.GaussianNLL()) + .with_cost_metric(diffid.GaussianNLL()) ) # Use with MCMC sampling for Bayesian inference -sampler = chron.MetropolisHastings().with_max_iter(10000) +sampler = diffid.MetropolisHastings().with_max_iter(10000) result = sampler.run(problem, initial_guess) ``` @@ -200,7 +200,7 @@ To implement a custom cost metric: 2. Expose through Python bindings 3. Rebuild the package -See the [Development Guide](../../development/architecture.md) for details on extending Chronopt. +See the [Development Guide](../../development/architecture.md) for details on extending Diffid. --- diff --git a/docs/api-reference/python/optimizers.md b/docs/api-reference/python/optimizers.md index a12b22f..65e73ae 100644 --- a/docs/api-reference/python/optimizers.md +++ b/docs/api-reference/python/optimizers.md @@ -12,7 +12,7 @@ Optimisation algorithms for finding parameter values that minimise the objective ## Nelder-Mead -::: chronopt.NelderMead +::: diffid.NelderMead options: show_root_heading: true show_source: false @@ -20,11 +20,11 @@ Optimisation algorithms for finding parameter values that minimise the objective ### Example Usage ```python -import chronopt as chron +import diffid as chron # Create optimiser with custom settings optimiser = ( - chron.NelderMead() + diffid.NelderMead() .with_max_iter(5000) .with_step_size(0.1) .with_threshold(1e-6) @@ -75,7 +75,7 @@ See the [Nelder-Mead Algorithm Guide](../../algorithms/optimizers/nelder-mead.md ## CMA-ES -::: chronopt.CMAES +::: diffid.CMAES options: show_root_heading: true show_source: false @@ -83,11 +83,11 @@ See the [Nelder-Mead Algorithm Guide](../../algorithms/optimizers/nelder-mead.md ### Example Usage ```python -import chronopt as chron +import diffid as chron # Create CMA-ES optimiser optimiser = ( - chron.CMAES() + diffid.CMAES() .with_max_iter(1000) .with_step_size(0.5) .with_population_size(20) @@ -148,7 +148,7 @@ See the [CMA-ES Algorithm Guide](../../algorithms/optimizers/cmaes.md) for more ## Adam -::: chronopt.Adam +::: diffid.Adam options: show_root_heading: true show_source: false @@ -156,11 +156,11 @@ See the [CMA-ES Algorithm Guide](../../algorithms/optimizers/cmaes.md) for more ### Example Usage ```python -import chronopt as chron +import diffid as chron # Create Adam optimiser optimiser = ( - chron.Adam() + diffid.Adam() .with_max_iter(5000) .with_step_size(0.01) # Learning rate .with_betas(0.9, 0.999) @@ -236,7 +236,7 @@ result = problem.optimise() # Uses Nelder-Mead with defaults ```python optimiser = ( - chron.CMAES() + diffid.CMAES() .with_max_iter(10000) # Maximum iterations .with_threshold(1e-8) # Objective threshold .with_patience(300.0) # Patience in seconds @@ -253,7 +253,7 @@ The optimiser stops when: For stochastic optimisers (CMA-ES), set a seed: ```python -optimiser = chron.CMAES().with_seed(42) +optimiser = diffid.CMAES().with_seed(42) result1 = optimiser.run(problem, [0.0, 0.0]) result2 = optimiser.run(problem, [0.0, 0.0]) # result1 == result2 (same random sequence) diff --git a/docs/api-reference/python/results.md b/docs/api-reference/python/results.md index 557a05c..926bc60 100644 --- a/docs/api-reference/python/results.md +++ b/docs/api-reference/python/results.md @@ -4,7 +4,7 @@ Result objects returned by optimisers and samplers containing optimal parameters ## OptimisationResults -::: chronopt.OptimisationResults +::: diffid.OptimisationResults options: show_root_heading: true show_source: false @@ -14,7 +14,7 @@ All optimisers return an `OptimisationResults` object with the following attribu ### Example Usage ```python -import chronopt as chron +import diffid as chron problem = builder.build() result = problem.optimise() @@ -46,7 +46,7 @@ Parameters are ordered according to `.with_parameter()` calls: ```python builder = ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_parameter("alpha", 1.0) # result.x[0] .with_parameter("beta", 2.0) # result.x[1] ) @@ -172,7 +172,7 @@ Results don't store parameter names. Track them manually if needed: ```python param_names = ["alpha", "beta", "gamma"] -builder = chron.ScalarBuilder().with_objective(func) +builder = diffid.ScalarBuilder().with_objective(func) for name in param_names: builder = builder.with_parameter(name, 1.0) diff --git a/docs/api-reference/python/samplers.md b/docs/api-reference/python/samplers.md index bd0b20a..9c271e2 100644 --- a/docs/api-reference/python/samplers.md +++ b/docs/api-reference/python/samplers.md @@ -23,11 +23,11 @@ MCMC sampling for exploring parameter posterior distributions. ### Planned API ```python -import chronopt as chron +import diffid as chron # Will be available in a future release sampler = ( - chron.MetropolisHastings() + diffid.MetropolisHastings() .with_max_iter(10000) .with_step_size(0.1) .with_burn_in(1000) @@ -72,11 +72,11 @@ Nested sampling for calculating model evidence (marginal likelihood) for model c ### Planned API ```python -import chronopt as chron +import diffid as chron # Will be available in a future release sampler = ( - chron.DynamicNestedSampling() + diffid.DynamicNestedSampling() .with_max_iter(5000) .with_n_live_points(500) .with_seed(42) @@ -147,11 +147,11 @@ graph TD ```python # Required for samplers builder = ( - chron.DiffsolBuilder() + diffid.DiffsolBuilder() .with_diffsl(dsl) .with_data(data) .with_parameter("k", 1.0) - .with_cost_metric(chron.GaussianNLL()) # Required! + .with_cost_metric(diffid.GaussianNLL()) # Required! ) ``` @@ -164,26 +164,26 @@ SSE and RMSE cannot be used with samplers as they lack probabilistic interpretat Typical workflow: optimise first, then sample for uncertainty: ```python -import chronopt as chron +import diffid as chron # 1. Build problem with GaussianNLL builder = ( - chron.DiffsolBuilder() + diffid.DiffsolBuilder() .with_diffsl(dsl) .with_data(data) .with_parameter("k", 1.0) - .with_cost_metric(chron.GaussianNLL()) + .with_cost_metric(diffid.GaussianNLL()) ) problem = builder.build() # 2. Find MAP estimate with optimiser -optimiser = chron.CMAES().with_max_iter(1000) +optimiser = diffid.CMAES().with_max_iter(1000) opt_result = optimiser.run(problem, [1.0]) print(f"MAP estimate: {opt_result.x}") # 3. Sample around MAP for uncertainty (future API) -# sampler = chron.MetropolisHastings().with_max_iter(10000) +# sampler = diffid.MetropolisHastings().with_max_iter(10000) # sample_result = sampler.run(problem, opt_result.x) # print(f"Posterior mean: {sample_result.samples.mean(axis=0)}") # print(f"Posterior std: {sample_result.samples.std(axis=0)}") @@ -205,8 +205,8 @@ Once samplers are available, see: Track sampler implementation progress: -- [GitHub Issue #XXX](https://github.com/bradyplanden/chronopt) - Metropolis-Hastings -- [GitHub Issue #XXX](https://github.com/bradyplanden/chronopt) - Dynamic Nested Sampling +- [GitHub Issue #XXX](https://github.com/bradyplanden/diffid) - Metropolis-Hastings +- [GitHub Issue #XXX](https://github.com/bradyplanden/diffid) - Dynamic Nested Sampling --- diff --git a/docs/api-reference/rust/index.md b/docs/api-reference/rust/index.md index f3d5a7a..09d9e1a 100644 --- a/docs/api-reference/rust/index.md +++ b/docs/api-reference/rust/index.md @@ -1,12 +1,12 @@ # Rust API Documentation -Chronopt's Rust core provides high-performance implementations of all optimisation and sampling algorithms. +Diffid's Rust core provides high-performance implementations of all optimisation and sampling algorithms. ## Official Documentation The complete Rust API documentation is hosted on docs.rs: -**[:octicons-arrow-right-24: Chronopt Rust Documentation on docs.rs](https://docs.rs/chronopt/latest/chronopt/)** +**[:octicons-arrow-right-24: Diffid Rust Documentation on docs.rs](https://docs.rs/diffid/latest/diffid/)** ## When to Use the Rust API @@ -16,14 +16,14 @@ Consider using the Rust crate directly when: - Building **Rust-native applications** - Need **zero-copy** data handling - Deploying to **embedded systems** or **constrained environments** -- Building **custom tooling** around Chronopt +- Building **custom tooling** around Diffid For most users, the Python API provides excellent performance with easier integration. ## Crate Structure ``` -chronopt/ +diffid/ ā”œā”€ā”€ builders/ # Problem builders (ScalarBuilder, DiffsolBuilder, etc.) ā”œā”€ā”€ optimisers/ # Optimisation algorithms │ ā”œā”€ā”€ nelder_mead/ # Nelder-Mead simplex @@ -38,7 +38,7 @@ chronopt/ ## Quick Example ```rust -use chronopt::prelude::*; +use diffid::prelude::*; use ndarray::array; // Define objective function @@ -64,13 +64,13 @@ fn main() { } ``` -## Adding Chronopt to Your Project +## Adding Diffid to Your Project Add to your `Cargo.toml`: ```toml [dependencies] -chronopt = "0.2" +diffid = "0.2" ndarray = "0.15" ``` @@ -78,7 +78,7 @@ For ODE support with DiffSL: ```toml [dependencies] -chronopt = { version = "0.2", features = ["diffsol"] } +diffid = { version = "0.2", features = ["diffsol"] } ``` ## Key Rust Features @@ -149,7 +149,7 @@ Key modules (click through on docs.rs for full details): Problem construction with fluent API. ```rust -pub use chronopt::builders::{ScalarBuilder, DiffsolBuilder, VectorBuilder}; +pub use diffid::builders::{ScalarBuilder, DiffsolBuilder, VectorBuilder}; ``` ### `optimisers` @@ -157,7 +157,7 @@ pub use chronopt::builders::{ScalarBuilder, DiffsolBuilder, VectorBuilder}; Optimisation algorithms. ```rust -pub use chronopt::optimisers::{NelderMead, CMAES, Adam}; +pub use diffid::optimisers::{NelderMead, CMAES, Adam}; ``` ### `cost` @@ -165,7 +165,7 @@ pub use chronopt::optimisers::{NelderMead, CMAES, Adam}; Cost metrics for objective functions. ```rust -pub use chronopt::cost::{CostMetric, SSE, RMSE, GaussianNLL}; +pub use diffid::cost::{CostMetric, SSE, RMSE, GaussianNLL}; ``` ### `problem` @@ -173,7 +173,7 @@ pub use chronopt::cost::{CostMetric, SSE, RMSE, GaussianNLL}; Problem types and evaluation. ```rust -pub use chronopt::problem::{Problem, ScalarProblem, VectorProblem}; +pub use diffid::problem::{Problem, ScalarProblem, VectorProblem}; ``` ## Performance Tips @@ -194,7 +194,7 @@ cargo run --example rosenbrock cargo run --example ode_fitting ``` -Browse examples on GitHub: [rust/examples/](https://github.com/bradyplanden/chronopt/tree/main/rust/examples) +Browse examples on GitHub: [rust/examples/](https://github.com/bradyplanden/diffid/tree/main/rust/examples) ## See Also diff --git a/docs/assets/diffid.drawio b/docs/assets/diffid.drawio new file mode 100644 index 0000000..3882c4f --- /dev/null +++ b/docs/assets/diffid.drawio @@ -0,0 +1,188 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/diffid.svg b/docs/assets/diffid.svg new file mode 100644 index 0000000..ef51290 --- /dev/null +++ b/docs/assets/diffid.svg @@ -0,0 +1,4 @@ + + + +
Conventional Approach
Python (interpreted)
Problem Definition
Optimisation Loop
(for i in range(n):)
Sampler Loop
(MCMC iterations)
repeated FFI calls (N iterations)
C / Fortran Bindings (fast)
Forward Model
(single evaluation)
FFI Boundary
⚠ Bottleneck: Python loop overhead per iteration
Configuration Approach
Python (configuration only)
builder = DiffsolBuilder()
Ā  .with_diffsl(model_code)
Ā  .with_data(observations)
Ā  .with_parameter("k", ..)
problem = builder.build()
Problem definition
(declarative)
result = problem.optimise()
Single call,
returns results
One-time handoff
FFI Boundary (crossed once)
Rust Core (compiled, fast)
Optimisation / Sampling Loop
Optimisers
Samplers
Cost
Metrics
Objective
+ Gradient
Forward Model
(DiffSL/Diffsol)
Parallel Batch
updated parameters
\ No newline at end of file diff --git a/docs/development/architecture.md b/docs/development/architecture.md index 9576393..6c9f9df 100644 --- a/docs/development/architecture.md +++ b/docs/development/architecture.md @@ -3,7 +3,7 @@ ## High-Level Overview
- ![Paradigm Comparison](../chronopt.drawio.svg){ width="100%" } + ![Paradigm Comparison](../assets/diffid.svg){ width="100%" }
## Key Design Patterns diff --git a/docs/development/contributing.md b/docs/development/contributing.md index 1fe3fce..c28c44e 100644 --- a/docs/development/contributing.md +++ b/docs/development/contributing.md @@ -1,4 +1,4 @@ -# Contributing to Chronopt +# Contributing to Diffid !!! info "Coming Soon" Detailed contributing guidelines are being written. @@ -7,8 +7,8 @@ ```bash # Fork and clone -git clone https://github.com/YOUR_USERNAME/chronopt.git -cd chronopt +git clone https://github.com/YOUR_USERNAME/diffid.git +cd diffid # Set up environment uv sync @@ -32,4 +32,4 @@ cargo test ## See Also - [Architecture](architecture.md) -- [GitHub Issues](https://github.com/bradyplanden/chronopt/issues) +- [GitHub Issues](https://github.com/bradyplanden/diffid/issues) diff --git a/docs/development/index.md b/docs/development/index.md index 67ed534..72422d6 100644 --- a/docs/development/index.md +++ b/docs/development/index.md @@ -1,6 +1,6 @@ # Development -Resources for contributors and developers working with Chronopt. +Resources for contributors and developers working with Diffid. ## Getting Started with Development @@ -18,7 +18,7 @@ Resources for contributors and developers working with Chronopt. --- - Understanding Chronopt's Rust core and PyO3 bindings design. + Understanding Diffid's Rust core and PyO3 bindings design. [:octicons-arrow-right-24: Architecture Overview](architecture.md) @@ -28,8 +28,8 @@ Resources for contributors and developers working with Chronopt. ```bash # Clone the repository -git clone https://github.com/bradyplanden/chronopt.git -cd chronopt +git clone https://github.com/bradyplanden/diffid.git +cd diffid # Create Python environment uv sync @@ -72,7 +72,7 @@ cargo test && uv run pytest -v If you modified Python bindings: ```bash -uv run cargo run -p chronopt-py --no-default-features --features stubgen --bin generate_stubs +uv run cargo run -p diffid-py --no-default-features --features stubgen --bin generate_stubs ``` ### 5. Format and Lint @@ -90,7 +90,7 @@ uv run ruff format . ## Project Structure ``` -chronopt/ +diffid/ ā”œā”€ā”€ rust/ # Rust core implementation │ ā”œā”€ā”€ src/ │ │ ā”œā”€ā”€ builders/ # Problem builders @@ -101,10 +101,10 @@ chronopt/ │ ā”œā”€ā”€ Cargo.toml │ └── tests/ # Rust tests ā”œā”€ā”€ python/ # Python bindings -│ ā”œā”€ā”€ src/chronopt/ +│ ā”œā”€ā”€ src/diffid/ │ │ ā”œā”€ā”€ __init__.py -│ │ └── _chronopt.pyi # Generated type stubs -│ └── chronopt/ # PyO3 bindings source +│ │ └── _diffid.pyi # Generated type stubs +│ └── diffid/ # PyO3 bindings source ā”œā”€ā”€ examples/ # Example scripts ā”œā”€ā”€ tests/ # Python tests ā”œā”€ā”€ docs/ # Documentation (this site) @@ -144,7 +144,7 @@ Python integration tests in `tests/`: ```python def test_optimisation(): - builder = chron.ScalarBuilder().with_objective(func) + builder = diffid.ScalarBuilder().with_objective(func) # ... assert result.success ``` diff --git a/docs/examples/gallery.md b/docs/examples/gallery.md index 42dc673..f59d7ec 100644 --- a/docs/examples/gallery.md +++ b/docs/examples/gallery.md @@ -1,9 +1,9 @@ # Examples Gallery -Visual gallery of Chronopt applications and use cases. +Visual gallery of Diffid applications and use cases. !!! info "Gallery Under Construction" - This gallery is being populated with examples. Check the [examples directory](https://github.com/bradyplanden/chronopt/tree/main/examples) for current code. + This gallery is being populated with examples. Check the [examples directory](https://github.com/bradyplanden/diffid/tree/main/examples) for current code. ## Available Examples @@ -14,8 +14,8 @@ Classic 2D optimisation test problem. **Files:** -- [python_problem.py](https://github.com/bradyplanden/chronopt/blob/main/examples/python_problem.py) -- [python_contour.py](https://github.com/bradyplanden/chronopt/blob/main/examples/python_contour.py) +- [python_problem.py](https://github.com/bradyplanden/diffid/blob/main/examples/python_problem.py) +- [python_contour.py](https://github.com/bradyplanden/diffid/blob/main/examples/python_contour.py) **Topics:** ScalarBuilder, contour plots, optimiser comparison @@ -26,7 +26,7 @@ Classic 2D optimisation test problem. #### Logistic Growth Single-variable ODE with DiffSL. -**File:** [logistic_growth.py](https://github.com/bradyplanden/chronopt/blob/main/examples/logistic_growth.py) +**File:** [logistic_growth.py](https://github.com/bradyplanden/diffid/blob/main/examples/logistic_growth.py) **Topics:** DiffsolBuilder, DiffSL syntax, data fitting @@ -37,8 +37,8 @@ Physics-based model with event handling. **Files:** -- [bouncy_ball.py](https://github.com/bradyplanden/chronopt/blob/main/examples/bouncy_ball.py) -- [bouncy_ball_sampling.py](https://github.com/bradyplanden/chronopt/blob/main/examples/bouncy_ball_sampling.py) +- [bouncy_ball.py](https://github.com/bradyplanden/diffid/blob/main/examples/bouncy_ball.py) +- [bouncy_ball_sampling.py](https://github.com/bradyplanden/diffid/blob/main/examples/bouncy_ball_sampling.py) **Topics:** Event detection, parameter uncertainty, MCMC @@ -51,8 +51,8 @@ Comparing different bicycle dynamics formulations. **Files:** -- [bicycle_model_diffsol.py](https://github.com/bradyplanden/chronopt/blob/main/examples/bicycle_model_diffsol.py) -- [bicycle_model_evidence.py](https://github.com/bradyplanden/chronopt/blob/main/examples/bicycle_model_evidence.py) +- [bicycle_model_diffsol.py](https://github.com/bradyplanden/diffid/blob/main/examples/bicycle_model_diffsol.py) +- [bicycle_model_evidence.py](https://github.com/bradyplanden/diffid/blob/main/examples/bicycle_model_evidence.py) **Topics:** Model selection, evidence calculation, Bayes factors @@ -65,9 +65,9 @@ Lotka-Volterra equations with multiple solver backends. **Files:** -- [predator_prey_diffsol.py](https://github.com/bradyplanden/chronopt/blob/main/examples/predator_prey/predator_prey_diffsol.py) -- [predator_prey_diffrax.py](https://github.com/bradyplanden/chronopt/blob/main/examples/predator_prey/predator_prey_diffrax.py) -- [predator_prey_diffeqpy.py](https://github.com/bradyplanden/chronopt/blob/main/examples/predator_prey/predator_prey_diffeqpy.py) +- [predator_prey_diffsol.py](https://github.com/bradyplanden/diffid/blob/main/examples/predator_prey/predator_prey_diffsol.py) +- [predator_prey_diffrax.py](https://github.com/bradyplanden/diffid/blob/main/examples/predator_prey/predator_prey_diffrax.py) +- [predator_prey_diffeqpy.py](https://github.com/bradyplanden/diffid/blob/main/examples/predator_prey/predator_prey_diffeqpy.py) **Topics:** VectorBuilder, JAX/Diffrax, Julia/DifferentialEquations.jl, performance comparison @@ -78,14 +78,14 @@ Lotka-Volterra equations with multiple solver backends. Clone the repository: ```bash -git clone https://github.com/bradyplanden/chronopt.git -cd chronopt +git clone https://github.com/bradyplanden/diffid.git +cd diffid ``` Install dependencies: ```bash -pip install chronopt matplotlib +pip install diffid matplotlib ``` Run an example: diff --git a/docs/getting-started/concepts.md b/docs/getting-started/concepts.md index 9e559e2..78624ee 100644 --- a/docs/getting-started/concepts.md +++ b/docs/getting-started/concepts.md @@ -1,14 +1,14 @@ # Core Concepts -This guide explains the fundamental concepts and patterns in Chronopt. +This guide explains the fundamental concepts and patterns in Diffid. ## The Builder Pattern -Chronopt uses the **builder pattern** for constructing problems. This provides a fluent, chainable API for configuration: +Diffid uses the **builder pattern** for constructing problems. This provides a fluent, chainable API for configuration: ```python builder = ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_objective(my_function) .with_parameter("x", 1.0) .with_parameter("y", 2.0) @@ -25,7 +25,7 @@ problem = builder.build() ## Problem Types -Chronopt provides different builders for different problem types: +Diffid provides different builders for different problem types: ### ScalarBuilder @@ -36,7 +36,7 @@ def objective(x): return np.asarray([x[0]**2 + x[1]**2]) problem = ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_objective(objective) .with_parameter("x", 0.0) .with_parameter("y", 0.0) @@ -62,7 +62,7 @@ F_i { (r * y) * (1 - (y / k)) } """ problem = ( - chron.DiffsolBuilder() + diffid.DiffsolBuilder() .with_diffsl(dsl) .with_data(data) .with_parameter("k", 1.0) @@ -88,7 +88,7 @@ def solve_ode(params): return predictions problem = ( - chron.VectorBuilder() + diffid.VectorBuilder() .with_objective(solve_ode) .with_data(data) .with_parameter("alpha", 1.0) @@ -111,7 +111,7 @@ Parameters are the decision variables you want to optimise: ```python builder = ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_parameter("x", initial_value=1.0) # Name and initial guess .with_parameter("y", initial_value=-1.0) ) @@ -125,7 +125,7 @@ builder = ( ## Optimisers vs Samplers -Chronopt provides two types of algorithms: +Diffid provides two types of algorithms: ### Optimisers: Finding the Best Solution @@ -144,7 +144,7 @@ Chronopt provides two types of algorithms: result = problem.optimise() # Specific optimiser -optimiser = chron.CMAES().with_max_iter(1000) +optimiser = diffid.CMAES().with_max_iter(1000) result = optimiser.run(problem, initial_guess) ``` @@ -162,7 +162,7 @@ result = optimiser.run(problem, initial_guess) **Usage:** ```python -sampler = chron.MetropolisHastings().with_max_iter(10000) +sampler = diffid.MetropolisHastings().with_max_iter(10000) result = sampler.run(problem, initial_guess) # Result contains samples, not a single optimum @@ -184,10 +184,10 @@ See [Choosing an Optimiser](../guides/choosing-optimiser.md) and [Choosing a Sam ## The Ask/Tell Pattern -For advanced use cases, Chronopt supports the **ask/tell pattern** for manual control of the optimisation loop: +For advanced use cases, Diffid supports the **ask/tell pattern** for manual control of the optimisation loop: ```python -optimiser = chron.CMAES().with_max_iter(1000) +optimiser = diffid.CMAES().with_max_iter(1000) # Ask for candidates candidates = optimiser.ask(n_candidates=10) @@ -219,7 +219,7 @@ result = optimiser.get_result() Cost metrics define how model predictions are compared to observations: ```python -from chronopt import SSE, RMSE, GaussianNLL +from diffid import SSE, RMSE, GaussianNLL # Sum of squared errors (default) builder = builder.with_cost_metric(SSE()) @@ -278,7 +278,7 @@ print(result.samples) # Posterior samples ## Parallelisation -Chronopt automatically parallelises where possible: +Diffid automatically parallelises where possible: - **DiffsolBuilder**: Multi-threaded ODE solving - **CMA-ES**: Parallel candidate evaluation @@ -291,7 +291,7 @@ Control parallelism: builder = builder.with_max_threads(4) # Population size for CMA-ES (larger = more parallel work) -optimiser = chron.CMAES().with_population_size(20) +optimiser = diffid.CMAES().with_population_size(20) ``` See the [Parallel Execution Guide](../guides/parallel-execution.md) for details. diff --git a/docs/getting-started/first-ode-fit.md b/docs/getting-started/first-ode-fit.md index 3fffa32..ce2fd7c 100644 --- a/docs/getting-started/first-ode-fit.md +++ b/docs/getting-started/first-ode-fit.md @@ -1,6 +1,6 @@ # First ODE Fit -This tutorial demonstrates how to fit ordinary differential equations (ODEs) to data using Chronopt's DiffSL integration with the Diffsol solver. +This tutorial demonstrates how to fit ordinary differential equations (ODEs) to data using Diffid's DiffSL integration with the Diffsol solver. ## The Problem: Logistic Growth @@ -14,7 +14,7 @@ where: $r$ is the growth rate, $k$ is the carrying capacity, and $y$ is the popu ```python import numpy as np -import chronopt as chron +import diffid as chron # Define the ODE model in DiffSL syntax dsl = """ @@ -37,7 +37,7 @@ data = np.column_stack((t, observations)) # Build the problem builder = ( - chron.DiffsolBuilder() + diffid.DiffsolBuilder() .with_diffsl(dsl) .with_data(data) .with_parameter("r", 0.5) # Initial guess for growth rate @@ -47,7 +47,7 @@ builder = ( problem = builder.build() # Run optimisation with CMA-ES -optimiser = chron.CMAES().with_max_iter(1000) +optimiser = diffid.CMAES().with_max_iter(1000) result = optimiser.run(problem, [0.5, 1.0]) # Display results @@ -70,7 +70,7 @@ F_i { (r * y) * (1 - (y / k)) } # Right-hand side of dy/dt = ... ## Data Format -Chronopt expects data as a 2D NumPy array where: +Diffid expects data as a 2D NumPy array where: - **First column**: Time points - **Remaining columns**: Observed values for each variable @@ -158,14 +158,14 @@ F_i { k * sin(t) - y } ## Cost Metrics -By default, Chronopt uses sum of squared errors (SSE). You can specify different cost metrics as shown below. See the [Cost Metrics Guide](../guides/cost-metrics.md) for more details. +By default, Diffid uses sum of squared errors (SSE). You can specify different cost metrics as shown below. See the [Cost Metrics Guide](../guides/cost-metrics.md) for more details. ```python -from chronopt import GaussianNLL, RMSE +from diffid import GaussianNLL, RMSE # Use Gaussian negative log-likelihood builder = ( - chron.DiffsolBuilder() + diffid.DiffsolBuilder() .with_diffsl(dsl) .with_data(data) .with_parameter("k", 1.0) @@ -191,7 +191,7 @@ result = problem.optimise() # Uses Nelder-Mead ### CMA-ES ```python -optimiser = chron.CMAES().with_max_iter(1000).with_step_size(0.5) +optimiser = diffid.CMAES().with_max_iter(1000).with_step_size(0.5) result = optimiser.run(problem, initial_guess) ``` @@ -200,7 +200,7 @@ result = optimiser.run(problem, initial_guess) ### Adam ```python -optimiser = chron.Adam().with_max_iter(1000).with_step_size(0.01) +optimiser = diffid.Adam().with_max_iter(1000).with_step_size(0.01) result = optimiser.run(problem, initial_guess) ``` diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index e7e8740..e3280eb 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -1,6 +1,6 @@ -# Getting Started with Chronopt +# Getting Started with Diffid -Welcome to Chronopt! This section will help you get up and running with time-series inference and optimisation. +Welcome to Diffid! This section will help you get up and running with time-series inference and optimisation. ## Learning Path @@ -22,7 +22,7 @@ We recommend following this sequence: By the end of this section, you will be able to: -- Install Chronopt on your platform +- Install Diffid on your platform - Create and solve scalar optimisation problems - Fit differential equations to experimental data - Understand the builder pattern and problem types @@ -34,6 +34,6 @@ If you encounter issues: 1. Check the [Troubleshooting](../guides/troubleshooting.md) guide 2. Browse the [examples gallery](../examples/gallery.md) -3. Open an issue on [GitHub](https://github.com/bradyplanden/chronopt/issues) +3. Open an issue on [GitHub](https://github.com/bradyplanden/diffid/issues) Ready to begin? Start with [Installation](installation.md). diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 4a645be..59e8ff3 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -1,6 +1,6 @@ # Installation -Chronopt is available as a Python package with pre-built wheels for most platforms. +Diffid is available as a Python package with pre-built wheels for most platforms. ## Installation Methods @@ -9,44 +9,44 @@ Chronopt is available as a Python package with pre-built wheels for most platfor [uv](https://docs.astral.sh/uv/) is a fast Python package installer and resolver. ```bash - uv pip install chronopt + uv pip install diffid ``` === "pip" ```bash - pip install chronopt + pip install diffid ``` ### Optional Dependencies -Chronopt has optional plotting support via matplotlib: +Diffid has optional plotting support via matplotlib: === "uv" ```bash - uv pip install "chronopt[plotting]" + uv pip install "diffid[plotting]" ``` === "pip" ```bash - pip install "chronopt[plotting]" + pip install "diffid[plotting]" ``` ## Verifying Installation -After installation, verify that Chronopt is working correctly: +After installation, verify that Diffid is working correctly: ```python -import chronopt as chron +import diffid import numpy as np # Simple test def test_func(x): return np.asarray([(x[0] - 1.0) ** 2]) -builder = chron.ScalarBuilder().with_objective(test_func).with_parameter("x", 0.0) +builder = diffid.ScalarBuilder().with_objective(test_func).with_parameter("x", 0.0) problem = builder.build() result = problem.optimise() @@ -92,8 +92,8 @@ If you need to build from source (for development or if pre-built wheels aren't ```bash # Clone the repository -git clone https://github.com/bradyplanden/chronopt.git -cd chronopt +git clone https://github.com/bradyplanden/diffid.git +cd diffid # Create Python environment uv sync @@ -104,9 +104,9 @@ uv run maturin develop # Run tests uv run pytest -v ``` - + For additional troubleshooting, see the [Troubleshooting Guide](../guides/troubleshooting.md). ## Next Steps -Now that Chronopt is installed, proceed to the [5-Minute Quickstart](quickstart.md) to run your first optimisation. +Now that Diffid is installed, proceed to the [5-Minute Quickstart](quickstart.md) to run your first optimisation. diff --git a/docs/getting-started/logistic_fit.png b/docs/getting-started/logistic_fit.png new file mode 100644 index 0000000..bc18c80 Binary files /dev/null and b/docs/getting-started/logistic_fit.png differ diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md index 97fd9b2..155330b 100644 --- a/docs/getting-started/quickstart.md +++ b/docs/getting-started/quickstart.md @@ -14,7 +14,7 @@ The global minimum is at $(x, y) = (1, 1)$ with $f(1, 1) = 0$. ```python import numpy as np -import chronopt as chron +import diffid def rosenbrock(x): @@ -25,7 +25,7 @@ def rosenbrock(x): # Build the problem builder = ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_objective(rosenbrock) .with_parameter("x", 1.5) # Initial guess .with_parameter("y", -1.5) # Initial guess @@ -70,7 +70,7 @@ You can specify which optimiser to use: ```python # Use CMA-ES for global search -optimiser = chron.CMAES().with_max_iter(1000).with_step_size(0.5) +optimiser = diffid.CMAES().with_max_iter(1000).with_step_size(0.5) result = optimiser.run(problem, [1.5, -1.5]) print(f"Optimal parameters: {result.x}") @@ -81,7 +81,7 @@ print(f"Objective value: {result.value:.3e}") ```python # Use Adam optimiser -optimiser = chron.Adam().with_max_iter(1000).with_step_size(0.01) +optimiser = diffid.Adam().with_max_iter(1000).with_step_size(0.01) result = optimiser.run(problem, [1.5, -1.5]) print(f"Optimal parameters: {result.x}") @@ -95,7 +95,7 @@ If you installed the `plotting` extra, you can visualise the optimisation landsc ```python import numpy as np import matplotlib.pyplot as plt -import chronopt as chron +import diffid def rosenbrock(x): @@ -124,7 +124,7 @@ plt.plot(1.0, 1.0, 'r*', markersize=20, label='Global minimum') # Run optimisation and plot path builder = ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_objective(rosenbrock) .with_parameter("x", -1.5) .with_parameter("y", -0.5) diff --git a/docs/getting-started/rosenbrock_contour.png b/docs/getting-started/rosenbrock_contour.png new file mode 100644 index 0000000..85f218f Binary files /dev/null and b/docs/getting-started/rosenbrock_contour.png differ diff --git a/docs/guides/custom-solvers.md b/docs/guides/custom-solvers.md index 40e840d..0f04ba2 100644 --- a/docs/guides/custom-solvers.md +++ b/docs/guides/custom-solvers.md @@ -19,7 +19,7 @@ def custom_solver(params): return predictions builder = ( - chron.VectorBuilder() + diffid.VectorBuilder() .with_objective(custom_solver) .with_data(data) .with_parameter("alpha", 1.0) @@ -29,8 +29,8 @@ builder = ( ## Examples See the predator-prey examples: -- [predator_prey_diffrax.py](https://github.com/bradyplanden/chronopt/blob/main/examples/predator_prey/predator_prey_diffrax.py) -- [predator_prey_diffeqpy.py](https://github.com/bradyplanden/chronopt/blob/main/examples/predator_prey/predator_prey_diffeqpy.py) +- [predator_prey_diffrax.py](https://github.com/bradyplanden/diffid/blob/main/examples/predator_prey/predator_prey_diffrax.py) +- [predator_prey_diffeqpy.py](https://github.com/bradyplanden/diffid/blob/main/examples/predator_prey/predator_prey_diffeqpy.py) ## See Also diff --git a/docs/guides/diffsol-backend.md b/docs/guides/diffsol-backend.md index 91925f9..05db37c 100644 --- a/docs/guides/diffsol-backend.md +++ b/docs/guides/diffsol-backend.md @@ -14,7 +14,7 @@ ```python builder = ( - chron.DiffsolBuilder() + diffid.DiffsolBuilder() .with_diffsl(dsl) .with_data(data) .with_backend("dense") # or "sparse" diff --git a/docs/guides/index.md b/docs/guides/index.md index b371d3a..a1ec664 100644 --- a/docs/guides/index.md +++ b/docs/guides/index.md @@ -1,6 +1,6 @@ # User Guides -In-depth guides for making the most of Chronopt's optimisation and sampling capabilities. +In-depth guides for making the most of Diffid's optimisation and sampling capabilities. ## Algorithm Selection diff --git a/docs/guides/parallel-execution.md b/docs/guides/parallel-execution.md index 8882c80..dc4fa86 100644 --- a/docs/guides/parallel-execution.md +++ b/docs/guides/parallel-execution.md @@ -16,7 +16,7 @@ builder = builder.with_max_threads(4) # Population size for CMA-ES -optimiser = chron.CMAES().with_population_size(20) +optimiser = diffid.CMAES().with_population_size(20) ``` ## See Also diff --git a/docs/guides/troubleshooting.md b/docs/guides/troubleshooting.md index 6b1d8da..fead903 100644 --- a/docs/guides/troubleshooting.md +++ b/docs/guides/troubleshooting.md @@ -3,10 +3,10 @@ ### Import Error ```python -ImportError: No module named 'chronopt' +ImportError: No module named 'diffid' ``` -**Solution**: Install Chronopt: `pip install chronopt` +**Solution**: Install Diffid: `pip install diffid` ### Poor Fit Quality @@ -29,4 +29,4 @@ ImportError: No module named 'chronopt' - [Installation](../getting-started/installation.md) - [Tuning Optimisers](tuning-optimizers.md) -- [GitHub Issues](https://github.com/bradyplanden/chronopt/issues) +- [GitHub Issues](https://github.com/bradyplanden/diffid/issues) diff --git a/docs/index.md b/docs/index.md index ad25c0a..5f9481a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,17 +1,17 @@ -# Chronopt +# Diffid -**chron**os-**opt**imum is a Rust-first toolkit for time-series inference and optimisation with ergonomic Python bindings. It couples high-performance solvers with a highly customisable builder API for identification and optimisation of differential systems. +differential identification is a Rust-first toolkit for time-series inference and optimisation with ergonomic Python bindings. It couples high-performance solvers with a highly customisable builder API for identification and optimisation of differential systems. -## Why Chronopt? +## Why Diffid? -Chronopt offers a different paradigm for a parameter inference library. Conventionally, Python-based inference libraries are constructed via python bindings to a high-performance forward model with the inference algorithms implemented in Python. This package instead introduces an alternative, where the Python layer acts purely as a declarative configuration interface, +Diffid offers a different paradigm for a parameter inference library. Conventionally, Python-based inference libraries are constructed via python bindings to a high-performance forward model with the inference algorithms implemented in Python. This package instead introduces an alternative, where the Python layer acts purely as a declarative configuration interface, while all computationally intensive work (the optimisation / sampling loop, gradient calculations, etc.) happens entirely within the Rust runtime without crossing the FFI boundary repeatedly. This is architecture is presented visually below,
- ![Paradigm Comparison](chronopt.drawio.svg){ width="100%" } -
Conventional approach vs Chronopt: the optimisation loop moves from Python to Rust
+ ![Paradigm Comparison](assets/diffid.svg){ width="100%" } +
Conventional vs configuration approach: the optimisation loop moves from Python to Rust
@@ -30,7 +30,7 @@ while all computationally intensive work (the optimisation / sampling loop, grad --- - Get started with Chronopt in 5 minutes with a simple scalar optimisation example. + Get started with Diffid in 5 minutes with a simple scalar optimisation example. [:octicons-arrow-right-24: Quickstart](getting-started/quickstart.md) @@ -62,38 +62,38 @@ while all computationally intensive work (the optimisation / sampling loop, grad ## Installation -Chronopt targets Python >= 3.11. Windows builds are currently marked experimental. +Diffid targets Python >= 3.11. Windows builds are currently marked experimental. === "pip" ```bash - pip install chronopt + pip install diffid # Optional extras for plotting - pip install "chronopt[plotting]" + pip install "diffid[plotting]" ``` === "uv" ```bash - uv pip install chronopt + uv pip install diffid # Optional extras for plotting - uv pip install "chronopt[plotting]" + uv pip install "diffid[plotting]" ``` ## Example: Scalar Optimisation ```python import numpy as np -import chronopt as chron +import diffid def rosenbrock(x): value = (1 - x[0]) ** 2 + 100 * (x[1] - x[0] ** 2) ** 2 return np.asarray([value]) builder = ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_objective(rosenbrock) .with_parameter("x", 1.5) .with_parameter("y", -1.5) @@ -110,7 +110,7 @@ print(f"Success: {result.success}") ```python import numpy as np -import chronopt as chron +import diffid # Logistic growth model in DiffSL dsl = """ @@ -124,7 +124,7 @@ observations = np.exp(-1.3 * t) data = np.column_stack((t, observations)) builder = ( - chron.DiffsolBuilder() + diffid.DiffsolBuilder() .with_diffsl(dsl) .with_data(data) .with_parameter("k", 1.0) @@ -132,7 +132,7 @@ builder = ( ) problem = builder.build() -optimiser = chron.CMAES().with_max_iter(1000) +optimiser = diffid.CMAES().with_max_iter(1000) result = optimiser.run(problem, [0.5, 0.5]) print(result.x) diff --git a/docs/tutorials/index.md b/docs/tutorials/index.md index 9120487..588f65a 100644 --- a/docs/tutorials/index.md +++ b/docs/tutorials/index.md @@ -1,6 +1,6 @@ # Tutorials -Interactive Jupyter notebooks for hands-on learning with Chronopt. +Interactive Jupyter notebooks for hands-on learning with Diffid. ## Learning Paths @@ -8,7 +8,7 @@ Follow these progressive learning paths based on your experience level and goals ### šŸŽÆ Beginner Track -Perfect for those new to Chronopt or optimisation: +Perfect for those new to Diffid or optimisation:
@@ -101,26 +101,26 @@ Complex problems and advanced techniques: ### Installation -Install Chronopt with Jupyter and plotting support: +Install Diffid with Jupyter and plotting support: === "pip" ```bash - pip install chronopt jupyter matplotlib + pip install diffid jupyter matplotlib ``` === "uv" ```bash - uv pip install chronopt jupyter matplotlib + uv pip install diffid jupyter matplotlib ``` ### Clone and Run ```bash # Clone the repository -git clone https://github.com/bradyplanden/chronopt.git -cd chronopt/docs/tutorials/notebooks +git clone https://github.com/bradyplanden/diffid.git +cd diffid/docs/tutorials/notebooks # Launch Jupyter jupyter notebook @@ -134,13 +134,13 @@ You can also run these notebooks in Google Colab (coming soon with hosted versio By completing all tutorials, you will: -āœ… Understand Chronopt's builder pattern and API -āœ… Optimize both scalar functions and ODE parameters -āœ… Compare and tune different optimisation algorithms -āœ… Quantify parameter uncertainty with MCMC -āœ… Compare models using Bayesian evidence -āœ… Integrate custom ODE solvers (JAX, Julia) -āœ… Make informed decisions about algorithm selection +- Understand Diffid's builder pattern and API +- Optimize both scalar functions and ODE parameters +- Compare and tune different optimisation algorithms +- Quantify parameter uncertainty with MCMC +- Compare models using Bayesian evidence +- Integrate custom ODE solvers (JAX, Julia) +- Make informed decisions about algorithm selection ## Notebook Structure @@ -157,7 +157,7 @@ Each tutorial follows a consistent structure: ## Alternative: Python Scripts -Prefer scripts to notebooks? Check out the [examples directory](https://github.com/bradyplanden/chronopt/tree/main/examples): +Prefer scripts to notebooks? Check out the [examples directory](https://github.com/bradyplanden/diffid/tree/main/examples): - `python_problem.py` - Basic scalar optimisation - `logistic_growth.py` - ODE fitting @@ -183,10 +183,10 @@ Feel free to reuse these in your own projects! ### Import Errors ```python -ModuleNotFoundError: No module named 'chronopt' +ModuleNotFoundError: No module named 'diffid' ``` -**Solution**: Install Chronopt: `pip install chronopt` +**Solution**: Install Diffid: `pip install diffid` ### Notebook Kernel Issues @@ -222,13 +222,13 @@ If MCMC sampling is slow: - **Documentation**: Browse the [complete docs](../index.md) - **Examples**: See the [examples gallery](../examples/gallery.md) - **API Reference**: Check the [API docs](../api-reference/index.md) -- **Issues**: Report problems on [GitHub](https://github.com/bradyplanden/chronopt/issues) +- **Issues**: Report problems on [GitHub](https://github.com/bradyplanden/diffid/issues) ## Contributing Found an issue or want to improve a tutorial? -1. Fork the [repository](https://github.com/bradyplanden/chronopt) +1. Fork the [repository](https://github.com/bradyplanden/diffid) 2. Edit notebooks in `docs/tutorials/notebooks/` 3. Test your changes locally 4. Submit a pull request @@ -253,7 +253,7 @@ After completing the tutorials: More applications and use cases -- [:material-github:{ .lg .middle } __GitHub Repository__](https://github.com/bradyplanden/chronopt) +- [:material-github:{ .lg .middle } __GitHub Repository__](https://github.com/bradyplanden/diffid) Source code and development diff --git a/docs/tutorials/notebooks/01_optimization_basics.ipynb b/docs/tutorials/notebooks/01_optimization_basics.ipynb index d916426..61743db 100644 --- a/docs/tutorials/notebooks/01_optimization_basics.ipynb +++ b/docs/tutorials/notebooks/01_optimization_basics.ipynb @@ -28,7 +28,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-10T22:21:26.832609Z", @@ -40,22 +40,16 @@ "outputs": [], "source": [ "# Import plotting utilities\n", - "import chronopt as chron\n", + "import diffid\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", - "from chronopt.plotting import contour_2d" + "from diffid.plotting import contour_2d" ] }, { "cell_type": "markdown", "metadata": {}, - "source": [ - "## Define the Objective Function\n", - "\n", - "In Chronopt, objective functions must:\n", - "1. Accept a NumPy array as input\n", - "2. Return a NumPy array as output (even for scalar values)" - ] + "source": "## Define the Objective Function\n\nIn Diffid, objective functions must:\n1. Accept a NumPy array as input\n2. Return a NumPy array as output (even for scalar values)" }, { "cell_type": "code", @@ -87,7 +81,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-10T22:21:27.533691Z", @@ -96,19 +90,10 @@ "shell.execute_reply": "2026-01-10T22:21:27.537575Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Problem built successfully!\n", - "Number of parameters: 2\n" - ] - } - ], + "outputs": [], "source": [ "builder = (\n", - " chron.ScalarBuilder()\n", + " diffid.ScalarBuilder()\n", " .with_objective(rosenbrock)\n", " .with_parameter(\"x\", 10.0) # Initial guess\n", " .with_parameter(\"y\", 10.0) # Initial guess\n", @@ -122,12 +107,7 @@ { "cell_type": "markdown", "metadata": {}, - "source": [ - "## Optimise with Default Settings\n", - "\n", - "The problem class is constructed as the core object in chronopt, as such\n", - " an `optimise()` method if provided for fast optimisation. This uses Nelder-Mead by default:" - ] + "source": "## Optimise with Default Settings\n\nThe problem class is constructed as the core object in diffid, as such\n an `optimise()` method if provided for fast optimisation. This uses Nelder-Mead by default:" }, { "cell_type": "code", @@ -240,7 +220,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-10T22:21:27.973637Z", @@ -249,34 +229,16 @@ "shell.execute_reply": "2026-01-10T22:21:28.084924Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "======================================================================\n", - "OPTIMISER COMPARISON\n", - "======================================================================\n", - "Optimiser Success Final Value Iterations Evaluations\n", - "----------------------------------------------------------------------\n", - "Nelder-Mead True 2.866e-07 130 290\n", - "CMA-ES True 3.681e-08 95 571\n", - "Adam False 6.927e-09 2000 2001\n", - "\n", - "Final parameters:\n", - "Nelder-Mead x = [1.0001253 1.00019857]\n", - "CMA-ES x = [1.00017279 1.00035395]\n", - "Adam x = [1.00008317 1.00016666]\n" - ] - } - ], + "outputs": [], "source": [ "# Define optimisers\n", "optimisers = {\n", - " \"Nelder-Mead\": chron.NelderMead().with_max_iter(1000),\n", - " \"CMA-ES\": chron.CMAES().with_max_iter(300).with_step_size(0.5),\n", - " \"Adam\": chron.Adam().with_max_iter(2000).with_step_size(0.25).with_threshold(1e-12),\n", + " \"Nelder-Mead\": diffid.NelderMead().with_max_iter(1000),\n", + " \"CMA-ES\": diffid.CMAES().with_max_iter(300).with_step_size(0.5),\n", + " \"Adam\": diffid.Adam()\n", + " .with_max_iter(2000)\n", + " .with_step_size(0.25)\n", + " .with_threshold(1e-12),\n", "}\n", "\n", "# Test starting point\n", @@ -408,7 +370,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-10T22:21:28.447521Z", @@ -417,42 +379,7 @@ "shell.execute_reply": "2026-01-10T22:21:28.460670Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "============================================================\n", - "STARTING POINT SENSITIVITY\n", - "============================================================\n", - "\n", - "Start 1: [-1.5, -0.5]\n", - " Final: [0.99938601 0.99871559]\n", - " Iterations: 60\n", - " Error: 1.424e-03\n", - " Success: True\n", - "\n", - "Start 2: [1.5, 1.5]\n", - " Final: [1.00023483 1.00043031]\n", - " Iterations: 79\n", - " Error: 4.902e-04\n", - " Success: True\n", - "\n", - "Start 3: [0.0, 2.0]\n", - " Final: [1.00004402 1.00004508]\n", - " Iterations: 65\n", - " Error: 6.301e-05\n", - " Success: True\n", - "\n", - "Start 4: [-1.0, 1.0]\n", - " Final: [1.00040626 1.0008615 ]\n", - " Iterations: 105\n", - " Error: 9.525e-04\n", - " Success: True\n" - ] - } - ], + "outputs": [], "source": [ "# Try multiple starting points\n", "starting_points = [\n", @@ -462,7 +389,7 @@ " [-1.0, 1.0],\n", "]\n", "\n", - "optimiser = chron.NelderMead().with_max_iter(1000)\n", + "optimiser = diffid.NelderMead().with_max_iter(1000)\n", "\n", "print(\"\\n\" + \"=\" * 60)\n", "print(\"STARTING POINT SENSITIVITY\")\n", @@ -536,4 +463,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/docs/tutorials/notebooks/02_ode_fitting_diffsol.ipynb b/docs/tutorials/notebooks/02_ode_fitting_diffsol.ipynb index a59ba93..7a471e9 100644 --- a/docs/tutorials/notebooks/02_ode_fitting_diffsol.ipynb +++ b/docs/tutorials/notebooks/02_ode_fitting_diffsol.ipynb @@ -33,7 +33,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-10T22:21:29.582370Z", @@ -43,13 +43,7 @@ } }, "outputs": [], - "source": [ - "# Import plotting utilities\n", - "import chronopt as chron\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "from chronopt.plotting import ode_fit" - ] + "source": "# Import plotting utilities\nimport diffid\nimport matplotlib.pyplot as plt\nimport numpy as np\nfrom diffid.plotting import ode_fit" }, { "cell_type": "markdown", @@ -118,7 +112,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-10T22:21:30.052995Z", @@ -127,48 +121,8 @@ "shell.execute_reply": "2026-01-10T22:21:30.079161Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Generated 100 data points\n", - "Time range: [0.00, 4.00]\n", - "Value range: [0.102, 0.856]\n", - "Noise level: 0.01\n", - "\n", - "True parameters: r = 1.0, k = 1.0\n" - ] - } - ], - "source": [ - "# Time points\n", - "t_span = np.linspace(0, 4, 100)\n", - "\n", - "# True logistic growth solution with known parameters\n", - "# y(t) = y0 * exp(r*t) / (1 + y0 * (exp(r*t) - 1) / k)\n", - "y0 = 0.1\n", - "r_true = 1.0\n", - "k_true = 1.0\n", - "\n", - "# Generate clean data\n", - "exp_rt = np.exp(r_true * t_span)\n", - "y_data = y0 * exp_rt / (1 + y0 * (exp_rt - 1) / k_true)\n", - "\n", - "# Add some noise\n", - "np.random.seed(42)\n", - "noise_level = 0.01\n", - "y_noisy = y_data + np.random.normal(0, noise_level, size=y_data.shape)\n", - "\n", - "# Chronopt expects data as [time, observation] columns\n", - "data = np.column_stack((t_span, y_noisy))\n", - "\n", - "print(f\"Generated {len(t_span)} data points\")\n", - "print(f\"Time range: [{t_span[0]:.2f}, {t_span[-1]:.2f}]\")\n", - "print(f\"Value range: [{y_noisy.min():.3f}, {y_noisy.max():.3f}]\")\n", - "print(f\"Noise level: {noise_level}\")\n", - "print(f\"\\nTrue parameters: r = {r_true}, k = {k_true}\")" - ] + "outputs": [], + "source": "# Time points\nt_span = np.linspace(0, 4, 100)\n\n# True logistic growth solution with known parameters\n# y(t) = y0 * exp(r*t) / (1 + y0 * (exp(r*t) - 1) / k)\ny0 = 0.1\nr_true = 1.0\nk_true = 1.0\n\n# Generate clean data\nexp_rt = np.exp(r_true * t_span)\ny_data = y0 * exp_rt / (1 + y0 * (exp_rt - 1) / k_true)\n\n# Add some noise\nnp.random.seed(42)\nnoise_level = 0.01\ny_noisy = y_data + np.random.normal(0, noise_level, size=y_data.shape)\n\n# Diffid expects data as [time, observation] columns\ndata = np.column_stack((t_span, y_noisy))\n\nprint(f\"Generated {len(t_span)} data points\")\nprint(f\"Time range: [{t_span[0]:.2f}, {t_span[-1]:.2f}]\")\nprint(f\"Value range: [{y_noisy.min():.3f}, {y_noisy.max():.3f}]\")\nprint(f\"Noise level: {noise_level}\")\nprint(f\"\\nTrue parameters: r = {r_true}, k = {k_true}\")" }, { "cell_type": "markdown", @@ -228,7 +182,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-10T22:21:30.232622Z", @@ -237,36 +191,8 @@ "shell.execute_reply": "2026-01-10T22:21:30.236189Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Problem built successfully!\n", - "\n", - "Initial parameter guesses: r = 50.0, k = 50.0\n", - "(Far from true values: r = 1.0, k = 1.0)\n" - ] - } - ], - "source": [ - "# Create builder\n", - "builder = (\n", - " chron.DiffsolBuilder()\n", - " .with_diffsl(dsl_model)\n", - " .with_data(data)\n", - " .with_tolerances(1e-6, 1e-8) # Relative and absolute tolerances\n", - " .with_parameter(\"r\", 10.0) # Initial guess (deliberately wrong)\n", - " .with_parameter(\"k\", 10.0) # Initial guess (deliberately wrong)\n", - " .with_parallel(True) # Enable parallel evaluation\n", - ")\n", - "\n", - "problem = builder.build()\n", - "\n", - "print(\"Problem built successfully!\")\n", - "print(\"\\nInitial parameter guesses: r = 50.0, k = 50.0\")\n", - "print(f\"(Far from true values: r = {r_true}, k = {k_true})\")" - ] + "outputs": [], + "source": "# Create builder\nbuilder = (\n diffid.DiffsolBuilder()\n .with_diffsl(dsl_model)\n .with_data(data)\n .with_tolerances(1e-6, 1e-8) # Relative and absolute tolerances\n .with_parameter(\"r\", 10.0) # Initial guess (deliberately wrong)\n .with_parameter(\"k\", 10.0) # Initial guess (deliberately wrong)\n .with_parallel(True) # Enable parallel evaluation\n)\n\nproblem = builder.build()\n\nprint(\"Problem built successfully!\")\nprint(\"\\nInitial parameter guesses: r = 50.0, k = 50.0\")\nprint(f\"(Far from true values: r = {r_true}, k = {k_true})\")" }, { "cell_type": "markdown", @@ -279,7 +205,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-10T22:21:30.238643Z", @@ -288,62 +214,8 @@ "shell.execute_reply": "2026-01-10T22:21:34.759178Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "============================================================\n", - "OPTIMIZATION RESULTS\n", - "============================================================\n", - "Success: True\n", - "\n", - "Fitted parameters:\n", - " r = 0.996728 (true: 1.0)\n", - " k = 1.002875 (true: 1.0)\n", - "\n", - "Optimization details:\n", - " Final cost: 8.220e-03\n", - " Iterations: 545\n", - " Function evaluations: 3271\n", - " Time: 514.311 milliseconds\n", - " Message: Function tolerance met\n", - "\n", - "Parameter errors:\n", - " r: 0.327%\n", - " k: 0.287%\n" - ] - } - ], - "source": [ - "# Create optimiser\n", - "optimiser = chron.CMAES().with_max_iter(1000).with_threshold(1e-12)\n", - "\n", - "# Run optimization\n", - "result = optimiser.run(problem, problem.initial_values())\n", - "\n", - "print(\"\\n\" + \"=\" * 60)\n", - "print(\"OPTIMIZATION RESULTS\")\n", - "print(\"=\" * 60)\n", - "print(f\"Success: {result.success}\")\n", - "print(\"\\nFitted parameters:\")\n", - "print(f\" r = {result.x[0]:.6f} (true: {r_true})\")\n", - "print(f\" k = {result.x[1]:.6f} (true: {k_true})\")\n", - "print(\"\\nOptimization details:\")\n", - "print(f\" Final cost: {result.value:.3e}\")\n", - "print(f\" Iterations: {result.iterations}\")\n", - "print(f\" Function evaluations: {result.evaluations}\")\n", - "print(f\" Time: {result.time.microseconds / 1e3:.3f} milliseconds\")\n", - "print(f\" Message: {result.message}\")\n", - "\n", - "# Calculate parameter errors\n", - "r_error = abs(result.x[0] - r_true) / r_true * 100\n", - "k_error = abs(result.x[1] - k_true) / k_true * 100\n", - "print(\"\\nParameter errors:\")\n", - "print(f\" r: {r_error:.3f}%\")\n", - "print(f\" k: {k_error:.3f}%\")" - ] + "outputs": [], + "source": "# Create optimiser\noptimiser = diffid.CMAES().with_max_iter(1000).with_threshold(1e-12)\n\n# Run optimization\nresult = optimiser.run(problem, problem.initial_values())\n\nprint(\"\\n\" + \"=\" * 60)\nprint(\"OPTIMIZATION RESULTS\")\nprint(\"=\" * 60)\nprint(f\"Success: {result.success}\")\nprint(\"\\nFitted parameters:\")\nprint(f\" r = {result.x[0]:.6f} (true: {r_true})\")\nprint(f\" k = {result.x[1]:.6f} (true: {k_true})\")\nprint(\"\\nOptimization details:\")\nprint(f\" Final cost: {result.value:.3e}\")\nprint(f\" Iterations: {result.iterations}\")\nprint(f\" Function evaluations: {result.evaluations}\")\nprint(f\" Time: {result.time.microseconds / 1e3:.3f} milliseconds\")\nprint(f\" Message: {result.message}\")\n\n# Calculate parameter errors\nr_error = abs(result.x[0] - r_true) / r_true * 100\nk_error = abs(result.x[1] - k_true) / k_true * 100\nprint(\"\\nParameter errors:\")\nprint(f\" r: {r_error:.3f}%\")\nprint(f\" k: {k_error:.3f}%\")" }, { "cell_type": "markdown", @@ -513,7 +385,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-10T22:21:42.612961Z", @@ -522,51 +394,8 @@ "shell.execute_reply": "2026-01-10T22:21:46.725244Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "================================================================================\n", - "OPTIMISER COMPARISON\n", - "================================================================================\n", - "Algorithm r (fitted) k (fitted) Cost Iters Time (s)\n", - "--------------------------------------------------------------------------------\n", - "Nelder-Mead 2.527654 0.604107 2.331e+00 37 0.204\n", - "CMA-ES 0.996760 1.002815 8.220e-03 57 0.529\n", - "Adam 0.953633 1.070344 1.648e-02 1000 0.374\n", - "\n", - "True values: 1.000000 1.000000 \n" - ] - } - ], - "source": [ - "optimisers = {\n", - " \"Nelder-Mead\": chron.NelderMead().with_max_iter(1000),\n", - " \"CMA-ES\": chron.CMAES().with_max_iter(500),\n", - " \"Adam\": chron.Adam().with_max_iter(1000).with_step_size(0.01),\n", - "}\n", - "\n", - "initial = [2.0, 2.0]\n", - "\n", - "print(\"\\n\" + \"=\" * 80)\n", - "print(\"OPTIMISER COMPARISON\")\n", - "print(\"=\" * 80)\n", - "print(\n", - " f\"{'Algorithm':<15} {'r (fitted)':<15} {'k (fitted)':<15} {'Cost':<15} {'Iters':<8} {'Time (s)'}\"\n", - ")\n", - "print(\"-\" * 80)\n", - "\n", - "for name, opt in optimisers.items():\n", - " result = opt.run(problem, initial)\n", - " print(\n", - " f\"{name:<15} {result.x[0]:<15.6f} {result.x[1]:<15.6f} \"\n", - " f\"{result.value:<15.3e} {result.iterations:<8} {result.time.microseconds / 1e6:.3f}\"\n", - " )\n", - "\n", - "print(f\"\\nTrue values: {r_true:<15.6f} {k_true:<15.6f}\")" - ] + "outputs": [], + "source": "optimisers = {\n \"Nelder-Mead\": diffid.NelderMead().with_max_iter(1000),\n \"CMA-ES\": diffid.CMAES().with_max_iter(500),\n \"Adam\": diffid.Adam().with_max_iter(1000).with_step_size(0.01),\n}\n\ninitial = [2.0, 2.0]\n\nprint(\"\\n\" + \"=\" * 80)\nprint(\"OPTIMISER COMPARISON\")\nprint(\"=\" * 80)\nprint(\n f\"{'Algorithm':<15} {'r (fitted)':<15} {'k (fitted)':<15} {'Cost':<15} {'Iters':<8} {'Time (s)'}\"\n)\nprint(\"-\" * 80)\n\nfor name, opt in optimisers.items():\n result = opt.run(problem, initial)\n print(\n f\"{name:<15} {result.x[0]:<15.6f} {result.x[1]:<15.6f} \"\n f\"{result.value:<15.3e} {result.iterations:<8} {result.time.microseconds / 1e6:.3f}\"\n )\n\nprint(f\"\\nTrue values: {r_true:<15.6f} {k_true:<15.6f}\")" }, { "cell_type": "markdown", @@ -714,4 +543,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/docs/tutorials/notebooks/03_parameter_uncertainty.ipynb b/docs/tutorials/notebooks/03_parameter_uncertainty.ipynb index b7c1c67..51c7ae7 100644 --- a/docs/tutorials/notebooks/03_parameter_uncertainty.ipynb +++ b/docs/tutorials/notebooks/03_parameter_uncertainty.ipynb @@ -32,7 +32,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-10T22:21:48.226019Z", @@ -42,15 +42,7 @@ } }, "outputs": [], - "source": [ - "# Import plotting utilities\n", - "import chronopt as chron\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "from chronopt.plotting import parameter_distributions, parameter_traces\n", - "\n", - "np.random.seed(42) # For reproducibility" - ] + "source": "# Import plotting utilities\nimport diffid\nimport matplotlib.pyplot as plt\nimport numpy as np\nfrom diffid.plotting import parameter_distributions, parameter_traces\n\nnp.random.seed(42) # For reproducibility" }, { "cell_type": "markdown", @@ -83,7 +75,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-10T22:21:48.861095Z", @@ -92,57 +84,8 @@ "shell.execute_reply": "2026-01-10T22:21:48.866205Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Generated 61 observations\n", - "Time span: [0, 0.999] seconds\n", - "Noise level: σ = 0.1\n", - "\n", - "True parameters:\n", - " g = 9.81 m/s²\n", - " h = 10.0 m\n" - ] - } - ], - "source": [ - "def ball_states(t, g, h):\n", - " \"\"\"Analytical solution for ball trajectory.\"\"\"\n", - " height = h - 0.5 * g * t**2\n", - " height = np.maximum(height, 0.0) # Can't go below ground\n", - " velocity = -g * t\n", - " return height, velocity\n", - "\n", - "\n", - "# True parameters\n", - "g_true = 9.81 # m/s²\n", - "h_true = 10.0 # meters\n", - "\n", - "# Time to hit ground: t = sqrt(2h/g)\n", - "t_stop = np.sqrt(2.0 * h_true / g_true)\n", - "t_final = 0.7 * t_stop # Stop before hitting ground\n", - "t_span = np.linspace(0.0, t_final, 61)\n", - "\n", - "# Generate clean data\n", - "height, velocity = ball_states(t_span, g_true, h_true)\n", - "\n", - "# Add measurement noise\n", - "noise_std = 0.1\n", - "height_noisy = height + np.random.normal(0, noise_std, len(t_span))\n", - "velocity_noisy = velocity + np.random.normal(0, noise_std, len(t_span))\n", - "\n", - "# Format for Chronopt: [time, height, velocity]\n", - "data = np.column_stack((t_span, height_noisy, velocity_noisy))\n", - "\n", - "print(f\"Generated {len(t_span)} observations\")\n", - "print(f\"Time span: [0, {t_final:.3f}] seconds\")\n", - "print(f\"Noise level: σ = {noise_std}\")\n", - "print(\"\\nTrue parameters:\")\n", - "print(f\" g = {g_true} m/s²\")\n", - "print(f\" h = {h_true} m\")" - ] + "outputs": [], + "source": "def ball_states(t, g, h):\n \"\"\"Analytical solution for ball trajectory.\"\"\"\n height = h - 0.5 * g * t**2\n height = np.maximum(height, 0.0) # Can't go below ground\n velocity = -g * t\n return height, velocity\n\n\n# True parameters\ng_true = 9.81 # m/s²\nh_true = 10.0 # meters\n\n# Time to hit ground: t = sqrt(2h/g)\nt_stop = np.sqrt(2.0 * h_true / g_true)\nt_final = 0.7 * t_stop # Stop before hitting ground\nt_span = np.linspace(0.0, t_final, 61)\n\n# Generate clean data\nheight, velocity = ball_states(t_span, g_true, h_true)\n\n# Add measurement noise\nnoise_std = 0.1\nheight_noisy = height + np.random.normal(0, noise_std, len(t_span))\nvelocity_noisy = velocity + np.random.normal(0, noise_std, len(t_span))\n\n# Format for Diffid: [time, height, velocity]\ndata = np.column_stack((t_span, height_noisy, velocity_noisy))\n\nprint(f\"Generated {len(t_span)} observations\")\nprint(f\"Time span: [0, {t_final:.3f}] seconds\")\nprint(f\"Noise level: σ = {noise_std}\")\nprint(\"\\nTrue parameters:\")\nprint(f\" g = {g_true} m/s²\")\nprint(f\" h = {h_true} m\")" }, { "cell_type": "markdown", @@ -267,7 +210,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-10T22:21:49.152830Z", @@ -276,55 +219,8 @@ "shell.execute_reply": "2026-01-10T22:21:49.634461Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "============================================================\n", - "OPTIMIZATION RESULTS (MAP Estimate)\n", - "============================================================\n", - "Success: True\n", - "\n", - "Fitted parameters:\n", - " g = 9.8035 m/s² (true: 9.81)\n", - " h = 9.9841 m (true: 10.0)\n", - "\n", - "Cost: 0.183349\n", - "Iterations: 197\n" - ] - } - ], - "source": [ - "# Build problem\n", - "builder = (\n", - " chron.DiffsolBuilder()\n", - " .with_diffsl(dsl_model)\n", - " .with_data(data)\n", - " .with_parameter(\"g\", 5.0) # Initial guess\n", - " .with_parameter(\"h\", 5.0) # Initial guess\n", - " .with_cost(chron.RMSE(2.0)) # 2 observables (height + velocity)\n", - ")\n", - "\n", - "problem = builder.build()\n", - "\n", - "# Optimize\n", - "optimizer = chron.Adam().with_step_size(0.05).with_max_iter(1500)\n", - "opt_result = optimizer.run(problem, [5.0, 5.0])\n", - "\n", - "print(\"\\n\" + \"=\" * 60)\n", - "print(\"OPTIMIZATION RESULTS (MAP Estimate)\")\n", - "print(\"=\" * 60)\n", - "print(f\"Success: {opt_result.success}\")\n", - "print(\"\\nFitted parameters:\")\n", - "print(f\" g = {opt_result.x[0]:.4f} m/s² (true: {g_true})\")\n", - "print(f\" h = {opt_result.x[1]:.4f} m (true: {h_true})\")\n", - "print(f\"\\nCost: {opt_result.value:.6f}\")\n", - "print(f\"Iterations: {opt_result.iterations}\")\n", - "\n", - "g_map, h_map = opt_result.x" - ] + "outputs": [], + "source": "# Build problem\nbuilder = (\n diffid.DiffsolBuilder()\n .with_diffsl(dsl_model)\n .with_data(data)\n .with_parameter(\"g\", 5.0) # Initial guess\n .with_parameter(\"h\", 5.0) # Initial guess\n .with_cost(diffid.RMSE(2.0)) # 2 observables (height + velocity)\n)\n\nproblem = builder.build()\n\n# Optimize\noptimizer = diffid.Adam().with_step_size(0.05).with_max_iter(1500)\nopt_result = optimizer.run(problem, [5.0, 5.0])\n\nprint(\"\\n\" + \"=\" * 60)\nprint(\"OPTIMIZATION RESULTS (MAP Estimate)\")\nprint(\"=\" * 60)\nprint(f\"Success: {opt_result.success}\")\nprint(\"\\nFitted parameters:\")\nprint(f\" g = {opt_result.x[0]:.4f} m/s² (true: {g_true})\")\nprint(f\" h = {opt_result.x[1]:.4f} m (true: {h_true})\")\nprint(f\"\\nCost: {opt_result.value:.6f}\")\nprint(f\"Iterations: {opt_result.iterations}\")\n\ng_map, h_map = opt_result.x" }, { "cell_type": "markdown", @@ -337,7 +233,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-10T22:21:49.638310Z", @@ -346,72 +242,8 @@ "shell.execute_reply": "2026-01-10T22:21:58.286103Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "============================================================\n", - "MCMC SAMPLING\n", - "============================================================\n", - "\n", - "Starting from MAP estimate: g = 9.8035, h = 9.9841\n", - "\n", - "Running MCMC... (this may take a minute)\n", - "\n", - "Sampling complete!\n", - "Samples shape: (10010, 2)\n", - "Acceptance rate: [0.284 0.295 0.258 0.24 0.248 0.267 0.284 0.24 0.262 0.277]\n", - "Target acceptance: 0.20 - 0.40\n", - "āœ“ Acceptance rate looks good!\n" - ] - } - ], - "source": [ - "# Rebuild problem with GaussianNLL (required for sampling)\n", - "builder_sampling = (\n", - " chron.DiffsolBuilder()\n", - " .with_diffsl(dsl_model)\n", - " .with_data(data)\n", - " .with_parameter(\"g\", g_map) # Start from MAP\n", - " .with_parameter(\"h\", h_map)\n", - " # .with_parallel(True)\n", - " .with_cost(chron.GaussianNLL(variance=noise_std**2))\n", - ")\n", - "\n", - "problem_sampling = builder_sampling.build()\n", - "\n", - "# Setup MCMC sampler\n", - "sampler = (\n", - " chron.MetropolisHastings()\n", - " .with_num_chains(10)\n", - " .with_iterations(1000)\n", - " .with_step_size(0.035)\n", - ")\n", - "\n", - "print(\"\\n\" + \"=\" * 60)\n", - "print(\"MCMC SAMPLING\")\n", - "print(\"=\" * 60)\n", - "print(f\"\\nStarting from MAP estimate: g = {g_map:.4f}, h = {h_map:.4f}\")\n", - "print(\"\\nRunning MCMC... (this may take a minute)\")\n", - "\n", - "# Run sampling\n", - "mcmc_result = sampler.run(problem_sampling, [g_map, h_map])\n", - "\n", - "print(\"\\nSampling complete!\")\n", - "print(f\"Samples shape: {mcmc_result.samples.shape}\")\n", - "print(f\"Acceptance rate: {mcmc_result.acceptance_rate}\")\n", - "print(\"Target acceptance: 0.20 - 0.40\")\n", - "\n", - "if np.any(mcmc_result.acceptance_rate < 0.15) or np.any(\n", - " mcmc_result.acceptance_rate > 0.50\n", - "):\n", - " print(\"āš ļø Warning: Acceptance rate is outside optimal range\")\n", - " print(\" Consider adjusting step_size\")\n", - "else:\n", - " print(\"āœ“ Acceptance rate looks good!\")" - ] + "outputs": [], + "source": "# Rebuild problem with GaussianNLL (required for sampling)\nbuilder_sampling = (\n diffid.DiffsolBuilder()\n .with_diffsl(dsl_model)\n .with_data(data)\n .with_parameter(\"g\", g_map) # Start from MAP\n .with_parameter(\"h\", h_map)\n # .with_parallel(True)\n .with_cost(diffid.GaussianNLL(variance=noise_std**2))\n)\n\nproblem_sampling = builder_sampling.build()\n\n# Setup MCMC sampler\nsampler = (\n diffid.MetropolisHastings()\n .with_num_chains(10)\n .with_iterations(1000)\n .with_step_size(0.035)\n)\n\nprint(\"\\n\" + \"=\" * 60)\nprint(\"MCMC SAMPLING\")\nprint(\"=\" * 60)\nprint(f\"\\nStarting from MAP estimate: g = {g_map:.4f}, h = {h_map:.4f}\")\nprint(\"\\nRunning MCMC... (this may take a minute)\")\n\n# Run sampling\nmcmc_result = sampler.run(problem_sampling, [g_map, h_map])\n\nprint(\"\\nSampling complete!\")\nprint(f\"Samples shape: {mcmc_result.samples.shape}\")\nprint(f\"Acceptance rate: {mcmc_result.acceptance_rate}\")\nprint(\"Target acceptance: 0.20 - 0.40\")\n\nif np.any(mcmc_result.acceptance_rate < 0.15) or np.any(\n mcmc_result.acceptance_rate > 0.50\n):\n print(\"āš ļø Warning: Acceptance rate is outside optimal range\")\n print(\" Consider adjusting step_size\")\nelse:\n print(\"āœ“ Acceptance rate looks good!\")" }, { "cell_type": "markdown", @@ -846,4 +678,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/docs/tutorials/notebooks/05_advanced_predator_prey.ipynb b/docs/tutorials/notebooks/05_advanced_predator_prey.ipynb index 8a7b5e9..165a3b9 100644 --- a/docs/tutorials/notebooks/05_advanced_predator_prey.ipynb +++ b/docs/tutorials/notebooks/05_advanced_predator_prey.ipynb @@ -10,7 +10,7 @@ "- Use VectorBuilder for custom ODE solvers\n", "- Compare Diffsol, Diffrax (JAX), and DifferentialEquations.jl\n", "- Understand performance trade-offs\n", - "- Integrate external solvers with Chronopt\n", + "- Integrate external solvers with Diffid\n", "\n", "**Prerequisites:** Tutorials 1-2, basic JAX or Julia knowledge (optional)\n", "\n", @@ -20,136 +20,7 @@ { "cell_type": "markdown", "metadata": {}, - "source": [ - "## Introduction\n", - "\n", - "Chronopt's **DiffsolBuilder** provides high-performance ODE solving for most cases. But sometimes you need:\n", - "\n", - "- **JAX/Diffrax**: Automatic differentiation, GPU acceleration\n", - "- **Julia/DifferentialEquations.jl**: Specialized solvers, stiff equations\n", - "- **Custom simulators**: Agent-based models, PDEs, hybrid systems\n", - "\n", - "**VectorBuilder** lets you integrate any Python-callable forward model with Chronopt's optimizers.\n", - "\n", - "## The Lotka-Volterra Model\n", - "\n", - "The predator-prey equations:\n", - "\n", - "$$\\begin{aligned}\n", - "\\frac{dx}{dt} &= \\alpha x - \\beta x y \\\\\n", - "\\frac{dy}{dt} &= \\delta x y - \\gamma y\n", - "\\end{aligned}$$\n", - "\n", - "where:\n", - "- $x$ is prey population\n", - "- $y$ is predator population\n", - "- $\\alpha, \\beta, \\gamma, \\delta$ are interaction rates\n", - "\n", - "This tutorial demonstrates parameter fitting with three different solver backends.\n", - "\n", - "## Coming Soon\n", - "\n", - "This advanced tutorial is under development. It will cover:\n", - "\n", - "### Backend 1: Diffsol (Built-in)\n", - "```python\n", - "builder = (\n", - " chron.DiffsolBuilder()\n", - " .with_diffsl(lotka_volterra_dsl)\n", - " .with_data(data)\n", - " .with_parameter(\"alpha\", 1.0)\n", - " # ... more parameters\n", - ")\n", - "```\n", - "\n", - "### Backend 2: JAX/Diffrax\n", - "```python\n", - "import jax\n", - "from diffrax import diffeqsolve, ODETerm, Tsit5\n", - "\n", - "def diffrax_solver(params):\n", - " # Your Diffrax integration\n", - " return predictions\n", - "\n", - "builder = (\n", - " chron.VectorBuilder()\n", - " .with_objective(diffrax_solver)\n", - " .with_data(data)\n", - " .with_parameter(\"alpha\", 1.0)\n", - ")\n", - "```\n", - "\n", - "### Backend 3: Julia/DifferentialEquations.jl\n", - "```python\n", - "from diffeqpy import de\n", - "\n", - "def diffeqpy_solver(params):\n", - " # Your Julia integration\n", - " return predictions\n", - "\n", - "builder = (\n", - " chron.VectorBuilder()\n", - " .with_objective(diffeqpy_solver)\n", - " .with_data(data)\n", - " .with_parameter(\"alpha\", 1.0)\n", - ")\n", - "```\n", - "\n", - "## Performance Comparison\n", - "\n", - "The tutorial will benchmark all three backends:\n", - "\n", - "- **Accuracy**: Parameter recovery quality\n", - "- **Speed**: Time per function evaluation\n", - "- **Ease of use**: Setup complexity\n", - "- **Special features**: Gradients, GPU, stiff solvers\n", - "\n", - "## When to Use Each Backend\n", - "\n", - "| Backend | Best For |\n", - "|---------|----------|\n", - "| **Diffsol** | General purpose, fast, built-in |\n", - "| **JAX/Diffrax** | Gradients, GPU, neural ODEs |\n", - "| **Julia/DiffEq** | Stiff systems, specialized solvers, DAEs |\n", - "| **Custom** | Non-ODE models, complex physics |\n", - "\n", - "## Example Data\n", - "\n", - "The predator-prey examples directory contains:\n", - "- `generate_data_diffrax.py`: Creates synthetic data\n", - "- `predator_prey_diffsol.py`: Diffsol backend\n", - "- `predator_prey_diffrax.py`: JAX/Diffrax backend\n", - "- `predator_prey_diffeqpy.py`: Julia backend\n", - "\n", - "Run these scripts directly to see the backends in action!\n", - "\n", - "## Installation\n", - "\n", - "For JAX/Diffrax:\n", - "```bash\n", - "pip install jax diffrax\n", - "```\n", - "\n", - "For Julia/DifferentialEquations.jl:\n", - "```bash\n", - "pip install diffeqpy\n", - "python -c \"from diffeqpy import de; de.install()\"\n", - "```\n", - "\n", - "## Key Takeaways\n", - "\n", - "1. **VectorBuilder** integrates any Python callable\n", - "2. **Diffsol** is the default - fast and easy\n", - "3. **JAX/Diffrax** for gradients and GPU\n", - "4. **Julia/DiffEq** for specialized solvers\n", - "5. All backends work with Chronopt's optimizers\n", - "\n", - "## Next Steps\n", - "\n", - "- [Custom Solvers Guide](../../guides/custom-solvers.md) - Detailed integration guide\n", - "- [VectorBuilder API](../../api-reference/python/builders.md#vectorbuilder)\n", - "- [Examples Gallery](../../examples/gallery.md) - More backend examples" - ] + "source": "## Introduction\n\nDiffid's **DiffsolBuilder** provides high-performance ODE solving for most cases. But sometimes you need:\n\n- **JAX/Diffrax**: Automatic differentiation, GPU acceleration\n- **Julia/DifferentialEquations.jl**: Specialized solvers, stiff equations\n- **Custom simulators**: Agent-based models, PDEs, hybrid systems\n\n**VectorBuilder** lets you integrate any Python-callable forward model with Diffid's optimizers.\n\n## The Lotka-Volterra Model\n\nThe predator-prey equations:\n\n$$\\begin{aligned}\n\\frac{dx}{dt} &= \\alpha x - \\beta x y \\\\\n\\frac{dy}{dt} &= \\delta x y - \\gamma y\n\\end{aligned}$$\n\nwhere:\n- $x$ is prey population\n- $y$ is predator population\n- $\\alpha, \\beta, \\gamma, \\delta$ are interaction rates\n\nThis tutorial demonstrates parameter fitting with three different solver backends.\n\n## Coming Soon\n\nThis advanced tutorial is under development. It will cover:\n\n### Backend 1: Diffsol (Built-in)\n```python\nbuilder = (\n diffid.DiffsolBuilder()\n .with_diffsl(lotka_volterra_dsl)\n .with_data(data)\n .with_parameter(\"alpha\", 1.0)\n # ... more parameters\n)\n```\n\n### Backend 2: JAX/Diffrax\n```python\nimport jax\nfrom diffrax import diffeqsolve, ODETerm, Tsit5\n\ndef diffrax_solver(params):\n # Your Diffrax integration\n return predictions\n\nbuilder = (\n diffid.VectorBuilder()\n .with_objective(diffrax_solver)\n .with_data(data)\n .with_parameter(\"alpha\", 1.0)\n)\n```\n\n### Backend 3: Julia/DifferentialEquations.jl\n```python\nfrom diffeqpy import de\n\ndef diffeqpy_solver(params):\n # Your Julia integration\n return predictions\n\nbuilder = (\n diffid.VectorBuilder()\n .with_objective(diffeqpy_solver)\n .with_data(data)\n .with_parameter(\"alpha\", 1.0)\n)\n```\n\n## Performance Comparison\n\nThe tutorial will benchmark all three backends:\n\n- **Accuracy**: Parameter recovery quality\n- **Speed**: Time per function evaluation\n- **Ease of use**: Setup complexity\n- **Special features**: Gradients, GPU, stiff solvers\n\n## When to Use Each Backend\n\n| Backend | Best For |\n|---------|----------|\n| **Diffsol** | General purpose, fast, built-in |\n| **JAX/Diffrax** | Gradients, GPU, neural ODEs |\n| **Julia/DiffEq** | Stiff systems, specialized solvers, DAEs |\n| **Custom** | Non-ODE models, complex physics |\n\n## Example Data\n\nThe predator-prey examples directory contains:\n- `generate_data_diffrax.py`: Creates synthetic data\n- `predator_prey_diffsol.py`: Diffsol backend\n- `predator_prey_diffrax.py`: JAX/Diffrax backend\n- `predator_prey_diffeqpy.py`: Julia backend\n\nRun these scripts directly to see the backends in action!\n\n## Installation\n\nFor JAX/Diffrax:\n```bash\npip install jax diffrax\n```\n\nFor Julia/DifferentialEquations.jl:\n```bash\npip install diffeqpy\npython -c \"from diffeqpy import de; de.install()\"\n```\n\n## Key Takeaways\n\n1. **VectorBuilder** integrates any Python callable\n2. **Diffsol** is the default - fast and easy\n3. **JAX/Diffrax** for gradients and GPU\n4. **Julia/DiffEq** for specialized solvers\n5. All backends work with Diffid's optimizers\n\n## Next Steps\n\n- [Custom Solvers Guide](../../guides/custom-solvers.md) - Detailed integration guide\n- [VectorBuilder API](../../api-reference/python/builders.md#vectorbuilder)\n- [Examples Gallery](../../examples/gallery.md) - More backend examples" } ], "metadata": { @@ -173,4 +44,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/docs/tutorials/notebooks/06_custom_solver_integration.ipynb b/docs/tutorials/notebooks/06_custom_solver_integration.ipynb index ba17ab5..2bb03ca 100644 --- a/docs/tutorials/notebooks/06_custom_solver_integration.ipynb +++ b/docs/tutorials/notebooks/06_custom_solver_integration.ipynb @@ -20,32 +20,14 @@ { "cell_type": "markdown", "metadata": {}, - "source": [ - "## Introduction\n", - "\n", - "While `DiffsolBuilder` provides a convenient interface for ODE fitting using the DiffSL language, `VectorBuilder` offers maximum flexibility by allowing you to use **any** ODE solver. This enables:\n", - "\n", - "1. **GPU acceleration** via JAX/Diffrax\n", - "2. **Specialized solvers** from Julia's DifferentialEquations.jl\n", - "3. **Custom dynamics** that don't fit the DiffSL syntax\n", - "4. **Pre-existing code** integration\n", - "\n", - "In this tutorial, we'll solve the predator-prey model using different backends and compare their performance." - ] + "source": "## Introduction\n\nWhile `DiffsolBuilder` provides a convenient interface for ODE fitting using the DiffSL language, `VectorBuilder` offers maximum flexibility by allowing you to use **any** ODE solver. This enables:\n\n1. **GPU acceleration** via JAX/Diffrax\n2. **Specialized solvers** from Julia's DifferentialEquations.jl\n3. **Custom dynamics** that don't fit the DiffSL syntax\n4. **Pre-existing code** integration\n\nIn this tutorial, we'll solve the predator-prey model using different backends and compare their performance." }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "# Import plotting utilities\n", - "import time\n", - "\n", - "import chronopt as chron\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np" - ] + "source": "# Import plotting utilities\nimport time\n\nimport diffid\nimport matplotlib.pyplot as plt\nimport numpy as np" }, { "cell_type": "markdown", @@ -175,56 +157,12 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "# Fit with DiffsolBuilder\n", - "start_time = time.time()\n", - "\n", - "# Combine times and observations for DiffsolBuilder\n", - "# Data format: first column is time, remaining columns are observations\n", - "data = np.column_stack((t_data, y_observed))\n", - "\n", - "result_diffsol = (\n", - " chron.DiffsolBuilder()\n", - " .with_diffsl(model_str)\n", - " .with_data(data)\n", - " .with_parameter(\"alpha\", 1.0) # initial guess\n", - " .with_parameter(\"beta\", 0.3)\n", - " .with_parameter(\"delta\", 0.05)\n", - " .with_parameter(\"gamma\", 0.5)\n", - " .with_cost(chron.SSE())\n", - " .with_optimiser(chron.NelderMead().with_max_iter(500))\n", - " .build()\n", - " .optimise()\n", - ")\n", - "\n", - "diffsol_time = time.time() - start_time\n", - "\n", - "print(\"\\n\" + \"=\" * 60)\n", - "print(\"DIFFSOL RESULTS\")\n", - "print(\"=\" * 60)\n", - "print(f\"True parameters: {list(true_params.values())}\")\n", - "print(f\"Estimated parameters: {result_diffsol.x}\")\n", - "print(f\"Final SSE: {result_diffsol.value:.6f}\")\n", - "print(f\"Iterations: {result_diffsol.iterations}\")\n", - "print(f\"Function evals: {result_diffsol.evaluations}\")\n", - "print(f\"Time: {diffsol_time:.3f}s\")\n", - "print(f\"Success: {result_diffsol.success}\")" - ] + "source": "# Fit with DiffsolBuilder\nstart_time = time.time()\n\n# Combine times and observations for DiffsolBuilder\n# Data format: first column is time, remaining columns are observations\ndata = np.column_stack((t_data, y_observed))\n\nresult_diffsol = (\n diffid.DiffsolBuilder()\n .with_diffsl(model_str)\n .with_data(data)\n .with_parameter(\"alpha\", 1.0) # initial guess\n .with_parameter(\"beta\", 0.3)\n .with_parameter(\"delta\", 0.05)\n .with_parameter(\"gamma\", 0.5)\n .with_cost(diffid.SSE())\n .with_optimiser(diffid.NelderMead().with_max_iter(500))\n .build()\n .optimise()\n)\n\ndiffsol_time = time.time() - start_time\n\nprint(\"\\n\" + \"=\" * 60)\nprint(\"DIFFSOL RESULTS\")\nprint(\"=\" * 60)\nprint(f\"True parameters: {list(true_params.values())}\")\nprint(f\"Estimated parameters: {result_diffsol.x}\")\nprint(f\"Final SSE: {result_diffsol.value:.6f}\")\nprint(f\"Iterations: {result_diffsol.iterations}\")\nprint(f\"Function evals: {result_diffsol.evaluations}\")\nprint(f\"Time: {diffsol_time:.3f}s\")\nprint(f\"Success: {result_diffsol.success}\")" }, { "cell_type": "markdown", "metadata": {}, - "source": [ - "## Method 2: VectorBuilder with JAX/Diffrax\n", - "\n", - "`VectorBuilder` accepts any callable that maps parameters to predicted outputs. This enables using **JAX/Diffrax** for GPU-accelerated ODE solving with automatic differentiation.\n", - "\n", - "### Advantages:\n", - "- ⚔ GPU acceleration\n", - "- šŸ”„ JIT compilation\n", - "- šŸ“ Automatic differentiation (gradients for free)\n", - "- šŸš€ Fast iteration for gradient-based optimisers" - ] + "source": "## Method 2: VectorBuilder with JAX/Diffrax\n\n`VectorBuilder` accepts any callable that maps parameters to predicted outputs. This enables using **JAX/Diffrax** for GPU-accelerated ODE solving with automatic differentiation.\n\n### Advantages:\n- ⚔ GPU acceleration\n- šŸ”„ JIT compilation\n- šŸ“ Automatic differentiation (gradients for free)\n- šŸš€ Fast iteration for gradient-based optimisers" }, { "cell_type": "code", @@ -295,12 +233,12 @@ " return sol.ys # Shape: (n_times, 2)\n", "\n", " def simulate_numpy(params):\n", - " \"\"\"NumPy wrapper for Chronopt compatibility.\"\"\"\n", + " \"\"\"NumPy wrapper for Diffid compatibility.\"\"\"\n", " return np.asarray(simulate_jax(jnp.asarray(params)))\n", "\n", " # Warm up JIT compiler\n", " _ = simulate_numpy([1.0, 0.4, 0.1, 0.4])\n", - " print(\"āœ… JAX/Diffrax solver ready (JIT compiled)\")" + " print(\"JAX/Diffrax solver ready (JIT compiled)\")" ] }, { @@ -308,39 +246,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "if JAX_AVAILABLE:\n", - " # Fit with VectorBuilder + JAX/Diffrax\n", - " start_time = time.time()\n", - "\n", - " result_diffrax = (\n", - " chron.VectorBuilder()\n", - " .with_objective(simulate_numpy)\n", - " .with_data(y_observed)\n", - " .with_parameter(\"alpha\", 1.0)\n", - " .with_parameter(\"beta\", 0.3)\n", - " .with_parameter(\"delta\", 0.05)\n", - " .with_parameter(\"gamma\", 0.5)\n", - " .with_cost(chron.SSE())\n", - " .with_optimiser(chron.NelderMead().with_max_iter(500))\n", - " .build()\n", - " .optimise()\n", - " )\n", - "\n", - " diffrax_time = time.time() - start_time\n", - "\n", - " print(\"\\n\" + \"=\" * 60)\n", - " print(\"DIFFRAX (JAX) RESULTS\")\n", - " print(\"=\" * 60)\n", - " print(f\"True parameters: {list(true_params.values())}\")\n", - " print(f\"Estimated parameters: {result_diffrax.x}\")\n", - " print(f\"Final SSE: {result_diffrax.value:.6f}\")\n", - " print(f\"Iterations: {result_diffrax.iterations}\")\n", - " print(f\"Function evals: {result_diffrax.evaluations}\")\n", - " print(f\"Time: {diffrax_time:.3f}s\")\n", - " print(f\"Success: {result_diffrax.success}\")\n", - " print(f\"\\nSpeedup vs Diffsol: {diffsol_time / diffrax_time:.2f}x\")" - ] + "source": "if JAX_AVAILABLE:\n # Fit with VectorBuilder + JAX/Diffrax\n start_time = time.time()\n\n result_diffrax = (\n diffid.VectorBuilder()\n .with_objective(simulate_numpy)\n .with_data(y_observed)\n .with_parameter(\"alpha\", 1.0)\n .with_parameter(\"beta\", 0.3)\n .with_parameter(\"delta\", 0.05)\n .with_parameter(\"gamma\", 0.5)\n .with_cost(diffid.SSE())\n .with_optimiser(diffid.NelderMead().with_max_iter(500))\n .build()\n .optimise()\n )\n\n diffrax_time = time.time() - start_time\n\n print(\"\\n\" + \"=\" * 60)\n print(\"DIFFRAX (JAX) RESULTS\")\n print(\"=\" * 60)\n print(f\"True parameters: {list(true_params.values())}\")\n print(f\"Estimated parameters: {result_diffrax.x}\")\n print(f\"Final SSE: {result_diffrax.value:.6f}\")\n print(f\"Iterations: {result_diffrax.iterations}\")\n print(f\"Function evals: {result_diffrax.evaluations}\")\n print(f\"Time: {diffrax_time:.3f}s\")\n print(f\"Success: {result_diffrax.success}\")\n print(f\"\\nSpeedup vs Diffsol: {diffsol_time / diffrax_time:.2f}x\")" }, { "cell_type": "markdown", @@ -404,7 +310,7 @@ "\n", " # Test\n", " test_output = simulate_julia([1.0, 0.4, 0.1, 0.4])\n", - " print(\"āœ… Julia/DifferentialEquations.jl ready\")\n", + " print(\"Julia/DifferentialEquations.jl ready\")\n", " print(f\" Output shape: {test_output.shape}\")" ] }, @@ -413,39 +319,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "if JULIA_AVAILABLE:\n", - " # Fit with VectorBuilder + Julia\n", - " start_time = time.time()\n", - "\n", - " result_julia = (\n", - " chron.VectorBuilder()\n", - " .with_objective(simulate_julia)\n", - " .with_data(y_observed)\n", - " .with_parameter(\"alpha\", 1.0)\n", - " .with_parameter(\"beta\", 0.3)\n", - " .with_parameter(\"delta\", 0.05)\n", - " .with_parameter(\"gamma\", 0.5)\n", - " .with_cost(chron.SSE())\n", - " .with_optimiser(chron.NelderMead().with_max_iter(500))\n", - " .build()\n", - " .optimise()\n", - " )\n", - "\n", - " julia_time = time.time() - start_time\n", - "\n", - " print(\"\\n\" + \"=\" * 60)\n", - " print(\"JULIA DIFFERENTIALEQUATIONS.JL RESULTS\")\n", - " print(\"=\" * 60)\n", - " print(f\"True parameters: {list(true_params.values())}\")\n", - " print(f\"Estimated parameters: {result_julia.x}\")\n", - " print(f\"Final SSE: {result_julia.value:.6f}\")\n", - " print(f\"Iterations: {result_julia.iterations}\")\n", - " print(f\"Function evals: {result_julia.evaluations}\")\n", - " print(f\"Time: {julia_time:.3f}s\")\n", - " print(f\"Success: {result_julia.success}\")\n", - " print(f\"\\nSpeedup vs Diffsol: {diffsol_time / julia_time:.2f}x\")" - ] + "source": "if JULIA_AVAILABLE:\n # Fit with VectorBuilder + Julia\n start_time = time.time()\n\n result_julia = (\n diffid.VectorBuilder()\n .with_objective(simulate_julia)\n .with_data(y_observed)\n .with_parameter(\"alpha\", 1.0)\n .with_parameter(\"beta\", 0.3)\n .with_parameter(\"delta\", 0.05)\n .with_parameter(\"gamma\", 0.5)\n .with_cost(diffid.SSE())\n .with_optimiser(diffid.NelderMead().with_max_iter(500))\n .build()\n .optimise()\n )\n\n julia_time = time.time() - start_time\n\n print(\"\\n\" + \"=\" * 60)\n print(\"JULIA DIFFERENTIALEQUATIONS.JL RESULTS\")\n print(\"=\" * 60)\n print(f\"True parameters: {list(true_params.values())}\")\n print(f\"Estimated parameters: {result_julia.x}\")\n print(f\"Final SSE: {result_julia.value:.6f}\")\n print(f\"Iterations: {result_julia.iterations}\")\n print(f\"Function evals: {result_julia.evaluations}\")\n print(f\"Time: {julia_time:.3f}s\")\n print(f\"Success: {result_julia.success}\")\n print(f\"\\nSpeedup vs Diffsol: {diffsol_time / julia_time:.2f}x\")" }, { "cell_type": "markdown", @@ -656,45 +530,7 @@ { "cell_type": "markdown", "metadata": {}, - "source": [ - "## Key Takeaways\n", - "\n", - "### When to Use Each Backend\n", - "\n", - "**DiffsolBuilder (Diffsol):**\n", - "- āœ… Quick prototyping with DiffSL syntax\n", - "- āœ… Standard ODE problems\n", - "- āœ… No extra dependencies\n", - "- āŒ Limited to DiffSL expressiveness\n", - "\n", - "**VectorBuilder + JAX/Diffrax:**\n", - "- āœ… GPU acceleration for large problems\n", - "- āœ… Automatic differentiation (enables gradient-based optimisers)\n", - "- āœ… JIT compilation for speed\n", - "- āœ… Excellent for high-dimensional problems\n", - "- āŒ Requires JAX ecosystem\n", - "\n", - "**VectorBuilder + Julia/DifferentialEquations.jl:**\n", - "- āœ… Largest solver collection (stiff, stochastic, DAE, DDE, etc.)\n", - "- āœ… Advanced features (callbacks, sensitivity analysis)\n", - "- āœ… Best for specialized problems\n", - "- āŒ Requires Julia installation\n", - "\n", - "### Performance Insights\n", - "\n", - "1. **JAX/Diffrax** typically fastest after JIT warmup\n", - "2. **Diffsol** excellent balance of speed and simplicity\n", - "3. **Julia/DiffEq** best for problems requiring specialized solvers\n", - "4. All backends produce equivalent parameter estimates\n", - "\n", - "### VectorBuilder Flexibility\n", - "\n", - "The key advantage of `VectorBuilder` is **total control**:\n", - "- Any Python callable works\n", - "- Integrate pre-existing simulation code\n", - "- Mix solver backends in the same workflow\n", - "- Enable advanced features like GPU acceleration" - ] + "source": "## Key Takeaways\n\n### When to Use Each Backend\n\n**DiffsolBuilder (Diffsol):**\n- Quick prototyping with DiffSL syntax\n- āœ… Standard ODE problems\n- āœ… No extra dependencies\n- āŒ Limited to DiffSL expressiveness\n\n**VectorBuilder + JAX/Diffrax:**\n- āœ… GPU acceleration for large problems\n- āœ… Automatic differentiation (enables gradient-based optimisers)\n- āœ… JIT compilation for speed\n- āœ… Excellent for high-dimensional problems\n- āŒ Requires JAX ecosystem\n\n**VectorBuilder + Julia/DifferentialEquations.jl:**\n- āœ… Largest solver collection (stiff, stochastic, DAE, DDE, etc.)\n- āœ… Advanced features (callbacks, sensitivity analysis)\n- āœ… Best for specialized problems\n- āŒ Requires Julia installation\n\n### Performance Insights\n\n1. **JAX/Diffrax** typically fastest after JIT warmup\n2. **Diffsol** excellent balance of speed and simplicity\n3. **Julia/DiffEq** best for problems requiring specialized solvers\n4. All backends produce equivalent parameter estimates\n\n### VectorBuilder Flexibility\n\nThe key advantage of `VectorBuilder` is **total control**:\n- Any Python callable works\n- Integrate pre-existing simulation code\n- Mix solver backends in the same workflow\n- Enable advanced features like GPU acceleration" }, { "cell_type": "markdown", diff --git a/docs/tutorials/notebooks/07_parallel_optimization.ipynb b/docs/tutorials/notebooks/07_parallel_optimization.ipynb index a542a35..5608a42 100644 --- a/docs/tutorials/notebooks/07_parallel_optimization.ipynb +++ b/docs/tutorials/notebooks/07_parallel_optimization.ipynb @@ -21,11 +21,11 @@ { "cell_type": "markdown", "metadata": {}, - "source": "## Introduction\n\nChronopt supports **parallel evaluation** for population-based optimisers (CMA-ES, Dynamic Nested Sampling). However, the effectiveness depends on the evaluation cost and backend used.\n\n### Key Insights\n\n1. **DiffsolBuilder** with `.with_parallel(True)` uses Rust's rayon for parallel ODE integration\n2. **Fast evaluations** (< 1ms) may see limited speedup due to efficient solver caching\n3. **Python callables** cannot be parallelised with threads (GIL), but **multiprocessing** works\n\nThis tutorial covers:\n- Parallel ODE fitting with `DiffsolBuilder`\n- Using `multiprocessing` for expensive Python callables\n- Understanding when parallelism helps" + "source": "## Introduction\n\nDiffid supports **parallel evaluation** for population-based optimisers (CMA-ES, Dynamic Nested Sampling). However, the effectiveness depends on the evaluation cost and backend used.\n\n### Key Insights\n\n1. **DiffsolBuilder** with `.with_parallel(True)` uses Rust's rayon for parallel ODE integration\n2. **Fast evaluations** (< 1ms) may see limited speedup due to efficient solver caching\n3. **Python callables** cannot be parallelised with threads (GIL), but **multiprocessing** works\n\nThis tutorial covers:\n- Parallel ODE fitting with `DiffsolBuilder`\n- Using `multiprocessing` for expensive Python callables\n- Understanding when parallelism helps" }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2026-01-22T19:00:18.792193Z", @@ -38,28 +38,8 @@ "shell.execute_reply": "2026-01-10T22:22:07.286361Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Available CPU cores: 8\n" - ] - } - ], - "source": [ - "import multiprocessing\n", - "import time\n", - "\n", - "import chronopt as chron\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "from scipy.integrate import solve_ivp\n", - "\n", - "# Detect available cores\n", - "n_cores = multiprocessing.cpu_count()\n", - "print(f\"Available CPU cores: {n_cores}\")" - ] + "outputs": [], + "source": "import multiprocessing\nimport time\n\nimport diffid\nimport matplotlib.pyplot as plt\nimport numpy as np\nfrom scipy.integrate import solve_ivp\n\n# Detect available cores\nn_cores = multiprocessing.cpu_count()\nprint(f\"Available CPU cores: {n_cores}\")" }, { "cell_type": "markdown", @@ -155,7 +135,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2026-01-22T19:00:20.020454Z", @@ -168,58 +148,8 @@ "shell.execute_reply": "2026-01-10T22:22:08.804066Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Running CMA-ES (sequential)...\n", - "\n", - "============================================================\n", - "CMA-ES (Sequential)\n", - "============================================================\n", - "Solution: [1.14387427 0.51852859 0.48038414 0.75238014]\n", - "True params: [1.1, 0.4, 0.1, 0.4]\n", - "Final SSE: 11886.345\n", - "Evaluations: 801\n", - "Time: 0.98s\n", - "Time per eval: 1.22ms\n" - ] - } - ], - "source": [ - "# Sequential execution (parallel=False)\n", - "print(\"Running CMA-ES (sequential)...\")\n", - "\n", - "start = time.time()\n", - "\n", - "result_seq = (\n", - " chron.DiffsolBuilder()\n", - " .with_diffsl(model_str)\n", - " .with_data(data)\n", - " .with_parameter(\"alpha\", 0.8)\n", - " .with_parameter(\"beta\", 0.3)\n", - " .with_parameter(\"delta\", 0.05)\n", - " .with_parameter(\"gamma\", 0.3)\n", - " .with_cost(chron.SSE())\n", - " .with_parallel(False) # Sequential evaluation\n", - " .with_optimiser(chron.CMAES().with_max_iter(100).with_step_size(0.3))\n", - " .build()\n", - " .optimise()\n", - ")\n", - "\n", - "time_seq = time.time() - start\n", - "\n", - "print(\"\\n\" + \"=\" * 60)\n", - "print(\"CMA-ES (Sequential)\")\n", - "print(\"=\" * 60)\n", - "print(f\"Solution: {result_seq.x}\")\n", - "print(f\"True params: {list(true_params.values())}\")\n", - "print(f\"Final SSE: {result_seq.value:.3f}\")\n", - "print(f\"Evaluations: {result_seq.evaluations}\")\n", - "print(f\"Time: {time_seq:.2f}s\")\n", - "print(f\"Time per eval: {time_seq / result_seq.evaluations * 1000:.2f}ms\")" - ] + "outputs": [], + "source": "# Sequential execution (parallel=False)\nprint(\"Running CMA-ES (sequential)...\")\n\nstart = time.time()\n\nresult_seq = (\n diffid.DiffsolBuilder()\n .with_diffsl(model_str)\n .with_data(data)\n .with_parameter(\"alpha\", 0.8)\n .with_parameter(\"beta\", 0.3)\n .with_parameter(\"delta\", 0.05)\n .with_parameter(\"gamma\", 0.3)\n .with_cost(diffid.SSE())\n .with_parallel(False) # Sequential evaluation\n .with_optimiser(diffid.CMAES().with_max_iter(100).with_step_size(0.3))\n .build()\n .optimise()\n)\n\ntime_seq = time.time() - start\n\nprint(\"\\n\" + \"=\" * 60)\nprint(\"CMA-ES (Sequential)\")\nprint(\"=\" * 60)\nprint(f\"Solution: {result_seq.x}\")\nprint(f\"True params: {list(true_params.values())}\")\nprint(f\"Final SSE: {result_seq.value:.3f}\")\nprint(f\"Evaluations: {result_seq.evaluations}\")\nprint(f\"Time: {time_seq:.2f}s\")\nprint(f\"Time per eval: {time_seq / result_seq.evaluations * 1000:.2f}ms\")" }, { "cell_type": "markdown", @@ -228,7 +158,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2026-01-22T19:00:20.380944Z", @@ -241,67 +171,12 @@ "shell.execute_reply": "2026-01-10T22:22:12.217981Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Running CMA-ES (parallel)...\n", - "\n", - "============================================================\n", - "CMA-ES (Parallel)\n", - "============================================================\n", - "Solution: [0.8 0.3 0.05 0.3 ]\n", - "True params: [1.1, 0.4, 0.1, 0.4]\n", - "Final SSE: 29190.956\n", - "Evaluations: 9\n", - "Time: 0.14s\n", - "Time per eval: 15.72ms\n", - "\n", - "šŸš€ Speedup: 6.90x (with 8 cores)\n" - ] - } - ], - "source": [ - "# Parallel execution (parallel=True)\n", - "print(\"Running CMA-ES (parallel)...\")\n", - "\n", - "start = time.time()\n", - "\n", - "result_par = (\n", - " chron.DiffsolBuilder()\n", - " .with_diffsl(model_str)\n", - " .with_data(data)\n", - " .with_parameter(\"alpha\", 0.8)\n", - " .with_parameter(\"beta\", 0.3)\n", - " .with_parameter(\"delta\", 0.05)\n", - " .with_parameter(\"gamma\", 0.3)\n", - " .with_cost(chron.SSE())\n", - " .with_parallel(True) # Parallel evaluation!\n", - " .with_optimiser(chron.CMAES().with_max_iter(100).with_step_size(0.3))\n", - " .build()\n", - " .optimise()\n", - ")\n", - "\n", - "time_par = time.time() - start\n", - "\n", - "print(\"\\n\" + \"=\" * 60)\n", - "print(\"CMA-ES (Parallel)\")\n", - "print(\"=\" * 60)\n", - "print(f\"Solution: {result_par.x}\")\n", - "print(f\"True params: {list(true_params.values())}\")\n", - "print(f\"Final SSE: {result_par.value:.3f}\")\n", - "print(f\"Evaluations: {result_par.evaluations}\")\n", - "print(f\"Time: {time_par:.2f}s\")\n", - "print(f\"Time per eval: {time_par / result_par.evaluations * 1000:.2f}ms\")\n", - "\n", - "speedup = time_seq / time_par\n", - "print(f\"\\nšŸš€ Speedup: {speedup:.2f}x (with {n_cores} cores)\")" - ] + "outputs": [], + "source": "# Parallel execution (parallel=True)\nprint(\"Running CMA-ES (parallel)...\")\n\nstart = time.time()\n\nresult_par = (\n diffid.DiffsolBuilder()\n .with_diffsl(model_str)\n .with_data(data)\n .with_parameter(\"alpha\", 0.8)\n .with_parameter(\"beta\", 0.3)\n .with_parameter(\"delta\", 0.05)\n .with_parameter(\"gamma\", 0.3)\n .with_cost(diffid.SSE())\n .with_parallel(True) # Parallel evaluation!\n .with_optimiser(diffid.CMAES().with_max_iter(100).with_step_size(0.3))\n .build()\n .optimise()\n)\n\ntime_par = time.time() - start\n\nprint(\"\\n\" + \"=\" * 60)\nprint(\"CMA-ES (Parallel)\")\nprint(\"=\" * 60)\nprint(f\"Solution: {result_par.x}\")\nprint(f\"True params: {list(true_params.values())}\")\nprint(f\"Final SSE: {result_par.value:.3f}\")\nprint(f\"Evaluations: {result_par.evaluations}\")\nprint(f\"Time: {time_par:.2f}s\")\nprint(f\"Time per eval: {time_par / result_par.evaluations * 1000:.2f}ms\")\n\nspeedup = time_seq / time_par\nprint(f\"\\nšŸš€ Speedup: {speedup:.2f}x (with {n_cores} cores)\")" }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2026-01-22T19:00:20.695642Z", @@ -314,68 +189,8 @@ "shell.execute_reply": "2026-01-10T22:22:19.043950Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Running CMA-ES (parallel, large population)...\n", - "\n", - "============================================================\n", - "CMA-ES (Parallel, Population=16)\n", - "============================================================\n", - "Solution: [1.18267573 0.602622 0.37072684 0.67821184]\n", - "True params: [1.1, 0.4, 0.1, 0.4]\n", - "Final SSE: 8171.282\n", - "Evaluations: 801\n", - "Time: 0.26s\n", - "Time per eval: 0.32ms\n", - "\n", - "šŸš€ Speedup: 3.81x\n" - ] - } - ], - "source": [ - "# Parallel with larger population (more parallel work per generation)\n", - "print(\"Running CMA-ES (parallel, large population)...\")\n", - "\n", - "start = time.time()\n", - "\n", - "result_par_large = (\n", - " chron.DiffsolBuilder()\n", - " .with_diffsl(model_str)\n", - " .with_data(data)\n", - " .with_parameter(\"alpha\", 0.8)\n", - " .with_parameter(\"beta\", 0.3)\n", - " .with_parameter(\"delta\", 0.05)\n", - " .with_parameter(\"gamma\", 0.3)\n", - " .with_cost(chron.SSE())\n", - " .with_parallel(True)\n", - " .with_optimiser(\n", - " chron.CMAES()\n", - " .with_max_iter(50)\n", - " .with_step_size(0.3)\n", - " .with_population_size(2 * n_cores) # Match population to available cores\n", - " )\n", - " .build()\n", - " .optimise()\n", - ")\n", - "\n", - "time_par_large = time.time() - start\n", - "\n", - "print(\"\\n\" + \"=\" * 60)\n", - "print(f\"CMA-ES (Parallel, Population={2 * n_cores})\")\n", - "print(\"=\" * 60)\n", - "print(f\"Solution: {result_par_large.x}\")\n", - "print(f\"True params: {list(true_params.values())}\")\n", - "print(f\"Final SSE: {result_par_large.value:.3f}\")\n", - "print(f\"Evaluations: {result_par_large.evaluations}\")\n", - "print(f\"Time: {time_par_large:.2f}s\")\n", - "print(f\"Time per eval: {time_par_large / result_par_large.evaluations * 1000:.2f}ms\")\n", - "\n", - "speedup_large = time_seq / time_par_large\n", - "print(f\"\\nšŸš€ Speedup: {speedup_large:.2f}x\")" - ] + "outputs": [], + "source": "# Parallel with larger population (more parallel work per generation)\nprint(\"Running CMA-ES (parallel, large population)...\")\n\nstart = time.time()\n\nresult_par_large = (\n diffid.DiffsolBuilder()\n .with_diffsl(model_str)\n .with_data(data)\n .with_parameter(\"alpha\", 0.8)\n .with_parameter(\"beta\", 0.3)\n .with_parameter(\"delta\", 0.05)\n .with_parameter(\"gamma\", 0.3)\n .with_cost(diffid.SSE())\n .with_parallel(True)\n .with_optimiser(\n diffid.CMAES()\n .with_max_iter(50)\n .with_step_size(0.3)\n .with_population_size(2 * n_cores) # Match population to available cores\n )\n .build()\n .optimise()\n)\n\ntime_par_large = time.time() - start\n\nprint(\"\\n\" + \"=\" * 60)\nprint(f\"CMA-ES (Parallel, Population={2 * n_cores})\")\nprint(\"=\" * 60)\nprint(f\"Solution: {result_par_large.x}\")\nprint(f\"True params: {list(true_params.values())}\")\nprint(f\"Final SSE: {result_par_large.value:.3f}\")\nprint(f\"Evaluations: {result_par_large.evaluations}\")\nprint(f\"Time: {time_par_large:.2f}s\")\nprint(f\"Time per eval: {time_par_large / result_par_large.evaluations * 1000:.2f}ms\")\n\nspeedup_large = time_seq / time_par_large\nprint(f\"\\nšŸš€ Speedup: {speedup_large:.2f}x\")" }, { "cell_type": "markdown", @@ -384,7 +199,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2026-01-22T19:00:20.944100Z", @@ -397,82 +212,8 @@ "shell.execute_reply": "2026-01-10T22:22:30.395476Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Testing 20 sequential evaluations...\n", - "Sequential: 0.20s (9.8ms/eval)\n", - "\n", - "šŸ’” To parallelise in a script, use multiprocessing:\n", - "\n", - "from concurrent.futures import ProcessPoolExecutor\n", - "\n", - "def evaluate_batch_parallel(params_list, n_workers=8):\n", - " with ProcessPoolExecutor(max_workers=n_workers) as executor:\n", - " return list(executor.map(expensive_objective, params_list))\n", - "\n", - "# This provides near-linear speedup for expensive functions\n", - "\n" - ] - } - ], - "source": [ - "# Define an expensive objective function\n", - "def expensive_objective(params):\n", - " \"\"\"\n", - " An expensive objective function that simulates computation.\n", - " In practice, this could be a complex simulation, ML model, etc.\n", - " \"\"\"\n", - " alpha, beta, delta, gamma = params\n", - "\n", - " # Simulate expensive computation (ODE integration with scipy)\n", - " sol = solve_ivp(\n", - " lotka_volterra,\n", - " [0, 100],\n", - " [10.0, 5.0],\n", - " args=(alpha, beta, delta, gamma),\n", - " t_eval=t_data,\n", - " method=\"RK45\",\n", - " )\n", - "\n", - " if not sol.success:\n", - " return 1e10 # Return large value for failed integrations\n", - "\n", - " # Compute SSE\n", - " y_pred = sol.y.T\n", - " sse = np.sum((y_pred - y_observed) ** 2)\n", - " return sse\n", - "\n", - "\n", - "# Test single evaluation time\n", - "n_evals = 20\n", - "test_params = [[0.8 + i * 0.02, 0.3, 0.05, 0.3] for i in range(n_evals)]\n", - "\n", - "print(f\"Testing {n_evals} sequential evaluations...\")\n", - "\n", - "start = time.time()\n", - "seq_results = [expensive_objective(p) for p in test_params]\n", - "time_seq_python = time.time() - start\n", - "print(\n", - " f\"Sequential: {time_seq_python:.2f}s ({time_seq_python / n_evals * 1000:.1f}ms/eval)\"\n", - ")\n", - "\n", - "# Note: Multiprocessing in notebooks has pickling limitations\n", - "# In a regular Python script, you would use:\n", - "print(\"\"\"\n", - "šŸ’” To parallelise in a script, use multiprocessing:\n", - "\n", - "from concurrent.futures import ProcessPoolExecutor\n", - "\n", - "def evaluate_batch_parallel(params_list, n_workers=8):\n", - " with ProcessPoolExecutor(max_workers=n_workers) as executor:\n", - " return list(executor.map(expensive_objective, params_list))\n", - "\n", - "# This provides near-linear speedup for expensive functions\n", - "\"\"\")" - ] + "outputs": [], + "source": "# Define an expensive objective function\ndef expensive_objective(params):\n \"\"\"\n An expensive objective function that simulates computation.\n In practice, this could be a complex simulation, ML model, etc.\n \"\"\"\n alpha, beta, delta, gamma = params\n\n # Simulate expensive computation (ODE integration with scipy)\n sol = solve_ivp(\n lotka_volterra,\n [0, 100],\n [10.0, 5.0],\n args=(alpha, beta, delta, gamma),\n t_eval=t_data,\n method=\"RK45\",\n )\n\n if not sol.success:\n return 1e10 # Return large value for failed integrations\n\n # Compute SSE\n y_pred = sol.y.T\n sse = np.sum((y_pred - y_observed) ** 2)\n return sse\n\n\n# Test single evaluation time\nn_evals = 20\ntest_params = [[0.8 + i * 0.02, 0.3, 0.05, 0.3] for i in range(n_evals)]\n\nprint(f\"Testing {n_evals} sequential evaluations...\")\n\nstart = time.time()\nseq_results = [expensive_objective(p) for p in test_params]\ntime_seq_python = time.time() - start\nprint(\n f\"Sequential: {time_seq_python:.2f}s ({time_seq_python / n_evals * 1000:.1f}ms/eval)\"\n)\n\n# Note: Multiprocessing in notebooks has pickling limitations\n# In a regular Python script, you would use:\nprint(\"\"\"\nšŸ’” To parallelise in a script, use multiprocessing:\n\nfrom concurrent.futures import ProcessPoolExecutor\n\ndef evaluate_batch_parallel(params_list, n_workers=8):\n with ProcessPoolExecutor(max_workers=n_workers) as executor:\n return list(executor.map(expensive_objective, params_list))\n\n# This provides near-linear speedup for expensive functions\n\"\"\")" }, { "cell_type": "code", @@ -573,7 +314,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2026-01-22T19:00:23.060489Z", @@ -586,94 +327,8 @@ "shell.execute_reply": "2026-01-10T22:23:09.260026Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Testing DiffsolBuilder scaling with data size...\n", - "\n", - "N=100: Sequential=0.11s, Parallel=0.25s, Speedup=0.46x\n", - "N=200: Sequential=0.67s, Parallel=0.22s, Speedup=3.10x\n", - "N=500: Sequential=0.17s, Parallel=0.22s, Speedup=0.76x\n" - ] - } - ], - "source": [ - "# Test DiffsolBuilder scaling with data size\n", - "data_sizes = [100, 200, 500]\n", - "scaling_results = []\n", - "\n", - "print(\"Testing DiffsolBuilder scaling with data size...\\n\")\n", - "\n", - "for n_points in data_sizes:\n", - " # Generate data with different sizes\n", - " t_test = np.linspace(0, 100, n_points)\n", - " sol_test = solve_ivp(\n", - " lotka_volterra,\n", - " [t_test[0], t_test[-1]],\n", - " [10.0, 5.0],\n", - " args=(\n", - " true_params[\"alpha\"],\n", - " true_params[\"beta\"],\n", - " true_params[\"delta\"],\n", - " true_params[\"gamma\"],\n", - " ),\n", - " t_eval=t_test,\n", - " method=\"RK45\",\n", - " )\n", - " y_test = sol_test.y.T + np.random.normal(0, 0.3, (n_points, 2))\n", - " data_test = np.column_stack((t_test, y_test))\n", - "\n", - " # Sequential\n", - " start = time.time()\n", - " _ = (\n", - " chron.DiffsolBuilder()\n", - " .with_diffsl(model_str)\n", - " .with_data(data_test)\n", - " .with_parameter(\"alpha\", 0.8)\n", - " .with_parameter(\"beta\", 0.3)\n", - " .with_parameter(\"delta\", 0.05)\n", - " .with_parameter(\"gamma\", 0.3)\n", - " .with_cost(chron.SSE())\n", - " .with_parallel(False)\n", - " .with_optimiser(chron.CMAES().with_max_iter(30).with_step_size(0.3))\n", - " .build()\n", - " .optimise()\n", - " )\n", - " t_seq_scale = time.time() - start\n", - "\n", - " # Parallel\n", - " start = time.time()\n", - " _ = (\n", - " chron.DiffsolBuilder()\n", - " .with_diffsl(model_str)\n", - " .with_data(data_test)\n", - " .with_parameter(\"alpha\", 0.8)\n", - " .with_parameter(\"beta\", 0.3)\n", - " .with_parameter(\"delta\", 0.05)\n", - " .with_parameter(\"gamma\", 0.3)\n", - " .with_cost(chron.SSE())\n", - " .with_parallel(True)\n", - " .with_optimiser(chron.CMAES().with_max_iter(30).with_step_size(0.3))\n", - " .build()\n", - " .optimise()\n", - " )\n", - " t_par_scale = time.time() - start\n", - "\n", - " sp = t_seq_scale / t_par_scale\n", - " scaling_results.append(\n", - " {\n", - " \"n_points\": n_points,\n", - " \"time_seq\": t_seq_scale,\n", - " \"time_par\": t_par_scale,\n", - " \"speedup\": sp,\n", - " }\n", - " )\n", - " print(\n", - " f\"N={n_points:3d}: Sequential={t_seq_scale:.2f}s, Parallel={t_par_scale:.2f}s, Speedup={sp:.2f}x\"\n", - " )" - ] + "outputs": [], + "source": "# Test DiffsolBuilder scaling with data size\ndata_sizes = [100, 200, 500]\nscaling_results = []\n\nprint(\"Testing DiffsolBuilder scaling with data size...\\n\")\n\nfor n_points in data_sizes:\n # Generate data with different sizes\n t_test = np.linspace(0, 100, n_points)\n sol_test = solve_ivp(\n lotka_volterra,\n [t_test[0], t_test[-1]],\n [10.0, 5.0],\n args=(\n true_params[\"alpha\"],\n true_params[\"beta\"],\n true_params[\"delta\"],\n true_params[\"gamma\"],\n ),\n t_eval=t_test,\n method=\"RK45\",\n )\n y_test = sol_test.y.T + np.random.normal(0, 0.3, (n_points, 2))\n data_test = np.column_stack((t_test, y_test))\n\n # Sequential\n start = time.time()\n _ = (\n diffid.DiffsolBuilder()\n .with_diffsl(model_str)\n .with_data(data_test)\n .with_parameter(\"alpha\", 0.8)\n .with_parameter(\"beta\", 0.3)\n .with_parameter(\"delta\", 0.05)\n .with_parameter(\"gamma\", 0.3)\n .with_cost(diffid.SSE())\n .with_parallel(False)\n .with_optimiser(diffid.CMAES().with_max_iter(30).with_step_size(0.3))\n .build()\n .optimise()\n )\n t_seq_scale = time.time() - start\n\n # Parallel\n start = time.time()\n _ = (\n diffid.DiffsolBuilder()\n .with_diffsl(model_str)\n .with_data(data_test)\n .with_parameter(\"alpha\", 0.8)\n .with_parameter(\"beta\", 0.3)\n .with_parameter(\"delta\", 0.05)\n .with_parameter(\"gamma\", 0.3)\n .with_cost(diffid.SSE())\n .with_parallel(True)\n .with_optimiser(diffid.CMAES().with_max_iter(30).with_step_size(0.3))\n .build()\n .optimise()\n )\n t_par_scale = time.time() - start\n\n sp = t_seq_scale / t_par_scale\n scaling_results.append(\n {\n \"n_points\": n_points,\n \"time_seq\": t_seq_scale,\n \"time_par\": t_par_scale,\n \"speedup\": sp,\n }\n )\n print(\n f\"N={n_points:3d}: Sequential={t_seq_scale:.2f}s, Parallel={t_par_scale:.2f}s, Speedup={sp:.2f}x\"\n )" }, { "cell_type": "code", @@ -749,7 +404,7 @@ { "cell_type": "markdown", "metadata": {}, - "source": "## When to Use Each Approach\n\n### DiffsolBuilder with `.with_parallel(True)`\n\nBest for:\n- ODE fitting problems where evaluations are moderately expensive\n- When you want automatic parallelism without code changes\n- Lower overhead than multiprocessing\n\n```python\nproblem = (\n chron.DiffsolBuilder()\n .with_diffsl(model)\n .with_data(data)\n .with_parallel(True) # Enable rayon parallelism\n .build()\n)\n```\n\n### Multiprocessing for Python Callables\n\nBest for:\n- Expensive Python simulations (>10ms per evaluation)\n- Complex custom objective functions\n- When DiffsolBuilder isn't applicable\n\n```python\nfrom concurrent.futures import ProcessPoolExecutor\n\ndef evaluate_parallel(params_list):\n with ProcessPoolExecutor(max_workers=n_cores) as executor:\n return list(executor.map(expensive_objective, params_list))\n```" + "source": "## When to Use Each Approach\n\n### DiffsolBuilder with `.with_parallel(True)`\n\nBest for:\n- ODE fitting problems where evaluations are moderately expensive\n- When you want automatic parallelism without code changes\n- Lower overhead than multiprocessing\n\n```python\nproblem = (\n diffid.DiffsolBuilder()\n .with_diffsl(model)\n .with_data(data)\n .with_parallel(True) # Enable rayon parallelism\n .build()\n)\n```\n\n### Multiprocessing for Python Callables\n\nBest for:\n- Expensive Python simulations (>10ms per evaluation)\n- Complex custom objective functions\n- When DiffsolBuilder isn't applicable\n\n```python\nfrom concurrent.futures import ProcessPoolExecutor\n\ndef evaluate_parallel(params_list):\n with ProcessPoolExecutor(max_workers=n_cores) as executor:\n return list(executor.map(expensive_objective, params_list))\n```" }, { "cell_type": "markdown", @@ -850,4 +505,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/docs/tutorials/notebooks/08_advanced_cost_functions.ipynb b/docs/tutorials/notebooks/08_advanced_cost_functions.ipynb index 5257a88..edf95a9 100644 --- a/docs/tutorials/notebooks/08_advanced_cost_functions.ipynb +++ b/docs/tutorials/notebooks/08_advanced_cost_functions.ipynb @@ -24,7 +24,7 @@ "source": [ "## Introduction\n", "\n", - "The **cost function** (or loss function) quantifies how well model predictions match observations. Chronopt provides built-in metrics, but real-world problems often require:\n", + "The **cost function** (or loss function) quantifies how well model predictions match observations. Diffid provides built-in metrics, but real-world problems often require:\n", "\n", "- **Weighted fitting** when measurement errors vary\n", "- **Custom metrics** for domain-specific requirements\n", @@ -54,7 +54,7 @@ "# Import plotting utilities\n", "from functools import partial\n", "\n", - "import chronopt as chron\n", + "import diffid\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", "\n", @@ -67,7 +67,7 @@ "source": [ "## Built-in Cost Metrics\n", "\n", - "Chronopt provides three standard metrics:\n", + "Diffid provides three standard metrics:\n", "\n", "### 1. Sum of Squared Errors (SSE)\n", "\n", @@ -193,7 +193,7 @@ "\n", "# Define problem\n", "builder = (\n", - " chron.VectorBuilder()\n", + " diffid.VectorBuilder()\n", " .with_objective(linear_model)\n", " .with_data(y_observed)\n", " .with_parameter(\"slope\", 1.0)\n", @@ -201,7 +201,11 @@ ")\n", "\n", "# Test each metric\n", - "metrics = {\"SSE\": chron.SSE(), \"RMSE\": chron.RMSE(), \"GaussianNLL\": chron.GaussianNLL()}\n", + "metrics = {\n", + " \"SSE\": diffid.SSE(),\n", + " \"RMSE\": diffid.RMSE(),\n", + " \"GaussianNLL\": diffid.GaussianNLL(),\n", + "}\n", "\n", "results = {}\n", "for name, metric in metrics.items():\n", @@ -386,19 +390,19 @@ "\n", "# Fit WITHOUT weights (standard SSE)\n", "result_unweighted = (\n", - " chron.VectorBuilder()\n", + " diffid.VectorBuilder()\n", " .with_objective(linear_model_hetero)\n", " .with_data(y_hetero)\n", " .with_parameter(\"slope\", 1.0)\n", " .with_parameter(\"intercept\", 0.0)\n", - " .with_cost(chron.SSE()) # Standard SSE\n", + " .with_cost(diffid.SSE()) # Standard SSE\n", " .build()\n", " .optimise()\n", ")\n", "\n", "# Fit with weights\n", "result_weighted = (\n", - " chron.ScalarBuilder()\n", + " diffid.ScalarBuilder()\n", " .with_objective(lambda x: weighted_sse(linear_model_hetero(x), y_hetero))\n", " .with_parameter(\"slope\", 1.0)\n", " .with_parameter(\"intercept\", 0.0)\n", @@ -626,21 +630,21 @@ " return sse\n", "\n", "\n", - "# Note: Chronopt's built-in costs don't support parameter access yet,\n", + "# Note: Diffid's built-in costs don't support parameter access yet,\n", "# so we'll compare by manually adding regularisation to parameter update\n", "\n", "# Unregularised fit\n", "initial_params = np.zeros(degree + 1)\n", "initial_params[0] = np.mean(y_poly)\n", "\n", - "builder_poly = chron.VectorBuilder().with_objective(polynomial_model).with_data(y_poly)\n", + "builder_poly = diffid.VectorBuilder().with_objective(polynomial_model).with_data(y_poly)\n", "\n", "for i in range(degree + 1):\n", " builder_poly = builder_poly.with_parameter(f\"c{i}\", initial_params[i])\n", "\n", "result_unreg = (\n", - " builder_poly.with_cost(chron.SSE())\n", - " .with_optimiser(chron.NelderMead().with_max_iter(2000))\n", + " builder_poly.with_cost(diffid.SSE())\n", + " .with_optimiser(diffid.NelderMead().with_max_iter(2000))\n", " .build()\n", " .optimise()\n", ")\n", @@ -836,7 +840,7 @@ "for w_smooth in weights_smooth:\n", " # Construct custom cost\n", " multi_cost = MultiObjectiveCost(weight_fit=1.0, weight_smooth=w_smooth)\n", - " result = chron.ScalarBuilder().with_objective(\n", + " result = diffid.ScalarBuilder().with_objective(\n", " partial(wrapper, y_poly=y_poly, cost_func=multi_cost)\n", " )\n", "\n", @@ -844,7 +848,9 @@ " result = result.with_parameter(f\"c{i}\", initial_params[i])\n", "\n", " result = (\n", - " result.with_optimiser(chron.NelderMead().with_max_iter(2000)).build().optimise()\n", + " result.with_optimiser(diffid.NelderMead().with_max_iter(2000))\n", + " .build()\n", + " .optimise()\n", " )\n", "\n", " results_multi[w_smooth] = result\n", diff --git a/examples/bicycle_model_diffsol.py b/examples/bicycle_model_diffsol.py index 1d8808b..a03f2a7 100644 --- a/examples/bicycle_model_diffsol.py +++ b/examples/bicycle_model_diffsol.py @@ -1,6 +1,6 @@ from __future__ import annotations -import chronopt as chron +import diffid import numpy as np TRUE_L = 2.5 # wheelbase @@ -36,10 +36,10 @@ stacked_data = np.column_stack((t_span, x_obs, y_obs, psi_obs)) -optimiser = chron.CMAES().with_max_iter(500).with_threshold(1e-10) +optimiser = diffid.CMAES().with_max_iter(500).with_threshold(1e-10) builder = ( - chron.DiffsolBuilder() + diffid.DiffsolBuilder() .with_diffsl(dsl) .with_data(stacked_data) .with_tolerances(rtol=1e-6, atol=1e-8) diff --git a/examples/bicycle_model_evidence.py b/examples/bicycle_model_evidence.py index 33769ba..00998bd 100644 --- a/examples/bicycle_model_evidence.py +++ b/examples/bicycle_model_evidence.py @@ -1,6 +1,6 @@ from __future__ import annotations -import chronopt as chron +import diffid import numpy as np TRUE_L = 2.5 # wheelbase @@ -36,11 +36,11 @@ stacked_data = np.column_stack((t_span, x_obs, y_obs, psi_obs)) -optimiser = chron.CMAES().with_max_iter(500).with_threshold(1e-10) -cost = chron.GaussianNLL(0.05) +optimiser = diffid.CMAES().with_max_iter(500).with_threshold(1e-10) +cost = diffid.GaussianNLL(0.05) builder = ( - chron.DiffsolBuilder() + diffid.DiffsolBuilder() .with_diffsl(dsl) .with_data(stacked_data) .with_tolerances(rtol=1e-6, atol=1e-8) @@ -52,7 +52,7 @@ results = problem.optimise() print(results) -sampler = chron.DynamicNestedSampler().with_live_points(128) +sampler = diffid.DynamicNestedSampler().with_live_points(128) samples = sampler.run(problem, initial=results.x) print("time :", samples.time) diff --git a/examples/bouncy_ball.py b/examples/bouncy_ball.py index 2965422..bc8b98e 100644 --- a/examples/bouncy_ball.py +++ b/examples/bouncy_ball.py @@ -1,4 +1,4 @@ -import chronopt as chron +import diffid import numpy as np @@ -30,14 +30,14 @@ def ball_states(t: np.ndarray, g: float, h: float) -> tuple[np.ndarray, np.ndarr # Configure the problem initial_values = [4.0, 4.0] builder = ( - chron.DiffsolBuilder() + diffid.DiffsolBuilder() .with_diffsl(dsl) .with_data(data) .with_parameter("g", initial_values[0]) .with_parameter("h", initial_values[1]) - .with_optimiser(chron.Adam().with_step_size(0.05).with_max_iter(1500)) - # .with_cost(chron.GaussianNLL(variance=0.01)) - .with_cost(chron.RMSE(2.0)) + .with_optimiser(diffid.Adam().with_step_size(0.05).with_max_iter(1500)) + # .with_cost(diffid.GaussianNLL(variance=0.01)) + .with_cost(diffid.RMSE(2.0)) ) problem = builder.build() diff --git a/examples/bouncy_ball_sampling.py b/examples/bouncy_ball_sampling.py index 70294d4..48cc9ef 100644 --- a/examples/bouncy_ball_sampling.py +++ b/examples/bouncy_ball_sampling.py @@ -1,4 +1,4 @@ -import chronopt as chron +import diffid import numpy as np @@ -30,20 +30,20 @@ def ball_states(t: np.ndarray, g: float, h: float) -> tuple[np.ndarray, np.ndarr # Configure the problem initial_values = [4.0, 4.0] builder = ( - chron.DiffsolBuilder() + diffid.DiffsolBuilder() .with_diffsl(dsl) .with_data(data) .with_parameter("g", initial_values[0]) .with_parameter("h", initial_values[1]) .with_parallel(True) - .with_cost(chron.GaussianNLL(variance=0.01)) + .with_cost(diffid.GaussianNLL(variance=0.01)) ) problem = builder.build() # Setup sampler sampler = ( - chron.MetropolisHastings() + diffid.MetropolisHastings() .with_num_chains(100) .with_iterations(1000) .with_step_size(0.25) diff --git a/examples/logistic_growth.py b/examples/logistic_growth.py index c55ed85..3c362f4 100644 --- a/examples/logistic_growth.py +++ b/examples/logistic_growth.py @@ -1,4 +1,4 @@ -import chronopt as chron +import diffid import numpy as np # Example diffsol ODE (logistic growth) @@ -15,11 +15,11 @@ # Create an optimiser -optimiser = chron.CMAES().with_max_iter(1000).with_threshold(1e-12) +optimiser = diffid.CMAES().with_max_iter(1000).with_threshold(1e-12) # Simple API builder = ( - chron.DiffsolBuilder() + diffid.DiffsolBuilder() .with_diffsl(ds) .with_data(stacked_data) .with_tolerances(1e-6, 1e-8) diff --git a/examples/model_evidence.py b/examples/model_evidence.py index 69a95a9..4ac136a 100644 --- a/examples/model_evidence.py +++ b/examples/model_evidence.py @@ -2,7 +2,7 @@ from __future__ import annotations -import chronopt as chron +import diffid def rosenbrock(x: list[float]) -> float: @@ -11,17 +11,17 @@ def rosenbrock(x: list[float]) -> float: builder = ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_objective(rosenbrock) .with_parameter("x", initial_value=1.2) .with_parameter("y", initial_value=1.4) - .with_optimiser(chron.NelderMead().with_max_iter(2000)) + .with_optimiser(diffid.NelderMead().with_max_iter(2000)) ) problem = builder.build() optimised = problem.optimise() -sampler = chron.DynamicNestedSampler().with_live_points(256).with_seed(1234) +sampler = diffid.DynamicNestedSampler().with_live_points(256).with_seed(1234) samples = sampler.run(problem, initial=optimised.x) print("time :", samples.time) diff --git a/examples/model_evidence_diffsol.py b/examples/model_evidence_diffsol.py index 8919b3a..955f9c6 100644 --- a/examples/model_evidence_diffsol.py +++ b/examples/model_evidence_diffsol.py @@ -2,7 +2,7 @@ from __future__ import annotations -import chronopt as chron +import diffid import numpy as np # Example diffsol ODE (logistic growth) @@ -18,7 +18,7 @@ stacked_data = np.column_stack((t_span, data)) builder = ( - chron.DiffsolBuilder() + diffid.DiffsolBuilder() .with_diffsl(ds) .with_data(stacked_data) .with_parameter("r", initial_value=1.2) @@ -29,7 +29,7 @@ optimised = problem.optimise() -sampler = chron.DynamicNestedSampler().with_live_points(256).with_seed(1234) +sampler = diffid.DynamicNestedSampler().with_live_points(256).with_seed(1234) samples = sampler.run(problem, initial=optimised.x) print("time :", samples.time) diff --git a/examples/predator_prey/README.md b/examples/predator_prey/README.md index 767e29a..5388abb 100644 --- a/examples/predator_prey/README.md +++ b/examples/predator_prey/README.md @@ -1,7 +1,7 @@ # Predator-Prey Parameter Identification Examples This directory showcases how to recover the parameters of the Lotka–Volterra -predator-prey model using Chronopt with several ODE solver backends. All +predator-prey model using Diffid with several ODE solver backends. All examples share a synthetic dataset so that the optimisation results can be compared side by side. @@ -12,7 +12,7 @@ compared side by side. - `predator_prey_diffrax.py` – fits the model using the JAX/Diffrax simulator. - `predator_prey_diffeqpy.py` – fits the model using the Julia DifferentialEquations.jl stack via diffeqpy. -- `predator_prey_diffsol.py` – fits the model using Chronopt's Diffsol backend. +- `predator_prey_diffsol.py` – fits the model using Diffid's Diffsol backend. - `synthetic_data.npz` – cached dataset generated by the script above (re-created on demand). @@ -24,7 +24,7 @@ compared side by side. ```bash # Base requirements for all examples - pip install chronopt numpy + pip install diffid numpy # Diffrax example pip install jax diffrax @@ -35,7 +35,7 @@ compared side by side. ``` The Diffsol example only needs the base requirements because the solver is - provided by Chronopt. + provided by Diffid. ## Generating the dataset @@ -69,10 +69,10 @@ performance across solver backends. platform has compatible JAX wheels. - Diffeqpy: the `de.install()` command downloads a Julia runtime if one is not already configured. This can take a few minutes. -- Diffsol: Chronopt bundles the solver; no extra setup is required beyond the +- Diffsol: Diffid bundles the solver; no extra setup is required beyond the base dependencies. ## Further reading -Check the top-level project README for more background on Chronopt and links to +Check the top-level project README for more background on Diffid and links to additional examples. \ No newline at end of file diff --git a/examples/predator_prey/predator_prey_diffeqpy.py b/examples/predator_prey/predator_prey_diffeqpy.py index 1d67721..a31620f 100644 --- a/examples/predator_prey/predator_prey_diffeqpy.py +++ b/examples/predator_prey/predator_prey_diffeqpy.py @@ -1,12 +1,12 @@ -"""Predator-prey parameter identification using DifferentialEquations.jl and Chronopt. +"""Predator-prey parameter identification using DifferentialEquations.jl and diffid. Demonstrates parameter estimation for the Lotka-Volterra model by: 1. Defining the predator-prey ODE in Julia via diffeqpy 2. Generating synthetic noisy observations -3. Recovering parameters using Chronopt optimization +3. Recovering parameters using diffid optimization Prerequisites: - pip install diffeqpy chronopt numpy + pip install diffeqpy diffid numpy python -c "from diffeqpy import de; de.install()" """ @@ -15,7 +15,7 @@ import importlib.util import pathlib -import chronopt as chron +import diffid import numpy as np from diffeqpy import de @@ -67,15 +67,15 @@ def simulate(params): # Parameter identification result = ( - chron.VectorBuilder() + diffid.VectorBuilder() .with_objective(simulate) .with_data(observed) .with_parameter("alpha", 0.8) .with_parameter("beta", 0.3) .with_parameter("delta", 0.05) .with_parameter("gamma", 0.6) - .with_cost(chron.SSE()) - .with_optimiser(chron.NelderMead().with_max_iter(1000)) + .with_cost(diffid.SSE()) + .with_optimiser(diffid.NelderMead().with_max_iter(1000)) .build() .optimise() ) diff --git a/examples/predator_prey/predator_prey_diffrax.py b/examples/predator_prey/predator_prey_diffrax.py index 51e4ab0..98cfae5 100644 --- a/examples/predator_prey/predator_prey_diffrax.py +++ b/examples/predator_prey/predator_prey_diffrax.py @@ -1,12 +1,12 @@ -"""Predator-prey parameter identification using JAX/Diffrax and Chronopt. +"""Predator-prey parameter identification using JAX/Diffrax and diffid. Demonstrates parameter estimation for the Lotka-Volterra model by: 1. Defining the predator-prey ODE in JAX 2. Generating synthetic noisy observations -3. Recovering parameters using Chronopt optimization +3. Recovering parameters using diffid optimization Prerequisites: - pip install chronopt diffrax jax numpy + pip install diffid diffrax jax numpy """ from __future__ import annotations @@ -14,7 +14,7 @@ import importlib.util import pathlib -import chronopt as chron +import diffid import diffrax as dfx import jax.numpy as jnp import numpy as np @@ -67,7 +67,7 @@ def simulate_jax(params): def simulate(params): - """NumPy wrapper for Chronopt compatibility.""" + """NumPy wrapper for diffid compatibility.""" return np.asarray(simulate_jax(jnp.asarray(params))) @@ -89,15 +89,15 @@ def simulate(params): # Parameter identification result = ( - chron.VectorBuilder() + diffid.VectorBuilder() .with_objective(simulate) .with_data(observed) .with_parameter("alpha", 1.3) .with_parameter("beta", 0.3) .with_parameter("delta", 0.05) .with_parameter("gamma", 0.6) - .with_cost(chron.SSE()) - .with_optimiser(chron.NelderMead().with_max_iter(1000)) + .with_cost(diffid.SSE()) + .with_optimiser(diffid.NelderMead().with_max_iter(1000)) .build() .optimise() ) diff --git a/examples/predator_prey/predator_prey_diffsol.py b/examples/predator_prey/predator_prey_diffsol.py index 4dc015e..b3eda65 100644 --- a/examples/predator_prey/predator_prey_diffsol.py +++ b/examples/predator_prey/predator_prey_diffsol.py @@ -1,7 +1,7 @@ import importlib.util import pathlib -import chronopt as chron +import diffid import numpy as np # Example diffsol ODE (logistic growth) @@ -32,7 +32,7 @@ # Simple API builder = ( - chron.DiffsolBuilder() + diffid.DiffsolBuilder() .with_diffsl(ode) .with_data(stacked_data) .with_tolerances(rtol=1e-6, atol=1e-8) @@ -42,7 +42,7 @@ .with_parameter("d", 0.6) .with_parallel(True) .with_optimiser( - chron.NelderMead().with_max_iter(1000) + diffid.NelderMead().with_max_iter(1000) ) # Override default optimiser ) problem = builder.build() diff --git a/examples/python_contour.png b/examples/python_contour.png new file mode 100644 index 0000000..24ab72e Binary files /dev/null and b/examples/python_contour.png differ diff --git a/examples/python_contour.py b/examples/python_contour.py index 6c5503e..8144922 100644 --- a/examples/python_contour.py +++ b/examples/python_contour.py @@ -2,7 +2,7 @@ from pathlib import Path -import chronopt as chron +import diffid import matplotlib.pyplot as plt import numpy as np @@ -15,7 +15,7 @@ def rosenbrock(x: np.ndarray) -> float: # Setup builder = ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_objective(rosenbrock) .with_parameter("x", 1.0) .with_parameter("y", 1.0) @@ -23,11 +23,11 @@ def rosenbrock(x: np.ndarray) -> float: problem = builder.build() # Optimise -optimiser = chron.NelderMead().with_max_iter(500).with_threshold(1e-8) +optimiser = diffid.NelderMead().with_max_iter(500).with_threshold(1e-8) result = optimiser.run(problem, [-1.5, 1.5]) # Plot -contour_set = chron.plotting.contour( +contour_set = diffid.plotting.contour( problem, x_bounds=(-2.0, 2.0), y_bounds=(-1.0, 3.0), diff --git a/examples/python_problem.py b/examples/python_problem.py index 65552e2..4de56e3 100644 --- a/examples/python_problem.py +++ b/examples/python_problem.py @@ -1,4 +1,4 @@ -import chronopt as chron +import diffid import numpy as np @@ -10,11 +10,11 @@ def rosenbrock(x): # Simple API builder = ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_objective(rosenbrock) .with_parameter("x", 1.0) .with_parameter("y", 1.0) - .with_optimiser(chron.NelderMead().with_max_iter(1000)) + .with_optimiser(diffid.NelderMead().with_max_iter(1000)) ) problem = builder.build() result = problem.optimise(initial=[10.0, 10.0]) diff --git a/mkdocs.yml b/mkdocs.yml index 6b7ab6e..d83766f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,9 +1,9 @@ -site_name: Chronopt +site_name: Diffid site_description: High-performance time-series inference and optimisation toolkit with Rust core and ergonomic Python bindings site_author: Brady Planden -site_url: https://bradyplanden.github.io/chronopt/ -repo_name: bradyplanden/chronopt -repo_url: https://github.com/bradyplanden/chronopt +site_url: https://bradyplanden.github.io/diffid/ +repo_name: bradyplanden/diffid +repo_url: https://github.com/bradyplanden/diffid edit_uri: edit/main/docs/ theme: @@ -124,9 +124,9 @@ markdown_extensions: extra: social: - icon: fontawesome/brands/github - link: https://github.com/bradyplanden/chronopt + link: https://github.com/bradyplanden/diffid - icon: fontawesome/brands/python - link: https://pypi.org/project/chronopt/ + link: https://pypi.org/project/diffid/ version: provider: mike default: latest @@ -195,5 +195,5 @@ nav: strict: true # Fail on warnings watch: - - python/src/chronopt + - python/src/diffid - docs diff --git a/pyproject.toml b/pyproject.toml index 24d034c..ecde2dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ requires = ["maturin>=1.9,<2.0"] build-backend = "maturin" [project] -name = "chronopt" +name = "diffid" requires-python = ">=3.11" dynamic = ["version"] license = { file = "LICENSE" } @@ -31,7 +31,7 @@ diffeqpy = [ [tool.maturin] manifest-path = "python/Cargo.toml" -module-name = "chronopt._chronopt" +module-name = "diffid._diffid" features = ["extension-module"] python-source = "python/src" sdist-include = ["LICENSE", "README.md"] diff --git a/python/Cargo.toml b/python/Cargo.toml index 32f36cd..f7f26fb 100644 --- a/python/Cargo.toml +++ b/python/Cargo.toml @@ -1,12 +1,12 @@ [package] -name = "chronopt-py" +name = "diffid-py" version.workspace = true edition.workspace = true license.workspace = true readme = "../README.md" [package.metadata.maturin] -name = "chronopt" +name = "diffid" python-source = "src" artifact = "dist" @@ -16,7 +16,7 @@ extension-module = ["pyo3/extension-module"] stubgen = ["clap", "pyo3-stub-gen", "pyo3-stub-gen-derive"] [lib] -name = "_chronopt" +name = "_diffid" crate-type = ["cdylib", "rlib"] [dependencies] pyo3 = { workspace = true, default-features = false, features = ["macros"] } @@ -27,10 +27,10 @@ pyo3-stub-gen-derive = { version = "0.17.2", optional = true } clap = { version = "4.5", optional = true, features = ["derive"] } [target.'cfg(not(windows))'.dependencies] -chronopt_core = { package = "chronopt", path = "../rust" } +diffid_core = { package = "diffid", path = "../rust" } [target.'cfg(windows)'.dependencies] -chronopt_core = { package = "chronopt", path = "../rust", default-features = false, features = ["cranelift-backend"] } +diffid_core = { package = "diffid", path = "../rust", default-features = false, features = ["cranelift-backend"] } [build-dependencies] pyo3-build-config = "0.27.1" diff --git a/python/src/bin/generate_stubs.rs b/python/src/bin/generate_stubs.rs index 254f01f..fe6dc72 100644 --- a/python/src/bin/generate_stubs.rs +++ b/python/src/bin/generate_stubs.rs @@ -4,7 +4,7 @@ use std::fs; use std::io::Write; use std::path::{Path, PathBuf}; -use chronopt::{stub_info, stub_info_from}; +use _diffid::{stub_info, stub_info_from}; use clap::Parser; use pyo3_stub_gen::Result; @@ -38,7 +38,7 @@ fn main() -> Result<()> { } fn post_process_sampler_stub() -> Result<()> { - let sampler_stub_path = resolve_workspace_root()?.join("python/src/chronopt/sampler.pyi"); + let sampler_stub_path = resolve_workspace_root()?.join("python/src/diffid/sampler.pyi"); let contents = fs::read_to_string(&sampler_stub_path)?; let mut lines: Vec<&str> = contents.lines().collect(); diff --git a/python/src/builders.rs b/python/src/builders.rs index 52229a3..b95c9ec 100644 --- a/python/src/builders.rs +++ b/python/src/builders.rs @@ -6,11 +6,11 @@ use pyo3::types::PyDict; use std::collections::HashMap; use std::sync::Arc; -use chronopt_core::builders::{ +use diffid_core::builders::{ DiffsolBackend, DiffsolProblemBuilder, ScalarProblemBuilder, VectorProblemBuilder, }; -use chronopt_core::common::Unbounded; -use chronopt_core::problem::{NoFunction, NoGradient}; +use diffid_core::common::Unbounded; +use diffid_core::problem::{NoFunction, NoGradient}; #[cfg(feature = "stubgen")] use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; @@ -333,7 +333,7 @@ impl PyScalarBuilder { .push((name.clone(), initial_value, bounds)); // Convert Option<(f64, f64)> to ParameterRange - let range: chronopt_core::problem::ParameterRange = + let range: diffid_core::problem::ParameterRange = bounds.map(|b| b.into()).unwrap_or_else(|| Unbounded.into()); slf.state = match std::mem::replace( diff --git a/python/src/chronopt/_chronopt.pyi b/python/src/chronopt/_chronopt.pyi deleted file mode 100644 index fff8acb..0000000 --- a/python/src/chronopt/_chronopt.pyi +++ /dev/null @@ -1,441 +0,0 @@ -# This file is automatically generated by pyo3_stub_gen -# ruff: noqa: E501, F401 - -import builtins -import datetime -import typing - -import numpy -import numpy.typing - -@typing.final -class Adam: - r""" - Adaptive Moment Estimation (Adam) gradient-based optimiser. - """ - def __new__(cls) -> Adam: - r""" - Create an Adam optimiser with library defaults. - """ - def with_max_iter(self, max_iter: builtins.int) -> Adam: - r""" - Limit the maximum number of optimisation iterations. - """ - def with_threshold(self, threshold: builtins.float) -> Adam: - r""" - Set the stopping threshold on the gradient norm. - """ - def with_step_size(self, step_size: builtins.float) -> Adam: - r""" - Configure the base learning rate / step size. - """ - def with_betas(self, beta1: builtins.float, beta2: builtins.float) -> Adam: - r""" - Override the exponential decay rates for the first and second moments. - """ - def with_eps(self, eps: builtins.float) -> Adam: - r""" - Override the numerical stability constant added to the denominator. - """ - def with_patience(self, patience_seconds: builtins.float) -> Adam: - r""" - Abort the run once the patience window has elapsed. - """ - def run( - self, problem: Problem, initial: typing.Sequence[builtins.float] - ) -> OptimisationResults: - r""" - Optimise the given problem using Adam starting from the provided point. - """ - -@typing.final -class CMAES: - r""" - Covariance Matrix Adaptation Evolution Strategy optimiser. - """ - def __new__(cls) -> CMAES: - r""" - Create a CMA-ES optimiser with library defaults. - """ - def with_max_iter(self, max_iter: builtins.int) -> CMAES: - r""" - Limit the number of iterations/generations before termination. - """ - def with_threshold(self, threshold: builtins.float) -> CMAES: - r""" - Set the stopping threshold on the best objective value. - """ - def with_step_size(self, step_size: builtins.float) -> CMAES: - r""" - Set the initial global step-size (standard deviation). - """ - def with_patience(self, patience_seconds: builtins.float) -> CMAES: - r""" - Abort the run if no improvement occurs for the given wall-clock duration. - """ - def with_population_size(self, population_size: builtins.int) -> CMAES: - r""" - Specify the number of offspring evaluated per generation. - """ - def with_seed(self, seed: builtins.int) -> CMAES: - r""" - Initialise the internal RNG for reproducible runs. - """ - def run( - self, problem: Problem, initial: typing.Sequence[builtins.float] - ) -> OptimisationResults: - r""" - Optimise the given problem starting from the provided mean vector. - """ - -@typing.final -class CostMetric: - @property - def name(self) -> builtins.str: - r""" - Name of the cost metric. - """ - def __repr__(self) -> builtins.str: ... - -@typing.final -class DiffsolBuilder: - r""" - Differential equation solver builder. - """ - def __new__(cls) -> DiffsolBuilder: - r""" - Create an empty differential solver builder. - """ - def __copy__(self) -> DiffsolBuilder: ... - def __deepcopy__(self, _memo: dict) -> DiffsolBuilder: ... - def with_diffsl(self, dsl: builtins.str) -> DiffsolBuilder: - r""" - Register the DiffSL program describing the system dynamics. - """ - def remove_diffsl(self) -> DiffsolBuilder: - r""" - Remove any registered DiffSL program. - """ - def with_data(self, data: numpy.typing.NDArray[numpy.float64]) -> DiffsolBuilder: - r""" - Attach observed data used to fit the differential equation. - - The first column must contain the time samples (t_span) and the remaining - columns the observed trajectories. - """ - def remove_data(self) -> DiffsolBuilder: - r""" - Remove any previously attached data along with its time span. - """ - def with_backend(self, backend: builtins.str) -> DiffsolBuilder: - r""" - Choose whether to use dense or sparse diffusion solvers. - """ - def with_parallel(self, parallel: builtins.bool | None = None) -> DiffsolBuilder: - r""" - Opt into parallel proposal generation when supported by the backend. - """ - def with_config( - self, config: typing.Mapping[builtins.str, builtins.float] - ) -> DiffsolBuilder: ... - def with_rtol(self, rtol: builtins.float) -> DiffsolBuilder: - r""" - Adjust the relative integration tolerance. - """ - def with_atol(self, atol: builtins.float) -> DiffsolBuilder: - r""" - Adjust the absolute integration tolerance. - """ - def with_parameter( - self, - name: builtins.str, - initial_value: builtins.float, - bounds: tuple[builtins.float, builtins.float] | None = None, - ) -> DiffsolBuilder: - r""" - Register a named optimisation variable in the order it appears in vectors. - """ - def clear_parameters(self) -> DiffsolBuilder: - r""" - Remove previously provided parameter defaults. - """ - def with_cost(self, cost: CostMetric) -> DiffsolBuilder: - r""" - Select the error metric used to compare simulated and observed data. - """ - def remove_cost(self) -> DiffsolBuilder: - r""" - Reset the cost metric to the default sum of squared errors. - """ - def with_optimiser(self, optimiser: NelderMead | CMAES | Adam) -> DiffsolBuilder: - r""" - Configure the default optimiser used when `Problem.optimise` omits one. - """ - def build(self) -> Problem: - r""" - Create a `Problem` representing the differential solver model. - """ - -@typing.final -class NelderMead: - r""" - Classic simplex-based direct search optimiser. - """ - def __new__(cls) -> NelderMead: - r""" - Create a Nelder-Mead optimiser with default coefficients. - """ - def with_max_iter(self, max_iter: builtins.int) -> NelderMead: - r""" - Limit the number of simplex iterations. - """ - def with_threshold(self, threshold: builtins.float) -> NelderMead: - r""" - Set the stopping threshold on simplex size or objective reduction. - """ - def with_position_tolerance(self, tolerance: builtins.float) -> NelderMead: - r""" - Stop once simplex vertices fall within the supplied positional tolerance. - """ - def with_max_evaluations(self, max_evaluations: builtins.int) -> NelderMead: - r""" - Abort after evaluating the objective `max_evaluations` times. - """ - def with_coefficients( - self, - alpha: builtins.float, - gamma: builtins.float, - rho: builtins.float, - sigma: builtins.float, - ) -> NelderMead: - r""" - Override the reflection, expansion, contraction, and shrink coefficients. - """ - def with_patience(self, patience_seconds: builtins.float) -> NelderMead: - r""" - Abort if the objective fails to improve within the allotted time. - """ - def run( - self, problem: Problem, initial: typing.Sequence[builtins.float] - ) -> OptimisationResults: - r""" - Optimise the given problem starting from the provided initial simplex centre. - """ - -@typing.final -class OptimisationResults: - r""" - Container for optimiser outputs and diagnostic metadata. - """ - @property - def x(self) -> builtins.list[builtins.float]: - r""" - Decision vector corresponding to the best-found objective value. - """ - @property - def fun(self) -> builtins.float: - r""" - Objective value evaluated at `x`. - """ - @property - def nit(self) -> builtins.int: - r""" - Number of iterations performed by the optimiser. - """ - @property - def evaluations(self) -> builtins.int: - r""" - Total number of objective function evaluations. - """ - @property - def time(self) -> datetime.timedelta: - r""" - Total number of objective function evaluations. - """ - @property - def success(self) -> builtins.bool: - r""" - Whether the run satisfied its convergence criteria. - """ - @property - def message(self) -> builtins.str: - r""" - Human-readable status message summarising the termination state. - """ - @property - def termination_reason(self) -> builtins.str: - r""" - Structured termination flag describing why the run ended. - """ - @property - def final_simplex(self) -> builtins.list[builtins.list[builtins.float]]: - r""" - Simplex vertices at termination, when provided by the optimiser. - """ - @property - def final_simplex_values(self) -> builtins.list[builtins.float]: - r""" - Objective values corresponding to `final_simplex`. - """ - @property - def covariance( - self, - ) -> builtins.list[builtins.list[builtins.float]] | None: - r""" - Estimated covariance of the search distribution, if available. - """ - def __repr__(self) -> builtins.str: - r""" - Render a concise summary of the optimisation outcome. - """ - -@typing.final -class Problem: - r""" - Executable optimisation problem wrapping the Chronopt core implementation. - """ - def evaluate(self, x: typing.Sequence[builtins.float]) -> builtins.float: - r""" - Evaluate the configured objective function at `x`. - """ - def evaluate_gradient( - self, x: typing.Sequence[builtins.float] - ) -> builtins.list[builtins.float] | None: - r""" - Evaluate the gradient of the objective function at `x` if available. - """ - def optimise( - self, - initial: typing.Sequence[builtins.float] | None = None, - optimiser: NelderMead | CMAES | Adam | None = None, - ) -> OptimisationResults: - r""" - Solve the problem starting from `initial` using the supplied optimiser. - """ - def get_config(self, key: builtins.str) -> builtins.float | None: - r""" - Return the numeric configuration value stored under `key` if present. - """ - def dimension(self) -> builtins.int: - r""" - Return the number of parameters the problem expects. - """ - def parameters( - self, - ) -> builtins.list[ - tuple[ - builtins.str, - builtins.float, - tuple[builtins.float, builtins.float] | None, - ] - ]: ... - def default_parameters(self) -> builtins.list[builtins.float]: - r""" - Return the default parameter vector implied by the builder. - """ - def config(self) -> builtins.dict[builtins.str, builtins.float]: - r""" - Return a copy of the problem configuration dictionary. - """ - -@typing.final -class ScalarBuilder: - r""" - High-level builder for optimisation `Problem` instances exposed to Python. - """ - def __new__(cls) -> ScalarBuilder: - r""" - Create an empty builder with no objective, parameters, or default optimiser. - """ - def with_optimiser(self, optimiser: NelderMead | CMAES | Adam) -> ScalarBuilder: - r""" - Configure the default optimiser used when `Problem.optimise` omits one. - """ - def with_callable(self, obj: typing.Any) -> ScalarBuilder: - r""" - Attach the objective function callable executed during optimisation. - """ - def with_gradient(self, obj: typing.Any) -> ScalarBuilder: - r""" - Attach the gradient callable returning derivatives of the objective. - """ - def with_parameter( - self, - name: builtins.str, - initial_value: builtins.float, - bounds: tuple[builtins.float, builtins.float] | None = None, - ) -> ScalarBuilder: - r""" - Register a named optimisation variable in the order it appears in vectors. - """ - def build(self) -> Problem: - r""" - Finalize the builder into an executable `Problem`. - """ - -@typing.final -class VectorBuilder: - r""" - Time-series problem builder for vector-valued objectives. - """ - def __new__(cls) -> VectorBuilder: - r""" - Create an empty vector problem builder. - """ - def with_objective(self, objective: typing.Any) -> VectorBuilder: - r""" - Register a callable that produces predictions matching the data shape. - - The callable should accept a parameter vector and return a numpy array - of the same shape as the observed data. - """ - def with_data(self, data: numpy.typing.NDArray[numpy.float64]) -> VectorBuilder: - r""" - Attach observed data used to fit the model. - - The data should be a 1D numpy array. The shape will be inferred - from the data length. - """ - def with_config(self, key: builtins.str, value: builtins.float) -> VectorBuilder: - r""" - Stores an optimisation configuration value keyed by name. - """ - def with_parameter( - self, - name: builtins.str, - initial_value: builtins.float, - bounds: tuple[builtins.float, builtins.float] | None = None, - ) -> VectorBuilder: - r""" - Register a named optimisation variable in the order it appears in vectors. - """ - def clear_parameters(self) -> VectorBuilder: - r""" - Remove previously provided parameter defaults. - """ - def with_cost(self, cost: CostMetric) -> VectorBuilder: - r""" - Select the error metric used to compare predictions and observed data. - """ - def remove_cost(self) -> VectorBuilder: - r""" - Reset the cost metric to the default sum of squared errors. - """ - def with_optimiser(self, optimiser: NelderMead | CMAES | Adam) -> VectorBuilder: - r""" - Configure the default optimiser used when `Problem.optimise` omits one. - """ - def build(self) -> Problem: - r""" - Create a `Problem` representing the vector optimisation model. - """ - -def GaussianNLL( - variance: builtins.float, weight: builtins.float = 1.0 -) -> CostMetric: ... -def RMSE(weight: builtins.float = 1.0) -> CostMetric: ... -def SSE(weight: builtins.float = 1.0) -> CostMetric: ... -def builder_factory_py() -> ScalarBuilder: - r""" - Return a convenience factory for creating `Builder` instances. - """ diff --git a/python/src/chronopt/sampler.pyi b/python/src/chronopt/sampler.pyi deleted file mode 100644 index e5b66d2..0000000 --- a/python/src/chronopt/sampler.pyi +++ /dev/null @@ -1,83 +0,0 @@ -# This file is automatically generated by pyo3_stub_gen -# ruff: noqa: E501, F401 - -import builtins -import datetime -import typing - -from chronopt._chronopt import Problem - -@typing.final -class DynamicNestedSampler: - r""" - Dynamic nested sampler binding exposing DNS configuration knobs. - """ - def __new__(cls) -> DynamicNestedSampler: ... - def with_live_points(self, live_points: builtins.int) -> DynamicNestedSampler: ... - def with_expansion_factor( - self, expansion_factor: builtins.float - ) -> DynamicNestedSampler: ... - def with_termination_tolerance( - self, tolerance: builtins.float - ) -> DynamicNestedSampler: ... - def with_seed(self, seed: builtins.int) -> DynamicNestedSampler: ... - def run( - self, - problem: Problem, - initial: typing.Sequence[builtins.float] | None = None, - ) -> NestedSamples: ... - -@typing.final -class MetropolisHastings: - r""" - Basic Metropolis-Hastings sampler binding mirroring the optimiser API. - """ - def __new__(cls) -> MetropolisHastings: ... - def with_num_chains(self, num_chains: builtins.int) -> MetropolisHastings: ... - def set_number_of_chains(self, num_chains: builtins.int) -> MetropolisHastings: ... - def with_iterations(self, iterations: builtins.int) -> MetropolisHastings: ... - def with_num_steps(self, steps: builtins.int) -> MetropolisHastings: ... - def with_step_size(self, step_size: builtins.float) -> MetropolisHastings: ... - def with_seed(self, seed: builtins.int) -> MetropolisHastings: ... - def run( - self, problem: Problem, initial: typing.Sequence[builtins.float] - ) -> Samples: ... - -@typing.final -class NestedSamples: - r""" - Nested sampling results including evidence estimates. - """ - @property - def posterior( - self, - ) -> builtins.list[ - tuple[builtins.list[builtins.float], builtins.float, builtins.float] - ]: ... - @property - def mean(self) -> builtins.list[builtins.float]: ... - @property - def draws(self) -> builtins.int: ... - @property - def log_evidence(self) -> builtins.float: ... - @property - def information(self) -> builtins.float: ... - @property - def time(self) -> datetime.timedelta: ... - def to_samples(self) -> Samples: ... - def __repr__(self) -> builtins.str: ... - -@typing.final -class Samples: - r""" - Container for sampler draws and diagnostics. - """ - @property - def chains(self) -> builtins.list[builtins.list[builtins.list[builtins.float]]]: ... - @property - def mean_x(self) -> builtins.list[builtins.float]: ... - @property - def draws(self) -> builtins.int: ... - @property - def time(self) -> datetime.timedelta: ... - def __repr__(self) -> builtins.str: ... diff --git a/python/src/chronopt/__init__.py b/python/src/diffid/__init__.py similarity index 87% rename from python/src/chronopt/__init__.py rename to python/src/diffid/__init__.py index ebb5280..8ca3963 100644 --- a/python/src/chronopt/__init__.py +++ b/python/src/diffid/__init__.py @@ -1,22 +1,22 @@ -"""Chronopt public Python API.""" +"""Diffid public Python API.""" from __future__ import annotations # Error hierarchy -from chronopt.errors import ( +from diffid.errors import ( AlreadyTerminated, BuildError, - ChronoptError, + DiffidError, EvaluationError, ResultCountMismatch, TellError, ) # Plotting module -from chronopt import plotting +from diffid import plotting # Core bindings - optimisers -from chronopt._chronopt import ( +from diffid._diffid import ( Adam, AdamState, CMAES, @@ -26,7 +26,7 @@ ) # Core bindings - samplers -from chronopt._chronopt import ( +from diffid._diffid import ( DynamicNestedSampler, DynamicNestedSamplerState, MetropolisHastings, @@ -36,14 +36,14 @@ ) # Core bindings - builders -from chronopt._chronopt import ( +from diffid._diffid import ( DiffsolBuilder, ScalarBuilder, VectorBuilder, ) # Core bindings - results and problems -from chronopt._chronopt import ( +from diffid._diffid import ( CostMetric, Done, Evaluate, @@ -52,9 +52,9 @@ ) # Cost metric factory functions -from chronopt._chronopt import RMSE as _RMSE -from chronopt._chronopt import SSE as _SSE -from chronopt._chronopt import GaussianNLL as _GaussianNLL +from diffid._diffid import RMSE as _RMSE +from diffid._diffid import SSE as _SSE +from diffid._diffid import GaussianNLL as _GaussianNLL def SSE(weight: float = 1.0) -> CostMetric: @@ -116,7 +116,7 @@ def GaussianNLL(variance: float = 1.0, weight: float = 1.0) -> CostMetric: # Modules "plotting", # Errors - "ChronoptError", + "DiffidError", "EvaluationError", "BuildError", "TellError", diff --git a/python/src/diffid/_diffid.pyi b/python/src/diffid/_diffid.pyi new file mode 100644 index 0000000..ab356e0 --- /dev/null +++ b/python/src/diffid/_diffid.pyi @@ -0,0 +1,956 @@ +# This file is automatically generated by pyo3_stub_gen +# ruff: noqa: E501, F401 + +import builtins +import datetime +import typing + +import numpy +import numpy.typing + +from diffid.sampler import DynamicNestedSampler, MetropolisHastings + +@typing.final +class Adam: + r""" + Adaptive Moment Estimation (Adam) gradient-based optimiser. + """ + def __new__(cls) -> Adam: + r""" + Create an Adam optimiser with library defaults. + """ + def with_max_iter(self, max_iter: builtins.int) -> Adam: + r""" + Limit the maximum number of optimisation iterations. + """ + def with_threshold(self, threshold: builtins.float) -> Adam: + r""" + Set the stopping threshold on the gradient norm. + """ + def with_step_size(self, step_size: builtins.float) -> Adam: + r""" + Configure the base learning rate / step size. + """ + def with_betas(self, beta1: builtins.float, beta2: builtins.float) -> Adam: + r""" + Override the exponential decay rates for the first and second moments. + """ + def with_eps(self, eps: builtins.float) -> Adam: + r""" + Override the numerical stability constant added to the denominator. + """ + def with_patience(self, patience: typing.Any) -> Adam: + r""" + Abort the run once the patience window has elapsed. + + Parameters + ---------- + patience : float or timedelta + Either seconds (float) or a timedelta object + """ + def run( + self, problem: Problem, initial: typing.Sequence[builtins.float] + ) -> OptimisationResults: + r""" + Optimise the given problem using Adam starting from the provided point. + """ + def init( + self, + initial: typing.Sequence[builtins.float], + bounds: typing.Sequence[tuple[builtins.float, builtins.float]] | None = None, + ) -> AdamState: + r""" + Initialize ask-tell optimization state. + + Returns an AdamState object that can be used for incremental optimization + via the ask-tell interface. + + Parameters + ---------- + initial : list[float] + Initial parameter vector + bounds : list[tuple[float, float]], optional + Parameter bounds as [(lower, upper), ...]. If None, unbounded. + + Returns + ------- + AdamState + State object for ask-tell optimization + + Examples + -------- + >>> optimiser = diffid.Adam() + >>> state = optimiser.init(initial=[1.0, 2.0]) + >>> while True: + ... result = state.ask() + ... if isinstance(result, diffid.Done): + ... break + ... values = [evaluate_with_gradient(pt) for pt in result.points] + ... state.tell(values) + """ + +@typing.final +class AdamState: + r""" + Ask-tell state for incremental Adam optimization. + + This state object allows step-by-step control over the optimization process. + Use `ask()` to get points to evaluate, and `tell()` to provide results. + + Examples + -------- + >>> optimiser = diffid.Adam().with_max_iter(100) + >>> state = optimiser.init(initial=[1.0, 2.0]) + >>> while True: + ... result = state.ask() + ... if isinstance(result, diffid.Done): + ... print(f"Final result: {result.result}") + ... break + ... # Adam requires gradient information + ... values = [(f(pt), grad_f(pt)) for pt in result.points] + ... state.tell(values) + """ + def ask(self) -> typing.Any: + r""" + Get the next action: evaluate points or optimization complete. + + Returns + ------- + Evaluate | Done + Either Evaluate(points) requiring function evaluations, + or Done(result) indicating completion. + + Examples + -------- + >>> result = state.ask() + >>> if isinstance(result, diffid.Evaluate): + ... print(f"Need to evaluate {len(result.points)} points") + >>> elif isinstance(result, diffid.Done): + ... print(f"Optimization complete: {result.result}") + """ + def tell( + self, result: tuple[builtins.float, typing.Sequence[builtins.float]] + ) -> None: + r""" + Provide evaluation results (value and gradient) for the requested points. + + Parameters + ---------- + result : tuple[float, list[float]] + Tuple of (value, gradient) where gradient is a list of partial derivatives. + Adam requires gradient information. + + Raises + ------ + TellError + If called after optimization has terminated or if result format is invalid + EvaluationError + If the evaluation failed or contained invalid values + + Examples + -------- + >>> result = state.ask() + >>> if isinstance(result, diffid.Evaluate): + ... point = result.points[0] + ... value = objective(point) + ... gradient = compute_gradient(point) + ... state.tell((value, gradient)) + """ + def iterations(self) -> builtins.int: + r""" + Get the current iteration count. + + Returns + ------- + int + Number of iterations completed + """ + def evaluations(self) -> builtins.int: + r""" + Get the total number of function evaluations. + + Returns + ------- + int + Number of function evaluations performed + """ + def best( + self, + ) -> tuple[builtins.list[builtins.float], builtins.float] | None: + r""" + Get the current best point and value found so far. + + Returns + ------- + tuple[list[float], float] | None + (best_point, best_value) or None if no valid evaluations yet + """ + def current_position(self) -> builtins.list[builtins.float]: + r""" + Get the current parameter position. + + Returns + ------- + list[float] + Current parameter vector + """ + def __repr__(self) -> builtins.str: ... + def __str__(self) -> builtins.str: ... + +@typing.final +class CMAES: + r""" + Covariance Matrix Adaptation Evolution Strategy optimiser. + """ + def __new__(cls) -> CMAES: + r""" + Create a CMA-ES optimiser with library defaults. + """ + def with_max_iter(self, max_iter: builtins.int) -> CMAES: + r""" + Limit the number of iterations/generations before termination. + """ + def with_threshold(self, threshold: builtins.float) -> CMAES: + r""" + Set the stopping threshold on the best objective value. + """ + def with_step_size(self, step_size: builtins.float) -> CMAES: + r""" + Set the initial global step-size (standard deviation). + """ + def with_patience(self, patience: typing.Any) -> CMAES: + r""" + Abort the run if no improvement occurs for the given wall-clock duration. + + Parameters + ---------- + patience : float or timedelta + Either seconds (float) or a timedelta object + """ + def with_population_size(self, population_size: builtins.int) -> CMAES: + r""" + Specify the number of offspring evaluated per generation. + """ + def with_seed(self, seed: builtins.int) -> CMAES: + r""" + Initialise the internal RNG for reproducible runs. + """ + def run( + self, problem: Problem, initial: typing.Sequence[builtins.float] + ) -> OptimisationResults: + r""" + Optimise the given problem starting from the provided mean vector. + """ + def init( + self, + initial: typing.Sequence[builtins.float], + bounds: typing.Sequence[tuple[builtins.float, builtins.float]] | None = None, + ) -> CMAESState: + r""" + Initialize ask-tell optimization state. + + Returns a CMAESState object that can be used for incremental optimization + via the ask-tell interface. + + Parameters + ---------- + initial : list[float] + Initial mean vector for the search distribution + bounds : list[tuple[float, float]], optional + Parameter bounds as [(lower, upper), ...]. If None, unbounded. + + Returns + ------- + CMAESState + State object for ask-tell optimization + + Examples + -------- + >>> optimiser = diffid.CMAES() + >>> state = optimiser.init(initial=[1.0, 2.0]) + >>> while True: + ... result = state.ask() + ... if isinstance(result, diffid.Done): + ... break + ... values = [evaluate(pt) for pt in result.points] + ... state.tell(values) + """ + +@typing.final +class CMAESState: + r""" + Ask-tell state for incremental CMA-ES optimization. + + This state object allows step-by-step control over the optimization process. + Use `ask()` to get a population of points to evaluate, and `tell()` to provide results. + + Examples + -------- + >>> optimiser = diffid.CMAES().with_max_iter(100) + >>> state = optimiser.init(initial=[1.0, 2.0]) + >>> while True: + ... result = state.ask() + ... if isinstance(result, diffid.Done): + ... print(f"Final result: {result.result}") + ... break + ... values = [f(pt) for pt in result.points] + ... state.tell(values) + """ + def ask(self) -> typing.Any: + r""" + Get the next action: evaluate points or optimization complete. + + Returns + ------- + Evaluate | Done + Either Evaluate(points) requiring function evaluations, + or Done(result) indicating completion. + + Notes + ----- + CMA-ES evaluates a population of points each iteration. The number + of points returned depends on the population_size setting. + """ + def tell(self, results: typing.Sequence[builtins.float]) -> None: + r""" + Provide evaluation results for the requested population of points. + + Parameters + ---------- + results : list[float] + List of objective function values corresponding to the points + from the last ask() call. Must match the number of points. + + Raises + ------ + TellError + If called after optimization has terminated or if wrong number + of results provided + EvaluationError + If evaluations failed or contained invalid values + """ + def iterations(self) -> builtins.int: + r""" + Get the current iteration (generation) count. + + Returns + ------- + int + Number of generations completed + """ + def evaluations(self) -> builtins.int: + r""" + Get the total number of function evaluations. + + Returns + ------- + int + Number of function evaluations performed + """ + def best( + self, + ) -> tuple[builtins.list[builtins.float], builtins.float] | None: + r""" + Get the current best point and value found so far. + + Returns + ------- + tuple[list[float], float] | None + (best_point, best_value) or None if no valid evaluations yet + """ + def mean(self) -> builtins.list[builtins.float]: + r""" + Get the current mean of the search distribution. + + Returns + ------- + list[float] + Current mean vector + """ + def sigma(self) -> builtins.float: + r""" + Get the current step size (sigma). + + Returns + ------- + float + Current global step size + """ + def __repr__(self) -> builtins.str: ... + def __str__(self) -> builtins.str: ... + +@typing.final +class CostMetric: + @property + def name(self) -> builtins.str: + r""" + Name of the cost metric. + """ + def __repr__(self) -> builtins.str: ... + +@typing.final +class DiffsolBuilder: + r""" + Differential equation solver builder. + """ + def __new__(cls) -> DiffsolBuilder: + r""" + Create an empty differential solver builder. + """ + def __copy__(self) -> DiffsolBuilder: ... + def __deepcopy__(self, _memo: dict) -> DiffsolBuilder: ... + def with_diffsl(self, dsl: builtins.str) -> DiffsolBuilder: + r""" + Register the DiffSL program describing the system dynamics. + """ + def with_data(self, data: numpy.typing.NDArray[numpy.float64]) -> DiffsolBuilder: + r""" + Attach observed data used to fit the differential equation. + + The first column must contain the time samples (t_span) and the remaining + columns the observed trajectories. + """ + def remove_data(self) -> DiffsolBuilder: + r""" + Remove any previously attached data along with its time span. + """ + def with_backend(self, backend: builtins.str) -> DiffsolBuilder: + r""" + Choose whether to use dense or sparse diffusion solvers. + """ + def with_parallel(self, parallel: builtins.bool | None = None) -> DiffsolBuilder: + r""" + Opt into parallel proposal generation when supported by the backend. + """ + def with_config( + self, config: typing.Mapping[builtins.str, builtins.float] + ) -> DiffsolBuilder: ... + def with_tolerances( + self, rtol: builtins.float, atol: builtins.float + ) -> DiffsolBuilder: + r""" + Adjust the relative and absolute integration tolerances. + """ + def with_parameter( + self, + name: builtins.str, + initial_value: builtins.float, + bounds: tuple[builtins.float, builtins.float] | None = None, + ) -> DiffsolBuilder: + r""" + Register a named optimisation variable in the order it appears in vectors. + """ + def clear_parameters(self) -> DiffsolBuilder: + r""" + Clear all previously registered parameters while preserving other configuration. + """ + def with_cost(self, cost: CostMetric) -> DiffsolBuilder: + r""" + Select the error metric used to compare simulated and observed data. + """ + def remove_cost(self) -> DiffsolBuilder: + r""" + Reset the cost metric to the default sum of squared errors. + """ + def with_optimiser(self, optimiser: NelderMead | CMAES | Adam) -> DiffsolBuilder: + r""" + Configure the default optimiser used when `Problem.optimise` omits one. + """ + def build(self) -> Problem: + r""" + Create a `Problem` representing the differential solver model. + """ + +@typing.final +class Done: + r""" + Optimization/sampling is complete with final results. + + This is returned by `ask()` when the algorithm has terminated. + Access the results via the `result` attribute. + + Examples + -------- + >>> while True: + ... result = state.ask() + ... if isinstance(result, diffid.Done): + ... print(f"Optimization complete: {result.result}") + ... break + """ + @property + def result(self) -> typing.Any: ... + def __repr__(self) -> builtins.str: ... + def __str__(self) -> builtins.str: ... + +@typing.final +class Evaluate: + r""" + Request to evaluate objective function at specific points. + + This is returned by `ask()` when the optimiser/sampler needs function + evaluations. Call `tell()` with the results after evaluation. + + Examples + -------- + >>> state = optimiser.init(problem, initial=[1.0, 2.0]) + >>> result = state.ask() + >>> if isinstance(result, diffid.Evaluate): + ... values = [problem.evaluate(pt) for pt in result.points] + ... state.tell(values) + """ + @property + def points(self) -> builtins.list[builtins.list[builtins.float]]: ... + def __new__( + cls, points: typing.Sequence[typing.Sequence[builtins.float]] + ) -> Evaluate: ... + def __repr__(self) -> builtins.str: ... + def __str__(self) -> builtins.str: ... + +@typing.final +class NelderMead: + r""" + Classic simplex-based direct search optimiser. + """ + def __new__(cls) -> NelderMead: + r""" + Create a Nelder-Mead optimiser with default coefficients. + """ + def with_step_size(self, step_size: builtins.float) -> NelderMead: + r""" + Set the initial global step-size (standard deviation). + """ + def with_max_iter(self, max_iter: builtins.int) -> NelderMead: + r""" + Limit the number of simplex iterations. + """ + def with_threshold(self, threshold: builtins.float) -> NelderMead: + r""" + Set the stopping threshold on simplex size or objective reduction. + """ + def with_position_tolerance(self, tolerance: builtins.float) -> NelderMead: + r""" + Stop once simplex vertices fall within the supplied positional tolerance. + """ + def with_max_evaluations(self, max_evaluations: builtins.int) -> NelderMead: + r""" + Abort after evaluating the objective `max_evaluations` times. + """ + def with_coefficients( + self, + alpha: builtins.float, + gamma: builtins.float, + rho: builtins.float, + sigma: builtins.float, + ) -> NelderMead: + r""" + Override the reflection, expansion, contraction, and shrink coefficients. + """ + def with_patience(self, patience: typing.Any) -> NelderMead: + r""" + Abort if the objective fails to improve within the allotted time. + + Parameters + ---------- + patience : float or timedelta + Either seconds (float) or a timedelta object + """ + def run( + self, problem: Problem, initial: typing.Sequence[builtins.float] + ) -> OptimisationResults: + r""" + Optimise the given problem starting from the provided initial simplex centre. + """ + def init( + self, + initial: typing.Sequence[builtins.float], + bounds: typing.Sequence[tuple[builtins.float, builtins.float]] | None = None, + ) -> NelderMeadState: + r""" + Initialize ask-tell optimization state. + + Returns a NelderMeadState object that can be used for incremental optimization + via the ask-tell interface. + + Parameters + ---------- + initial : list[float] + Initial parameter vector (simplex center) + bounds : list[tuple[float, float]], optional + Parameter bounds as [(lower, upper), ...]. If None, unbounded. + + Returns + ------- + NelderMeadState + State object for ask-tell optimization + + Examples + -------- + >>> optimiser = diffid.NelderMead() + >>> state = optimiser.init(initial=[1.0, 2.0]) + >>> while True: + ... result = state.ask() + ... if isinstance(result, diffid.Done): + ... break + ... values = [evaluate(pt) for pt in result.points] + ... state.tell(values) + """ + +@typing.final +class NelderMeadState: + r""" + Ask-tell state for incremental Nelder-Mead optimization. + + This state object allows step-by-step control over the optimization process. + Use `ask()` to get points to evaluate, and `tell()` to provide results. + + Examples + -------- + >>> optimiser = diffid.NelderMead().with_max_iter(100) + >>> state = optimiser.init(initial=[1.0, 2.0]) + >>> while True: + ... result = state.ask() + ... if isinstance(result, diffid.Done): + ... print(f"Final result: {result.result}") + ... break + ... values = [f(pt) for pt in result.points] + ... state.tell(values) + """ + def ask(self) -> typing.Any: + r""" + Get the next action: evaluate points or optimization complete. + + Returns + ------- + Evaluate | Done + Either Evaluate(points) requiring function evaluations, + or Done(result) indicating completion. + """ + def tell(self, result: builtins.float) -> None: + r""" + Provide evaluation result (scalar value) for the requested point. + + Parameters + ---------- + result : float + Scalar objective function value + + Raises + ------ + TellError + If called after optimization has terminated + EvaluationError + If the evaluation failed or contained invalid values + """ + def iterations(self) -> builtins.int: + r""" + Get the current iteration count. + + Returns + ------- + int + Number of iterations completed + """ + def evaluations(self) -> builtins.int: + r""" + Get the total number of function evaluations. + + Returns + ------- + int + Number of function evaluations performed + """ + def best( + self, + ) -> tuple[builtins.list[builtins.float], builtins.float] | None: + r""" + Get the current best point and value from the simplex. + + Returns + ------- + tuple[list[float], float] | None + (best_point, best_value) or None if no valid evaluations yet + """ + def __repr__(self) -> builtins.str: ... + def __str__(self) -> builtins.str: ... + +@typing.final +class NestedSamplesIterator: + r""" + Iterator for NestedSamples posterior + """ + def __iter__(self) -> NestedSamplesIterator: ... + def __next__( + self, + ) -> ( + tuple[builtins.list[builtins.float], builtins.float, builtins.float] | None + ): ... + +@typing.final +class OptimisationResults: + r""" + Container for optimiser outputs and diagnostic metadata. + """ + @property + def x(self) -> numpy.typing.NDArray[numpy.float64]: + r""" + Decision vector corresponding to the best-found objective value. + + Returns + ------- + numpy.ndarray + Best parameter vector as a NumPy array + """ + @property + def value(self) -> builtins.float: + r""" + Objective value evaluated at `x`. + """ + @property + def iterations(self) -> builtins.int: + r""" + Number of iterations performed by the optimiser. + """ + @property + def evaluations(self) -> builtins.int: + r""" + Total number of objective function evaluations. + """ + @property + def time(self) -> datetime.timedelta: + r""" + Total number of objective function evaluations. + """ + @property + def success(self) -> builtins.bool: + r""" + Whether the run satisfied its convergence criteria. + """ + @property + def message(self) -> builtins.str: + r""" + Human-readable status message summarising the termination state. + """ + @property + def termination_reason(self) -> builtins.str: + r""" + Structured termination flag describing why the run ended. + """ + @property + def final_simplex(self) -> builtins.list[builtins.list[builtins.float]]: + r""" + Simplex vertices at termination, when provided by the optimiser. + """ + @property + def final_simplex_values(self) -> builtins.list[builtins.float]: + r""" + Objective values corresponding to `final_simplex`. + """ + @property + def covariance( + self, + ) -> builtins.list[builtins.list[builtins.float]] | None: + r""" + Estimated covariance of the search distribution, if available. + """ + def __repr__(self) -> builtins.str: + r""" + Render a concise summary of the optimisation outcome. + """ + def __str__(self) -> builtins.str: + r""" + Return a human-readable summary of the result. + """ + def __bool__(self) -> builtins.bool: + r""" + Return truthiness based on optimization success. + + Allows using `if result:` instead of `if result.success:`. + """ + +@typing.final +class Problem: + r""" + Executable optimisation problem wrapping the Diffid core implementation. + """ + def evaluate(self, x: typing.Sequence[builtins.float]) -> builtins.float: + r""" + Evaluate the configured objective function at `x`. + """ + def evaluate_gradient( + self, x: typing.Sequence[builtins.float] + ) -> builtins.list[builtins.float] | None: + r""" + Evaluate the gradient of the objective function at `x` if available. + """ + def optimise( + self, + initial: typing.Sequence[builtins.float] | None = None, + optimiser: NelderMead | CMAES | Adam | None = None, + ) -> OptimisationResults: + r""" + Solve the problem starting from `initial` using the supplied optimiser. + """ + def sample( + self, + initial: typing.Sequence[builtins.float] | None = None, + sampler: MetropolisHastings | DynamicNestedSampler | None = None, + ) -> typing.Any: + r""" + Sample from the problem starting from `initial` using the supplied sampler. + """ + def get_config(self, _key: builtins.str) -> builtins.float | None: + r""" + Return the numeric configuration value stored under `key` if present. + """ + def dimension(self) -> builtins.int: + r""" + Return the number of parameters the problem expects. + """ + def bounds(self) -> builtins.list[tuple[builtins.float, builtins.float]]: + r""" + Return the parameter bounds for the problem as a list of (lower, upper) tuples. + """ + def parameters( + self, + ) -> builtins.list[ + tuple[ + builtins.str, + builtins.float, + tuple[builtins.float, builtins.float] | None, + ] + ]: ... + def initial_values(self) -> builtins.list[builtins.float]: ... + def default_parameters(self) -> builtins.list[builtins.float]: + r""" + Return the default parameter vector implied by the builder. + """ + def config(self) -> builtins.dict[builtins.str, builtins.float]: + r""" + Return a copy of the problem configuration dictionary. + """ + def __call__(self, x: typing.Sequence[builtins.float]) -> builtins.float: + r""" + Call the problem as a function (shorthand for evaluate). + + Allows using `problem(x)` instead of `problem.evaluate(x)`. + """ + def __repr__(self) -> builtins.str: + r""" + Return a detailed string representation of the problem. + """ + def __str__(self) -> builtins.str: + r""" + Return a concise string representation of the problem. + """ + +@typing.final +class SamplesIterator: + r""" + Iterator for Samples chains + """ + def __iter__(self) -> SamplesIterator: ... + def __next__( + self, + ) -> builtins.list[builtins.list[builtins.float]] | None: ... + +@typing.final +class ScalarBuilder: + r""" + High-level builder for optimisation `Problem` instances exposed to Python. + """ + def __new__(cls) -> ScalarBuilder: + r""" + Create an empty builder with no objective, parameters, or default optimiser. + """ + def __copy__(self) -> ScalarBuilder: ... + def __deepcopy__(self, _memo: dict) -> ScalarBuilder: ... + def with_optimiser(self, optimiser: NelderMead | CMAES | Adam) -> ScalarBuilder: + r""" + Configure the default optimiser used when `Problem.optimise` omits one. + """ + def with_objective(self, obj: typing.Any) -> ScalarBuilder: + r""" + Attach the objective function callable executed during optimisation. + """ + def with_gradient(self, obj: typing.Any) -> ScalarBuilder: + r""" + Attach the gradient callable returning derivatives of the objective. + """ + def with_parameter( + self, + name: builtins.str, + initial_value: builtins.float, + bounds: tuple[builtins.float, builtins.float] | None = None, + ) -> ScalarBuilder: + r""" + Register a named optimisation variable in the order it appears in vectors. + """ + def build(self) -> Problem: + r""" + Finalize the builder into an executable `Problem`. + """ + +@typing.final +class VectorBuilder: + r""" + Time-series problem builder for vector-valued objectives. + """ + def __new__(cls) -> VectorBuilder: + r""" + Create an empty vector problem builder. + """ + def __copy__(self) -> VectorBuilder: ... + def __deepcopy__(self, _memo: dict) -> VectorBuilder: ... + def with_objective(self, objective: typing.Any) -> VectorBuilder: + r""" + Register a callable that produces predictions matching the data shape. + + The callable should accept a parameter vector and return a numpy array + of the same shape as the observed data. + """ + def with_data(self, data: numpy.typing.NDArray[numpy.float64]) -> VectorBuilder: + r""" + Attach observed data used to fit the model. + + The data should be a 1D numpy array. The shape will be inferred + from the data length. + """ + def with_parameter( + self, + name: builtins.str, + initial_value: builtins.float, + bounds: tuple[builtins.float, builtins.float] | None = None, + ) -> VectorBuilder: + r""" + Register a named optimisation variable in the order it appears in vectors. + """ + def with_cost(self, cost: CostMetric) -> VectorBuilder: + r""" + Select the error metric used to compare predictions and observed data. + """ + def remove_cost(self) -> VectorBuilder: + r""" + Reset the cost metric to the default sum of squared errors. + """ + def with_optimiser(self, optimiser: NelderMead | CMAES | Adam) -> VectorBuilder: + r""" + Configure the default optimiser used when `Problem.optimise` omits one. + """ + def with_config(self, key: builtins.str, value: builtins.float) -> VectorBuilder: + r""" + Attach an arbitrary configuration value to the problem. + """ + def build(self) -> Problem: + r""" + Create a `Problem` representing the vector optimisation model. + """ + +def GaussianNLL( + variance: builtins.float, weight: builtins.float = 1.0 +) -> CostMetric: ... +def RMSE(weight: builtins.float = 1.0) -> CostMetric: ... +def SSE(weight: builtins.float = 1.0) -> CostMetric: ... +def builder_factory_py() -> ScalarBuilder: + r""" + Return a convenience factory for creating `Builder` instances. + """ diff --git a/python/src/chronopt/errors.py b/python/src/diffid/errors.py similarity index 90% rename from python/src/chronopt/errors.py rename to python/src/diffid/errors.py index 6946247..e22c963 100644 --- a/python/src/chronopt/errors.py +++ b/python/src/diffid/errors.py @@ -1,30 +1,30 @@ -"""Custom exception hierarchy for Chronopt. +"""Custom exception hierarchy for Diffid. -This module defines the exception hierarchy used throughout the chronopt library, +This module defines the exception hierarchy used throughout the diffid library, providing clear and actionable error messages for different failure modes. """ from __future__ import annotations -class ChronoptError(Exception): - """Base exception class for all Chronopt errors. +class DiffidError(Exception): + """Base exception class for all Diffid errors. - All custom exceptions in the chronopt library inherit from this class, - making it easy to catch any chronopt-specific error. + All custom exceptions in the diffid library inherit from this class, + making it easy to catch any diffid-specific error. Examples -------- >>> try: ... optimiser.run(problem, initial=[1.0, 2.0]) - ... except ChronoptError as e: - ... print(f"Chronopt error occurred: {e}") + ... except DiffidError as e: + ... print(f"Diffid error occurred: {e}") """ pass -class EvaluationError(ChronoptError): +class EvaluationError(DiffidError): """Raised when objective function evaluation fails. This exception is raised when the objective function (or callback) throws @@ -65,7 +65,7 @@ def __init__( self.original_error = original_error -class BuildError(ChronoptError): +class BuildError(DiffidError): """Raised when problem or optimiser construction fails. This exception is raised during the build phase when invalid parameters @@ -84,7 +84,7 @@ class BuildError(ChronoptError): pass -class TellError(ChronoptError): +class TellError(DiffidError): """Base exception for errors during the 'tell' phase of ask-tell interface. This exception is raised when providing results back to an optimiser or @@ -161,7 +161,7 @@ def __init__(self): __all__ = [ - "ChronoptError", + "DiffidError", "EvaluationError", "BuildError", "TellError", diff --git a/python/src/chronopt/plotting/__init__.py b/python/src/diffid/plotting/__init__.py similarity index 97% rename from python/src/chronopt/plotting/__init__.py rename to python/src/diffid/plotting/__init__.py index 2c73e1a..8335511 100644 --- a/python/src/chronopt/plotting/__init__.py +++ b/python/src/diffid/plotting/__init__.py @@ -1,4 +1,4 @@ -"""Plotting utilities for Chronopt. +"""Plotting utilities for Diffid. This module provides convenience helpers for visualising optimisation and sampling results. The implementation only depends on ``numpy`` at import time and lazily @@ -14,7 +14,7 @@ import numpy as np if TYPE_CHECKING: # pragma: no cover - type checking only - from chronopt import Problem + from diffid import Problem __all__ = [ "contour", @@ -70,7 +70,7 @@ def contour( ---------- objective: A callable mapping a two-dimensional input to a scalar value, or a - :class:`chronopt.Problem` instance whose ``evaluate`` method will be + :class:`diffid.Problem` instance whose ``evaluate`` method will be invoked. x_bounds, y_bounds: Inclusive ranges ``(min, max)`` spanning the region to sample along each @@ -116,7 +116,7 @@ def contour( import matplotlib.pyplot as plt except ModuleNotFoundError as exc: # pragma: no cover - import guard raise ModuleNotFoundError( - "matplotlib is required for plotting; install it via 'pip install chronopt[plotting]'" + "matplotlib is required for plotting; install it via 'pip install diffid[plotting]'" ) from exc xs = np.linspace(x_min, x_max, grid_size) @@ -193,7 +193,7 @@ def contour_2d( import matplotlib.pyplot as plt except ModuleNotFoundError as exc: raise ModuleNotFoundError( - "matplotlib is required for plotting; install it via 'pip install chronopt[plotting]'" + "matplotlib is required for plotting; install it via 'pip install diffid[plotting]'" ) from exc _setup_plotting() @@ -294,7 +294,7 @@ def ode_fit( import matplotlib.pyplot as plt except ModuleNotFoundError as exc: raise ModuleNotFoundError( - "matplotlib is required for plotting; install it via 'pip install chronopt[plotting]'" + "matplotlib is required for plotting; install it via 'pip install diffid[plotting]'" ) from exc _setup_plotting() @@ -359,7 +359,7 @@ def convergence( import matplotlib.pyplot as plt except ModuleNotFoundError as exc: raise ModuleNotFoundError( - "matplotlib is required for plotting; install it via 'pip install chronopt[plotting]'" + "matplotlib is required for plotting; install it via 'pip install diffid[plotting]'" ) from exc _setup_plotting() @@ -420,7 +420,7 @@ def parameter_traces( import matplotlib.pyplot as plt except ModuleNotFoundError as exc: raise ModuleNotFoundError( - "matplotlib is required for plotting; install it via 'pip install chronopt[plotting]'" + "matplotlib is required for plotting; install it via 'pip install diffid[plotting]'" ) from exc _setup_plotting() @@ -502,7 +502,7 @@ def parameter_distributions( import matplotlib.pyplot as plt except ModuleNotFoundError as exc: raise ModuleNotFoundError( - "matplotlib is required for plotting; install it via 'pip install chronopt[plotting]'" + "matplotlib is required for plotting; install it via 'pip install diffid[plotting]'" ) from exc _setup_plotting() @@ -598,7 +598,7 @@ def compare_models( import matplotlib.pyplot as plt except ModuleNotFoundError as exc: raise ModuleNotFoundError( - "matplotlib is required for plotting; install it via 'pip install chronopt[plotting]'" + "matplotlib is required for plotting; install it via 'pip install diffid[plotting]'" ) from exc _setup_plotting() diff --git a/python/src/chronopt/plotting/__init__.pyi b/python/src/diffid/plotting/__init__.pyi similarity index 100% rename from python/src/chronopt/plotting/__init__.pyi rename to python/src/diffid/plotting/__init__.pyi diff --git a/python/src/chronopt/py.typed b/python/src/diffid/py.typed similarity index 100% rename from python/src/chronopt/py.typed rename to python/src/diffid/py.typed diff --git a/python/src/diffid/sampler.pyi b/python/src/diffid/sampler.pyi new file mode 100644 index 0000000..6054e40 --- /dev/null +++ b/python/src/diffid/sampler.pyi @@ -0,0 +1,353 @@ +# This file is automatically generated by pyo3_stub_gen +# ruff: noqa: E501, F401 + +import builtins +import datetime +import typing + +import numpy +import numpy.typing + +from diffid._diffid import NestedSamplesIterator, Problem, SamplesIterator + +@typing.final +class DynamicNestedSampler: + r""" + Dynamic nested sampler binding exposing DNS configuration knobs. + """ + def __new__(cls) -> DynamicNestedSampler: ... + def with_live_points(self, live_points: builtins.int) -> DynamicNestedSampler: ... + def with_expansion_factor( + self, expansion_factor: builtins.float + ) -> DynamicNestedSampler: ... + def with_termination_tolerance( + self, tolerance: builtins.float + ) -> DynamicNestedSampler: ... + def with_seed(self, seed: builtins.int) -> DynamicNestedSampler: ... + def run( + self, + problem: Problem, + initial: typing.Sequence[builtins.float] | None = None, + ) -> NestedSamples: ... + def init( + self, + initial: typing.Sequence[builtins.float], + bounds: typing.Sequence[tuple[builtins.float, builtins.float]] | None = None, + ) -> DynamicNestedSamplerState: + r""" + Initialize ask-tell sampling state. + + Returns a DynamicNestedSamplerState object that can be used for incremental + sampling via the ask-tell interface. + + Parameters + ---------- + initial : list[float] + Initial point for the sampler + bounds : list[tuple[float, float]], optional + Parameter bounds as [(lower, upper), ...]. If None, unbounded. + + Returns + ------- + DynamicNestedSamplerState + State object for ask-tell sampling + + Examples + -------- + >>> sampler = diffid.DynamicNestedSampler() + >>> state = sampler.init(initial=[1.0, 2.0]) + >>> while True: + ... result = state.ask() + ... if isinstance(result, diffid.Done): + ... break + ... values = [evaluate(pt) for pt in result.points] + ... state.tell(values) + """ + +@typing.final +class DynamicNestedSamplerState: + r""" + Ask-tell state for incremental Dynamic Nested Sampling. + + This state object allows step-by-step control over the sampling process. + Use `ask()` to get points to evaluate, and `tell()` to provide results. + + Examples + -------- + >>> sampler = diffid.DynamicNestedSampler() + >>> state = sampler.init(initial=[1.0, 2.0]) + >>> while True: + ... result = state.ask() + ... if isinstance(result, diffid.Done): + ... print(f"Sampling complete: {result.result}") + ... break + ... values = [negative_log_likelihood(pt) for pt in result.points] + ... state.tell(values) + """ + def ask(self) -> typing.Any: + r""" + Get the next action: evaluate points or sampling complete. + + Returns + ------- + Evaluate | Done + Either Evaluate(points) requiring function evaluations, + or Done(result) indicating completion with NestedSamples. + """ + def tell(self, results: typing.Sequence[builtins.float]) -> None: + r""" + Provide evaluation results for the requested points. + + Parameters + ---------- + results : list[float] + Negative log-likelihood values for each requested point. + + Raises + ------ + TellError + If called after sampling has terminated or if wrong number + of results provided + """ + def iterations(self) -> builtins.int: + r""" + Get the current iteration count. + + Returns + ------- + int + Number of iterations completed + """ + def num_live_points(self) -> builtins.int: + r""" + Get the number of live points. + + Returns + ------- + int + Current number of live points in the sampler + """ + def __repr__(self) -> builtins.str: ... + def __str__(self) -> builtins.str: ... + +@typing.final +class MetropolisHastings: + r""" + Basic Metropolis-Hastings sampler binding mirroring the optimiser API. + """ + def __new__(cls) -> MetropolisHastings: ... + def with_num_chains(self, num_chains: builtins.int) -> MetropolisHastings: ... + def with_iterations(self, iterations: builtins.int) -> MetropolisHastings: ... + def with_step_size(self, step_size: builtins.float) -> MetropolisHastings: ... + def with_seed(self, seed: builtins.int) -> MetropolisHastings: ... + def run( + self, problem: Problem, initial: typing.Sequence[builtins.float] + ) -> Samples: ... + def init( + self, + initial: typing.Sequence[builtins.float], + bounds: typing.Sequence[tuple[builtins.float, builtins.float]] | None = None, + ) -> MetropolisHastingsState: + r""" + Initialize ask-tell sampling state. + + Returns a MetropolisHastingsState object that can be used for incremental + sampling via the ask-tell interface. + + Parameters + ---------- + initial : list[float] + Initial point for all chains + bounds : list[tuple[float, float]], optional + Parameter bounds as [(lower, upper), ...]. If None, unbounded. + + Returns + ------- + MetropolisHastingsState + State object for ask-tell sampling + + Examples + -------- + >>> sampler = diffid.MetropolisHastings().with_num_chains(4) + >>> state = sampler.init(initial=[1.0, 2.0]) + >>> while True: + ... result = state.ask() + ... if isinstance(result, diffid.Done): + ... break + ... values = [evaluate(pt) for pt in result.points] + ... state.tell(values) + """ + +@typing.final +class MetropolisHastingsState: + r""" + Ask-tell state for incremental Metropolis-Hastings MCMC sampling. + + This state object allows step-by-step control over the sampling process. + Use `ask()` to get proposal points to evaluate, and `tell()` to provide results. + + Examples + -------- + >>> sampler = diffid.MetropolisHastings().with_num_chains(4) + >>> state = sampler.init(initial=[1.0, 2.0]) + >>> while True: + ... result = state.ask() + ... if isinstance(result, diffid.Done): + ... print(f"Sampling complete: {result.result}") + ... break + ... values = [negative_log_likelihood(pt) for pt in result.points] + ... state.tell(values) + """ + def ask(self) -> typing.Any: + r""" + Get the next action: evaluate proposal points or sampling complete. + + Returns + ------- + Evaluate | Done + Either Evaluate(points) requiring function evaluations, + or Done(result) indicating completion with Samples. + + Notes + ----- + Returns one proposal point per chain. + """ + def tell(self, results: typing.Sequence[builtins.float]) -> None: + r""" + Provide evaluation results for the proposed points. + + Parameters + ---------- + results : list[float] + Negative log-likelihood values for each proposal point. + Must match the number of chains. + + Raises + ------ + TellError + If called after sampling has terminated or if wrong number + of results provided + """ + def iterations(self) -> builtins.int: + r""" + Get the current iteration count. + + Returns + ------- + int + Number of iterations completed + """ + def num_chains(self) -> builtins.int: + r""" + Get the number of chains being run. + + Returns + ------- + int + Number of parallel MCMC chains + """ + def __repr__(self) -> builtins.str: ... + def __str__(self) -> builtins.str: ... + +@typing.final +class NestedSamples: + r""" + Nested sampling results including evidence estimates. + """ + @property + def posterior( + self, + ) -> builtins.list[ + tuple[builtins.list[builtins.float], builtins.float, builtins.float] + ]: ... + @property + def mean(self) -> numpy.typing.NDArray[numpy.float64]: ... + @property + def draws(self) -> builtins.int: ... + @property + def log_evidence(self) -> builtins.float: ... + @property + def information(self) -> builtins.float: ... + @property + def time(self) -> datetime.timedelta: ... + def to_samples(self) -> Samples: ... + def __repr__(self) -> builtins.str: ... + def __str__(self) -> builtins.str: + r""" + Return a human-readable summary of the nested samples. + """ + def __len__(self) -> builtins.int: + r""" + Return the number of posterior samples. + """ + def __iter__(self) -> NestedSamplesIterator: + r""" + Iterate over posterior samples. + + Yields tuples of (position, log_likelihood, log_weight). + """ + def __getitem__( + self, idx: builtins.int + ) -> tuple[builtins.list[builtins.float], builtins.float, builtins.float]: + r""" + Get a specific posterior sample by index. + + Parameters + ---------- + idx : int + Sample index (0 to num_samples - 1) + + Returns + ------- + tuple[list[float], float, float] + Tuple of (position, log_likelihood, log_weight) + """ + +@typing.final +class Samples: + r""" + Container for sampler draws and diagnostics. + """ + @property + def chains(self) -> numpy.typing.NDArray[numpy.float64]: ... + @property + def samples(self) -> numpy.typing.NDArray[numpy.float64]: ... + @property + def mean_x(self) -> numpy.typing.NDArray[numpy.float64]: ... + @property + def acceptance_rate(self) -> numpy.typing.NDArray[numpy.float64]: ... + @property + def draws(self) -> builtins.int: ... + @property + def time(self) -> datetime.timedelta: ... + def __repr__(self) -> builtins.str: ... + def __str__(self) -> builtins.str: + r""" + Return a human-readable summary of the samples. + """ + def __len__(self) -> builtins.int: + r""" + Return the number of chains. + """ + def __iter__(self) -> SamplesIterator: + r""" + Iterate over chains. + + Yields each chain as a list of samples. + """ + def __getitem__( + self, idx: builtins.int + ) -> builtins.list[builtins.list[builtins.float]]: + r""" + Get a specific chain by index. + + Parameters + ---------- + idx : int + Chain index (0 to num_chains - 1) + + Returns + ------- + list[list[float]] + The requested chain + """ diff --git a/python/src/errors.rs b/python/src/errors.rs index 157da4a..de4a67c 100644 --- a/python/src/errors.rs +++ b/python/src/errors.rs @@ -2,11 +2,11 @@ use pyo3::exceptions::PyValueError; use pyo3::prelude::*; use pyo3::types::PyModule; -use chronopt_core::errors::{EvaluationError as CoreEvaluationError, TellError as CoreTellError}; +use diffid_core::errors::{EvaluationError as CoreEvaluationError, TellError as CoreTellError}; -/// Get the custom exception class from chronopt.errors module +/// Get the custom exception class from diffid.errors module fn get_exception_class<'py>(py: Python<'py>, name: &str) -> PyResult> { - let errors_module = PyModule::import(py, "chronopt.errors")?; + let errors_module = PyModule::import(py, "diffid.errors")?; errors_module.getattr(name) } diff --git a/python/src/lib.rs b/python/src/lib.rs index 828d30c..d2452f5 100644 --- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -10,9 +10,9 @@ use std::env; #[cfg(feature = "stubgen")] use std::path::PathBuf; -use chronopt_core::cost::{CostMetric, GaussianNll, RootMeanSquaredError, SumSquaredError}; -use chronopt_core::prelude::*; -use chronopt_core::sampler::SamplingResults; +use diffid_core::cost::{CostMetric, GaussianNll, RootMeanSquaredError, SumSquaredError}; +use diffid_core::prelude::*; +use diffid_core::sampler::SamplingResults; #[cfg(feature = "stubgen")] use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pyfunction, gen_stub_pymethods}; @@ -39,7 +39,7 @@ use samplers::Sampler; type ParameterSpecEntry = (String, f64, Option<(f64, f64)>); // Import objective types for the problem enum -use chronopt_core::problem::{DiffsolObjective, ScalarObjective, VectorObjective}; +use diffid_core::problem::{DiffsolObjective, ScalarObjective, VectorObjective}; // Enum to hold different Problem types internally pub(crate) enum DynProblem { @@ -57,7 +57,7 @@ pub(crate) enum DynProblem { } impl DynProblem { - fn evaluate(&self, x: &[f64]) -> Result { + fn evaluate(&self, x: &[f64]) -> Result { match self { Self::Scalar(p) => p.evaluate(x), Self::ScalarWithGradient(p) => p.evaluate(x), @@ -210,7 +210,7 @@ fn gaussian_nll(variance: f64, weight: f64) -> PyResult { } // Problem -/// Executable optimisation problem wrapping the Chronopt core implementation. +/// Executable optimisation problem wrapping the Diffid core implementation. #[cfg_attr(feature = "stubgen", gen_stub_pyclass)] #[pyclass(name = "Problem")] pub struct PyProblem { @@ -227,7 +227,7 @@ impl PyProblem { /// Evaluate the configured objective function at `x`. fn evaluate(&self, x: Vec) -> PyResult { self.inner.evaluate(&x).map_err(|e| { - crate::errors::evaluation_error_to_py(chronopt_core::errors::EvaluationError::message( + crate::errors::evaluation_error_to_py(diffid_core::errors::EvaluationError::message( format!("{}", e), )) }) @@ -239,7 +239,7 @@ impl PyProblem { DynProblem::ScalarWithGradient(p) => match p.evaluate_with_gradient(&x) { Ok((_val, grad_opt)) => Ok(grad_opt), Err(e) => Err(crate::errors::evaluation_error_to_py( - chronopt_core::errors::EvaluationError::message(format!("{}", e)), + diffid_core::errors::EvaluationError::message(format!("{}", e)), )), }, _ => Ok(None), @@ -399,7 +399,7 @@ fn builder_factory_py() -> PyScalarBuilder { } #[pymodule] -fn _chronopt(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { +fn _diffid(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { // Main classes m.add_class::()?; m.add_class::()?; @@ -461,11 +461,11 @@ fn _chronopt(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_submodule(&sampler_module)?; m.setattr("sampler", &sampler_module)?; - // Register submodules for `import chronopt.builder` and `chronopt.cost` + // Register submodules for `import diffid.builder` and `diffid.cost` let sys_modules = py.import("sys")?.getattr("modules")?; - sys_modules.set_item("chronopt.builder", &builder_module)?; - sys_modules.set_item("chronopt.cost", &cost_module)?; - sys_modules.set_item("chronopt.sampler", &sampler_module)?; + sys_modules.set_item("diffid.builder", &builder_module)?; + sys_modules.set_item("diffid.cost", &cost_module)?; + sys_modules.set_item("diffid.sampler", &sampler_module)?; // Factory function m.add_function(wrap_pyfunction!(builder_factory_py, m)?)?; diff --git a/python/src/optimisers.rs b/python/src/optimisers.rs index 5043383..4cf537b 100644 --- a/python/src/optimisers.rs +++ b/python/src/optimisers.rs @@ -2,9 +2,9 @@ use pyo3::exceptions::PyTypeError; use pyo3::prelude::*; use std::time::Duration; -use chronopt_core::common::{AskResult, Bounds}; -use chronopt_core::optimisers::{AdamState, CMAESState, NelderMeadState}; -use chronopt_core::prelude::*; +use diffid_core::common::{AskResult, Bounds}; +use diffid_core::optimisers::{AdamState, CMAESState, NelderMeadState}; +use diffid_core::prelude::*; #[cfg(feature = "stubgen")] use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; @@ -28,9 +28,9 @@ pub(crate) enum Optimiser { #[cfg(feature = "stubgen")] #[allow(dead_code)] pub(crate) fn optimiser_type_info() -> TypeInfo { - TypeInfo::unqualified("chronopt._chronopt.NelderMead") - | TypeInfo::unqualified("chronopt._chronopt.CMAES") - | TypeInfo::unqualified("chronopt._chronopt.Adam") + TypeInfo::unqualified("diffid._diffid.NelderMead") + | TypeInfo::unqualified("diffid._diffid.CMAES") + | TypeInfo::unqualified("diffid._diffid.Adam") } impl FromPyObject<'_, '_> for Optimiser { @@ -53,7 +53,7 @@ impl FromPyObject<'_, '_> for Optimiser { impl Optimiser { /// Convert to the core Optimiser enum - pub(crate) fn to_core(&self) -> chronopt_core::optimisers::Optimiser { + pub(crate) fn to_core(&self) -> diffid_core::optimisers::Optimiser { match self { Optimiser::NelderMead(nm) => nm.clone().into(), Optimiser::Cmaes(cma) => cma.clone().into(), @@ -178,11 +178,11 @@ impl PyNelderMead { /// /// Examples /// -------- - /// >>> optimiser = chronopt.NelderMead() + /// >>> optimiser = diffid.NelderMead() /// >>> state = optimiser.init(initial=[1.0, 2.0]) /// >>> while True: /// ... result = state.ask() - /// ... if isinstance(result, chronopt.Done): + /// ... if isinstance(result, diffid.Done): /// ... break /// ... values = [evaluate(pt) for pt in result.points] /// ... state.tell(values) @@ -305,11 +305,11 @@ impl PyCMAES { /// /// Examples /// -------- - /// >>> optimiser = chronopt.CMAES() + /// >>> optimiser = diffid.CMAES() /// >>> state = optimiser.init(initial=[1.0, 2.0]) /// >>> while True: /// ... result = state.ask() - /// ... if isinstance(result, chronopt.Done): + /// ... if isinstance(result, diffid.Done): /// ... break /// ... values = [evaluate(pt) for pt in result.points] /// ... state.tell(values) @@ -423,11 +423,11 @@ impl PyAdam { /// /// Examples /// -------- - /// >>> optimiser = chronopt.Adam() + /// >>> optimiser = diffid.Adam() /// >>> state = optimiser.init(initial=[1.0, 2.0]) /// >>> while True: /// ... result = state.ask() - /// ... if isinstance(result, chronopt.Done): + /// ... if isinstance(result, diffid.Done): /// ... break /// ... values = [evaluate_with_gradient(pt) for pt in result.points] /// ... state.tell(values) @@ -450,11 +450,11 @@ impl PyAdam { /// /// Examples /// -------- -/// >>> optimiser = chronopt.Adam().with_max_iter(100) +/// >>> optimiser = diffid.Adam().with_max_iter(100) /// >>> state = optimiser.init(initial=[1.0, 2.0]) /// >>> while True: /// ... result = state.ask() -/// ... if isinstance(result, chronopt.Done): +/// ... if isinstance(result, diffid.Done): /// ... print(f"Final result: {result.result}") /// ... break /// ... # Adam requires gradient information @@ -480,9 +480,9 @@ impl PyAdamState { /// Examples /// -------- /// >>> result = state.ask() - /// >>> if isinstance(result, chronopt.Evaluate): + /// >>> if isinstance(result, diffid.Evaluate): /// ... print(f"Need to evaluate {len(result.points)} points") - /// >>> elif isinstance(result, chronopt.Done): + /// >>> elif isinstance(result, diffid.Done): /// ... print(f"Optimization complete: {result.result}") fn ask(&self, py: Python<'_>) -> Py { match self.inner.ask() { @@ -511,7 +511,7 @@ impl PyAdamState { /// Examples /// -------- /// >>> result = state.ask() - /// >>> if isinstance(result, chronopt.Evaluate): + /// >>> if isinstance(result, diffid.Evaluate): /// ... point = result.points[0] /// ... value = objective(point) /// ... gradient = compute_gradient(point) @@ -588,11 +588,11 @@ impl PyAdamState { /// /// Examples /// -------- -/// >>> optimiser = chronopt.NelderMead().with_max_iter(100) +/// >>> optimiser = diffid.NelderMead().with_max_iter(100) /// >>> state = optimiser.init(initial=[1.0, 2.0]) /// >>> while True: /// ... result = state.ask() -/// ... if isinstance(result, chronopt.Done): +/// ... if isinstance(result, diffid.Done): /// ... print(f"Final result: {result.result}") /// ... break /// ... values = [f(pt) for pt in result.points] @@ -699,11 +699,11 @@ impl PyNelderMeadState { /// /// Examples /// -------- -/// >>> optimiser = chronopt.CMAES().with_max_iter(100) +/// >>> optimiser = diffid.CMAES().with_max_iter(100) /// >>> state = optimiser.init(initial=[1.0, 2.0]) /// >>> while True: /// ... result = state.ask() -/// ... if isinstance(result, chronopt.Done): +/// ... if isinstance(result, diffid.Done): /// ... print(f"Final result: {result.result}") /// ... break /// ... values = [f(pt) for pt in result.points] diff --git a/python/src/results.rs b/python/src/results.rs index 6ae8bdd..902ef66 100644 --- a/python/src/results.rs +++ b/python/src/results.rs @@ -2,7 +2,7 @@ use numpy::{PyArray1, ToPyArray}; use pyo3::prelude::*; use std::time::Duration; -use chronopt_core::prelude::OptimisationResults; +use diffid_core::prelude::OptimisationResults; #[cfg(feature = "stubgen")] use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; @@ -18,7 +18,7 @@ use crate::{PyNestedSamples, PySamples}; /// -------- /// >>> state = optimiser.init(problem, initial=[1.0, 2.0]) /// >>> result = state.ask() -/// >>> if isinstance(result, chronopt.Evaluate): +/// >>> if isinstance(result, diffid.Evaluate): /// ... values = [problem.evaluate(pt) for pt in result.points] /// ... state.tell(values) #[cfg_attr(feature = "stubgen", gen_stub_pyclass)] @@ -55,7 +55,7 @@ impl PyEvaluate { /// -------- /// >>> while True: /// ... result = state.ask() -/// ... if isinstance(result, chronopt.Done): +/// ... if isinstance(result, diffid.Done): /// ... print(f"Optimization complete: {result.result}") /// ... break #[cfg_attr(feature = "stubgen", gen_stub_pyclass)] diff --git a/python/src/samplers.rs b/python/src/samplers.rs index a51a2e9..1bfc918 100644 --- a/python/src/samplers.rs +++ b/python/src/samplers.rs @@ -3,8 +3,8 @@ use pyo3::exceptions::PyTypeError; use pyo3::prelude::*; use std::time::Duration; -use chronopt_core::common::{AskResult, Bounds}; -use chronopt_core::sampler::{ +use diffid_core::common::{AskResult, Bounds}; +use diffid_core::sampler::{ DynamicNestedSampler as CoreDynamicNestedSampler, DynamicNestedSamplerState as CoreDynamicNestedSamplerState, MetropolisHastings as CoreMetropolisHastings, @@ -34,8 +34,8 @@ pub(crate) enum Sampler { #[cfg(feature = "stubgen")] #[allow(dead_code)] pub(crate) fn sampler_type_info() -> TypeInfo { - TypeInfo::unqualified("chronopt._chronopt.MetropolisHastings") - | TypeInfo::unqualified("chronopt._chronopt.DynamicNestedSampler") + TypeInfo::unqualified("diffid._diffid.MetropolisHastings") + | TypeInfo::unqualified("diffid._diffid.DynamicNestedSampler") } impl FromPyObject<'_, '_> for Sampler { @@ -70,7 +70,7 @@ impl Sampler { /// Container for sampler draws and diagnostics. #[cfg_attr(feature = "stubgen", gen_stub_pyclass)] -#[pyclass(module = "chronopt.sampler", name = "Samples")] +#[pyclass(module = "diffid.sampler", name = "Samples")] pub struct PySamples { pub(crate) inner: CoreSamples, } @@ -225,7 +225,7 @@ impl SamplesIterator { /// Nested sampling results including evidence estimates. #[cfg_attr(feature = "stubgen", gen_stub_pyclass)] -#[pyclass(module = "chronopt.sampler", name = "NestedSamples")] +#[pyclass(module = "diffid.sampler", name = "NestedSamples")] #[derive(Clone)] pub struct PyNestedSamples { pub(crate) inner: CoreNestedSamples, @@ -394,7 +394,7 @@ impl NestedSamplesIterator { /// Basic Metropolis-Hastings sampler binding mirroring the optimiser API. #[cfg_attr(feature = "stubgen", gen_stub_pyclass)] -#[pyclass(module = "chronopt.sampler", name = "MetropolisHastings")] +#[pyclass(module = "diffid.sampler", name = "MetropolisHastings")] #[derive(Clone)] pub struct PyMetropolisHastings { pub(crate) inner: CoreMetropolisHastings, @@ -457,11 +457,11 @@ impl PyMetropolisHastings { /// /// Examples /// -------- - /// >>> sampler = chronopt.MetropolisHastings().with_num_chains(4) + /// >>> sampler = diffid.MetropolisHastings().with_num_chains(4) /// >>> state = sampler.init(initial=[1.0, 2.0]) /// >>> while True: /// ... result = state.ask() - /// ... if isinstance(result, chronopt.Done): + /// ... if isinstance(result, diffid.Done): /// ... break /// ... values = [evaluate(pt) for pt in result.points] /// ... state.tell(values) @@ -482,7 +482,7 @@ impl PyMetropolisHastings { /// Dynamic nested sampler binding exposing DNS configuration knobs. #[cfg_attr(feature = "stubgen", gen_stub_pyclass)] -#[pyclass(module = "chronopt.sampler", name = "DynamicNestedSampler")] +#[pyclass(module = "diffid.sampler", name = "DynamicNestedSampler")] #[derive(Clone)] pub struct PyDynamicNestedSampler { pub(crate) inner: CoreDynamicNestedSampler, @@ -553,11 +553,11 @@ impl PyDynamicNestedSampler { /// /// Examples /// -------- - /// >>> sampler = chronopt.DynamicNestedSampler() + /// >>> sampler = diffid.DynamicNestedSampler() /// >>> state = sampler.init(initial=[1.0, 2.0]) /// >>> while True: /// ... result = state.ask() - /// ... if isinstance(result, chronopt.Done): + /// ... if isinstance(result, diffid.Done): /// ... break /// ... values = [evaluate(pt) for pt in result.points] /// ... state.tell(values) @@ -583,17 +583,17 @@ impl PyDynamicNestedSampler { /// /// Examples /// -------- -/// >>> sampler = chronopt.MetropolisHastings().with_num_chains(4) +/// >>> sampler = diffid.MetropolisHastings().with_num_chains(4) /// >>> state = sampler.init(initial=[1.0, 2.0]) /// >>> while True: /// ... result = state.ask() -/// ... if isinstance(result, chronopt.Done): +/// ... if isinstance(result, diffid.Done): /// ... print(f"Sampling complete: {result.result}") /// ... break /// ... values = [negative_log_likelihood(pt) for pt in result.points] /// ... state.tell(values) #[cfg_attr(feature = "stubgen", gen_stub_pyclass)] -#[pyclass(module = "chronopt.sampler", name = "MetropolisHastingsState")] +#[pyclass(module = "diffid.sampler", name = "MetropolisHastingsState")] pub struct PyMetropolisHastingsState { inner: CoreMetropolisHastingsState, } @@ -685,17 +685,17 @@ impl PyMetropolisHastingsState { /// /// Examples /// -------- -/// >>> sampler = chronopt.DynamicNestedSampler() +/// >>> sampler = diffid.DynamicNestedSampler() /// >>> state = sampler.init(initial=[1.0, 2.0]) /// >>> while True: /// ... result = state.ask() -/// ... if isinstance(result, chronopt.Done): +/// ... if isinstance(result, diffid.Done): /// ... print(f"Sampling complete: {result.result}") /// ... break /// ... values = [negative_log_likelihood(pt) for pt in result.points] /// ... state.tell(values) #[cfg_attr(feature = "stubgen", gen_stub_pyclass)] -#[pyclass(module = "chronopt.sampler", name = "DynamicNestedSamplerState")] +#[pyclass(module = "diffid.sampler", name = "DynamicNestedSamplerState")] pub struct PyDynamicNestedSamplerState { inner: CoreDynamicNestedSamplerState, } diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 9b890ec..c27fb3a 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,12 +1,12 @@ [package] -name = "chronopt" +name = "diffid" version.workspace = true edition.workspace = true license.workspace = true description = "Time-series optimisation and inference toolkit with differential-system support" -repository = "https://github.com/BradyPlanden/chronopt" -homepage = "https://github.com/BradyPlanden/chronopt" -documentation = "https://docs.rs/chronopt" +repository = "https://github.com/BradyPlanden/diffid" +homepage = "https://github.com/BradyPlanden/diffid" +documentation = "https://docs.rs/diffid" readme = "../README.md" keywords = ["optimisation", "time-series", "differential-equations", "sampler", "inference"] categories = ["algorithms", "science", "simulation"] diff --git a/rust/benches/diffsol_benches.rs b/rust/benches/diffsol_benches.rs index bb1a20d..7a8b94f 100644 --- a/rust/benches/diffsol_benches.rs +++ b/rust/benches/diffsol_benches.rs @@ -1,6 +1,6 @@ -use chronopt::builders::DiffsolBackend; -use chronopt::prelude::*; use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; +use diffid::builders::DiffsolBackend; +use diffid::prelude::*; use nalgebra::DMatrix; use std::time::Duration; diff --git a/rust/benches/optimisers_benches.rs b/rust/benches/optimisers_benches.rs index 1f4e56e..03375e3 100644 --- a/rust/benches/optimisers_benches.rs +++ b/rust/benches/optimisers_benches.rs @@ -1,6 +1,6 @@ -use chronopt::common::Bounds; -use chronopt::prelude::*; use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use diffid::common::Bounds; +use diffid::prelude::*; use std::time::Duration; fn bench_nelder_mead_quadratic(c: &mut Criterion) { diff --git a/rust/benches/samplers_benches.rs b/rust/benches/samplers_benches.rs index fd30d3a..8d74010 100644 --- a/rust/benches/samplers_benches.rs +++ b/rust/benches/samplers_benches.rs @@ -1,5 +1,5 @@ -use chronopt::prelude::*; use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use diffid::prelude::*; use std::time::Duration; fn bench_metropolis_hastings_gaussian(c: &mut Criterion) { diff --git a/rust/src/common.rs b/rust/src/common.rs index 5346843..2c45560 100644 --- a/rust/src/common.rs +++ b/rust/src/common.rs @@ -32,7 +32,7 @@ pub struct Unbounded; /// # Examples /// /// ``` -/// use chronopt::common::Bounds; +/// use diffid::common::Bounds; /// /// // Create bounds for a two-dimensional parameter-space /// let bounds = Bounds::new(vec![(0.0, 1.0), (-5.0, 5.0)]); @@ -56,7 +56,7 @@ impl Bounds { /// # Examples /// /// ``` - /// use chronopt::common::Bounds; + /// use diffid::common::Bounds; /// /// let bounds = Bounds::new(vec![(0.0, 1.0), (-10.0, 10.0)]); /// ``` @@ -69,7 +69,7 @@ impl Bounds { /// # Examples /// /// ``` - /// use chronopt::common::Bounds; + /// use diffid::common::Bounds; /// /// let bounds = Bounds::new(vec![(0.0, 1.0), (-5.0, 5.0), (0.0, 100.0)]); /// assert_eq!(bounds.dimension(), 3); @@ -89,7 +89,7 @@ impl Bounds { /// /// # Examples /// ``` - /// use chronopt::common::Bounds; + /// use diffid::common::Bounds; /// /// let initial = [0.0]; /// let bounds = Bounds::unbounded_like(&initial); @@ -123,7 +123,7 @@ impl Bounds { /// # Examples /// /// ``` - /// use chronopt::common::Bounds; + /// use diffid::common::Bounds; /// /// let bounds = Bounds::new(vec![(0.0, 1.0), (-5.0, 5.0)]); /// let mut pos = vec![1.5, -10.0]; @@ -156,7 +156,7 @@ impl Bounds { /// # Examples /// /// ``` - /// use chronopt::common::Bounds; + /// use diffid::common::Bounds; /// use rand::SeedableRng; /// use rand::prelude::StdRng; /// @@ -205,7 +205,7 @@ impl From> for Bounds { /// # Examples /// /// ``` - /// use chronopt::common::Bounds; + /// use diffid::common::Bounds; /// /// let bounds: Bounds = vec![(0.0, 1.0), (-5.0, 5.0)].into(); /// assert_eq!(bounds.dimension(), 2); diff --git a/rust/src/lib.rs b/rust/src/lib.rs index d6ede1d..e57bdc5 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -8,7 +8,7 @@ pub mod problem; pub mod sampler; mod types; -// Convenience re-exports so users can `use chronopt::prelude::*;` +// Convenience re-exports so users can `use diffid::prelude::*;` pub mod prelude { pub use crate::builders::{ DiffsolConfig, DiffsolProblemBuilder, ScalarProblemBuilder, VectorProblemBuilder, diff --git a/rust/src/optimisers/mod.rs b/rust/src/optimisers/mod.rs index d712461..8d81fc4 100644 --- a/rust/src/optimisers/mod.rs +++ b/rust/src/optimisers/mod.rs @@ -52,8 +52,8 @@ impl ScalarOptimiser { /// /// # Example /// ``` - /// use chronopt::common::Bounds; - /// use chronopt::optimisers::{ScalarOptimiser, NelderMead}; + /// use diffid::common::Bounds; + /// use diffid::optimisers::{ScalarOptimiser, NelderMead}; /// /// let optimiser = ScalarOptimiser::from(NelderMead::new()); /// let result = optimiser.run( @@ -86,8 +86,8 @@ impl ScalarOptimiser { /// /// # Example /// ``` - /// use chronopt::common::Bounds; - /// use chronopt::optimisers::{ScalarOptimiser, NelderMead}; + /// use diffid::common::Bounds; + /// use diffid::optimisers::{ScalarOptimiser, NelderMead}; /// /// let optimiser = ScalarOptimiser::from(NelderMead::new()); /// let result = optimiser.run_batch( @@ -137,8 +137,8 @@ impl GradientOptimiser { /// /// # Example /// ``` - /// use chronopt::common::Bounds; - /// use chronopt::optimisers::{GradientOptimiser, Adam}; + /// use diffid::common::Bounds; + /// use diffid::optimisers::{GradientOptimiser, Adam}; /// /// let optimiser = GradientOptimiser::from(Adam::new()); /// let result = optimiser.run( diff --git a/rust/tests/diffsol_optimisation.rs b/rust/tests/diffsol_optimisation.rs index 300ed33..6003559 100644 --- a/rust/tests/diffsol_optimisation.rs +++ b/rust/tests/diffsol_optimisation.rs @@ -1,4 +1,4 @@ -use chronopt::prelude::*; +use diffid::prelude::*; use nalgebra::DMatrix; #[test] diff --git a/rust/tests/dynamic_nested.rs b/rust/tests/dynamic_nested.rs index 0b977f5..e584042 100644 --- a/rust/tests/dynamic_nested.rs +++ b/rust/tests/dynamic_nested.rs @@ -1,5 +1,5 @@ -use chronopt::builders::DiffsolBackend; -use chronopt::prelude::*; +use diffid::builders::DiffsolBackend; +use diffid::prelude::*; use nalgebra::DMatrix; #[test] diff --git a/rust/tests/dynamic_nested_advanced.rs b/rust/tests/dynamic_nested_advanced.rs index 7de0c13..d130439 100644 --- a/rust/tests/dynamic_nested_advanced.rs +++ b/rust/tests/dynamic_nested_advanced.rs @@ -1,5 +1,5 @@ -use chronopt::prelude::*; -use chronopt::problem::ParameterRange; +use diffid::prelude::*; +use diffid::problem::ParameterRange; /// Test evidence calculation against known analytical result. /// For a Gaussian N(x | 0, σ²) with uniform prior U(a, b), diff --git a/tests/integration/test_diffsol_dynamic_nested_parallel.py b/tests/integration/test_diffsol_dynamic_nested_parallel.py index 56a7543..cf5d431 100644 --- a/tests/integration/test_diffsol_dynamic_nested_parallel.py +++ b/tests/integration/test_diffsol_dynamic_nested_parallel.py @@ -1,4 +1,4 @@ -import chronopt as chron +import diffid import numpy as np @@ -16,10 +16,10 @@ def _logistic_data(n: int = 40) -> np.ndarray: return np.column_stack((t_span, y)) -def _build_diffsol_problem(parallel: bool) -> chron.Problem: +def _build_diffsol_problem(parallel: bool) -> diffid.Problem: data = _logistic_data(40) builder = ( - chron.DiffsolBuilder() + diffid.DiffsolBuilder() .with_diffsl(_logistic_dsl()) .with_data(data) .with_parameter("r", 1.0, bounds=(0.1, 3.0)) @@ -38,7 +38,7 @@ def test_dynamic_nested_diffsol_parallel_vs_sequential(): initial = [1.0, 1.0] sampler = ( - chron.DynamicNestedSampler() + diffid.DynamicNestedSampler() .with_live_points(32) .with_expansion_factor(0.3) .with_termination_tolerance(1e-3) @@ -66,14 +66,14 @@ def test_dynamic_nested_diffsol_parallel_vs_sequential(): def test_dynamic_nested_sampler_parallel_fallback_for_non_parallel_problems(): # Scalar problem does not support parallel evaluation; sampler should still work problem = ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_objective(lambda x: 0.5 * (x[0] - 0.5) ** 2) .with_parameter("x", 0.5, bounds=(-5.0, 5.0)) .build() ) sampler = ( - chron.DynamicNestedSampler() + diffid.DynamicNestedSampler() .with_live_points(24) .with_expansion_factor(0.2) .with_termination_tolerance(1e-3) diff --git a/tests/integration/test_diffsol_sampling.py b/tests/integration/test_diffsol_sampling.py index 84c550c..00fad37 100644 --- a/tests/integration/test_diffsol_sampling.py +++ b/tests/integration/test_diffsol_sampling.py @@ -1,4 +1,4 @@ -import chronopt as chron +import diffid import numpy as np @@ -27,12 +27,12 @@ def test_diffsol_sampling_tracks_bouncy_ball_parameters(): data = np.column_stack((t_span, height, velocity)) builder = ( - chron.DiffsolBuilder() + diffid.DiffsolBuilder() .with_diffsl(dsl) .with_data(data) .with_parameter("g", g_true) .with_parameter("h", h_true) - .with_cost(chron.SSE()) + .with_cost(diffid.SSE()) ) problem = builder.build() @@ -41,7 +41,7 @@ def test_diffsol_sampling_tracks_bouncy_ball_parameters(): initial_cost = problem.evaluate(initial_guess) sampler = ( - chron.MetropolisHastings() + diffid.MetropolisHastings() .with_num_chains(2) .with_iterations(250) .with_step_size(0.25) @@ -84,19 +84,19 @@ def test_diffsol_dynamic_nested_sampler_produces_evidence(): data = np.column_stack((t_span, height, velocity)) builder = ( - chron.DiffsolBuilder() + diffid.DiffsolBuilder() .with_diffsl(dsl) .with_data(data) .with_parameter("g", g_true) .with_parameter("h", h_true) .with_tolerances(rtol=1e-6, atol=1e-6) - .with_cost(chron.SSE()) + .with_cost(diffid.SSE()) ) problem = builder.build() sampler = ( - chron.DynamicNestedSampler() + diffid.DynamicNestedSampler() .with_live_points(64) .with_expansion_factor(0.15) .with_termination_tolerance(1e-3) diff --git a/tests/integration/test_mathematical_suite.py b/tests/integration/test_mathematical_suite.py index a61b21e..515d924 100644 --- a/tests/integration/test_mathematical_suite.py +++ b/tests/integration/test_mathematical_suite.py @@ -4,7 +4,7 @@ from functools import partial -import chronopt as chron +import diffid import numpy as np import pytest @@ -59,9 +59,9 @@ def ridge(x: list[float], alpha: float = 1.0) -> np.ndarray: return np.asarray([value], dtype=float) -def make_nelder_mead() -> chron.NelderMead: +def make_nelder_mead() -> diffid.NelderMead: return ( - chron.NelderMead() + diffid.NelderMead() .with_max_iter(800) .with_step_size(0.2) .with_threshold(1e-8) @@ -69,9 +69,9 @@ def make_nelder_mead() -> chron.NelderMead: ) -def make_cmaes() -> chron.CMAES: +def make_cmaes() -> diffid.CMAES: return ( - chron.CMAES() + diffid.CMAES() .with_max_iter(1500) .with_threshold(1e-8) .with_step_size(0.8) @@ -161,7 +161,7 @@ def test_python_objectives_converge( ): """Ensure optimisation reaches known minima for several analytic functions.""" - builder = chron.ScalarBuilder().with_objective(objective) + builder = diffid.ScalarBuilder().with_objective(objective) for idx in range(dimension): builder = builder.with_parameter(f"x{idx}", 1.0) @@ -196,7 +196,7 @@ def test_diffsol_logistic_convergence(): stacked_data = np.column_stack((time_points, data)) builder = ( - chron.DiffsolBuilder() + diffid.DiffsolBuilder() .with_diffsl(_LOGISTIC_DSL) .with_data(stacked_data) .with_tolerances(rtol=1e-6, atol=1e-6) @@ -207,7 +207,7 @@ def test_diffsol_logistic_convergence(): problem = builder.build() optimiser = ( - chron.NelderMead() + diffid.NelderMead() .with_max_iter(400) .with_threshold(1e-7) .with_position_tolerance(1e-6) diff --git a/tests/test_docs.py b/tests/test_docs.py index 9c770e0..3ac02b0 100644 --- a/tests/test_docs.py +++ b/tests/test_docs.py @@ -1,15 +1,12 @@ -"""Documentation quality and coverage tests. +"""Documentation quality tests. -This module validates that: -1. All public APIs are documented -2. Documentation builds without errors -3. All internal links resolve -4. Code examples in docs are valid +Validates that: +1. Documentation builds without errors +2. Notebooks are valid and well-structured """ -import inspect +import json import pathlib -import re import subprocess import pytest @@ -23,16 +20,14 @@ class TestDocumentationBuild: """Test that documentation builds successfully.""" - def test_mkdocs_config_exists(self): - """Check that mkdocs.yml exists.""" - assert MKDOCS_YML.exists(), "mkdocs.yml not found" - - def test_docs_directory_exists(self): - """Check that docs/ directory exists.""" - assert DOCS_DIR.exists(), "docs/ directory not found" - def test_mkdocs_build_succeeds(self): - """Test that mkdocs builds without errors.""" + """Test that mkdocs builds without errors. + + mkdocs strict mode validates: + - All internal links resolve + - No broken references + - Proper navigation structure + """ result = subprocess.run( ["mkdocs", "build", "--strict"], cwd=ROOT, @@ -40,268 +35,34 @@ def test_mkdocs_build_succeeds(self): text=True, ) - # Check return code assert result.returncode == 0, ( - f"mkdocs build failed with:\nSTDOUT:\n{result.stdout}\n\nSTDERR:\n{result.stderr}" - ) - - -class TestAPIDocumentation: - """Test that all public APIs are documented.""" - - def _get_public_apis(self, module) -> set[str]: - """Extract public API names from a module.""" - apis = set() - - for name, obj in inspect.getmembers(module): - # Skip private members - if name.startswith("_"): - continue - - # Include classes, functions, and constants - if ( - inspect.isclass(obj) - or inspect.isfunction(obj) - or isinstance(obj, (int, float, str)) - ): - apis.add(name) - - return apis - - def _find_documented_apis(self, doc_path: pathlib.Path) -> set[str]: - """Extract API names mentioned in documentation.""" - if not doc_path.exists(): - return set() - - content = doc_path.read_text() - - # Find patterns like `ClassName`, `function_name()`, etc. - # This is a simple heuristic - could be improved - patterns = [ - r"`(\w+)`", # Inline code - r"##\s+(\w+)", # Headers - r"class:\s+(\w+)", # Class references - ] - - documented = set() - for pattern in patterns: - matches = re.findall(pattern, content) - documented.update(matches) - - return documented - - @pytest.mark.skipif( - not (ROOT / "python" / "src" / "chronopt").exists(), - reason="chronopt package not installed", - ) - def test_core_apis_documented(self): - """Check that core chronopt APIs are documented.""" - try: - import chronopt as chron - except ImportError: - pytest.skip("chronopt not installed") - - # Get public APIs from chronopt - self._get_public_apis(chron) - - # Find documentation files - api_docs = list((DOCS_DIR / "api-reference" / "python").rglob("*.md")) - - # Collect documented APIs - documented_apis = set() - for doc_file in api_docs: - documented_apis.update(self._find_documented_apis(doc_file)) - - # Core APIs that should be documented - expected_apis = { - "ScalarBuilder", - "DiffsolBuilder", - "VectorBuilder", - "NelderMead", - "CMAES", - "Adam", - "SSE", - "RMSE", - "GaussianNLL", - } - - # Check coverage - missing = expected_apis - documented_apis - - if missing: - print(f"\nāš ļø APIs missing from documentation: {missing}") - print(f"Documented APIs: {documented_apis}") - - # We expect at least 80% coverage - coverage = len(expected_apis - missing) / len(expected_apis) - assert coverage >= 0.8, ( - f"API documentation coverage too low: {coverage:.0%}\nMissing: {missing}" + f"mkdocs build failed:\n{result.stdout}\n{result.stderr}" ) -class TestNotebookQuality: - """Test Jupyter notebook quality.""" - - def test_all_notebooks_exist(self): - """Check that all referenced notebooks exist.""" - # Parse mkdocs.yml for notebook references - content = MKDOCS_YML.read_text() - - # Find .ipynb references - notebook_refs = re.findall(r"tutorials/notebooks/(\d+_\w+\.ipynb)", content) - - # Check each exists - for notebook_name in notebook_refs: - notebook_path = DOCS_DIR / "tutorials" / "notebooks" / notebook_name - assert notebook_path.exists(), f"Notebook not found: {notebook_path}" - - def test_notebooks_have_metadata(self): - """Check that notebooks have proper metadata.""" - import json +class TestNotebookStructure: + """Test that notebooks are valid and well-structured.""" + def test_all_notebooks_are_valid_json(self): + """Verify all notebooks are valid JSON with proper structure.""" notebooks = list((DOCS_DIR / "tutorials" / "notebooks").glob("*.ipynb")) - for notebook_path in notebooks: - if notebook_path.name == "utils.py": # Skip utility file - continue + assert len(notebooks) > 0, "No notebooks found" - with open(notebook_path) as f: + for notebook_path in notebooks: + with open(notebook_path, encoding="utf-8") as f: nb_data = json.load(f) - # Check structure + # Validate basic notebook structure assert "cells" in nb_data, f"{notebook_path.name} missing cells" assert "metadata" in nb_data, f"{notebook_path.name} missing metadata" - - # Check that it has markdown cells (documentation) - has_markdown = any( - cell.get("cell_type") == "markdown" for cell in nb_data["cells"] - ) - assert has_markdown, f"{notebook_path.name} has no markdown cells" - - def test_notebooks_have_objectives(self): - """Check that notebooks start with objectives.""" - import json - - notebooks = list((DOCS_DIR / "tutorials" / "notebooks").glob("[0-9]*.ipynb")) - - for notebook_path in notebooks: - with open(notebook_path) as f: - nb_data = json.load(f) - - # First cell should be markdown with objectives - first_cell = nb_data["cells"][0] - assert first_cell["cell_type"] == "markdown", ( - f"{notebook_path.name} first cell is not markdown" - ) - - # Check for objectives - content = "".join(first_cell["source"]) - has_objectives = "Objectives" in content or "objectives" in content - assert has_objectives, ( - f"{notebook_path.name} missing objectives in first cell" - ) - - -class TestDocumentationStructure: - """Test documentation structure and organization.""" - - def test_required_sections_exist(self): - """Check that required documentation sections exist.""" - required = [ - "getting-started", - "tutorials", - "guides", - "algorithms", - "api-reference", - "examples", - "development", - ] - - for section in required: - section_path = DOCS_DIR / section - assert section_path.exists(), f"Required section missing: {section}" - assert section_path.is_dir(), f"Section is not a directory: {section}" - - def test_index_pages_exist(self): - """Check that all sections have index pages.""" - sections = [ - "getting-started", - "tutorials", - "guides", - "algorithms", - "api-reference", - "development", - ] - - for section in sections: - index_path = DOCS_DIR / section / "index.md" - assert index_path.exists(), f"Missing index page: {section}/index.md" - - def test_navigation_completeness(self): - """Check that mkdocs.yml nav includes all main sections.""" - content = MKDOCS_YML.read_text() - - required_nav = [ - "Getting Started", - "Tutorials", - "User Guides", - "Algorithms", - "API Reference", - "Examples", - "Development", - ] - - for section in required_nav: - assert section in content, f"Navigation missing section: {section}" - - -class TestInternalLinks: - """Test that internal links are valid.""" - - def _extract_md_links(self, content: str) -> list[str]: - """Extract markdown links from content.""" - # Match [text](path) but not [text](http://...) - pattern = r"\[([^\]]+)\]\((?!http)([^)]+)\)" - matches = re.findall(pattern, content) - return [path for _, path in matches] - - def test_getting_started_links(self): - """Test links in getting started guides.""" - getting_started = DOCS_DIR / "getting-started" - - for md_file in getting_started.glob("*.md"): - content = md_file.read_text() - links = self._extract_md_links(content) - - for link in links: - # Resolve relative link - if link.startswith("#"): # Anchor link - continue - - if link.startswith("../../"): - # Relative to docs root - target = DOCS_DIR / link.replace("../../", "") - elif link.startswith("../"): - # Relative to parent - target = getting_started.parent / link.replace("../", "") - else: - target = getting_started / link - - # Check if target exists (handle .md vs .html) - if not target.exists() and target.suffix == "": - target = target.with_suffix(".md") - - assert target.exists(), ( - f"Broken link in {md_file.name}: {link} -> {target}" - ) + assert len(nb_data["cells"]) > 0, f"{notebook_path.name} has no cells" def test_readme_not_in_docs(): """Ensure README doesn't conflict with index.md.""" readme = DOCS_DIR / "README.md" - assert not readme.exists(), ( - "docs/README.md conflicts with index.md (causes mkdocs warnings)" - ) + assert not readme.exists(), "docs/README.md conflicts with index.md" if __name__ == "__main__": diff --git a/tests/unit/optimisers/test_adam.py b/tests/unit/optimisers/test_adam.py index dabe27a..c937133 100644 --- a/tests/unit/optimisers/test_adam.py +++ b/tests/unit/optimisers/test_adam.py @@ -1,4 +1,4 @@ -import chronopt as chron +import diffid import numpy as np @@ -16,7 +16,7 @@ def quadratic_grad(x): def build_quadratic_problem_with_gradient(): return ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_objective(quadratic) .with_gradient(quadratic_grad) .with_parameter("x", 1.0) @@ -28,7 +28,9 @@ def build_quadratic_problem_with_gradient(): def test_adam_direct_run_minimises_quadratic(): problem = build_quadratic_problem_with_gradient() - optimiser = chron.Adam().with_step_size(0.1).with_max_iter(500).with_threshold(1e-8) + optimiser = ( + diffid.Adam().with_step_size(0.1).with_max_iter(500).with_threshold(1e-8) + ) result = optimiser.run(problem, [5.0, -4.0]) @@ -39,14 +41,16 @@ def test_adam_direct_run_minimises_quadratic(): def test_python_builder_optimise_with_adam_default(): builder = ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_objective(quadratic) .with_gradient(quadratic_grad) .with_parameter("x", 1.0) .with_parameter("y", 1.0) ) - optimiser = chron.Adam().with_step_size(0.1).with_max_iter(400).with_threshold(1e-8) + optimiser = ( + diffid.Adam().with_step_size(0.1).with_max_iter(400).with_threshold(1e-8) + ) builder.with_optimiser(optimiser) problem = builder.build() @@ -60,7 +64,7 @@ def test_python_builder_optimise_with_adam_default(): def test_adam_falls_back_with_numerical_grad(): builder = ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_objective(quadratic) .with_parameter("x", 0.0) .with_parameter("y", 0.0) @@ -68,7 +72,7 @@ def test_adam_falls_back_with_numerical_grad(): problem = builder.build() - optimiser = chron.Adam().with_max_iter(10) + optimiser = diffid.Adam().with_max_iter(10) result = optimiser.run(problem, [0.0, 0.0]) assert result.iterations == 10 diff --git a/tests/unit/optimisers/test_cmaes.py b/tests/unit/optimisers/test_cmaes.py index 0fe839a..1f0baa2 100644 --- a/tests/unit/optimisers/test_cmaes.py +++ b/tests/unit/optimisers/test_cmaes.py @@ -1,4 +1,4 @@ -import chronopt as chron +import diffid import numpy as np import pytest @@ -10,7 +10,7 @@ def rosenbrock(x): def build_rosenbrock_problem(): return ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_objective(rosenbrock) .with_parameter("x", 1.0) .with_parameter("y", 1.0) @@ -22,7 +22,7 @@ def test_cmaes_direct_run_minimises_rosenbrock(): problem = build_rosenbrock_problem() optimiser = ( - chron.CMAES() + diffid.CMAES() .with_max_iter(400) .with_threshold(1e-8) .with_step_size(0.6) @@ -38,14 +38,14 @@ def test_cmaes_direct_run_minimises_rosenbrock(): def test_python_builder_optimise_with_cmaes_default(): builder = ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_objective(rosenbrock) .with_parameter("x", 1.0) .with_parameter("y", 1.0) ) optimiser = ( - chron.CMAES() + diffid.CMAES() .with_max_iter(300) .with_threshold(1e-8) .with_step_size(0.5) @@ -63,7 +63,7 @@ def test_python_builder_optimise_with_cmaes_default(): def test_set_optimiser_rejects_unknown_type(): - builder = chron.ScalarBuilder() + builder = diffid.ScalarBuilder() with pytest.raises(TypeError): builder.with_optimiser(object()) @@ -72,7 +72,7 @@ def test_set_optimiser_rejects_unknown_type(): def test_cmaes_result_covariance_available(): problem = build_rosenbrock_problem() - optimiser = chron.CMAES().with_max_iter(50).with_seed(123) + optimiser = diffid.CMAES().with_max_iter(50).with_seed(123) result = optimiser.run(problem, [1.5, -1.5]) diff --git a/tests/unit/test_diffsol.py b/tests/unit/test_diffsol.py index 5ed2c83..404162d 100644 --- a/tests/unit/test_diffsol.py +++ b/tests/unit/test_diffsol.py @@ -1,6 +1,6 @@ import copy -import chronopt as chron +import diffid import numpy as np import pytest @@ -22,14 +22,14 @@ def test_diffsol_builder(): # Build the problem builder = ( - chron.DiffsolBuilder() + diffid.DiffsolBuilder() .with_diffsl(ds) .with_data(stacked_data) .with_tolerances(rtol=1e-6, atol=1e-6) .with_parameter("r", 1.0, None) .with_parameter("k", 1.0, None) - .with_cost(chron.SSE()) - .with_cost(chron.RMSE()) + .with_cost(diffid.SSE()) + .with_cost(diffid.RMSE()) ) problem = builder.build() @@ -44,7 +44,7 @@ def test_diffsol_builder(): # Test that we can optimise the problem optimiser = ( - chron.NelderMead().with_max_iter(500).with_threshold(1e-7).with_patience(10) + diffid.NelderMead().with_max_iter(500).with_threshold(1e-7).with_patience(10) ) result = optimiser.run(problem, x0) assert result.success @@ -62,10 +62,10 @@ def test_diffsol_builder_remove_methods(): data = t_span**2 stacked_data = np.column_stack((t_span, data)) - metric = chron.RMSE() + metric = diffid.RMSE() builder = ( - chron.DiffsolBuilder() + diffid.DiffsolBuilder() .with_diffsl(ds) .with_data(stacked_data) .with_parameter("a", 1.0, None) @@ -94,7 +94,7 @@ def test_diffsol_builder_remove_methods(): # Change cost builder = builder.remove_cost() - builder = builder.with_cost(chron.SSE()) + builder = builder.with_cost(diffid.SSE()) problem_5 = builder.build() # Check that problems are different @@ -120,7 +120,7 @@ def test_problem_optimise_defaults_to_builder_params(): stacked_data = np.column_stack((t_span, data)) problem = ( - chron.DiffsolBuilder() + diffid.DiffsolBuilder() .with_diffsl(ds) .with_tolerances(rtol=1e-4, atol=1e-4) .with_data(stacked_data) @@ -128,7 +128,7 @@ def test_problem_optimise_defaults_to_builder_params(): .build() ) - optimiser = chron.NelderMead().with_max_iter(0) + optimiser = diffid.NelderMead().with_max_iter(0) result = problem.optimise(optimiser=optimiser) @@ -154,7 +154,7 @@ def test_diffsol_cost_metrics(variance: float) -> None: def build_problem(cost_metric=None): builder = ( - chron.DiffsolBuilder() + diffid.DiffsolBuilder() .with_diffsl(ds) .with_data(stacked_data) .with_tolerances(rtol=1e-6, atol=1e-6) @@ -166,9 +166,9 @@ def build_problem(cost_metric=None): return builder.build() sse_problem = build_problem() - sse_problem_explicit = build_problem(chron.SSE()) - rmse_problem = build_problem(chron.RMSE()) - gaussian_problem = build_problem(chron.GaussianNLL(variance)) + sse_problem_explicit = build_problem(diffid.SSE()) + rmse_problem = build_problem(diffid.RMSE()) + gaussian_problem = build_problem(diffid.GaussianNLL(variance)) test_params = [0.8, 1.2] sse_cost = sse_problem.evaluate(test_params) @@ -189,7 +189,7 @@ def build_problem(cost_metric=None): assert pytest.approx(expected_gaussian, rel=1e-6, abs=1e-9) == gaussian_cost with pytest.raises(ValueError): - chron.GaussianNLL(0.0) + diffid.GaussianNLL(0.0) def test_diffsol_bicycle_model_neldermead_recovers_wheelbase() -> None: @@ -217,7 +217,7 @@ def test_diffsol_bicycle_model_neldermead_recovers_wheelbase() -> None: stacked_data = np.column_stack((t_span, y_true, psi_true)) builder = ( - chron.DiffsolBuilder() + diffid.DiffsolBuilder() .with_diffsl(ds) .with_data(stacked_data) .with_tolerances(rtol=1e-6, atol=1e-8) @@ -227,7 +227,7 @@ def test_diffsol_bicycle_model_neldermead_recovers_wheelbase() -> None: problem = builder.build() optimiser = ( - chron.NelderMead() + diffid.NelderMead() .with_max_iter(500) .with_threshold(1e-10) .with_position_tolerance(1e-8) diff --git a/tests/unit/test_dynamic_nested_sampler.py b/tests/unit/test_dynamic_nested_sampler.py index e79739e..50d4819 100644 --- a/tests/unit/test_dynamic_nested_sampler.py +++ b/tests/unit/test_dynamic_nested_sampler.py @@ -4,7 +4,7 @@ import math -import chronopt as chron +import diffid import numpy as np import pytest from scipy.stats import norm @@ -36,14 +36,14 @@ def gaussian_nll(x: list[float]) -> float: return 0.5 * (diff / sigma) ** 2 + np.log(sigma) + 0.5 * np.log(2 * np.pi) problem = ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_objective(gaussian_nll) .with_parameter("x", mu, bounds=(prior_lower, prior_upper)) .build() ) sampler = ( - chron.DynamicNestedSampler() + diffid.DynamicNestedSampler() .with_live_points(128) .with_expansion_factor(0.2) .with_termination_tolerance(1e-5) @@ -84,14 +84,14 @@ def exponential_nll(x: list[float]) -> float: return lambda_param * x[0] problem = ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_objective(exponential_nll) .with_parameter("x", 1.0, bounds=(0.0, x_max)) .build() ) sampler = ( - chron.DynamicNestedSampler() + diffid.DynamicNestedSampler() .with_live_points(128) .with_expansion_factor(0.01) .with_termination_tolerance(1e-6) @@ -123,14 +123,14 @@ def bimodal_nll(x: list[float]) -> float: return -(log_sum - np.log(2) - np.log(sigma) - 0.5 * np.log(2 * np.pi)) problem = ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_objective(bimodal_nll) .with_parameter("x", 0.0, bounds=(-10.0, 10.0)) .build() ) sampler = ( - chron.DynamicNestedSampler() + diffid.DynamicNestedSampler() .with_live_points(128) .with_expansion_factor(0.2) .with_termination_tolerance(1e-4) @@ -163,14 +163,14 @@ def pathological_nll(x: list[float]) -> float: return 0.5 * x[0] ** 2 problem = ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_objective(pathological_nll) .with_parameter("x", 0.0, bounds=(-5.0, 5.0)) .build() ) sampler = ( - chron.DynamicNestedSampler() + diffid.DynamicNestedSampler() .with_live_points(32) .with_expansion_factor(0.2) .with_seed(456) @@ -195,7 +195,7 @@ def high_dim_quadratic(x: list[float]) -> float: """Simple quadratic in high dimensions.""" return 0.5 * sum(xi**2 for xi in x) - problem = chron.ScalarBuilder().with_objective(high_dim_quadratic) + problem = diffid.ScalarBuilder().with_objective(high_dim_quadratic) for i in range(dimension): problem = problem.with_parameter(f"x{i}", 0.0, bounds=(-3.0, 3.0)) @@ -203,7 +203,7 @@ def high_dim_quadratic(x: list[float]) -> float: problem = problem.build() sampler = ( - chron.DynamicNestedSampler() + diffid.DynamicNestedSampler() .with_live_points(128) .with_expansion_factor(0.15) .with_termination_tolerance(1e-4) @@ -227,14 +227,14 @@ def sharp_peak(x: list[float]) -> float: return 0.5 * (x[0] / sigma) ** 2 problem = ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_objective(sharp_peak) .with_parameter("x", 0.0, bounds=(-1.0, 1.0)) .build() ) sampler = ( - chron.DynamicNestedSampler() + diffid.DynamicNestedSampler() .with_live_points(64) .with_expansion_factor(0.05) # Small expansion for narrow peak .with_termination_tolerance(1e-3) @@ -257,14 +257,14 @@ def large_offset(x: list[float]) -> float: return 100.0 + 0.5 * x[0] ** 2 problem = ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_objective(large_offset) .with_parameter("x", 0.0, bounds=(-5.0, 5.0)) .build() ) sampler = ( - chron.DynamicNestedSampler() + diffid.DynamicNestedSampler() .with_live_points(64) .with_expansion_factor(0.2) .with_seed(111) @@ -285,14 +285,14 @@ def simple_quadratic(x: list[float]) -> float: return 0.5 * x[0] ** 2 problem = ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_objective(simple_quadratic) .with_parameter("x", 0.0, bounds=(-5.0, 5.0)) .build() ) sampler = ( - chron.DynamicNestedSampler() + diffid.DynamicNestedSampler() .with_live_points(128) .with_expansion_factor(0.2) .with_seed(555) @@ -320,14 +320,14 @@ def quadratic(x: list[float]) -> float: return 0.5 * (x[0] - 1.0) ** 2 problem = ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_objective(quadratic) .with_parameter("x", 1.0, bounds=(-3.0, 5.0)) .build() ) sampler = ( - chron.DynamicNestedSampler() + diffid.DynamicNestedSampler() .with_live_points(128) .with_expansion_factor(0.2) .with_seed(777) @@ -359,14 +359,14 @@ def narrow_gaussian(x: list[float]) -> float: return 0.5 * (x[0] / sigma) ** 2 + np.log(sigma) + 0.5 * np.log(2 * np.pi) problem = ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_objective(narrow_gaussian) .with_parameter("x", 0.0, bounds=(-5.0, 5.0)) .build() ) sampler = ( - chron.DynamicNestedSampler() + diffid.DynamicNestedSampler() .with_live_points(64) .with_expansion_factor(0.1) .with_termination_tolerance(1e-3) @@ -388,21 +388,21 @@ def simple_problem(x: list[float]) -> float: return 0.5 * x[0] ** 2 problem = ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_objective(simple_problem) .with_parameter("x", 0.0, bounds=(-5.0, 5.0)) .build() ) sampler1 = ( - chron.DynamicNestedSampler() + diffid.DynamicNestedSampler() .with_live_points(64) .with_expansion_factor(0.2) .with_seed(12345) ) sampler2 = ( - chron.DynamicNestedSampler() + diffid.DynamicNestedSampler() .with_live_points(64) .with_expansion_factor(0.2) .with_seed(12345) @@ -430,14 +430,14 @@ def multimodal(x: list[float]) -> float: ) problem = ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_objective(multimodal) .with_parameter("x", 0.0, bounds=(-5.0, 5.0)) .build() ) sampler = ( - chron.DynamicNestedSampler() + diffid.DynamicNestedSampler() .with_live_points(32) .with_expansion_factor(0.5) # Allow significant expansion .with_seed(999) @@ -458,14 +458,14 @@ def bounded_quadratic(x: list[float]) -> float: return 0.5 * x[0] ** 2 problem = ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_objective(bounded_quadratic) .with_parameter("x", 0.0, bounds=(lower, upper)) .build() ) sampler = ( - chron.DynamicNestedSampler() + diffid.DynamicNestedSampler() .with_live_points(64) .with_expansion_factor(0.2) .with_seed(444) @@ -486,14 +486,14 @@ def simple_problem(x: list[float]) -> float: return 0.5 * x[0] ** 2 problem = ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_objective(simple_problem) .with_parameter("x", 0.0, bounds=(-5.0, 5.0)) .build() ) sampler = ( - chron.DynamicNestedSampler() + diffid.DynamicNestedSampler() .with_live_points(64) .with_expansion_factor(0.2) .with_seed(666) @@ -513,7 +513,7 @@ def nll(x: list[float]) -> float: return 0.5 * (x[0] / sigma) ** 2 + np.log(sigma) + 0.5 * np.log(2 * np.pi) return ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_objective(nll) .with_parameter("x", 0.0, bounds=(-10.0, 10.0)) .build() @@ -521,7 +521,7 @@ def nll(x: list[float]) -> float: def sampler_config(seed): return ( - chron.DynamicNestedSampler() + diffid.DynamicNestedSampler() .with_live_points(128) .with_expansion_factor(0.2) .with_seed(seed) diff --git a/tests/unit/test_optimisation.py b/tests/unit/test_optimisation.py index fe4ec1f..f857506 100644 --- a/tests/unit/test_optimisation.py +++ b/tests/unit/test_optimisation.py @@ -1,10 +1,10 @@ -import chronopt as chron +import diffid import numpy as np def test_builder_exposes_config_and_parameters(): builder = ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_objective(lambda x: np.asarray([float(x[0]) ** 2])) .with_parameter("x", 3.5, bounds=(0.0, 10.0)) ) @@ -30,7 +30,7 @@ def bounded_quadratic(x): def test_python_builder_rosenbrock(): builder = ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_objective(rosenbrock) .with_parameter("x", 1.2, None) .with_parameter("y", -1.2, None) @@ -39,7 +39,7 @@ def test_python_builder_rosenbrock(): # Create the optimisation optimiser = ( - chron.NelderMead().with_max_iter(500).with_threshold(1e-6).with_step_size(0.15) + diffid.NelderMead().with_max_iter(500).with_threshold(1e-6).with_step_size(0.15) ) results = optimiser.run(problem, [1.5, -1.5]) @@ -51,14 +51,14 @@ def test_python_builder_rosenbrock(): def test_python_builder_bounds_respected(): builder = ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_objective(bounded_quadratic) .with_parameter("x", 0.0, bounds=(0.0, 1.0)) .with_parameter("y", 0.0, bounds=(0.0, 2.0)) ) problem = builder.build() - optimiser = chron.NelderMead().with_max_iter(200).with_threshold(1e-8) + optimiser = diffid.NelderMead().with_max_iter(200).with_threshold(1e-8) results = optimiser.run(problem, [0.5, 1.0]) assert results.success diff --git a/tests/unit/test_optimisation_api.py b/tests/unit/test_optimisation_api.py index eb9e279..9f7db58 100644 --- a/tests/unit/test_optimisation_api.py +++ b/tests/unit/test_optimisation_api.py @@ -1,4 +1,4 @@ -import chronopt as chron +import diffid import numpy as np @@ -19,7 +19,7 @@ def _test_optimisation_api(): # Build the problem builder = ( - chron.DiffsolBuilder() + diffid.DiffsolBuilder() .with_diffsl(ds) .with_data(stacked_data) .with_config({"rtol": 1e-6}) @@ -47,7 +47,7 @@ def test_diffsol_builder_allows_multiple_builds(): stacked_data = np.column_stack((t_span, data)) builder = ( - chron.DiffsolBuilder() + diffid.DiffsolBuilder() .with_diffsl(ds) .with_data(stacked_data) .with_parameter("r", 1.0) @@ -70,7 +70,7 @@ def rosenbrock(x): return (1 - x[0]) ** 2 + 100 * (x[1] - x[0] ** 2) ** 2 builder = ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_objective(rosenbrock) .with_parameter("x", 1.0) .with_parameter("y", 1.0) @@ -92,7 +92,7 @@ def exponential_model(params): return y0 * np.exp(rate * t_span) builder = ( - chron.VectorBuilder() + diffid.VectorBuilder() .with_objective(exponential_model) .with_data(data) .with_parameter("rate", 1.0) @@ -110,17 +110,17 @@ def test_all_builders_support_copy(): import copy # ScalarBuilder - scalar_builder = chron.ScalarBuilder().with_objective(lambda x: x[0] ** 2) + scalar_builder = diffid.ScalarBuilder().with_objective(lambda x: x[0] ** 2) scalar_copy = copy.copy(scalar_builder) copy.deepcopy(scalar_builder) # Test deepcopy # DiffsolBuilder - diffsol_builder = chron.DiffsolBuilder().with_diffsl("in { a }") + diffsol_builder = diffid.DiffsolBuilder().with_diffsl("in { a }") diffsol_copy = copy.copy(diffsol_builder) copy.deepcopy(diffsol_builder) # Test deepcopy # VectorBuilder - vector_builder = chron.VectorBuilder().with_objective(lambda x: [x[0]]) + vector_builder = diffid.VectorBuilder().with_objective(lambda x: [x[0]]) vector_copy = copy.copy(vector_builder) copy.deepcopy(vector_builder) # Test deepcopy diff --git a/tests/unit/test_python_autodiff.py b/tests/unit/test_python_autodiff.py index 9b6e129..4e516cb 100644 --- a/tests/unit/test_python_autodiff.py +++ b/tests/unit/test_python_autodiff.py @@ -1,4 +1,4 @@ -import chronopt as chron +import diffid import numpy as np import pytest @@ -26,7 +26,7 @@ def _quadratic_gradient(x): def quadratic_problem(): """Creates a 3D quadratic optimization problem using ScalarBuilder""" return ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_objective(_quadratic_objective) .with_gradient(_quadratic_gradient) .with_parameter("x1", 1.0) diff --git a/tests/unit/test_samplers.py b/tests/unit/test_samplers.py index 43f846c..dba5fca 100644 --- a/tests/unit/test_samplers.py +++ b/tests/unit/test_samplers.py @@ -1,6 +1,6 @@ import math -import chronopt as chron +import diffid import numpy as np import pytest @@ -12,14 +12,14 @@ def quadratic_potential(x: np.ndarray) -> np.ndarray: def test_metropolis_hastings_runs_and_returns_samples(): problem = ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_objective(quadratic_potential) .with_parameter("x", 1.0) .build() ) sampler = ( - chron.MetropolisHastings() + diffid.MetropolisHastings() .with_num_chains(3) .with_iterations(400) .with_step_size(0.4) @@ -44,14 +44,14 @@ def test_metropolis_hastings_runs_and_returns_samples(): def test_dynamic_nested_sampler_runs_on_scalar_problem(): problem = ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_objective(quadratic_potential) .with_parameter("x", 0.5) .build() ) sampler = ( - chron.DynamicNestedSampler() + diffid.DynamicNestedSampler() .with_live_points(32) .with_expansion_factor(0.1) .with_seed(99) @@ -67,20 +67,20 @@ def test_dynamic_nested_sampler_runs_on_scalar_problem(): def test_dynamic_nested_invalid_live_points_are_clamped(): problem = ( - chron.ScalarBuilder() + diffid.ScalarBuilder() .with_objective(quadratic_potential) .with_parameter("x", 1.0) .build() ) - sampler = chron.DynamicNestedSampler().with_live_points(1) + sampler = diffid.DynamicNestedSampler().with_live_points(1) nested = sampler.run(problem) assert nested.draws >= 0 def test_dynamic_nested_requires_problem_instance(): - sampler = chron.DynamicNestedSampler() + sampler = diffid.DynamicNestedSampler() with pytest.raises(TypeError): sampler.run(object()) # type: ignore[arg-type] diff --git a/tests/unit/test_vector.py b/tests/unit/test_vector.py index 8629500..c49298e 100644 --- a/tests/unit/test_vector.py +++ b/tests/unit/test_vector.py @@ -1,4 +1,4 @@ -import chronopt as chron +import diffid import numpy as np import pytest @@ -17,12 +17,12 @@ def exponential_model(params): # Build the problem builder = ( - chron.VectorBuilder() + diffid.VectorBuilder() .with_objective(exponential_model) .with_data(data) .with_parameter("rate", 1.0, None) .with_parameter("y0", 1.0, None) - .with_cost(chron.SSE()) + .with_cost(diffid.SSE()) ) problem = builder.build() @@ -35,7 +35,7 @@ def exponential_model(params): assert cost >= 0, f"Cost should be non-negative, got {cost}" # Test optimization - optimiser = chron.NelderMead().with_max_iter(1000).with_threshold(1e-8) + optimiser = diffid.NelderMead().with_max_iter(1000).with_threshold(1e-8) result = problem.optimise(x0, optimiser) assert result.success @@ -55,7 +55,7 @@ def model(params): return params[0] * np.ones(n_points) + params[1] builder = ( - chron.VectorBuilder() + diffid.VectorBuilder() .with_objective(model) .with_data(data) .with_parameter("scale", 1.0) @@ -80,18 +80,18 @@ def sinusoid(params): return amp * np.sin(freq * t + phase) problem = ( - chron.VectorBuilder() + diffid.VectorBuilder() .with_objective(sinusoid) .with_data(data) .with_parameter("amplitude", 2.0, (0.0, 5.0)) .with_parameter("frequency", 1.0, (0.1, 3.0)) .with_parameter("phase", 0.0, (-np.pi, np.pi)) - .with_cost(chron.RMSE()) + .with_cost(diffid.RMSE()) .build() ) x0 = [2.0, 1.0, 0.0] - optimiser = chron.NelderMead().with_max_iter(2000).with_threshold(1e-9) + optimiser = diffid.NelderMead().with_max_iter(2000).with_threshold(1e-9) result = problem.optimise(x0, optimiser) # Note: Sinusoidal fitting can be challenging due to local minima @@ -114,7 +114,7 @@ def quadratic(params): def build_problem(cost_metric=None): builder = ( - chron.VectorBuilder() + diffid.VectorBuilder() .with_objective(quadratic) .with_data(data) .with_parameter("scale", 1.5) @@ -124,9 +124,9 @@ def build_problem(cost_metric=None): return builder.build() sse_problem = build_problem() - sse_problem_explicit = build_problem(chron.SSE()) - rmse_problem = build_problem(chron.RMSE()) - gaussian_problem = build_problem(chron.GaussianNLL(1.0)) + sse_problem_explicit = build_problem(diffid.SSE()) + rmse_problem = build_problem(diffid.RMSE()) + gaussian_problem = build_problem(diffid.GaussianNLL(1.0)) test_params = [1.5] sse_cost = sse_problem.evaluate(test_params) @@ -157,21 +157,21 @@ def model(params): # Build with SSE builder1 = ( - chron.VectorBuilder() + diffid.VectorBuilder() .with_objective(model) .with_data(data) .with_parameter("a", 1.0) - .with_cost(chron.SSE()) + .with_cost(diffid.SSE()) ) problem1 = builder1.build() # Build with RMSE builder2 = ( - chron.VectorBuilder() + diffid.VectorBuilder() .with_objective(model) .with_data(data) .with_parameter("a", 1.0) - .with_cost(chron.RMSE()) + .with_cost(diffid.RMSE()) ) problem2 = builder2.build() @@ -188,10 +188,10 @@ def test_vector_builder_with_default_optimiser(): def linear(params): return params[0] * np.arange(4) + params[1] - optimiser = chron.NelderMead().with_max_iter(100) + optimiser = diffid.NelderMead().with_max_iter(100) problem = ( - chron.VectorBuilder() + diffid.VectorBuilder() .with_objective(linear) .with_data(data) .with_parameter("slope", 0.5) @@ -214,7 +214,7 @@ def wrong_size(params): return params[0] * np.ones(5) # Wrong size! problem = ( - chron.VectorBuilder() + diffid.VectorBuilder() .with_objective(wrong_size) .with_data(data) .with_parameter("a", 1.0) @@ -222,7 +222,7 @@ def wrong_size(params): ) with pytest.raises( - chron.errors.EvaluationError, + diffid.errors.EvaluationError, match="Evaluation failed: Evaluation failed:: expected 3 elements, got 5", ): problem.evaluate([1.0]) @@ -237,16 +237,16 @@ def model(params): # Build two problems with same configuration builder = ( - chron.VectorBuilder() + diffid.VectorBuilder() .with_objective(model) .with_data(data) .with_parameter("scale", 1.0) - .with_cost(chron.RMSE()) + .with_cost(diffid.RMSE()) ) problem1 = builder.build() builder.remove_cost() - builder.with_cost(chron.SSE()) + builder.with_cost(diffid.SSE()) problem2 = builder.build() # Should produce same results @@ -261,7 +261,7 @@ def model(params): return params[0] * data problem = ( - chron.VectorBuilder() + diffid.VectorBuilder() .with_objective(model) .with_data(data) .with_parameter("a", 1.0) diff --git a/uv.lock b/uv.lock index 5b9b837..de4eaaf 100644 --- a/uv.lock +++ b/uv.lock @@ -259,86 +259,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, ] -[[package]] -name = "chronopt" -source = { editable = "." } -dependencies = [ - { name = "numpy" }, -] - -[package.optional-dependencies] -diffeqpy = [ - { name = "diffeqpy" }, -] -jax = [ - { name = "diffrax" }, - { name = "jax", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, - { name = "jaxlib", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, -] -plotting = [ - { name = "matplotlib" }, -] - -[package.dev-dependencies] -dev = [ - { name = "pytest" }, - { name = "scipy" }, -] -docs = [ - { name = "matplotlib" }, - { name = "mkdocs" }, - { name = "mkdocs-git-revision-date-localized-plugin" }, - { name = "mkdocs-jupyter" }, - { name = "mkdocs-material" }, - { name = "mkdocs-minify-plugin" }, - { name = "mkdocstrings", extra = ["python"] }, - { name = "nbconvert" }, - { name = "pymdown-extensions" }, -] -examples = [ - { name = "diffeqpy" }, - { name = "diffrax", marker = "(python_full_version < '3.15' and platform_machine != 'x86_64') or (python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.15' and sys_platform != 'darwin')" }, - { name = "jax", marker = "python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'darwin'" }, - { name = "jaxlib", marker = "python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'darwin'" }, - { name = "matplotlib" }, -] - -[package.metadata] -requires-dist = [ - { name = "diffeqpy", marker = "extra == 'diffeqpy'" }, - { name = "diffrax", marker = "extra == 'jax'", specifier = ">=0.7.0" }, - { name = "jax", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin' and extra == 'jax'", specifier = "==0.4.38" }, - { name = "jaxlib", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin' and extra == 'jax'", specifier = "==0.4.38" }, - { name = "matplotlib", marker = "extra == 'plotting'", specifier = ">=3.8" }, - { name = "numpy", specifier = ">=1.24.4" }, -] -provides-extras = ["diffeqpy", "jax", "plotting"] - -[package.metadata.requires-dev] -dev = [ - { name = "pytest", specifier = ">=8.3.5" }, - { name = "scipy", specifier = ">=1.16.2" }, -] -docs = [ - { name = "matplotlib", specifier = ">=3.8" }, - { name = "mkdocs", specifier = ">=1.5.0" }, - { name = "mkdocs-git-revision-date-localized-plugin", specifier = ">=1.2.0" }, - { name = "mkdocs-jupyter", specifier = ">=0.24.0" }, - { name = "mkdocs-material", specifier = ">=9.5.0" }, - { name = "mkdocs-minify-plugin", specifier = ">=0.8.0" }, - { name = "mkdocstrings", extras = ["python"], specifier = ">=0.24.0" }, - { name = "nbconvert", specifier = ">=7.0" }, - { name = "pymdown-extensions", specifier = ">=10.7" }, -] -examples = [ - { name = "diffeqpy" }, - { name = "diffrax", marker = "python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'darwin'", specifier = ">=0.7.0" }, - { name = "diffrax", marker = "(python_full_version < '3.15' and platform_machine != 'x86_64') or (python_full_version < '3.15' and sys_platform != 'darwin')", specifier = ">=0.7.0" }, - { name = "jax", marker = "python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'darwin'", specifier = "==0.4.38" }, - { name = "jaxlib", marker = "python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'darwin'", specifier = "==0.4.38" }, - { name = "matplotlib", specifier = ">=3.8" }, -] - [[package]] name = "click" version = "8.3.0" @@ -522,6 +442,86 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/5b/3866770a8e01811e767bd1b560c39f65dc4b57f74e70a2865526f7177999/diffeqpy-2.5.4-py3-none-any.whl", hash = "sha256:8214d747dfacfb5fc49690e0a3b8182c14d82e1b609d045086c036a1bceb9370", size = 17342, upload-time = "2025-09-08T01:33:37.032Z" }, ] +[[package]] +name = "diffid" +source = { editable = "." } +dependencies = [ + { name = "numpy" }, +] + +[package.optional-dependencies] +diffeqpy = [ + { name = "diffeqpy" }, +] +jax = [ + { name = "diffrax" }, + { name = "jax", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "jaxlib", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, +] +plotting = [ + { name = "matplotlib" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "scipy" }, +] +docs = [ + { name = "matplotlib" }, + { name = "mkdocs" }, + { name = "mkdocs-git-revision-date-localized-plugin" }, + { name = "mkdocs-jupyter" }, + { name = "mkdocs-material" }, + { name = "mkdocs-minify-plugin" }, + { name = "mkdocstrings", extra = ["python"] }, + { name = "nbconvert" }, + { name = "pymdown-extensions" }, +] +examples = [ + { name = "diffeqpy" }, + { name = "diffrax", marker = "(python_full_version < '3.15' and platform_machine != 'x86_64') or (python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.15' and sys_platform != 'darwin')" }, + { name = "jax", marker = "python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "jaxlib", marker = "python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "matplotlib" }, +] + +[package.metadata] +requires-dist = [ + { name = "diffeqpy", marker = "extra == 'diffeqpy'" }, + { name = "diffrax", marker = "extra == 'jax'", specifier = ">=0.7.0" }, + { name = "jax", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin' and extra == 'jax'", specifier = "==0.4.38" }, + { name = "jaxlib", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin' and extra == 'jax'", specifier = "==0.4.38" }, + { name = "matplotlib", marker = "extra == 'plotting'", specifier = ">=3.8" }, + { name = "numpy", specifier = ">=1.24.4" }, +] +provides-extras = ["diffeqpy", "jax", "plotting"] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=8.3.5" }, + { name = "scipy", specifier = ">=1.16.2" }, +] +docs = [ + { name = "matplotlib", specifier = ">=3.8" }, + { name = "mkdocs", specifier = ">=1.5.0" }, + { name = "mkdocs-git-revision-date-localized-plugin", specifier = ">=1.2.0" }, + { name = "mkdocs-jupyter", specifier = ">=0.24.0" }, + { name = "mkdocs-material", specifier = ">=9.5.0" }, + { name = "mkdocs-minify-plugin", specifier = ">=0.8.0" }, + { name = "mkdocstrings", extras = ["python"], specifier = ">=0.24.0" }, + { name = "nbconvert", specifier = ">=7.0" }, + { name = "pymdown-extensions", specifier = ">=10.7" }, +] +examples = [ + { name = "diffeqpy" }, + { name = "diffrax", marker = "python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'darwin'", specifier = ">=0.7.0" }, + { name = "diffrax", marker = "(python_full_version < '3.15' and platform_machine != 'x86_64') or (python_full_version < '3.15' and sys_platform != 'darwin')", specifier = ">=0.7.0" }, + { name = "jax", marker = "python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'darwin'", specifier = "==0.4.38" }, + { name = "jaxlib", marker = "python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'darwin'", specifier = "==0.4.38" }, + { name = "matplotlib", specifier = ">=3.8" }, +] + [[package]] name = "diffrax" version = "0.7.0"