├── .all-contributorsrc ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── feature_request.yml ├── PULL_REQUEST_TEMPLATE.md ├── release_workflow.md └── workflows │ ├── lychee_links.yaml │ ├── nightly_dependency_tests.yaml │ ├── periodic_benchmarks.yaml │ ├── release_action.yaml │ ├── scheduled_tests.yaml │ └── test_on_pull_request.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── CHANGELOG.md ├── CITATION.cff ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── assets ├── BayOpt_Arch.pdf ├── BayesOpt_Arch.svg ├── PyBOP-high-level.svg ├── PyBOP_Architecture.drawio ├── UKRI.png ├── UKRI.svg ├── faraday-logo.jpg ├── logo-farger.pdf ├── logo-farger.svg ├── logo │ ├── PyBOP_logo_flat.png │ ├── PyBOP_logo_flat.svg │ ├── PyBOP_logo_flat_inverse.png │ ├── PyBOP_logo_flat_inverse.svg │ ├── PyBOP_logo_inverse.png │ ├── PyBOP_logo_inverse.svg │ ├── PyBOP_logo_mark.png │ ├── PyBOP_logo_mark.svg │ ├── PyBOP_logo_mark_circle.png │ ├── PyBOP_logo_mark_circle.svg │ ├── PyBOP_logo_mark_mono.png │ ├── PyBOP_logo_mark_mono.svg │ ├── PyBOP_logo_mark_mono_inverse.png │ ├── PyBOP_logo_mark_mono_inverse.svg │ ├── PyBOP_logo_mono.png │ ├── PyBOP_logo_mono.svg │ ├── PyBOP_logo_mono_inverse.png │ └── PyBOP_logo_mono_inverse.svg ├── pybop_architecture.pdf ├── pybop_architecture.png ├── pybop_architecture.svg └── roadmap_logo.png ├── asv.conf.json ├── benchmarks ├── README.md ├── __init__.py ├── benchmark_model.py ├── benchmark_optim_construction.py ├── benchmark_parameterisation.py ├── benchmark_track_parameterisation.py └── benchmark_utils.py ├── conftest.py ├── docs ├── Contributing.md ├── Makefile ├── _extension │ └── gallery_directive.py ├── _static │ ├── custom-icon.js │ └── switcher.json ├── _templates │ └── autoapi │ │ └── index.rst ├── conf.py ├── index.md ├── installation.rst ├── make.bat └── quick_start.rst ├── examples ├── README.md ├── data │ ├── LG_M50_ECM │ │ └── data │ │ │ ├── LGM50_5Ah_OCV.mat │ │ │ ├── LGM50_5Ah_Pulse.mat │ │ │ └── LGM50_5Ah_RateTest.mat │ ├── README.md │ ├── Samsung_INR21700 │ │ ├── multipulse_hppc.xlsx │ │ ├── sample_drive_cycle.xlsx │ │ └── sample_hppc_pulse.xlsx │ ├── Tesla_4680 │ │ ├── 601-828_Capacity_03_MB_CB1_subset.txt │ │ ├── README.md │ │ └── T-cell_pOCV_data.txt │ └── synthetic │ │ ├── README.md │ │ ├── dfn_charge_discharge_75.csv │ │ ├── dfn_pulse_15.csv │ │ ├── dfn_pulse_50.csv │ │ ├── discharge_charge_data_gen.py │ │ ├── pulse_data_gen.py │ │ ├── spm_charge_discharge_75.csv │ │ ├── spm_pulse_15.csv │ │ ├── spm_pulse_50.csv │ │ ├── spme_charge_discharge_75.csv │ │ ├── spme_pulse_15.csv │ │ └── spme_pulse_50.csv ├── notebooks │ ├── battery_parameterisation │ │ ├── ecm_trust-constr.ipynb │ │ ├── electrode_balancing.ipynb │ │ ├── equivalent_circuit_identification.ipynb │ │ ├── equivalent_circuit_identification_hppc.ipynb │ │ ├── equivalent_circuit_identification_multipulse.ipynb │ │ ├── monte_carlo_ecm_identification.ipynb │ │ ├── multi_model_identification.ipynb │ │ ├── pouch_cell_identification.ipynb │ │ └── single_pulse_circuit_model.ipynb │ ├── comparison_examples │ │ ├── comparing_cost_functions.ipynb │ │ ├── multi_optimiser_identification.ipynb │ │ ├── optimiser_calibration.ipynb │ │ └── solver_selection.ipynb │ ├── design_optimisation │ │ └── energy_based_electrode_design.ipynb │ └── getting_started │ │ ├── adamw_identification.ipynb │ │ ├── cost_compute_methods.ipynb │ │ ├── creating_a_model.ipynb │ │ ├── maximum_a_posteriori.ipynb │ │ ├── optimiser_interface.ipynb │ │ └── transformation_introduction.ipynb ├── parameters │ ├── example_BPX.json │ └── initial_ecm_parameters.json ├── scripts │ ├── battery_parameterisation │ │ ├── ecm_tau_constraints.py │ │ ├── full_cell_balancing.py │ │ ├── gitt_fitting.py │ │ ├── gitt_pulse.py │ │ ├── ocp_averaging.py │ │ ├── simple_ecm.py │ │ ├── simple_eis.py │ │ ├── simple_pulse_fit.py │ │ ├── stoichiometry_fitting.py │ │ └── tau_reparameterised_ecm.py │ ├── comparison_examples │ │ ├── adamw.py │ │ ├── covariance_matrix_adaptation.py │ │ ├── cuckoo.py │ │ ├── exponential_decay.py │ │ ├── exponential_natural_evolution.py │ │ ├── gitt_models.py │ │ ├── gradient_descent.py │ │ ├── grouped_SPMe.py │ │ ├── irpropmin.py │ │ ├── jaxified_idaklu_benchmarks.py │ │ ├── maximum_a_posteriori.py │ │ ├── maximum_likelihood.py │ │ ├── nelder_mead.py │ │ ├── particle_swarm.py │ │ ├── random_search.py │ │ ├── scipy_minimize.py │ │ ├── selecting_a_solver.py │ │ ├── simulated_annealing.py │ │ ├── stochastic_natural_evolution.py │ │ └── unscented_kalman_filter.py │ ├── design_optimisation │ │ ├── maximising_energy.py │ │ └── maximising_power.py │ └── getting_started │ │ ├── ask-tell-interface.py │ │ ├── functional_parameters.py │ │ ├── jax-solver-example.py │ │ ├── linked_parameters.py │ │ ├── mcmc_example.py │ │ ├── multi_fitting.py │ │ ├── multi_start_optimisation.py │ │ ├── simple_BPX.py │ │ ├── simple_dfn.py │ │ └── weighted_cost.py └── standalone │ ├── cost.py │ ├── model.py │ ├── optimiser.py │ └── problem.py ├── noxfile.py ├── papers └── Hallemans et al │ ├── Data │ ├── LGM50LT │ │ ├── LG_M50LT_Uneg.csv │ │ ├── LG_M50LT_Upos.csv │ │ ├── Notes on LG M50LT OCP.pdf │ │ ├── OCP_LGM50LT.mat │ │ └── impedanceLGM50LT_Hybrid_4h.mat │ ├── Z_SPMegrouped_SOC_chen2020.mat │ └── timeDomainSimulation_SPMegrouped.mat │ ├── Fig11_tauSensitivity.py │ ├── Fig12_EIS_Fitting_Simulation.py │ ├── Fig2_timeDomainSimulation.py │ ├── Fig4_comparisonBruteForceFreqDomain.py │ ├── Fig5_impedance_models.py │ ├── Fig7_impedance_SOC_SPMegrouped.py │ ├── Fig8_groupedParamSensitivity.py │ ├── README.md │ └── timeDomain_Fitting_Simulation.py ├── pybop ├── __init__.py ├── _classification.py ├── _dataset.py ├── _evaluation.py ├── _experiment.py ├── _utils.py ├── _version.py ├── applications │ ├── __init__.py │ ├── base_method.py │ ├── gitt_methods.py │ └── ocp_methods.py ├── costs │ ├── __init__.py │ ├── _likelihoods.py │ ├── _weighted_cost.py │ ├── base_cost.py │ ├── design_costs.py │ ├── error_measures.py │ └── fitting_costs.py ├── experimental │ └── jax_costs.py ├── models │ ├── __init__.py │ ├── _exponential_decay.py │ ├── base_model.py │ ├── empirical │ │ ├── __init__.py │ │ ├── base_ecm.py │ │ └── ecm.py │ └── lithium_ion │ │ ├── __init__.py │ │ ├── base_echem.py │ │ ├── basic_SPMe.py │ │ ├── basic_SP_diffusion.py │ │ ├── echem.py │ │ └── weppner_huggins.py ├── observers │ ├── __init__.py │ ├── observer.py │ └── unscented_kalman.py ├── optimisers │ ├── __init__.py │ ├── _adamw.py │ ├── _cost_interface.py │ ├── _cuckoo.py │ ├── _gradient_descent.py │ ├── _irprop_plus.py │ ├── _random_search.py │ ├── _result.py │ ├── _simulated_annealing.py │ ├── base_optimiser.py │ ├── base_pints_optimiser.py │ ├── optimisation.py │ ├── pints_optimisers.py │ └── scipy_optimisers.py ├── parameters │ ├── __init__.py │ ├── parameter.py │ ├── parameter_set.py │ └── priors.py ├── plot │ ├── __init__.py │ ├── contour.py │ ├── convergence.py │ ├── dataset.py │ ├── nyquist.py │ ├── parameters.py │ ├── plotly_manager.py │ ├── problem.py │ ├── standard_plots.py │ └── voronoi.py ├── problems │ ├── __init__.py │ ├── base_problem.py │ ├── design_problem.py │ ├── fitting_problem.py │ └── multi_fitting_problem.py ├── samplers │ ├── __init__.py │ ├── base_pints_sampler.py │ ├── base_sampler.py │ ├── chain_processor.py │ ├── mcmc_sampler.py │ ├── mcmc_summary.py │ └── pints_samplers.py └── transformation │ ├── base_transformation.py │ └── transformations.py ├── pyproject.toml ├── readthedocs.yaml ├── scripts └── ci │ └── build_matrix.sh ├── tests ├── docs │ └── test_docs.py ├── examples │ └── test_examples.py ├── integration │ ├── test_applications.py │ ├── test_classification.py │ ├── test_eis_parameterisation.py │ ├── test_half_cell_model.py │ ├── test_jax_parameterisations.py │ ├── test_model_experiment_changes.py │ ├── test_monte_carlo.py │ ├── test_monte_carlo_thevenin.py │ ├── test_observer_parameterisation.py │ ├── test_optimisation_options.py │ ├── test_spm_parameterisations.py │ ├── test_thevenin_parameterisation.py │ ├── test_transformation.py │ └── test_weighted_cost.py ├── plotting │ └── test_plotly_manager.py └── unit │ ├── experimental │ └── test_jax_costs.py │ ├── test_classifier.py │ ├── test_cost.py │ ├── test_cost_interface.py │ ├── test_dataset.py │ ├── test_experiment.py │ ├── test_import.py │ ├── test_likelihoods.py │ ├── test_models.py │ ├── test_observer_unscented_kalman.py │ ├── test_observers.py │ ├── test_optimisation.py │ ├── test_parameter_sets.py │ ├── test_parameters.py │ ├── test_plots.py │ ├── test_posterior.py │ ├── test_priors.py │ ├── test_problem.py │ ├── test_sampling.py │ ├── test_solvers.py │ ├── test_standalone.py │ └── test_transformations.py └── uv.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Hide the diff for this file unless prompted 5 | uv.lock linguist-generated=true 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Create a bug report 3 | title: "[Bug]: " 4 | labels: ["bug"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for filling out this report to help us improve! 10 | - type: input 11 | id: python-version 12 | attributes: 13 | label: Python Version 14 | description: What python version are you using? 15 | placeholder: python version 16 | validations: 17 | required: true 18 | - type: textarea 19 | id: bug-description 20 | attributes: 21 | label: Describe the bug 22 | description: A clear and concise description of the bug. 23 | validations: 24 | required: true 25 | - type: textarea 26 | id: reproduce 27 | attributes: 28 | label: Steps to reproduce the behaviour 29 | description: Tell us how to reproduce this behaviour. Please try to include a [Minimum Workable Example](https://stackoverflow.com/help/minimal-reproducible-example) 30 | validations: 31 | required: true 32 | - type: textarea 33 | id: logs 34 | attributes: 35 | label: Relevant log output 36 | description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. 37 | render: shell 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest a feature 3 | labels: ["enhancement"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for filling out this report to help us improve! 9 | - type: textarea 10 | id: feature 11 | attributes: 12 | label: Feature description 13 | description: Describe the feature you'd like. 14 | validations: 15 | required: true 16 | - type: textarea 17 | id: motivation 18 | attributes: 19 | label: Motivation 20 | description: Please enter the motivation for this feature i.e (problem, performance, etc). 21 | - type: textarea 22 | id: possible-implementation 23 | attributes: 24 | label: Possible implementation 25 | description: Please enter any possible implementation for this feature. 26 | - type: textarea 27 | id: context 28 | attributes: 29 | label: Additional context 30 | description: Add any additional context about the feature request. 31 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. 4 | 5 | ## Issue reference 6 | Fixes # (issue-number) 7 | 8 | ## Review 9 | Before you mark your PR as ready for review, please ensure that you've considered the following: 10 | - Updated the [CHANGELOG.md](https://github.com/pybop-team/PyBOP/blob/develop/CHANGELOG.md) in reverse chronological order (newest at the top) with a concise description of the changes, including the PR number. 11 | - Noted any breaking changes, including details on how it might impact existing functionality. 12 | 13 | ## Type of change 14 | - [ ] New Feature: A non-breaking change that adds new functionality. 15 | - [ ] Optimization: A code change that improves performance. 16 | - [ ] Examples: A change to existing or additional examples. 17 | - [ ] Bug Fix: A non-breaking change that addresses an issue. 18 | - [ ] Documentation: Updates to documentation or new documentation for new features. 19 | - [ ] Refactoring: Non-functional changes that improve the codebase. 20 | - [ ] Style: Non-functional changes related to code style (formatting, naming, etc). 21 | - [ ] Testing: Additional tests to improve coverage or confirm functionality. 22 | - [ ] Other: (Insert description of change) 23 | 24 | # Key checklist: 25 | 26 | - [ ] No style issues: `$ pre-commit run` (or `$ nox -s pre-commit`) (see [CONTRIBUTING.md](https://github.com/pybop-team/PyBOP/blob/develop/CONTRIBUTING.md#installing-and-using-pre-commit) for how to set this up to run automatically when committing locally, in just two lines of code) 27 | - [ ] All unit tests pass: `$ nox -s tests` 28 | - [ ] The documentation builds: `$ nox -s doctest` 29 | 30 | You can run integration tests, unit tests, and doctests together at once, using `$ nox -s quick`. 31 | 32 | ## Further checks: 33 | - [ ] Code is well-commented, especially in complex or unclear areas. 34 | - [ ] Added tests that prove my fix is effective or that my feature works. 35 | - [ ] Checked that coverage remains or improves, and added tests if necessary to maintain or increase coverage. 36 | 37 | Thank you for contributing to our project! Your efforts help us to deliver great software. 38 | -------------------------------------------------------------------------------- /.github/release_workflow.md: -------------------------------------------------------------------------------- 1 | # Release Workflow 2 | 3 | This document outlines the release workflow for publishing to PyPI and TestPyPI using GitHub Actions. 4 | 5 | ## Creating a New Release 6 | 7 | To create a new release, follow these steps: 8 | 9 | 1. **Prepare the Release:** 10 | - Create a new branch for the release (i.e. `24.XX`) from `develop`. 11 | - Increment the following: 12 | - The version number in the `pyproject.toml` and `CITATION.cff` files following CalVer versioning. 13 | - The `CHANGELOG.md` version with the changes for the new version. 14 | - Add a new entry for the documentation site version switcher located at `docs/_static/switcher.json`. 15 | - Open a PR to the `main` branch. Once the PR is merged, proceed to the next step. 16 | 17 | 2. **Create a GitHub Release:** 18 | - Go to the "Releases" section on GitHub. 19 | - Click "Draft a new release". 20 | - Select the `main` branch as the release target. 21 | - Create a release tag: 22 | - For a full release, use the following CalVer format: `vYY.M` (i.e. `v24.3`). 23 | - For a release candidate, use: `vYY.Mrc.X` (i.e. `v24.3rc.1`). 24 | - Note: GitHub provides the option to create this tag on publish if you start typing a new tag 25 | - Fill in the release title and description. Add any major changes and link to the `CHANGELOG.md` for a list of total changes. 26 | - If it's a pre-release (release candidate), check the "This is a pre-release" checkbox. 27 | - Click "Publish release" to create the release. 28 | 29 | 3. **Monitor the Workflow:** 30 | - Go to the "Actions" tab of the repository and monitor the workflow's progress. 31 | - The workflow will build the distribution packages and then publish them to PyPI or TestPyPI, depending on whether the release is a full release or a pre-release. 32 | 33 | 4. **Verify the Release:** 34 | - Check PyPI or TestPyPI to ensure that your package is available and has been updated to the new version. 35 | - Test installing the package using `pip` to ensure everything works as expected (run an example as confirmation). 36 | 37 | 5. **Merge Main into Develop** 38 | - Finally, open a PR from `main` to `develop` to synchronise the release changes. This ensures `develop` is always ahead of `main`, reducing the work required for future releases. 39 | -------------------------------------------------------------------------------- /.github/workflows/lychee_links.yaml: -------------------------------------------------------------------------------- 1 | # Lychee Link Checking 2 | 3 | name: Links 4 | on: 5 | workflow_dispatch: 6 | pull_request: 7 | push: 8 | branches: 9 | - main 10 | schedule: 11 | - cron: '0 6 * * 0' # Run weekly on Sundays at 06:00 UTC 12 | 13 | jobs: 14 | Lychee: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | 20 | - name: Restore lychee cache 21 | uses: actions/cache@v4 22 | with: 23 | path: .lycheecache 24 | key: cache-lychee-${{ github.sha }} 25 | restore-keys: cache-lychee- 26 | 27 | - name: Set up Lychee 28 | uses: lycheeverse/lychee-action@v1.10.0 29 | with: 30 | args: >- 31 | --cache 32 | --no-progress 33 | --max-cache-age 2d 34 | --timeout 10 35 | --max-retries 5 36 | --skip-missing 37 | --exclude-loopback 38 | --accept 200,403,429,999 39 | --exclude "https://tiles.stadiamaps.com/*|https://b.tile.openstreetmap.org/*" 40 | --exclude "https://cartodb-basemaps-c.global.ssl.fastly.net/*" 41 | --exclude "https://events.mapbox.com/*|https://events.mapbox.cn/*|https://api.mapbox.cn/*" 42 | --exclude "https://github.com/mikolalysenko/glsl-read-float/*" 43 | --exclude "https://fonts.openmaptiles.org/*" 44 | --exclude "https://a.tile.openstreetmap.org/*" 45 | --exclude "https://openstreetmap.org/*|https://www.openstreetmap.org/*" 46 | --exclude "https://cdn.plot.ly/*" 47 | --exclude "http://www.w3.org/*|https://www.w3.org/*" 48 | --exclude "https://doi.org/*" 49 | --exclude "https://raw.githubusercontent.com/paramm-team/pybamm-param/develop/pbparam/input/data/" 50 | --exclude-path ./CHANGELOG.md 51 | --exclude-path asv.conf.json 52 | --exclude-path docs/conf.py 53 | './**/*.rst' 54 | './**/*.md' 55 | './**/*.py' 56 | './**/*.ipynb' 57 | './**/*.json' 58 | './**/*.toml' 59 | fail: true 60 | jobSummary: true 61 | format: markdown 62 | -------------------------------------------------------------------------------- /.github/workflows/nightly_dependency_tests.yaml: -------------------------------------------------------------------------------- 1 | name: Nightly dependencies at develop 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "0 23 * * 1" 7 | 8 | concurrency: 9 | # github.workflow: name of the workflow, so that we don't cancel other workflows 10 | # github.run_id || github.event_name: either the unique identifier for the job or the event that triggered it 11 | group: ${{ github.workflow }}-${{ github.run_id || github.event_name }} 12 | # Cancel in-progress runs when a new workflow with the same group name is triggered 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | nightly_tests: 17 | runs-on: ${{ matrix.os }} 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | os: [ubuntu-latest, macos-14] 22 | python-version: ["3.12"] 23 | suite: ["unit", "integration", "examples"] 24 | 25 | name: Test-${{ matrix.os }}-py-${{ matrix.python-version }}-${{ matrix.suite }}) 26 | 27 | steps: 28 | - uses: actions/checkout@v4 29 | - name: Set up Python ${{ matrix.python-version }} 30 | uses: actions/setup-python@v4 31 | with: 32 | python-version: ${{ matrix.python-version }} 33 | - name: Install dependencies 34 | run: | 35 | python -m pip install -e .[all,dev] 36 | python -m pip uninstall -y pybamm 37 | python -m pip install "pybamm[all] @ git+https://github.com/pybamm-team/PyBaMM@develop" 38 | 39 | 40 | - name: Run ${{ matrix.suite }} tests 41 | run: | 42 | if [[ "${{ matrix.suite }}" == "unit" ]]; then 43 | python -m pytest --unit 44 | elif [[ "${{ matrix.suite }}" == "integration" ]]; then 45 | python -m pytest --integration 46 | elif [[ "${{ matrix.suite }}" == "examples" ]]; then 47 | python -m pytest --nbmake --examples 48 | fi 49 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | ci: 2 | autoupdate_commit_msg: "chore: update pre-commit hooks" 3 | autofix_commit_msg: "style: pre-commit fixes" 4 | 5 | repos: 6 | - repo: https://github.com/astral-sh/ruff-pre-commit 7 | rev: "v0.11.11" 8 | hooks: 9 | - id: ruff 10 | args: [--fix, --show-fixes] 11 | types_or: [python, pyi, jupyter] 12 | - id: ruff-format 13 | types_or: [python, pyi, jupyter] 14 | 15 | - repo: https://github.com/pre-commit/pre-commit-hooks 16 | rev: v5.0.0 17 | hooks: 18 | - id: check-added-large-files 19 | args: ['--maxkb=2000'] 20 | - id: check-case-conflict 21 | - id: check-merge-conflict 22 | - id: check-yaml 23 | - id: debug-statements 24 | - id: end-of-file-fixer 25 | - id: mixed-line-ending 26 | - id: trailing-whitespace 27 | 28 | - repo: https://github.com/pre-commit/pygrep-hooks 29 | rev: v1.10.0 30 | hooks: 31 | - id: python-check-blanket-type-ignore 32 | - id: rst-backticks 33 | - id: rst-directive-colons 34 | - id: rst-inline-touching-normal 35 | 36 | - repo: https://github.com/kynan/nbstripout 37 | rev: 0.8.1 38 | hooks: 39 | - id: nbstripout 40 | args: ['--keep-output', '--drop-empty-cells'] 41 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-20.04 5 | tools: 6 | python: "3.11" 7 | 8 | # Build documentation in the "docs/" directory with Sphinx 9 | sphinx: 10 | configuration: docs/conf.py 11 | 12 | formats: 13 | - htmlzip 14 | - pdf 15 | - epub 16 | 17 | python: 18 | install: 19 | - method: pip 20 | path: .[docs] 21 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | title: 'PyBOP: A Python package for battery model optimisation and parameterisation' 3 | message: >- 4 | If you use this software, please cite the article below. 5 | authors: 6 | - given-names: Brady 7 | family-names: Planden 8 | orcid: "https://orcid.org/0000-0002-1082-9125" 9 | - given-names: Nicola 10 | family-names: Courtier 11 | orcid: "https://orcid.org/0000-0002-5714-1096" 12 | - given-names: Martin 13 | family-names: Robinson 14 | orcid: "https://orcid.org/0000-0002-1572-6782" 15 | - given-names: Agriya 16 | family-names: Khetarpal 17 | orcid: "https://orcid.org/0000-0002-1112-1786" 18 | - given-names: Ferran 19 | family-names: Brosa Planella 20 | orcid: "https://orcid.org/0000-0001-6363-2812" 21 | - given-names: David 22 | family-names: Howey 23 | orcid: "https://orcid.org/0000-0002-0620-3955" 24 | 25 | keywords: 26 | - "python" 27 | - "battery models" 28 | - "parameter inference" 29 | - "optimization" 30 | 31 | journal: "arXiv" 32 | date-released: 2024-12-20 33 | doi: 10.48550/arXiv.2412.15859 34 | version: "25.3" # Update this alongside new releases 35 | repository-code: 'https://www.github.com/pybop-team/pybop' 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2023, pybop-team 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /assets/BayOpt_Arch.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/assets/BayOpt_Arch.pdf -------------------------------------------------------------------------------- /assets/UKRI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/assets/UKRI.png -------------------------------------------------------------------------------- /assets/UKRI.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/faraday-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/assets/faraday-logo.jpg -------------------------------------------------------------------------------- /assets/logo-farger.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/assets/logo-farger.pdf -------------------------------------------------------------------------------- /assets/logo/PyBOP_logo_flat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/assets/logo/PyBOP_logo_flat.png -------------------------------------------------------------------------------- /assets/logo/PyBOP_logo_flat.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/logo/PyBOP_logo_flat_inverse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/assets/logo/PyBOP_logo_flat_inverse.png -------------------------------------------------------------------------------- /assets/logo/PyBOP_logo_flat_inverse.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/logo/PyBOP_logo_inverse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/assets/logo/PyBOP_logo_inverse.png -------------------------------------------------------------------------------- /assets/logo/PyBOP_logo_inverse.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/logo/PyBOP_logo_mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/assets/logo/PyBOP_logo_mark.png -------------------------------------------------------------------------------- /assets/logo/PyBOP_logo_mark.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/logo/PyBOP_logo_mark_circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/assets/logo/PyBOP_logo_mark_circle.png -------------------------------------------------------------------------------- /assets/logo/PyBOP_logo_mark_circle.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/logo/PyBOP_logo_mark_mono.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/assets/logo/PyBOP_logo_mark_mono.png -------------------------------------------------------------------------------- /assets/logo/PyBOP_logo_mark_mono.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/logo/PyBOP_logo_mark_mono_inverse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/assets/logo/PyBOP_logo_mark_mono_inverse.png -------------------------------------------------------------------------------- /assets/logo/PyBOP_logo_mark_mono_inverse.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/logo/PyBOP_logo_mono.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/assets/logo/PyBOP_logo_mono.png -------------------------------------------------------------------------------- /assets/logo/PyBOP_logo_mono.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/logo/PyBOP_logo_mono_inverse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/assets/logo/PyBOP_logo_mono_inverse.png -------------------------------------------------------------------------------- /assets/logo/PyBOP_logo_mono_inverse.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/pybop_architecture.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/assets/pybop_architecture.pdf -------------------------------------------------------------------------------- /assets/pybop_architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/assets/pybop_architecture.png -------------------------------------------------------------------------------- /assets/roadmap_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/assets/roadmap_logo.png -------------------------------------------------------------------------------- /asv.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "project": "PyBOP", 4 | "project_url": "https://github.com/pybop-team/pybop", 5 | "repo": ".", 6 | "build_command": [ 7 | "python -m pip install build", 8 | "python -m build --wheel -o {build_cache_dir} {build_dir}" 9 | ], 10 | "default_benchmark_timeout": 180, 11 | "branches": ["HEAD"], 12 | "environment_type": "virtualenv", 13 | "matrix": { 14 | "req":{ 15 | "pybamm": [], 16 | "numpy": [], 17 | "scipy": [], 18 | "pints": [] 19 | } 20 | }, 21 | "build_cache_dir": ".asv/cache", 22 | "build_dir": ".asv/build" 23 | } 24 | -------------------------------------------------------------------------------- /benchmarks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/benchmarks/__init__.py -------------------------------------------------------------------------------- /benchmarks/benchmark_model.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import pybop 4 | from benchmarks.benchmark_utils import set_random_seed 5 | 6 | 7 | class BenchmarkModel: 8 | param_names = ["model", "parameter_set"] 9 | params = [ 10 | [pybop.lithium_ion.SPM, pybop.lithium_ion.SPMe], 11 | ["Chen2020"], 12 | ] 13 | 14 | def setup(self, model, parameter_set): 15 | """ 16 | Setup the model and problem for predict and simulate benchmarks. 17 | 18 | Args: 19 | model (pybop.Model): The model class to be benchmarked. 20 | parameter_set (str): The name of the parameter set to be used. 21 | """ 22 | # Set random seed 23 | set_random_seed() 24 | 25 | # Create model instance 26 | self.model = model(parameter_set=pybop.ParameterSet(parameter_set)) 27 | 28 | # Define fitting parameters 29 | parameters = pybop.Parameters( 30 | pybop.Parameter( 31 | "Current function [A]", 32 | prior=pybop.Gaussian(0.4, 0.02), 33 | bounds=[0.2, 0.7], 34 | initial_value=0.4, 35 | ) 36 | ) 37 | 38 | # Generate synthetic data 39 | sigma = 0.001 40 | self.t_eval = np.arange(0, 900, 2) 41 | self.init_state = {"Initial SoC": 0.5} 42 | values = self.model.predict(t_eval=self.t_eval, initial_state=self.init_state) 43 | corrupt_values = values["Voltage [V]"].data + np.random.normal( 44 | 0, sigma, len(self.t_eval) 45 | ) 46 | 47 | self.inputs = { 48 | "Current function [A]": 0.4, 49 | } 50 | 51 | # Create dataset 52 | dataset = pybop.Dataset( 53 | { 54 | "Time [s]": self.t_eval, 55 | "Current function [A]": values["Current [A]"].data, 56 | "Voltage [V]": corrupt_values, 57 | } 58 | ) 59 | 60 | # Create fitting problem 61 | self.problem = pybop.FittingProblem( 62 | model=self.model, dataset=dataset, parameters=parameters 63 | ) 64 | 65 | def time_model_predict(self, model, parameter_set): 66 | """ 67 | Benchmark the predict method of the model. 68 | 69 | Args: 70 | model (pybop.Model): The model class being benchmarked. 71 | parameter_set (str): The name of the parameter set being used. 72 | """ 73 | self.model.predict( 74 | inputs=self.inputs, t_eval=self.t_eval, initial_state=self.init_state 75 | ) 76 | 77 | def time_model_simulate(self, model, parameter_set): 78 | """ 79 | Benchmark the simulate method of the model. 80 | 81 | Args: 82 | model (pybop.Model): The model class being benchmarked. 83 | parameter_set (str): The name of the parameter set being used. 84 | """ 85 | self.problem.model.simulate(inputs=self.inputs, t_eval=self.t_eval) 86 | 87 | def time_model_simulateS1(self, model, parameter_set): 88 | """ 89 | Benchmark the simulateS1 method of the model. 90 | 91 | Args: 92 | model (pybop.Model): The model class being benchmarked. 93 | parameter_set (str): The name of the parameter set being used. 94 | """ 95 | self.problem.model.simulateS1(inputs=self.inputs, t_eval=self.t_eval) 96 | -------------------------------------------------------------------------------- /benchmarks/benchmark_optim_construction.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import pybop 4 | from benchmarks.benchmark_utils import set_random_seed 5 | 6 | 7 | class BenchmarkOptimisationConstruction: 8 | param_names = ["model", "parameter_set", "optimiser"] 9 | params = [ 10 | [pybop.lithium_ion.SPM, pybop.lithium_ion.SPMe], 11 | ["Chen2020"], 12 | [pybop.CMAES], 13 | ] 14 | 15 | def setup(self, model, parameter_set, optimiser): 16 | """ 17 | Set up the model, problem, and cost for optimization benchmarking. 18 | 19 | Args: 20 | model (pybop.Model): The model class to be benchmarked. 21 | parameter_set (str): The name of the parameter set to be used. 22 | optimiser (pybop.Optimiser): The optimiser class to be used. 23 | """ 24 | # Set random seed 25 | set_random_seed() 26 | 27 | # Create model instance 28 | model_instance = model(parameter_set=pybop.ParameterSet(parameter_set)) 29 | 30 | # Define fitting parameters 31 | parameters = pybop.Parameters( 32 | pybop.Parameter( 33 | "Negative electrode active material volume fraction", 34 | prior=pybop.Gaussian(0.6, 0.02), 35 | bounds=[0.375, 0.7], 36 | initial_value=0.63, 37 | ), 38 | pybop.Parameter( 39 | "Positive electrode active material volume fraction", 40 | prior=pybop.Gaussian(0.5, 0.02), 41 | bounds=[0.375, 0.625], 42 | initial_value=0.51, 43 | ), 44 | ) 45 | 46 | # Generate synthetic data 47 | sigma = 0.001 48 | t_eval = np.arange(0, 900, 2) 49 | values = model_instance.predict(t_eval=t_eval) 50 | corrupt_values = values["Voltage [V]"].data + np.random.normal( 51 | 0, sigma, len(t_eval) 52 | ) 53 | 54 | # Create dataset 55 | dataset = pybop.Dataset( 56 | { 57 | "Time [s]": t_eval, 58 | "Current function [A]": values["Current [A]"].data, 59 | "Voltage [V]": corrupt_values, 60 | } 61 | ) 62 | 63 | # Create fitting problem 64 | problem = pybop.FittingProblem( 65 | model=model_instance, dataset=dataset, parameters=parameters 66 | ) 67 | 68 | # Create cost function 69 | self.cost = pybop.SumSquaredError(problem=problem) 70 | 71 | def time_optimisation_construction(self, model, parameter_set, optimiser): 72 | """ 73 | Benchmark the construction of the optimization class. 74 | 75 | Args: 76 | model (pybop.Model): The model class being benchmarked. 77 | parameter_set (str): The name of the parameter set being used. 78 | optimiser (pybop.Optimiser): The optimiser class being used. 79 | """ 80 | self.optim = pybop.Optimisation(self.cost, optimiser=optimiser) 81 | 82 | def time_cost_evaluate(self, model, parameter_set, optimiser): 83 | """ 84 | Benchmark the cost function evaluation. 85 | 86 | Args: 87 | model (pybop.Model): The model class being benchmarked. 88 | parameter_set (str): The name of the parameter set being used. 89 | optimiser (pybop.Optimiser): The optimiser class being used. 90 | """ 91 | self.cost([0.63, 0.51]) 92 | -------------------------------------------------------------------------------- /benchmarks/benchmark_utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def set_random_seed(seed_value=8): 5 | np.random.seed(seed_value) 6 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | import matplotlib 2 | import numpy as np 3 | import plotly 4 | import pytest 5 | 6 | plotly.io.renderers.default = None 7 | matplotlib.use("Template") 8 | 9 | 10 | def pytest_addoption(parser): 11 | parser.addoption( 12 | "--unit", action="store_true", default=False, help="run unit tests" 13 | ) 14 | parser.addoption( 15 | "--integration", 16 | action="store_true", 17 | default=False, 18 | help="run integration tests", 19 | ) 20 | parser.addoption( 21 | "--examples", action="store_true", default=False, help="run examples tests" 22 | ) 23 | parser.addoption( 24 | "--plots", action="store_true", default=False, help="run plot tests" 25 | ) 26 | parser.addoption( 27 | "--notebooks", action="store_true", default=False, help="run notebook tests" 28 | ) 29 | parser.addoption("--docs", action="store_true", default=False, help="run doc tests") 30 | 31 | 32 | def pytest_configure(config): 33 | config.addinivalue_line("markers", "unit: mark test as a unit test") 34 | config.addinivalue_line("markers", "integration: mark test as an integration test") 35 | config.addinivalue_line("markers", "examples: mark test as an example") 36 | config.addinivalue_line("markers", "plots: mark test as a plot test") 37 | config.addinivalue_line("markers", "notebook: mark test as a notebook test") 38 | config.addinivalue_line("markers", "docs: mark test as a doc test") 39 | 40 | 41 | def pytest_collection_modifyitems(config, items): 42 | options = { 43 | "unit": "unit", 44 | "examples": "examples", 45 | "integration": "integration", 46 | "plots": "plots", 47 | "notebooks": "notebooks", 48 | "docs": "docs", 49 | } 50 | selected_markers = [ 51 | marker for option, marker in options.items() if config.getoption(option) 52 | ] 53 | 54 | if ( 55 | "notebooks" in selected_markers 56 | ): # Notebooks are meant to be run as an individual session 57 | return 58 | 59 | # If no options were passed, skip all tests 60 | if not selected_markers: 61 | skip_all = pytest.mark.skip( 62 | reason="Need at least one of --unit, --examples, --integration, --docs, or --plots option to run" 63 | ) 64 | for item in items: 65 | item.add_marker(skip_all) 66 | return 67 | 68 | # Skip tests that don't match any of the selected markers 69 | for item in items: 70 | item_markers = { 71 | mark.name for mark in item.iter_markers() 72 | } # Gather markers of the test item 73 | if not item_markers.intersection( 74 | selected_markers 75 | ): # Skip if there's no intersection with selected markers 76 | skip_this = pytest.mark.skip( 77 | reason=f"Test does not match the selected options: {', '.join(selected_markers)}" 78 | ) 79 | item.add_marker(skip_this) 80 | 81 | 82 | @pytest.fixture(autouse=True) 83 | def set_random_seed(): 84 | np.random.seed(40) 85 | -------------------------------------------------------------------------------- /docs/Contributing.md: -------------------------------------------------------------------------------- 1 | --- 2 | myst: 3 | html_meta: 4 | "description lang=en": | 5 | Contributing docs.. 6 | --- 7 | 8 | ```{include} ../CONTRIBUTING.md 9 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_static/custom-icon.js: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Set a custom icon for pypi as it's not available in the fa built-in brands 3 | * Taken from: https://github.com/pydata/pydata-sphinx-theme/blob/main/docs/_static/custom-icon.js 4 | */ 5 | FontAwesome.library.add( 6 | (faListOldStyle = { 7 | prefix: "fa-custom", 8 | iconName: "pypi", 9 | icon: [ 10 | 17.313, // viewBox width 11 | 19.807, // viewBox height 12 | [], // ligature 13 | "e001", // unicode codepoint - private use area 14 | "m10.383 0.2-3.239 1.1769 3.1883 1.1614 3.239-1.1798zm-3.4152 1.2411-3.2362 1.1769 3.1855 1.1614 3.2369-1.1769zm6.7177 0.00281-3.2947 1.2009v3.8254l3.2947-1.1988zm-3.4145 1.2439-3.2926 1.1981v3.8254l0.17548-0.064132 3.1171-1.1347zm-6.6564 0.018325v3.8247l3.244 1.1805v-3.8254zm10.191 0.20931v2.3137l3.1777-1.1558zm3.2947 1.2425-3.2947 1.1988v3.8254l3.2947-1.1988zm-8.7058 0.45739c0.00929-1.931e-4 0.018327-2.977e-4 0.027485 0 0.25633 0.00851 0.4263 0.20713 0.42638 0.49826 1.953e-4 0.38532-0.29327 0.80469-0.65542 0.93662-0.36226 0.13215-0.65608-0.073306-0.65613-0.4588-6.28e-5 -0.38556 0.2938-0.80504 0.65613-0.93662 0.068422-0.024919 0.13655-0.038114 0.20156-0.039466zm5.2913 0.78369-3.2947 1.1988v3.8247l3.2947-1.1981zm-10.132 1.239-3.2362 1.1769 3.1883 1.1614 3.2362-1.1769zm6.7177 0.00213-3.2926 1.2016v3.8247l3.2926-1.2009zm-3.4124 1.2439-3.2947 1.1988v3.8254l3.2947-1.1988zm-6.6585 0.016195v3.8275l3.244 1.1805v-3.8254zm16.9 0.21143-3.2947 1.1988v3.8247l3.2947-1.1981zm-3.4145 1.2411-3.2926 1.2016v3.8247l3.2926-1.2009zm-3.4145 1.2411-3.2926 1.2016v3.8247l3.2926-1.2009zm-3.4124 1.2432-3.2947 1.1988v3.8254l3.2947-1.1988zm-6.6585 0.019027v3.8247l3.244 1.1805v-3.8254zm13.485 1.4497-3.2947 1.1988v3.8247l3.2947-1.1981zm-3.4145 1.2411-3.2926 1.2016v3.8247l3.2926-1.2009zm2.4018 0.38127c0.0093-1.83e-4 0.01833-3.16e-4 0.02749 0 0.25633 0.0085 0.4263 0.20713 0.42638 0.49826 1.97e-4 0.38532-0.29327 0.80469-0.65542 0.93662-0.36188 0.1316-0.65525-0.07375-0.65542-0.4588-1.95e-4 -0.38532 0.29328-0.80469 0.65542-0.93662 0.06842-0.02494 0.13655-0.03819 0.20156-0.03947zm-5.8142 0.86403-3.244 1.1805v1.4201l3.244 1.1805z", // svg path (https://simpleicons.org/icons/pypi.svg) 15 | ], 16 | }) 17 | ); 18 | -------------------------------------------------------------------------------- /docs/_static/switcher.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "version": "latest", 4 | "url": "https://pybop-docs.readthedocs.io/en/latest/" 5 | }, 6 | { 7 | "name": "v25.3 (stable)", 8 | "version": "v25.3", 9 | "url": "https://pybop-docs.readthedocs.io/en/v25.3/", 10 | "preferred": true 11 | }, 12 | { 13 | "name": "v25.1", 14 | "version": "v25.1", 15 | "url": "https://pybop-docs.readthedocs.io/en/v25.1/" 16 | }, 17 | { 18 | "name": "v24.12", 19 | "version": "v24.12", 20 | "url": "https://pybop-docs.readthedocs.io/en/v24.12/" 21 | }, 22 | { 23 | "name": "v24.9", 24 | "version": "v24.9", 25 | "url": "https://pybop-docs.readthedocs.io/en/v24.9.1/" 26 | }, 27 | { 28 | "name": "v24.6", 29 | "version": "v24.6", 30 | "url": "https://pybop-docs.readthedocs.io/en/v24.6.1/" 31 | }, 32 | { 33 | "name": "v24.3", 34 | "version": "v24.3", 35 | "url": "https://pybop-docs.readthedocs.io/en/v24.3/" 36 | }, 37 | { 38 | "name": "v23.12", 39 | "version": "v23.12", 40 | "url": "https://pybop-docs.readthedocs.io/en/v23.12/" 41 | } 42 | ] 43 | -------------------------------------------------------------------------------- /docs/_templates/autoapi/index.rst: -------------------------------------------------------------------------------- 1 | .. _api-reference: 2 | 3 | API Reference 4 | ============= 5 | 6 | This page contains auto-generated API reference documentation [#f1]_. 7 | 8 | .. toctree:: 9 | :titlesonly: 10 | :maxdepth: 2 11 | 12 | {% for page in pages %} 13 | {% if page.top_level_object and page.display %} 14 | {{ page.include_path }} 15 | {% endif %} 16 | {% endfor %} 17 | 18 | pybop/index 19 | 20 | .. [#f1] Created with `sphinx-autoapi `_ 21 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | myst: 3 | html_meta: 4 | "description lang=en": | 5 | High-level documentation for PyBOP, and corresponding links to the site. 6 | html_theme.sidebar_secondary.remove: true 7 | --- 8 | 9 | # PyBOP: Optimise and Parameterise Battery Models 10 | 11 | Welcome to PyBOP, a Python package dedicated to the optimisation and parameterisation of battery models. PyBOP is designed to streamline your workflow, whether you are conducting academic research, working in industry, or simply interested in battery technology and modelling. 12 | 13 | ```{gallery-grid} 14 | :grid-columns: 1 2 2 2 15 | 16 | - header: "{fas}`bolt;pst-color-primary` Installation" 17 | content: "Setting up PyBOP is straightforward. Follow our step-by-step guide to install PyBOP on your system." 18 | link: "installation.html" 19 | - header: "{fas}`circle-half-stroke;pst-color-primary` Quick Start" 20 | content: "Discover how to use PyBOP effectively. From basic tasks to advanced features, learn how to solve real-world problems with PyBOP." 21 | link: "quick_start.html" 22 | - header: "{fab}`python;pst-color-primary` Contributing" 23 | content: "Contribute to the PyBOP project and become a part of our growing community." 24 | link: "Contributing.html" 25 | - header: "{fab}`bootstrap;pst-color-primary` API Reference" 26 | content: "Get detailed information on functions, classes, and modules that allow you to fully leverage the power of PyBOP in your own projects." 27 | link: "api/index.html" 28 | ``` 29 | 30 | ```{toctree} 31 | :maxdepth: 2 32 | :hidden: 33 | 34 | installation 35 | quick_start 36 | Contributing 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/quick_start.rst: -------------------------------------------------------------------------------- 1 | Quick Start 2 | **************************** 3 | 4 | Welcome to the Quick Start Guide for PyBOP. This guide will help you get up and running with PyBOP. If you're new to PyBOP, we recommend you start here to learn the basics and get a feel for the package. 5 | 6 | Getting Started with PyBOP 7 | -------------------------- 8 | 9 | PyBOP is equipped with a series of robust tools that can help you optimise various parameters within your battery models to better match empirical data or to explore the effects of different parameters on battery behavior. 10 | 11 | To begin using PyBOP: 12 | 13 | 1. Install the package using pip: 14 | 15 | .. code-block:: console 16 | 17 | pip install pybop 18 | 19 | For detailed installation instructions, including how to install specific versions or from source, see the :ref:`installation` section. 20 | 21 | 2. Once PyBOP is installed, you can import it in your Python scripts or Jupyter notebooks: 22 | 23 | .. code-block:: python 24 | 25 | import pybop 26 | 27 | Now you're ready to utilise PyBOP's functionality in your projects! 28 | 29 | Exploring Examples 30 | ------------------ 31 | 32 | To help you get acquainted with PyBOP's capabilities, we provide a collection of examples that demonstrate common use cases and features of the package: 33 | 34 | - **Jupyter Notebooks**: Interactive notebooks that include detailed explanations alongside the live code, visualisations, and results. These are an excellent resource for learning and can be easily modified and executed to suit your needs. 35 | 36 | - **Python Scripts**: For those who prefer working in a text editor, IDE, or for integrating into larger projects, we provide equivalent examples in plain Python script format. 37 | 38 | You can find these resources in the ``examples`` folder of the PyBOP repository. To access the examples, navigate to the following path after cloning or downloading the repository: 39 | 40 | .. code-block:: console 41 | 42 | path/to/pybop/examples 43 | 44 | These examples are also available on our `GitHub repository `_. 45 | 46 | Next Steps 47 | ---------- 48 | 49 | Once you're comfortable with the basics demonstrated in the examples, you can dive deeper into the functionality of PyBOP by delving into the :ref:`api-reference` for detailed API documentation. 50 | 51 | Support and Contributions 52 | ------------------------- 53 | 54 | If you encounter any issues or have questions as you start using PyBOP, don't hesitate to reach out to our community: 55 | 56 | - **GitHub Issues**: Report bugs or request new features by opening an `Issue `_ 57 | - **GitHub Discussions**: Post your questions or feedback on our `GitHub Discussions `_ 58 | - **Contributions**: Interested in contributing to PyBOP? Check out our `Contributing Guide `_ for guidelines. 59 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | This directory contains example notebooks and scripts demonstrating how to use PyBOP. 4 | 5 | ## Directory Structure 6 | 7 | - `notebooks/`: Jupyter notebooks of example functionality with explanations 8 | - `scripts/`: Python scripts for quick reference and command-line usage 9 | - `standalone/`: Example scripts for using standalone classes 10 | 11 | ## Notebooks 12 | 13 | The `notebooks/` directory contains Jupyter notebooks that provide detailed, interactive examples of various features and use cases. These notebooks include explanations, code snippets, and visualisations. 14 | 15 | To view the notebooks with interactive figures without downloading the repository, please use nbviewer: 16 | 17 |
18 | 19 | [Notebooks on nbviewer](https://nbviewer.org/github/pybop-team/PyBOP/tree/develop/examples/notebooks/) 20 | 21 |
22 | 23 | ## Scripts 24 | 25 | The `scripts/` directory contains standalone Python scripts that demonstrate specific tasks or workflows. These scripts are designed for quick reference and can be run directly from the command line. 26 | 27 | ## Getting Started 28 | 29 | 1. Clone the repository: `git clone https://github.com/pybop-team/pybop.git` 30 | 2. Navigate to the examples directory: `cd pybop/examples` 31 | 3. Explore the notebooks and scripts in their respective directories. 32 | 4. To run the Jupyter notebooks locally: 33 | - Install Jupyter: `pip install jupyter` 34 | - Start Jupyter Notebook: `jupyter notebook` 35 | - Navigate to the `notebooks/` directory and open the desired notebook 36 | 37 | 5. To run the Python scripts: 38 | - Install PyBOP: `pip install pybop` 39 | - Run a script using Python: `python scripts/script_name.py` 40 | 41 | ## Contributing 42 | 43 | If you have additional examples or improvements to existing ones, please feel free to submit a pull request. We appreciate your contributions! 44 | -------------------------------------------------------------------------------- /examples/data/LG_M50_ECM/data/LGM50_5Ah_OCV.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/examples/data/LG_M50_ECM/data/LGM50_5Ah_OCV.mat -------------------------------------------------------------------------------- /examples/data/LG_M50_ECM/data/LGM50_5Ah_Pulse.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/examples/data/LG_M50_ECM/data/LGM50_5Ah_Pulse.mat -------------------------------------------------------------------------------- /examples/data/LG_M50_ECM/data/LGM50_5Ah_RateTest.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/examples/data/LG_M50_ECM/data/LGM50_5Ah_RateTest.mat -------------------------------------------------------------------------------- /examples/data/README.md: -------------------------------------------------------------------------------- 1 | ## Data directory 2 | This directory contains both the experimental and synthetic data used in the examples. 3 | -------------------------------------------------------------------------------- /examples/data/Samsung_INR21700/multipulse_hppc.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/examples/data/Samsung_INR21700/multipulse_hppc.xlsx -------------------------------------------------------------------------------- /examples/data/Samsung_INR21700/sample_drive_cycle.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/examples/data/Samsung_INR21700/sample_drive_cycle.xlsx -------------------------------------------------------------------------------- /examples/data/Samsung_INR21700/sample_hppc_pulse.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/examples/data/Samsung_INR21700/sample_hppc_pulse.xlsx -------------------------------------------------------------------------------- /examples/data/Tesla_4680/README.md: -------------------------------------------------------------------------------- 1 | # Tesla 4680 Dataset 2 | 3 | This repository contains a subset of the data from the supplementary material of the research paper: 4 | **M. Ank et al., "Lithium-Ion Cells in Automotive Applications: Tesla 4680 Cylindrical Cell Teardown and Characterization,"** *Journal of the Electrochemical Society*, vol. 170, no. 12, p. 120536, December 2023. [DOI: 10.1149/1945-7111/ad14d0](https://doi.org/10.1149/1945-7111/ad14d0) 5 | 6 | ## Accessing the Supplemental Material 7 | 8 | The supplemental material can be accessed through the following link: 9 | [Supplemental Material on MediaTUM](https://mediatum.ub.tum.de/1725661) 10 | 11 | 12 | ## Citation 13 | 14 | If you use this dataset in your work, please cite the original publication as follows: 15 | 16 | ```plaintext 17 | M. Ank et al., "Lithium-Ion Cells in Automotive Applications: Tesla 4680 Cylindrical Cell Teardown and Characterization," Journal of the Electrochemical Society, vol. 170, no. 12, p. 120536, Dec. 2023, doi: 10.1149/1945-7111/ad14d0. 18 | -------------------------------------------------------------------------------- /examples/data/synthetic/README.md: -------------------------------------------------------------------------------- 1 | # Data description 2 | The data files in this directory have the following metadata 3 | 4 | `{model}_charge_discharge_{soc}.csv` 5 | - Time, current, terminal voltage, and open-circuit voltage data for a Chen2020 parameters at the corresponding SOC. 0.5mV of noise is applied to both voltage signals. 6 | - See `discharge_charge_data_gen.py` for reference 7 | 8 | `{model}_pulse_{soc}.csv` 9 | - Time, current, terminal voltage, and open-circuit voltage data for a Chen2020 parameters at the corresponding SOC. 0.5mV of noise is applied to both voltage signals. 10 | - See `pulse_data_gen.py` for reference 11 | -------------------------------------------------------------------------------- /examples/data/synthetic/discharge_charge_data_gen.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | import pybamm 4 | 5 | import pybop 6 | 7 | # Define model and use high-performant solver for sensitivities 8 | solver = pybamm.CasadiSolver(atol=1e-7, rtol=1e-7) 9 | parameter_set = pybop.ParameterSet("Chen2020") 10 | models = [ 11 | (pybop.lithium_ion.DFN(parameter_set=parameter_set, solver=solver), "dfn"), 12 | (pybop.lithium_ion.SPMe(parameter_set=parameter_set, solver=solver), "spme"), 13 | (pybop.lithium_ion.SPM(parameter_set=parameter_set, solver=solver), "spm"), 14 | ] 15 | 16 | # Generate data 17 | sigma = 5e-4 18 | soc = 0.75 19 | experiment = pybop.Experiment( 20 | [ 21 | "Rest for 2 seconds (1 second period)", 22 | "Discharge at 0.5C for 40 minutes (20 second period)", 23 | "Charge at 0.5C for 20 minutes (20 second period)", 24 | ] 25 | ) 26 | for model, name in models: 27 | values = model.predict( 28 | initial_state={"Initial SoC": soc}, 29 | experiment=experiment, 30 | parameter_set=parameter_set, 31 | ) 32 | 33 | def noise(sigma, dict_obj): 34 | return np.random.normal(0, sigma, len(dict_obj["Voltage [V]"].data)) 35 | 36 | pd.DataFrame( 37 | { 38 | "Time [s]": np.round(values["Time [s]"].data, decimals=5), 39 | "Current function [A]": values["Current [A]"].data, 40 | "Voltage [V]": values["Voltage [V]"].data + noise(sigma, values), 41 | "Bulk open-circuit voltage [V]": values[ 42 | "Bulk open-circuit voltage [V]" 43 | ].data 44 | + noise(sigma, values), 45 | } 46 | ).drop_duplicates(subset=["Time [s]"]).to_csv( 47 | f"{name}_charge_discharge_{int(soc * 100)}.csv", index=False 48 | ) 49 | -------------------------------------------------------------------------------- /examples/data/synthetic/pulse_data_gen.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | import pybamm 4 | 5 | import pybop 6 | 7 | # Define model and use high-performant solver for sensitivities 8 | solver = pybamm.CasadiSolver(atol=1e-7, rtol=1e-7) 9 | parameter_set = pybop.ParameterSet("Chen2020") 10 | models = [ 11 | (pybop.lithium_ion.DFN(parameter_set=parameter_set, solver=solver), "dfn"), 12 | (pybop.lithium_ion.SPMe(parameter_set=parameter_set, solver=solver), "spme"), 13 | (pybop.lithium_ion.SPM(parameter_set=parameter_set, solver=solver), "spm"), 14 | ] 15 | 16 | # Generate data 17 | sigma = 5e-4 18 | soc = [0.15, 0.5] 19 | experiment = pybop.Experiment( 20 | [ 21 | "Rest for 2 seconds (1 second period)", 22 | "Discharge at 0.1C for 1 minute (2 second period)", 23 | "Rest for 10 minutes (8 second period)", 24 | "Charge at 0.1C for 1 minute (2 second period)", 25 | "Rest for 10 minutes (8 second period)", 26 | ] 27 | ) 28 | for model, name in models: 29 | for s in soc: 30 | values = model.predict( 31 | initial_state={"Initial SoC": s}, 32 | experiment=experiment, 33 | parameter_set=parameter_set, 34 | ) 35 | 36 | def noise(sigma, dict_obj): 37 | return np.random.normal(0, sigma, len(dict_obj["Voltage [V]"].data)) 38 | 39 | pd.DataFrame( 40 | { 41 | "Time [s]": np.round(values["Time [s]"].data, decimals=5), 42 | "Current function [A]": values["Current [A]"].data, 43 | "Voltage [V]": values["Voltage [V]"].data + noise(sigma, values), 44 | "Bulk open-circuit voltage [V]": values[ 45 | "Bulk open-circuit voltage [V]" 46 | ].data 47 | + noise(sigma, values), 48 | } 49 | ).drop_duplicates(subset=["Time [s]"]).to_csv( 50 | f"{name}_pulse_{int(s * 100)}.csv", index=False 51 | ) 52 | -------------------------------------------------------------------------------- /examples/parameters/initial_ecm_parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "chemistry": "ecm", 3 | "Initial SoC": 0.5, 4 | "Initial temperature [K]": 298.15, 5 | "Cell capacity [A.h]": 5, 6 | "Nominal cell capacity [A.h]": 5, 7 | "Ambient temperature [K]": 298.15, 8 | "Current function [A]": 5, 9 | "Upper voltage cut-off [V]": 4.2, 10 | "Lower voltage cut-off [V]": 3.0, 11 | "Cell thermal mass [J/K]": 1000, 12 | "Cell-jig heat transfer coefficient [W/K]": 10, 13 | "Jig thermal mass [J/K]": 500, 14 | "Jig-air heat transfer coefficient [W/K]": 10, 15 | "R0 [Ohm]": 0.001, 16 | "Element-1 initial overpotential [V]": 0, 17 | "Element-2 initial overpotential [V]": 0, 18 | "R1 [Ohm]": 0.0002, 19 | "R2 [Ohm]": 0.0003, 20 | "C1 [F]": 10000, 21 | "C2 [F]": 5000, 22 | "Entropic change [V/K]": 0.0004 23 | } 24 | -------------------------------------------------------------------------------- /examples/scripts/battery_parameterisation/full_cell_balancing.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import pybop 4 | 5 | # Parameter set definition 6 | parameter_set = pybop.ParameterSet("Chen2020") 7 | parameter_set["Lower voltage cut-off [V]"] = 2.3 8 | parameter_set["Upper voltage cut-off [V]"] = 4.4 9 | 10 | # Set initial state and unpack true values 11 | parameter_set.parameter_values.set_initial_stoichiometries(initial_value=1.0) 12 | cs_n_max = parameter_set["Maximum concentration in negative electrode [mol.m-3]"] 13 | cs_p_max = parameter_set["Maximum concentration in positive electrode [mol.m-3]"] 14 | cs_n_init = parameter_set["Initial concentration in negative electrode [mol.m-3]"] 15 | cs_p_init = parameter_set["Initial concentration in positive electrode [mol.m-3]"] 16 | 17 | # Model definition 18 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set) 19 | 20 | # Define fitting parameters for OCP balancing 21 | parameters = pybop.Parameters( 22 | pybop.Parameter( 23 | "Maximum concentration in negative electrode [mol.m-3]", 24 | prior=pybop.Gaussian(cs_n_max, 6e3), 25 | bounds=[cs_n_max * 0.75, cs_n_max * 1.25], 26 | true_value=cs_n_max, 27 | initial_value=cs_n_max * 0.8, 28 | ), 29 | pybop.Parameter( 30 | "Maximum concentration in positive electrode [mol.m-3]", 31 | prior=pybop.Gaussian(cs_p_max, 6e3), 32 | bounds=[cs_p_max * 0.75, cs_p_max * 1.25], 33 | true_value=cs_p_max, 34 | initial_value=cs_p_max * 0.8, 35 | ), 36 | pybop.Parameter( 37 | "Initial concentration in negative electrode [mol.m-3]", 38 | prior=pybop.Gaussian(cs_n_init, 6e3), 39 | bounds=[cs_n_max * 0.75, cs_n_max * 1.25], 40 | true_value=cs_n_init, 41 | initial_value=cs_n_max * 0.8, 42 | ), 43 | pybop.Parameter( 44 | "Initial concentration in positive electrode [mol.m-3]", 45 | prior=pybop.Gaussian(cs_p_init, 6e3), 46 | bounds=[0, cs_p_max * 0.5], 47 | true_value=cs_p_init, 48 | initial_value=cs_p_max * 0.2, 49 | ), 50 | ) 51 | 52 | # Generate synthetic data 53 | sigma = 5e-4 # Volts 54 | experiment = pybop.Experiment( 55 | [ 56 | "Discharge at 0.1C until 2.5V (3 min period)", 57 | "Charge at 0.1C until 4.2V (3 min period)", 58 | ] 59 | ) 60 | values = model.predict(experiment=experiment) 61 | 62 | 63 | def noisy(data, sigma): 64 | return data + np.random.normal(0, sigma, len(data)) 65 | 66 | 67 | # Form dataset 68 | dataset = pybop.Dataset( 69 | { 70 | "Time [s]": values["Time [s]"].data, 71 | "Current function [A]": values["Current [A]"].data, 72 | "Voltage [V]": noisy(values["Voltage [V]"].data, sigma), 73 | } 74 | ) 75 | 76 | # Generate problem, cost function, and optimisation class 77 | problem = pybop.FittingProblem(model, parameters, dataset) 78 | cost = pybop.GaussianLogLikelihoodKnownSigma(problem, sigma0=sigma) 79 | optim = pybop.SciPyMinimize(cost, max_iterations=125) 80 | 81 | # Run optimisation for Maximum Likelihood Estimate (MLE) 82 | results = optim.run() 83 | print("True parameters:", parameters.true_value()) 84 | 85 | # Plot the timeseries output 86 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison") 87 | 88 | # Plot convergence 89 | pybop.plot.convergence(optim) 90 | 91 | # Plot the parameter traces 92 | pybop.plot.parameters(optim) 93 | 94 | # Plot the cost landscape with optimisation path 95 | pybop.plot.contour(optim, steps=5) 96 | -------------------------------------------------------------------------------- /examples/scripts/battery_parameterisation/gitt_pulse.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import pybop 4 | 5 | # Define model 6 | parameter_set = pybop.ParameterSet("Xu2019") 7 | model = pybop.lithium_ion.SPMe( 8 | parameter_set=parameter_set, options={"working electrode": "positive"} 9 | ) 10 | 11 | # Generate data 12 | sigma = 1e-3 13 | initial_state = {"Initial SoC": 0.9} 14 | experiment = pybop.Experiment( 15 | [ 16 | "Rest for 1 second", 17 | "Discharge at 1C for 10 minutes (10 second period)", 18 | "Rest for 20 minutes", 19 | ] 20 | ) 21 | values = model.predict(initial_state=initial_state, experiment=experiment) 22 | corrupt_values = values["Voltage [V]"].data + np.random.normal( 23 | 0, sigma, len(values["Voltage [V]"].data) 24 | ) 25 | 26 | # Form dataset 27 | dataset = pybop.Dataset( 28 | { 29 | "Time [s]": values["Time [s]"].data, 30 | "Current function [A]": values["Current [A]"].data, 31 | "Discharge capacity [A.h]": values["Discharge capacity [A.h]"].data, 32 | "Voltage [V]": corrupt_values, 33 | } 34 | ) 35 | 36 | # Define parameter set 37 | parameter_set = pybop.lithium_ion.SPDiffusion.apply_parameter_grouping( 38 | model.parameter_set, electrode="positive" 39 | ) 40 | 41 | # Fit the GITT pulse using the single particle diffusion model 42 | gitt_fit = pybop.GITTPulseFit( 43 | gitt_pulse=dataset, 44 | parameter_set=parameter_set, 45 | electrode="positive", 46 | ) 47 | gitt_results = gitt_fit() 48 | 49 | # Plot the timeseries output 50 | pybop.plot.problem( 51 | gitt_fit.problem, problem_inputs=gitt_results.x, title="Optimised Comparison" 52 | ) 53 | 54 | # Plot convergence 55 | pybop.plot.convergence(gitt_fit.optim) 56 | 57 | # Plot the parameter traces 58 | pybop.plot.parameters(gitt_fit.optim) 59 | -------------------------------------------------------------------------------- /examples/scripts/battery_parameterisation/simple_ecm.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import pybop 4 | 5 | # Import the ECM parameter set from JSON 6 | parameter_set = pybop.ParameterSet( 7 | json_path="examples/parameters/initial_ecm_parameters.json" 8 | ) 9 | 10 | # Alternatively, define the initial parameter set with a dictionary 11 | # Add definitions for R's, C's, and initial overpotentials for any additional RC elements 12 | # parameter_set = pybop.ParameterSet( 13 | # params_dict={ 14 | # "chemistry": "ecm", 15 | # "Initial SoC": 0.5, 16 | # "Initial temperature [K]": 25 + 273.15, 17 | # "Cell capacity [A.h]": 5, 18 | # "Nominal cell capacity [A.h]": 5, 19 | # "Ambient temperature [K]": 25 + 273.15, 20 | # "Current function [A]": 5, 21 | # "Upper voltage cut-off [V]": 4.2, 22 | # "Lower voltage cut-off [V]": 3.0, 23 | # "Cell thermal mass [J/K]": 1000, 24 | # "Cell-jig heat transfer coefficient [W/K]": 10, 25 | # "Jig thermal mass [J/K]": 500, 26 | # "Jig-air heat transfer coefficient [W/K]": 10, 27 | # "Open-circuit voltage [V]": pybop.empirical.Thevenin().default_parameter_values[ 28 | # "Open-circuit voltage [V]" 29 | # ], 30 | # "R0 [Ohm]": 0.001, 31 | # "Element-1 initial overpotential [V]": 0, 32 | # "Element-2 initial overpotential [V]": 0, 33 | # "R1 [Ohm]": 0.0002, 34 | # "R2 [Ohm]": 0.0003, 35 | # "C1 [F]": 10000, 36 | # "C2 [F]": 5000, 37 | # "Entropic change [V/K]": 0.0004, 38 | # } 39 | # ) 40 | 41 | # Define the model 42 | model = pybop.empirical.Thevenin( 43 | parameter_set=parameter_set, options={"number of rc elements": 2} 44 | ) 45 | 46 | # Fitting parameters 47 | parameters = pybop.Parameters( 48 | pybop.Parameter( 49 | "R0 [Ohm]", 50 | prior=pybop.Gaussian(0.0002, 0.0001), 51 | bounds=[1e-4, 1e-2], 52 | ), 53 | pybop.Parameter( 54 | "R1 [Ohm]", 55 | prior=pybop.Gaussian(0.0001, 0.0001), 56 | bounds=[1e-5, 1e-2], 57 | ), 58 | ) 59 | 60 | sigma = 0.001 61 | t_eval = np.arange(0, 900, 3) 62 | values = model.predict(t_eval=t_eval) 63 | corrupt_values = values["Voltage [V]"].data + np.random.normal(0, sigma, len(t_eval)) 64 | 65 | # Form dataset 66 | dataset = pybop.Dataset( 67 | { 68 | "Time [s]": t_eval, 69 | "Current function [A]": values["Current [A]"].data, 70 | "Voltage [V]": corrupt_values, 71 | } 72 | ) 73 | 74 | # Generate problem, cost function, and optimisation class 75 | problem = pybop.FittingProblem(model, parameters, dataset) 76 | cost = pybop.SumSquaredError(problem) 77 | optim = pybop.CMAES(cost, max_iterations=100) 78 | 79 | results = optim.run() 80 | 81 | # Export the parameters to JSON 82 | parameter_set.export_parameters( 83 | "examples/parameters/fit_ecm_parameters.json", fit_params=parameters 84 | ) 85 | 86 | # Plot the time series 87 | pybop.plot.dataset(dataset) 88 | 89 | # Plot the timeseries output 90 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison") 91 | 92 | # Plot convergence 93 | pybop.plot.convergence(optim) 94 | 95 | # Plot the parameter traces 96 | pybop.plot.parameters(optim) 97 | 98 | # Plot the cost landscape 99 | pybop.plot.surface(optim) 100 | -------------------------------------------------------------------------------- /examples/scripts/battery_parameterisation/simple_eis.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import pybop 4 | 5 | # Define model 6 | parameter_set = pybop.ParameterSet("Chen2020") 7 | parameter_set["Contact resistance [Ohm]"] = 0.0 8 | initial_state = {"Initial SoC": 0.5} 9 | n_frequency = 20 10 | sigma0 = 1e-4 11 | f_eval = np.logspace(-4, 5, n_frequency) 12 | model = pybop.lithium_ion.SPM( 13 | parameter_set=parameter_set, 14 | eis=True, 15 | options={"surface form": "differential", "contact resistance": "true"}, 16 | ) 17 | 18 | # Create synthetic data for parameter inference 19 | sim = model.simulateEIS( 20 | inputs={ 21 | "Negative electrode active material volume fraction": 0.531, 22 | "Positive electrode active material volume fraction": 0.732, 23 | }, 24 | f_eval=f_eval, 25 | initial_state=initial_state, 26 | ) 27 | 28 | # Fitting parameters 29 | parameters = pybop.Parameters( 30 | pybop.Parameter( 31 | "Negative electrode active material volume fraction", 32 | prior=pybop.Uniform(0.4, 0.75), 33 | bounds=[0.375, 0.75], 34 | ), 35 | pybop.Parameter( 36 | "Positive electrode active material volume fraction", 37 | prior=pybop.Uniform(0.4, 0.75), 38 | bounds=[0.375, 0.75], 39 | ), 40 | ) 41 | 42 | 43 | def noisy(data, sigma): 44 | # Generate real part noise 45 | real_noise = np.random.normal(0, sigma, len(data)) 46 | 47 | # Generate imaginary part noise 48 | imag_noise = np.random.normal(0, sigma, len(data)) 49 | 50 | # Combine them into a complex noise 51 | return data + real_noise + 1j * imag_noise 52 | 53 | 54 | # Form dataset 55 | dataset = pybop.Dataset( 56 | { 57 | "Frequency [Hz]": f_eval, 58 | "Current function [A]": np.ones(n_frequency) * 0.0, 59 | "Impedance": noisy(sim["Impedance"], sigma0), 60 | } 61 | ) 62 | 63 | signal = ["Impedance"] 64 | # Generate problem, cost function, and optimisation class 65 | problem = pybop.FittingProblem(model, parameters, dataset, signal=signal) 66 | cost = pybop.GaussianLogLikelihoodKnownSigma(problem, sigma0=sigma0) 67 | optim = pybop.CMAES(cost, max_iterations=100, sigma0=0.25, max_unchanged_iterations=30) 68 | 69 | results = optim.run() 70 | 71 | # Plot the nyquist 72 | pybop.plot.nyquist(problem, problem_inputs=results.x, title="Optimised Comparison") 73 | 74 | # Plot convergence 75 | pybop.plot.convergence(optim) 76 | 77 | # Plot the parameter traces 78 | pybop.plot.parameters(optim) 79 | 80 | # Plot 2d landscape 81 | pybop.plot.surface(optim) 82 | -------------------------------------------------------------------------------- /examples/scripts/battery_parameterisation/simple_pulse_fit.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import numpy as np 4 | 5 | import pybop 6 | 7 | # This example introduces pulse fitting 8 | # within PyBOP. 5% SOC pulse data is loaded from a local `csv` file 9 | # and particle diffusivity identification for a SPMe model is performed. 10 | # Additionally, uncertainty metrics are computed. 11 | 12 | # Get the current directory location and convert to absolute path 13 | current_dir = os.path.dirname(os.path.abspath(__file__)) 14 | dataset_path = os.path.join(current_dir, "../../data/synthetic/spme_pulse_15.csv") 15 | 16 | # Define model and use high-performant solver for sensitivities 17 | parameter_set = pybop.ParameterSet("Chen2020") 18 | model = pybop.lithium_ion.SPMe( 19 | parameter_set=parameter_set, 20 | ) 21 | 22 | # Fitting parameters 23 | parameters = pybop.Parameters( 24 | pybop.Parameter( 25 | "Negative particle diffusivity [m2.s-1]", 26 | prior=pybop.Gaussian(4e-14, 1e-14), 27 | transformation=pybop.LogTransformation(), 28 | bounds=[1e-14, 1e-13], 29 | ), 30 | pybop.Parameter( 31 | "Positive particle diffusivity [m2.s-1]", 32 | prior=pybop.Gaussian(7e-15, 1e-15), 33 | transformation=pybop.LogTransformation(), 34 | bounds=[1e-15, 1e-14], 35 | ), 36 | ) 37 | 38 | # Import the synthetic dataset 39 | csv_data = np.loadtxt(dataset_path, delimiter=",", skiprows=1) 40 | 41 | 42 | # Form dataset 43 | dataset = pybop.Dataset( 44 | { 45 | "Time [s]": csv_data[:, 0], 46 | "Current function [A]": csv_data[:, 1], 47 | "Voltage [V]": csv_data[:, 2], 48 | } 49 | ) 50 | 51 | # Generate problem, cost function, and optimisation class 52 | # In this example, we initialise the SPMe at the first voltage 53 | # point in `csv_data`, an optimise without rebuilding the 54 | # model on every evaluation. 55 | initial_state = {"Initial open-circuit voltage [V]": csv_data[0, 2]} 56 | model.set_initial_state(initial_state=initial_state) 57 | problem = pybop.FittingProblem( 58 | model, 59 | parameters, 60 | dataset, 61 | ) 62 | 63 | likelihood = pybop.SumSquaredError(problem) 64 | optim = pybop.CMAES( 65 | likelihood, 66 | verbose=True, 67 | sigma0=0.02, 68 | max_iterations=100, 69 | max_unchanged_iterations=30, 70 | # compute_sensitivities=True, 71 | # n_sensitivity_samples=64, # Decrease samples for CI (increase for higher accuracy) 72 | ) 73 | 74 | # Slow the step-size shrinking (default is 0.5) 75 | optim.optimiser.eta_min = 0.7 76 | 77 | # Run optimisation 78 | results = optim.run() 79 | 80 | # Plot the timeseries output 81 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison") 82 | 83 | # Plot convergence 84 | pybop.plot.convergence(optim) 85 | 86 | # Plot the parameter traces 87 | pybop.plot.parameters(optim) 88 | 89 | # Plot the cost landscape with optimisation path 90 | pybop.plot.contour(optim) 91 | -------------------------------------------------------------------------------- /examples/scripts/battery_parameterisation/stoichiometry_fitting.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import pybop 4 | 5 | # Generate some synthetic data for testing 6 | parameter_set = pybop.ParameterSet("Chen2020") 7 | ocv_function = parameter_set["Positive electrode OCP [V]"] 8 | nom_capacity = parameter_set["Nominal cell capacity [A.h]"] 9 | 10 | 11 | def noise(sigma): 12 | return np.random.normal(0, sigma, 91) 13 | 14 | 15 | sto = np.linspace(0, 0.9, 91) 16 | voltage = ocv_function(sto) + noise(2e-3) 17 | 18 | # Create the OCV dataset 19 | ocv_dataset = pybop.Dataset( 20 | {"Charge capacity [A.h]": (sto + 0.1) * nom_capacity, "Voltage [V]": voltage} 21 | ) 22 | 23 | # Estimate the stoichiometry corresponding to the GITT-OCV 24 | ocv_fit = pybop.OCPCapacityToStoichiometry(ocv_dataset, ocv_function) 25 | fitted_dataset = ocv_fit() 26 | 27 | # Verify the method through plotting 28 | stoichiometry = np.linspace(0, 1, 101) 29 | fig = pybop.plot.trajectories( 30 | x=[stoichiometry, fitted_dataset["Stoichiometry"]], 31 | y=[ 32 | parameter_set["Positive electrode OCP [V]"](stoichiometry), 33 | fitted_dataset["Voltage [V]"], 34 | ], 35 | trace_names=["Ground truth", "Data vs. stoichiometry"], 36 | ) 37 | -------------------------------------------------------------------------------- /examples/scripts/comparison_examples/adamw.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import numpy as np 4 | 5 | import pybop 6 | 7 | # Get the current directory location and convert to absolute path 8 | current_dir = os.path.dirname(os.path.abspath(__file__)) 9 | dataset_path = os.path.join( 10 | current_dir, "../../data/synthetic/spm_charge_discharge_75.csv" 11 | ) 12 | 13 | # Define model and use high-performant solver for sensitivities 14 | parameter_set = pybop.ParameterSet("Chen2020") 15 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set) 16 | 17 | # Fitting parameters 18 | parameters = pybop.Parameters( 19 | pybop.Parameter( 20 | "Negative electrode active material volume fraction", 21 | prior=pybop.Gaussian(0.68, 0.05), 22 | initial_value=0.45, 23 | bounds=[0.4, 0.9], 24 | ), 25 | pybop.Parameter( 26 | "Positive electrode active material volume fraction", 27 | prior=pybop.Gaussian(0.58, 0.05), 28 | initial_value=0.45, 29 | bounds=[0.4, 0.9], 30 | ), 31 | ) 32 | 33 | # Import the synthetic dataset, set model initial state 34 | csv_data = np.loadtxt(dataset_path, delimiter=",", skiprows=1) 35 | initial_state = {"Initial open-circuit voltage [V]": csv_data[0, 2]} 36 | model.set_initial_state(initial_state=initial_state) 37 | 38 | # Form dataset 39 | dataset = pybop.Dataset( 40 | { 41 | "Time [s]": csv_data[:, 0], 42 | "Current function [A]": csv_data[:, 1], 43 | "Voltage [V]": csv_data[:, 2], 44 | "Bulk open-circuit voltage [V]": csv_data[:, 3], 45 | } 46 | ) 47 | 48 | signal = ["Voltage [V]", "Bulk open-circuit voltage [V]"] 49 | # Generate problem, cost function, and optimisation class 50 | problem = pybop.FittingProblem( 51 | model, 52 | parameters, 53 | dataset, 54 | signal=signal, 55 | ) 56 | cost = pybop.SumOfPower(problem, p=2.5) 57 | 58 | optim = pybop.AdamW( 59 | cost, 60 | verbose=True, 61 | verbose_print_rate=20, 62 | allow_infeasible_solutions=True, 63 | sigma0=0.02, 64 | max_iterations=100, 65 | max_unchanged_iterations=45, 66 | compute_sensitivities=True, 67 | n_sensitivity_samples=128, 68 | ) 69 | # Reduce the momentum influence 70 | # for the reduced number of optimiser iterations 71 | optim.optimiser.b1 = 0.9 72 | optim.optimiser.b2 = 0.9 73 | 74 | # Run optimisation 75 | results = optim.run() 76 | 77 | # Plot the timeseries output 78 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison") 79 | 80 | # Plot convergence 81 | pybop.plot.convergence(optim) 82 | 83 | # Plot the parameter traces 84 | pybop.plot.parameters(optim) 85 | 86 | # Plot the cost landscape with optimisation path 87 | pybop.plot.surface(optim) 88 | -------------------------------------------------------------------------------- /examples/scripts/comparison_examples/covariance_matrix_adaptation.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import pybop 4 | 5 | # Define model 6 | parameter_set = pybop.ParameterSet("Chen2020") 7 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set) 8 | 9 | # Fitting parameters 10 | parameters = pybop.Parameters( 11 | pybop.Parameter( 12 | "Negative particle radius [m]", 13 | prior=pybop.Gaussian(6e-06, 0.1e-6), 14 | bounds=[1e-6, 9e-6], 15 | true_value=parameter_set["Negative particle radius [m]"], 16 | transformation=pybop.LogTransformation(), 17 | ), 18 | pybop.Parameter( 19 | "Positive particle radius [m]", 20 | prior=pybop.Gaussian(4.5e-06, 0.1e-6), 21 | bounds=[1e-6, 9e-6], 22 | true_value=parameter_set["Positive particle radius [m]"], 23 | transformation=pybop.LogTransformation(), 24 | ), 25 | ) 26 | 27 | # Generate data 28 | sigma = 0.001 29 | t_eval = np.arange(0, 900, 5) 30 | values = model.predict(t_eval=t_eval) 31 | corrupt_values = values["Voltage [V]"].data + np.random.normal(0, sigma, len(t_eval)) 32 | 33 | # Form dataset 34 | dataset = pybop.Dataset( 35 | { 36 | "Time [s]": t_eval, 37 | "Current function [A]": values["Current [A]"].data, 38 | "Voltage [V]": corrupt_values, 39 | "Bulk open-circuit voltage [V]": values["Bulk open-circuit voltage [V]"].data, 40 | } 41 | ) 42 | 43 | signal = ["Voltage [V]", "Bulk open-circuit voltage [V]"] 44 | # Generate problem, cost function, and optimisation class 45 | problem = pybop.FittingProblem(model, parameters, dataset, signal=signal) 46 | cost = pybop.SumSquaredError(problem) 47 | optim = pybop.CMAES( 48 | cost, 49 | sigma0=0.25, 50 | max_unchanged_iterations=10, 51 | verbose=True, 52 | max_iterations=40, 53 | multistart=2, 54 | ) 55 | 56 | # Run the optimisation 57 | results = optim.run() 58 | print("True parameters:", parameters.true_value()) 59 | 60 | # Plot the time series 61 | pybop.plot.dataset(dataset) 62 | 63 | # Plot the timeseries output 64 | pybop.plot.problem(problem, problem_inputs=results.x_best, title="Optimised Comparison") 65 | 66 | # Plot convergence 67 | pybop.plot.convergence(optim) 68 | 69 | # Plot the parameter traces 70 | pybop.plot.parameters(optim) 71 | 72 | # Plot the cost landscape 73 | pybop.plot.surface(optim) 74 | -------------------------------------------------------------------------------- /examples/scripts/comparison_examples/cuckoo.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import numpy as np 4 | 5 | import pybop 6 | 7 | # Get the current directory location and convert to absolute path 8 | current_dir = os.path.dirname(os.path.abspath(__file__)) 9 | dataset_path = os.path.join( 10 | current_dir, "../../data/synthetic/spm_charge_discharge_75.csv" 11 | ) 12 | 13 | # Define model 14 | parameter_set = pybop.ParameterSet("Chen2020") 15 | parameter_set.update( 16 | { 17 | "Negative electrode active material volume fraction": 0.7, 18 | "Positive electrode active material volume fraction": 0.67, 19 | } 20 | ) 21 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set) 22 | 23 | # Fitting parameters 24 | parameters = pybop.Parameters( 25 | pybop.Parameter( 26 | "Negative electrode active material volume fraction", 27 | prior=pybop.Gaussian(0.6, 0.05), 28 | bounds=[0.4, 0.75], 29 | initial_value=0.41, 30 | ), 31 | pybop.Parameter( 32 | "Positive electrode active material volume fraction", 33 | prior=pybop.Gaussian(0.48, 0.05), 34 | bounds=[0.4, 0.75], 35 | initial_value=0.41, 36 | ), 37 | ) 38 | 39 | # Import the synthetic dataset, set model initial state 40 | csv_data = np.loadtxt(dataset_path, delimiter=",", skiprows=1) 41 | initial_state = {"Initial open-circuit voltage [V]": csv_data[0, 2]} 42 | model.set_initial_state(initial_state=initial_state) 43 | 44 | # Form dataset 45 | dataset = pybop.Dataset( 46 | { 47 | "Time [s]": csv_data[:, 0], 48 | "Current function [A]": csv_data[:, 1], 49 | "Voltage [V]": csv_data[:, 2], 50 | } 51 | ) 52 | 53 | # Generate problem, cost function, and optimisation class 54 | problem = pybop.FittingProblem( 55 | model, 56 | parameters, 57 | dataset, 58 | ) 59 | cost = pybop.GaussianLogLikelihood(problem, sigma0=8e-3) 60 | optim = pybop.Optimisation( 61 | cost, 62 | optimiser=pybop.CuckooSearch, 63 | max_iterations=100, 64 | ) 65 | 66 | results = optim.run() 67 | 68 | # Plot the timeseries output 69 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison") 70 | 71 | # Plot convergence 72 | pybop.plot.convergence(optim) 73 | 74 | # Plot the parameter traces 75 | pybop.plot.parameters(optim) 76 | 77 | # Plot the cost landscape with optimisation path 78 | pybop.plot.contour(optim, steps=15) 79 | -------------------------------------------------------------------------------- /examples/scripts/comparison_examples/exponential_decay.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pybamm 3 | 4 | import pybop 5 | 6 | # Define model and use high-performant solver for sensitivities 7 | parameter_set = pybamm.ParameterValues({"k": 1, "y0": 0.5}) 8 | model = pybop.ExponentialDecayModel(parameter_set=parameter_set, n_states=2) 9 | 10 | # Fitting parameters 11 | parameters = pybop.Parameters( 12 | pybop.Parameter( 13 | "k", 14 | prior=pybop.Gaussian(0.5, 0.05), 15 | ), 16 | pybop.Parameter( 17 | "y0", 18 | prior=pybop.Gaussian(0.2, 0.05), 19 | ), 20 | ) 21 | 22 | # Generate data 23 | sigma = 0.003 24 | t_eval = np.linspace(0, 10, 100) 25 | values = model.predict(t_eval=t_eval) 26 | 27 | 28 | def noisy(data, sigma): 29 | return data + np.random.normal(0, sigma, len(data)) 30 | 31 | 32 | # Form dataset 33 | dataset = pybop.Dataset( 34 | { 35 | "Time [s]": t_eval, 36 | "Current function [A]": 0 * t_eval, 37 | "y_0": noisy(values["y_0"].data, sigma), 38 | "y_1": noisy(values["y_1"].data, sigma), 39 | } 40 | ) 41 | 42 | signal = ["y_0", "y_1"] 43 | # Generate problem, cost function, and optimisation class 44 | problem = pybop.FittingProblem(model, parameters, dataset, signal=signal) 45 | cost = pybop.Minkowski(problem, p=2) 46 | optim = pybop.AdamW( 47 | cost, 48 | verbose=True, 49 | allow_infeasible_solutions=True, 50 | sigma0=0.02, 51 | max_iterations=100, 52 | max_unchanged_iterations=20, 53 | ) 54 | 55 | # Run optimisation 56 | results = optim.run() 57 | 58 | # Plot the timeseries output 59 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison") 60 | 61 | # Plot convergence 62 | pybop.plot.convergence(optim) 63 | 64 | # Plot the parameter traces 65 | pybop.plot.parameters(optim) 66 | 67 | # Plot the cost landscape with optimisation path 68 | pybop.plot.surface(optim) 69 | -------------------------------------------------------------------------------- /examples/scripts/comparison_examples/exponential_natural_evolution.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import pybop 4 | 5 | # Define model 6 | parameter_set = pybop.ParameterSet("Chen2020") 7 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set) 8 | 9 | # Fitting parameters 10 | parameters = pybop.Parameters( 11 | pybop.Parameter( 12 | "Negative electrode active material volume fraction", 13 | prior=pybop.Gaussian(0.68, 0.05), 14 | bounds=[0.5, 0.8], 15 | ), 16 | pybop.Parameter( 17 | "Positive electrode active material volume fraction", 18 | prior=pybop.Gaussian(0.58, 0.05), 19 | bounds=[0.4, 0.7], 20 | ), 21 | ) 22 | 23 | sigma = 0.001 24 | t_eval = np.arange(0, 900, 3) 25 | values = model.predict(t_eval=t_eval) 26 | corrupt_values = values["Voltage [V]"].data + np.random.normal(0, sigma, len(t_eval)) 27 | 28 | # Form dataset 29 | dataset = pybop.Dataset( 30 | { 31 | "Time [s]": t_eval, 32 | "Current function [A]": values["Current [A]"].data, 33 | "Voltage [V]": corrupt_values, 34 | } 35 | ) 36 | 37 | # Generate problem, cost function, and optimisation class 38 | problem = pybop.FittingProblem(model, parameters, dataset) 39 | cost = pybop.SumSquaredError(problem) 40 | optim = pybop.XNES(cost, max_iterations=100) 41 | 42 | results = optim.run() 43 | 44 | # Plot the timeseries output 45 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison") 46 | 47 | # Plot convergence 48 | pybop.plot.convergence(optim) 49 | 50 | # Plot the parameter traces 51 | pybop.plot.parameters(optim) 52 | 53 | # Plot the cost landscape with optimisation path 54 | pybop.plot.surface(optim) 55 | 56 | # Plot contour 57 | pybop.plot.contour(optim) 58 | -------------------------------------------------------------------------------- /examples/scripts/comparison_examples/gradient_descent.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import pybop 4 | 5 | # Define model and use high-performant solver for sensitivities 6 | parameter_set = pybop.ParameterSet("Chen2020") 7 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set) 8 | 9 | # Fitting parameters 10 | parameters = pybop.Parameters( 11 | pybop.Parameter( 12 | "Negative electrode active material volume fraction", 13 | prior=pybop.Gaussian(0.6, 0.1), 14 | ), 15 | pybop.Parameter( 16 | "Positive electrode active material volume fraction", 17 | prior=pybop.Gaussian(0.6, 0.1), 18 | ), 19 | ) 20 | 21 | # Generate data 22 | sigma = 0.001 23 | t_eval = np.arange(0, 900, 3) 24 | values = model.predict(t_eval=t_eval) 25 | corrupt_values = values["Voltage [V]"].data + np.random.normal( 26 | 0, sigma, len(values["Voltage [V]"].data) 27 | ) 28 | 29 | # Form dataset 30 | dataset = pybop.Dataset( 31 | { 32 | "Time [s]": values["Time [s]"].data, 33 | "Current function [A]": values["Current [A]"].data, 34 | "Voltage [V]": corrupt_values, 35 | } 36 | ) 37 | 38 | # Generate problem, cost function, and optimisation class 39 | problem = pybop.FittingProblem(model, parameters, dataset) 40 | cost = pybop.RootMeanSquaredError(problem) 41 | optim = pybop.GradientDescent( 42 | cost, 43 | sigma0=[0.6, 0.02], 44 | verbose=True, 45 | max_iterations=75, 46 | ) 47 | 48 | # Run optimisation 49 | results = optim.run() 50 | 51 | # Plot the timeseries output 52 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison") 53 | 54 | # Plot convergence 55 | pybop.plot.convergence(optim) 56 | 57 | # Plot the parameter traces 58 | pybop.plot.parameters(optim) 59 | 60 | # Plot the cost landscape with optimisation path 61 | bounds = np.asarray([[0.5, 0.8], [0.4, 0.7]]) 62 | pybop.plot.surface(optim, bounds=bounds) 63 | -------------------------------------------------------------------------------- /examples/scripts/comparison_examples/irpropmin.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import numpy as np 4 | 5 | import pybop 6 | 7 | # Get the current directory location and convert to absolute path 8 | current_dir = os.path.dirname(os.path.abspath(__file__)) 9 | dataset_path = os.path.join( 10 | current_dir, "../../data/synthetic/spm_charge_discharge_75.csv" 11 | ) 12 | 13 | # Define model and use high-performant solver for sensitivities 14 | parameter_set = pybop.ParameterSet("Chen2020") 15 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set) 16 | 17 | # Construct the fitting parameters 18 | # with initial values sampled from a different distribution 19 | parameters = pybop.Parameters( 20 | pybop.Parameter( 21 | "Negative electrode active material volume fraction", 22 | prior=pybop.Uniform(0.3, 0.9), 23 | bounds=[0.3, 0.8], 24 | initial_value=pybop.Uniform(0.4, 0.75).rvs()[0], 25 | true_value=parameter_set["Negative electrode active material volume fraction"], 26 | ), 27 | pybop.Parameter( 28 | "Positive electrode active material volume fraction", 29 | prior=pybop.Uniform(0.3, 0.9), 30 | initial_value=pybop.Uniform(0.4, 0.75).rvs()[0], 31 | true_value=parameter_set["Positive electrode active material volume fraction"], 32 | # no bounds 33 | ), 34 | ) 35 | 36 | # Import the synthetic dataset, set model initial state 37 | csv_data = np.loadtxt(dataset_path, delimiter=",", skiprows=1) 38 | initial_state = {"Initial open-circuit voltage [V]": csv_data[0, 2]} 39 | model.set_initial_state(initial_state=initial_state) 40 | 41 | # Form dataset 42 | dataset = pybop.Dataset( 43 | { 44 | "Time [s]": csv_data[:, 0], 45 | "Current function [A]": csv_data[:, 1], 46 | "Voltage [V]": csv_data[:, 2], 47 | } 48 | ) 49 | 50 | # Generate problem, cost function, and optimisation class 51 | problem = pybop.FittingProblem( 52 | model, 53 | parameters, 54 | dataset, 55 | ) 56 | likelihood = pybop.GaussianLogLikelihoodKnownSigma(problem, sigma0=2e-3 * 1.05) 57 | posterior = pybop.LogPosterior(likelihood) 58 | optim = pybop.IRPropMin( 59 | posterior, max_iterations=125, max_unchanged_iterations=60, sigma0=0.01 60 | ) 61 | 62 | results = optim.run() 63 | print(parameters.true_value()) 64 | 65 | # Plot the timeseries output 66 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison") 67 | 68 | # Plot convergence 69 | pybop.plot.convergence(optim) 70 | 71 | # Plot the parameter traces 72 | pybop.plot.parameters(optim) 73 | 74 | # Contour plot with optimisation path 75 | bounds = np.asarray([[0.5, 0.8], [0.4, 0.7]]) 76 | pybop.plot.surface(optim, bounds=bounds) 77 | -------------------------------------------------------------------------------- /examples/scripts/comparison_examples/maximum_a_posteriori.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import numpy as np 4 | 5 | import pybop 6 | 7 | # Get the current directory location and convert to absolute path 8 | current_dir = os.path.dirname(os.path.abspath(__file__)) 9 | dataset_path = os.path.join( 10 | current_dir, "../../data/synthetic/spm_charge_discharge_75.csv" 11 | ) 12 | 13 | # Construct and update initial parameter values 14 | parameter_set = pybop.ParameterSet("Chen2020") 15 | parameter_set.update( 16 | { 17 | "Negative electrode active material volume fraction": 0.43, 18 | "Positive electrode active material volume fraction": 0.56, 19 | } 20 | ) 21 | 22 | # Define model 23 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set) 24 | 25 | # Fitting parameters 26 | parameters = pybop.Parameters( 27 | pybop.Parameter( 28 | "Negative electrode active material volume fraction", 29 | prior=pybop.Uniform(0.3, 0.8), 30 | bounds=[0.3, 0.8], 31 | initial_value=0.653, 32 | true_value=parameter_set["Negative electrode active material volume fraction"], 33 | transformation=pybop.LogTransformation(), 34 | ), 35 | pybop.Parameter( 36 | "Positive electrode active material volume fraction", 37 | prior=pybop.Uniform(0.3, 0.8), 38 | bounds=[0.4, 0.7], 39 | initial_value=0.657, 40 | true_value=parameter_set["Positive electrode active material volume fraction"], 41 | transformation=pybop.LogTransformation(), 42 | ), 43 | ) 44 | 45 | # Import the synthetic dataset, set model initial state 46 | csv_data = np.loadtxt(dataset_path, delimiter=",", skiprows=1) 47 | initial_state = {"Initial open-circuit voltage [V]": csv_data[0, 2]} 48 | model.set_initial_state(initial_state=initial_state) 49 | 50 | # Form dataset 51 | dataset = pybop.Dataset( 52 | { 53 | "Time [s]": csv_data[:, 0], 54 | "Current function [A]": csv_data[:, 1], 55 | "Voltage [V]": csv_data[:, 2], 56 | "Bulk open-circuit voltage [V]": csv_data[:, 3], 57 | } 58 | ) 59 | 60 | # Generate problem, cost function, and optimisation class 61 | problem = pybop.FittingProblem( 62 | model, 63 | parameters, 64 | dataset, 65 | ) 66 | cost = pybop.LogPosterior(pybop.GaussianLogLikelihood(problem)) 67 | optim = pybop.IRPropMin( 68 | cost, 69 | sigma0=0.05, 70 | max_unchanged_iterations=20, 71 | min_iterations=20, 72 | max_iterations=100, 73 | ) 74 | 75 | # Run the optimisation 76 | results = optim.run() 77 | print("True parameters:", parameters.true_value()) 78 | 79 | # Plot the timeseries output 80 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison") 81 | 82 | # Plot convergence 83 | pybop.plot.convergence(optim) 84 | 85 | # Plot the parameter traces 86 | pybop.plot.parameters(optim) 87 | 88 | # Plot the cost landscape 89 | pybop.plot.contour(cost, steps=15) 90 | 91 | # Plot the cost landscape with optimisation path 92 | bounds = np.asarray([[0.35, 0.7], [0.45, 0.625]]) 93 | pybop.plot.contour(optim, bounds=bounds, steps=15) 94 | -------------------------------------------------------------------------------- /examples/scripts/comparison_examples/maximum_likelihood.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import numpy as np 4 | 5 | import pybop 6 | 7 | # Get the current directory location and convert to absolute path 8 | current_dir = os.path.dirname(os.path.abspath(__file__)) 9 | dataset_path = os.path.join( 10 | current_dir, "../../data/synthetic/spm_charge_discharge_75.csv" 11 | ) 12 | 13 | # Define model and set initial parameter values 14 | parameter_set = pybop.ParameterSet("Chen2020") 15 | parameter_set.update( 16 | { 17 | "Negative electrode active material volume fraction": 0.63, 18 | "Positive electrode active material volume fraction": 0.51, 19 | } 20 | ) 21 | model = pybop.lithium_ion.SPMe(parameter_set=parameter_set) 22 | 23 | # Fitting parameters 24 | parameters = pybop.Parameters( 25 | pybop.Parameter( 26 | "Negative electrode active material volume fraction", 27 | prior=pybop.Gaussian(0.6, 0.05), 28 | bounds=[0.5, 0.8], 29 | ), 30 | pybop.Parameter( 31 | "Positive electrode active material volume fraction", 32 | prior=pybop.Gaussian(0.48, 0.05), 33 | ), 34 | ) 35 | 36 | # Import the synthetic dataset, set model initial state 37 | csv_data = np.loadtxt(dataset_path, delimiter=",", skiprows=1) 38 | initial_state = {"Initial open-circuit voltage [V]": csv_data[0, 2]} 39 | model.set_initial_state(initial_state=initial_state) 40 | 41 | # Form dataset 42 | dataset = pybop.Dataset( 43 | { 44 | "Time [s]": csv_data[:, 0], 45 | "Current function [A]": csv_data[:, 1], 46 | "Voltage [V]": csv_data[:, 2], 47 | "Bulk open-circuit voltage [V]": csv_data[:, 3], 48 | } 49 | ) 50 | 51 | # Generate problem, cost function, and optimisation class 52 | problem = pybop.FittingProblem( 53 | model, 54 | parameters, 55 | dataset, 56 | ) 57 | likelihood = pybop.GaussianLogLikelihood(problem, sigma0=8e-3) 58 | optim = pybop.IRPropMin( 59 | likelihood, 60 | max_unchanged_iterations=20, 61 | min_iterations=20, 62 | max_iterations=100, 63 | ) 64 | 65 | # Run the optimisation 66 | results = optim.run() 67 | 68 | # Plot the timeseries output 69 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison") 70 | 71 | # Plot convergence 72 | pybop.plot.convergence(optim) 73 | 74 | # Plot the parameter traces 75 | pybop.plot.parameters(optim) 76 | 77 | # Plot the cost landscape with optimisation path 78 | bounds = np.asarray([[0.55, 0.77], [0.48, 0.68]]) 79 | pybop.plot.contour(optim, bounds=bounds, steps=15) 80 | -------------------------------------------------------------------------------- /examples/scripts/comparison_examples/nelder_mead.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import pybop 4 | 5 | # Parameter set and model definition 6 | parameter_set = pybop.ParameterSet("Chen2020") 7 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set) 8 | 9 | # Fitting parameters 10 | parameters = pybop.Parameters( 11 | pybop.Parameter( 12 | "Negative electrode active material volume fraction", 13 | prior=pybop.Gaussian(0.68, 0.05), 14 | ), 15 | pybop.Parameter( 16 | "Positive electrode active material volume fraction", 17 | prior=pybop.Gaussian(0.58, 0.05), 18 | ), 19 | ) 20 | 21 | # Generate data 22 | sigma = 0.003 23 | experiment = pybop.Experiment( 24 | [ 25 | ( 26 | "Discharge at 0.5C for 3 minutes (3 second period)", 27 | "Charge at 0.5C for 3 minutes (3 second period)", 28 | ), 29 | ] 30 | * 2 31 | ) 32 | values = model.predict(initial_state={"Initial SoC": 0.5}, experiment=experiment) 33 | 34 | 35 | def noisy(data, sigma): 36 | return data + np.random.normal(0, sigma, len(data)) 37 | 38 | 39 | # Form dataset 40 | dataset = pybop.Dataset( 41 | { 42 | "Time [s]": values["Time [s]"].data, 43 | "Current function [A]": values["Current [A]"].data, 44 | "Voltage [V]": noisy(values["Voltage [V]"].data, sigma), 45 | "Bulk open-circuit voltage [V]": noisy( 46 | values["Bulk open-circuit voltage [V]"].data, sigma 47 | ), 48 | } 49 | ) 50 | 51 | # Generate problem, cost function, and optimisation class 52 | signal = ["Voltage [V]", "Bulk open-circuit voltage [V]"] 53 | problem = pybop.FittingProblem( 54 | model, 55 | parameters, 56 | dataset, 57 | signal=signal, 58 | initial_state={"Initial open-circuit voltage [V]": dataset["Voltage [V]"][0]}, 59 | ) 60 | cost = pybop.RootMeanSquaredError(problem) 61 | optim = pybop.NelderMead( 62 | cost, 63 | verbose=True, 64 | allow_infeasible_solutions=True, 65 | sigma0=0.05, 66 | max_iterations=100, 67 | max_unchanged_iterations=20, 68 | ) 69 | 70 | # Run optimisation 71 | results = optim.run() 72 | 73 | # Plot the timeseries output 74 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison") 75 | 76 | # Plot convergence 77 | pybop.plot.convergence(optim) 78 | 79 | # Plot the parameter traces 80 | pybop.plot.parameters(optim) 81 | 82 | # Plot the cost landscape with optimisation path 83 | pybop.plot.surface(optim) 84 | -------------------------------------------------------------------------------- /examples/scripts/comparison_examples/particle_swarm.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import pybop 4 | 5 | # Define model 6 | parameter_set = pybop.ParameterSet("Chen2020") 7 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set) 8 | 9 | # Fitting parameters 10 | parameters = pybop.Parameters( 11 | pybop.Parameter( 12 | "Negative electrode active material volume fraction", 13 | prior=pybop.Gaussian(0.6, 0.05), 14 | bounds=[0.5, 0.8], 15 | ), 16 | pybop.Parameter( 17 | "Positive electrode active material volume fraction", 18 | prior=pybop.Gaussian(0.48, 0.05), 19 | bounds=[0.4, 0.7], 20 | ), 21 | ) 22 | 23 | sigma = 0.002 24 | t_eval = np.arange(0, 900, 3) 25 | values = model.predict(t_eval=t_eval) 26 | corrupt_values = values["Voltage [V]"].data + np.random.normal(0, sigma, len(t_eval)) 27 | 28 | # Form dataset 29 | dataset = pybop.Dataset( 30 | { 31 | "Time [s]": t_eval, 32 | "Current function [A]": values["Current [A]"].data, 33 | "Voltage [V]": corrupt_values, 34 | } 35 | ) 36 | 37 | # Generate problem, cost function, and optimisation class 38 | problem = pybop.FittingProblem(model, parameters, dataset) 39 | cost = pybop.SumSquaredError(problem) 40 | optim = pybop.Optimisation( 41 | cost, 42 | optimiser=pybop.PSO, 43 | sigma0=0.05, 44 | max_unchanged_iterations=20, 45 | max_iterations=100, 46 | ) 47 | 48 | results = optim.run() 49 | 50 | # Plot the timeseries output 51 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison") 52 | 53 | # Plot convergence 54 | pybop.plot.convergence(optim) 55 | 56 | # Plot the parameter traces 57 | pybop.plot.parameters(optim) 58 | 59 | # Plot the cost surface 60 | bounds = np.asarray([[0.5, 0.8], [0.4, 0.7]]) 61 | pybop.plot.surface(optim, bounds) 62 | 63 | # Plot the cost landscape with optimisation path 64 | pybop.plot.contour(optim, steps=15, bounds=bounds) 65 | -------------------------------------------------------------------------------- /examples/scripts/comparison_examples/random_search.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import pybop 4 | 5 | # Define model 6 | parameter_set = pybop.ParameterSet("Chen2020") 7 | parameter_set.update( 8 | { 9 | "Negative electrode active material volume fraction": 0.7, 10 | "Positive electrode active material volume fraction": 0.67, 11 | } 12 | ) 13 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set) 14 | 15 | # Fitting parameters 16 | parameters = pybop.Parameters( 17 | pybop.Parameter( 18 | "Negative electrode active material volume fraction", 19 | bounds=[0.4, 0.75], 20 | initial_value=0.41, 21 | ), 22 | pybop.Parameter( 23 | "Positive electrode active material volume fraction", 24 | bounds=[0.4, 0.75], 25 | initial_value=0.41, 26 | ), 27 | ) 28 | experiment = pybop.Experiment( 29 | [ 30 | ( 31 | "Discharge at 0.5C for 3 minutes (4 second period)", 32 | "Charge at 0.5C for 3 minutes (4 second period)", 33 | ), 34 | ] 35 | ) 36 | values = model.predict(initial_state={"Initial SoC": 0.7}, experiment=experiment) 37 | 38 | sigma = 0.002 39 | corrupt_values = values["Voltage [V]"].data + np.random.normal( 40 | 0, sigma, len(values["Voltage [V]"].data) 41 | ) 42 | 43 | # Form dataset 44 | dataset = pybop.Dataset( 45 | { 46 | "Time [s]": values["Time [s]"].data, 47 | "Current function [A]": values["Current [A]"].data, 48 | "Voltage [V]": corrupt_values, 49 | } 50 | ) 51 | 52 | # Generate problem, cost function, and optimisation class 53 | problem = pybop.FittingProblem(model, parameters, dataset) 54 | cost = pybop.GaussianLogLikelihood(problem, sigma0=sigma * 4) 55 | optim = pybop.Optimisation( 56 | cost, 57 | optimiser=pybop.RandomSearch, 58 | max_iterations=100, 59 | ) 60 | 61 | results = optim.run() 62 | 63 | # Plot the timeseries output 64 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison") 65 | 66 | # Plot convergence 67 | pybop.plot.convergence(optim) 68 | 69 | # Plot the parameter traces 70 | pybop.plot.parameters(optim) 71 | 72 | # Plot the cost landscape with optimisation path 73 | pybop.plot.contour(optim, steps=10) 74 | -------------------------------------------------------------------------------- /examples/scripts/comparison_examples/scipy_minimize.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import pybop 4 | 5 | # Define model 6 | parameter_set = pybop.ParameterSet("Chen2020") 7 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set) 8 | 9 | # Fitting parameters 10 | parameters = pybop.Parameters( 11 | pybop.Parameter( 12 | "Negative electrode active material volume fraction", 13 | prior=pybop.Gaussian(0.6, 0.05), 14 | bounds=[0.5, 0.8], 15 | ), 16 | pybop.Parameter( 17 | "Positive electrode active material volume fraction", 18 | prior=pybop.Gaussian(0.48, 0.05), 19 | bounds=[0.4, 0.7], 20 | ), 21 | ) 22 | 23 | sigma = 0.001 24 | t_eval = np.arange(0, 900, 3) 25 | values = model.predict(t_eval=t_eval) 26 | corrupt_values = values["Voltage [V]"].data + np.random.normal( 27 | 0, sigma, len(values["Voltage [V]"].data) 28 | ) 29 | 30 | dataset = pybop.Dataset( 31 | { 32 | "Time [s]": values["Time [s]"].data, 33 | "Current function [A]": values["Current [A]"].data, 34 | "Voltage [V]": corrupt_values, 35 | } 36 | ) 37 | 38 | # Generate problem, cost function, and optimisation class 39 | problem = pybop.FittingProblem(model, parameters, dataset) 40 | cost = pybop.SumSquaredError(problem) 41 | optim = pybop.SciPyMinimize( 42 | cost, 43 | max_iterations=100, 44 | multistart=1, 45 | method="L-BFGS-B", 46 | jac=True, 47 | n_sensitivity_samples=256, 48 | ) 49 | 50 | results = optim.run() 51 | 52 | # Plot the timeseries output 53 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison") 54 | 55 | # Plot convergence 56 | pybop.plot.convergence(optim) 57 | 58 | # Plot the parameter traces 59 | pybop.plot.parameters(optim) 60 | 61 | # Plot the cost landscape with optimisation path 62 | pybop.plot.surface(optim) 63 | -------------------------------------------------------------------------------- /examples/scripts/comparison_examples/selecting_a_solver.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import numpy as np 4 | import pybamm 5 | 6 | import pybop 7 | 8 | # Parameter set and model definition 9 | parameter_set = pybop.ParameterSet("Chen2020") 10 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set) 11 | 12 | solvers = [ 13 | pybamm.IDAKLUSolver(atol=1e-6, rtol=1e-6), 14 | pybamm.CasadiSolver(mode="safe", atol=1e-6, rtol=1e-6), 15 | pybamm.CasadiSolver(mode="fast with events", atol=1e-6, rtol=1e-6), 16 | ] 17 | 18 | # Fitting parameters 19 | parameters = pybop.Parameters( 20 | pybop.Parameter( 21 | "Negative electrode active material volume fraction", initial_value=0.55 22 | ), 23 | pybop.Parameter( 24 | "Positive electrode active material volume fraction", initial_value=0.55 25 | ), 26 | ) 27 | 28 | # Define test protocol and generate data 29 | experiment = pybop.Experiment([("Discharge at 0.5C for 10 minutes (3 second period)")]) 30 | values = model.predict( 31 | initial_state={"Initial open-circuit voltage [V]": 4.2}, experiment=experiment 32 | ) 33 | 34 | # Form dataset 35 | dataset = pybop.Dataset( 36 | { 37 | "Time [s]": values["Time [s]"].data, 38 | "Current function [A]": values["Current [A]"].data, 39 | "Voltage [V]": values["Voltage [V]"].data, 40 | } 41 | ) 42 | 43 | # Create the list of input dicts 44 | n = 150 # Number of solves 45 | inputs = list(zip(np.linspace(0.45, 0.6, n), np.linspace(0.45, 0.6, n))) 46 | 47 | # Iterate over the solvers and print benchmarks 48 | for solver in solvers: 49 | model.solver = solver 50 | problem = pybop.FittingProblem(model, parameters, dataset) 51 | 52 | start_time = time.time() 53 | for input_values in inputs: 54 | problem.evaluate(inputs=input_values) 55 | print(f"Time Evaluate {solver.name}: {time.time() - start_time:.3f}") 56 | 57 | start_time = time.time() 58 | for input_values in inputs: 59 | problem.evaluateS1(inputs=input_values) 60 | print(f"Time EvaluateS1 {solver.name}: {time.time() - start_time:.3f}") 61 | -------------------------------------------------------------------------------- /examples/scripts/comparison_examples/simulated_annealing.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import pybop 4 | 5 | # Define model and use high-performant solver for sensitivities 6 | parameter_set = pybop.ParameterSet.pybamm("Chen2020") 7 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set) 8 | 9 | # Fitting parameters 10 | parameters = pybop.Parameters( 11 | pybop.Parameter( 12 | "Negative electrode active material volume fraction", 13 | prior=pybop.Gaussian(0.6, 0.1), 14 | bounds=[0.4, 0.85], 15 | ), 16 | pybop.Parameter( 17 | "Positive electrode active material volume fraction", 18 | prior=pybop.Gaussian(0.6, 0.1), 19 | bounds=[0.4, 0.85], 20 | ), 21 | ) 22 | 23 | # Generate data 24 | sigma = 0.001 25 | t_eval = np.arange(0, 900, 3) 26 | values = model.predict(t_eval=t_eval) 27 | corrupt_values = values["Voltage [V]"].data + np.random.normal( 28 | 0, sigma, len(values["Voltage [V]"].data) 29 | ) 30 | 31 | # Form dataset 32 | dataset = pybop.Dataset( 33 | { 34 | "Time [s]": values["Time [s]"].data, 35 | "Current function [A]": values["Current [A]"].data, 36 | "Voltage [V]": corrupt_values, 37 | } 38 | ) 39 | 40 | # Generate problem, cost function, and optimisation class 41 | problem = pybop.FittingProblem(model, parameters, dataset) 42 | cost = pybop.RootMeanSquaredError(problem) 43 | optim = pybop.SimulatedAnnealing( 44 | cost, 45 | max_iterations=120, 46 | max_unchanged_iterations=60, 47 | ) 48 | 49 | # Update initial temperature and cooling rate 50 | # for the reduced number of iterations 51 | optim.optimiser.temperature = 0.9 52 | optim.optimiser.cooling_rate = 0.8 53 | 54 | # Run optimisation 55 | results = optim.run() 56 | 57 | # Plot the timeseries output 58 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison") 59 | 60 | # Plot convergence 61 | pybop.plot.convergence(optim) 62 | 63 | # Plot the parameter traces 64 | pybop.plot.parameters(optim) 65 | 66 | # Plot the cost landscape with optimisation path 67 | bounds = np.asarray([[0.5, 0.8], [0.4, 0.7]]) 68 | pybop.plot.surface(optim, bounds=bounds) 69 | -------------------------------------------------------------------------------- /examples/scripts/comparison_examples/stochastic_natural_evolution.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import pybop 4 | 5 | # Define model 6 | parameter_set = pybop.ParameterSet("Chen2020") 7 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set) 8 | 9 | # Fitting parameters 10 | parameters = pybop.Parameters( 11 | pybop.Parameter( 12 | "Negative electrode active material volume fraction", 13 | prior=pybop.Gaussian(0.6, 0.05), 14 | bounds=[0.5, 0.8], 15 | ), 16 | pybop.Parameter( 17 | "Positive electrode active material volume fraction", 18 | prior=pybop.Gaussian(0.48, 0.05), 19 | bounds=[0.4, 0.7], 20 | ), 21 | ) 22 | 23 | sigma = 0.001 24 | t_eval = np.arange(0, 900, 3) 25 | values = model.predict(t_eval=t_eval) 26 | corrupt_values = values["Voltage [V]"].data + np.random.normal(0, sigma, len(t_eval)) 27 | 28 | dataset = pybop.Dataset( 29 | { 30 | "Time [s]": t_eval, 31 | "Current function [A]": values["Current [A]"].data, 32 | "Voltage [V]": corrupt_values, 33 | } 34 | ) 35 | 36 | # Generate problem, cost function, and optimisation class 37 | problem = pybop.FittingProblem(model, parameters, dataset) 38 | cost = pybop.SumOfPower(problem, p=2) 39 | optim = pybop.SNES(cost, max_iterations=100) 40 | 41 | results = optim.run() 42 | 43 | # Plot the timeseries output 44 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison") 45 | 46 | # Plot convergence 47 | pybop.plot.convergence(optim) 48 | 49 | # Plot the parameter traces 50 | pybop.plot.parameters(optim) 51 | 52 | # Plot the cost landscape with optimisation path 53 | pybop.plot.surface(optim) 54 | -------------------------------------------------------------------------------- /examples/scripts/comparison_examples/unscented_kalman_filter.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from pybamm import CasadiSolver 3 | 4 | import pybop 5 | 6 | # Parameter set and model definition 7 | parameter_set = pybop.ParameterSet("Chen2020") 8 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set, solver=CasadiSolver()) 9 | 10 | # Fitting parameters 11 | parameters = pybop.Parameters( 12 | pybop.Parameter( 13 | "Negative electrode active material volume fraction", 14 | prior=pybop.Gaussian(0.6, 0.05), 15 | bounds=[0.5, 0.8], 16 | ), 17 | pybop.Parameter( 18 | "Positive electrode active material volume fraction", 19 | prior=pybop.Gaussian(0.48, 0.05), 20 | bounds=[0.4, 0.7], 21 | ), 22 | ) 23 | 24 | # Make a prediction with measurement noise 25 | sigma = 0.001 26 | t_eval = np.arange(0, 150, 0.5) 27 | values = model.predict(t_eval=t_eval) 28 | corrupt_values = values["Voltage [V]"].data + np.random.normal(0, sigma, len(t_eval)) 29 | 30 | # Form dataset 31 | dataset = pybop.Dataset( 32 | { 33 | "Time [s]": t_eval, 34 | "Current function [A]": values["Current [A]"].data, 35 | "Voltage [V]": corrupt_values, 36 | } 37 | ) 38 | 39 | # Build the model to get the number of states 40 | model.build(dataset=dataset, parameters=parameters) 41 | 42 | # Define the UKF observer, setting the particle boundaries as uncertain states 43 | covariance = np.diag([0] * 20 + [sigma**2] + [0] * 20 + [sigma**2]) 44 | process_noise = np.diag([0] * 20 + [1e-6] + [0] * 20 + [1e-6]) 45 | measurement_noise = np.diag([sigma**2]) 46 | observer = pybop.UnscentedKalmanFilterObserver( 47 | parameters, 48 | model, 49 | covariance, 50 | process_noise, 51 | measurement_noise, 52 | dataset, 53 | ) 54 | 55 | # Generate problem, cost function, and optimisation class 56 | cost = pybop.ObserverCost(observer) 57 | optim = pybop.NelderMead(cost, verbose=True) 58 | 59 | # Parameter identification using the current observer implementation is very slow 60 | # so let's restrict the number of iterations and reduce the number of plots 61 | optim.set_max_iterations(5) 62 | 63 | # Run optimisation 64 | results = optim.run() 65 | 66 | # Plot the timeseries output (requires model that returns Voltage) 67 | pybop.plot.problem(observer, problem_inputs=results.x, title="Optimised Comparison") 68 | 69 | # Plot convergence 70 | pybop.plot.convergence(optim) 71 | 72 | # Plot the parameter traces 73 | pybop.plot.parameters(optim) 74 | 75 | # Plot the cost landscape with optimisation path 76 | pybop.plot.surface(optim) 77 | -------------------------------------------------------------------------------- /examples/scripts/design_optimisation/maximising_energy.py: -------------------------------------------------------------------------------- 1 | from pybamm import Parameter 2 | 3 | import pybop 4 | 5 | # A design optimisation example loosely based on work by L.D. Couto 6 | # available at https://doi.org/10.1016/j.energy.2022.125966. 7 | 8 | # The target is to maximise the energy density over a range of 9 | # possible design parameter values, including for example: 10 | # cross-sectional area = height x width (only need change one) 11 | # electrode widths, particle radii, volume fractions and 12 | # separator width. 13 | 14 | # Define parameter set and additional parameters needed for the cost function 15 | parameter_set = pybop.ParameterSet("Chen2020", formation_concentrations=True) 16 | parameter_set.update( 17 | { 18 | "Electrolyte density [kg.m-3]": Parameter("Separator density [kg.m-3]"), 19 | "Negative electrode active material density [kg.m-3]": Parameter( 20 | "Negative electrode density [kg.m-3]" 21 | ), 22 | "Negative electrode carbon-binder density [kg.m-3]": Parameter( 23 | "Negative electrode density [kg.m-3]" 24 | ), 25 | "Positive electrode active material density [kg.m-3]": Parameter( 26 | "Positive electrode density [kg.m-3]" 27 | ), 28 | "Positive electrode carbon-binder density [kg.m-3]": Parameter( 29 | "Positive electrode density [kg.m-3]" 30 | ), 31 | }, 32 | check_already_exists=False, 33 | ) 34 | 35 | # Define model 36 | model = pybop.lithium_ion.SPMe(parameter_set=parameter_set) 37 | 38 | # Fitting parameters 39 | parameters = pybop.Parameters( 40 | pybop.Parameter( 41 | "Positive electrode thickness [m]", 42 | prior=pybop.Gaussian(7.56e-05, 0.1e-05), 43 | bounds=[65e-06, 10e-05], 44 | ), 45 | pybop.Parameter( 46 | "Positive particle radius [m]", 47 | prior=pybop.Gaussian(5.22e-06, 0.1e-06), 48 | bounds=[2e-06, 9e-06], 49 | ), 50 | ) 51 | 52 | # Define test protocol 53 | experiment = pybop.Experiment( 54 | [ 55 | "Discharge at 1C until 2.5 V (10 seconds period)", 56 | "Hold at 2.5 V for 30 minutes or until 10 mA (10 seconds period)", 57 | ], 58 | ) 59 | signal = ["Voltage [V]", "Current [A]"] 60 | 61 | # Generate problem 62 | problem = pybop.DesignProblem( 63 | model, 64 | parameters, 65 | experiment, 66 | signal=signal, 67 | initial_state={"Initial SoC": 1.0}, 68 | update_capacity=True, 69 | ) 70 | 71 | # Generate multiple cost functions and combine them 72 | cost1 = pybop.GravimetricEnergyDensity(problem) 73 | cost2 = pybop.VolumetricEnergyDensity(problem) 74 | cost = pybop.WeightedCost(cost1, cost2, weights=[1, 1e-3]) 75 | 76 | # Run optimisation 77 | optim = pybop.PSO( 78 | cost, verbose=True, allow_infeasible_solutions=False, max_iterations=10 79 | ) 80 | results = optim.run() 81 | print(f"Initial gravimetric energy density: {cost1(optim.x0):.2f} Wh.kg-1") 82 | print(f"Optimised gravimetric energy density: {cost1(results.x):.2f} Wh.kg-1") 83 | print(f"Initial volumetric energy density: {cost2(optim.x0):.2f} Wh.m-3") 84 | print(f"Optimised volumetric energy density: {cost2(results.x):.2f} Wh.m-3") 85 | 86 | # Plot the timeseries output 87 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison") 88 | 89 | # Plot the cost landscape with optimisation path 90 | pybop.plot.surface(optim) 91 | -------------------------------------------------------------------------------- /examples/scripts/design_optimisation/maximising_power.py: -------------------------------------------------------------------------------- 1 | from pybamm import Parameter 2 | 3 | import pybop 4 | 5 | # Define parameter set and additional parameters needed for the cost function 6 | parameter_set = pybop.ParameterSet("Chen2020", formation_concentrations=True) 7 | parameter_set.update( 8 | { 9 | "Electrolyte density [kg.m-3]": Parameter("Separator density [kg.m-3]"), 10 | "Negative electrode active material density [kg.m-3]": Parameter( 11 | "Negative electrode density [kg.m-3]" 12 | ), 13 | "Negative electrode carbon-binder density [kg.m-3]": Parameter( 14 | "Negative electrode density [kg.m-3]" 15 | ), 16 | "Positive electrode active material density [kg.m-3]": Parameter( 17 | "Positive electrode density [kg.m-3]" 18 | ), 19 | "Positive electrode carbon-binder density [kg.m-3]": Parameter( 20 | "Positive electrode density [kg.m-3]" 21 | ), 22 | }, 23 | check_already_exists=False, 24 | ) 25 | 26 | # Define model 27 | model = pybop.lithium_ion.SPMe(parameter_set=parameter_set) 28 | 29 | # Define useful quantities 30 | nominal_capacity = parameter_set["Nominal cell capacity [A.h]"] 31 | target_c_rate = 2 32 | discharge_rate = target_c_rate * nominal_capacity 33 | 34 | # Fitting parameters 35 | parameters = pybop.Parameters( 36 | pybop.Parameter( 37 | "Positive electrode thickness [m]", 38 | prior=pybop.Gaussian(7.56e-05, 0.5e-05), 39 | bounds=[65e-06, 10e-05], 40 | ), 41 | pybop.Parameter( 42 | "Nominal cell capacity [A.h]", # controls the C-rate in the experiment 43 | prior=pybop.Gaussian(discharge_rate, 0.2), 44 | bounds=[0.8 * discharge_rate, 1.2 * discharge_rate], 45 | ), 46 | ) 47 | 48 | # Define test protocol 49 | experiment = pybop.Experiment( 50 | ["Discharge at 1C for 30 minutes or until 2.5 V (5 seconds period)"], 51 | ) 52 | signal = ["Voltage [V]", "Current [A]"] 53 | 54 | # Generate problem 55 | problem = pybop.DesignProblem( 56 | model, 57 | parameters, 58 | experiment, 59 | signal=signal, 60 | initial_state={"Initial SoC": 1.0}, 61 | ) 62 | 63 | # Generate multiple cost functions and combine them 64 | cost1 = pybop.GravimetricPowerDensity(problem, target_time=3600 / target_c_rate) 65 | cost2 = pybop.VolumetricPowerDensity(problem, target_time=3600 / target_c_rate) 66 | cost = pybop.WeightedCost(cost1, cost2, weights=[1, 1e-3]) 67 | 68 | # Run optimisation 69 | optim = pybop.XNES( 70 | cost, verbose=True, allow_infeasible_solutions=False, max_iterations=10 71 | ) 72 | results = optim.run() 73 | print(f"Initial gravimetric power density: {cost1(optim.x0):.2f} W.kg-1") 74 | print(f"Optimised gravimetric power density: {cost1(results.x):.2f} W.kg-1") 75 | print(f"Initial volumetric power density: {cost2(optim.x0):.2f} W.m-3") 76 | print(f"Optimised volumetric power density: {cost2(results.x):.2f} W.m-3") 77 | print( 78 | f"Optimised discharge rate: {results.x[-1]:.2f} A = {results.x[-1] / nominal_capacity:.2f} C" 79 | ) 80 | 81 | # Plot the timeseries output 82 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison") 83 | 84 | # Plot the cost landscape with optimisation path 85 | pybop.plot.surface(optim) 86 | -------------------------------------------------------------------------------- /examples/scripts/getting_started/ask-tell-interface.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import numpy as np 4 | 5 | import pybop 6 | 7 | # Get the current directory location and convert to absolute path 8 | current_dir = os.path.dirname(os.path.abspath(__file__)) 9 | dataset_path = os.path.join( 10 | current_dir, "../../data/synthetic/spm_charge_discharge_75.csv" 11 | ) 12 | 13 | # Define model 14 | parameter_set = pybop.ParameterSet.pybamm("Chen2020") 15 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set) 16 | 17 | # Fitting parameters 18 | parameters = pybop.Parameters( 19 | pybop.Parameter( 20 | "Negative electrode active material volume fraction", 21 | prior=pybop.Gaussian(0.55, 0.05), 22 | ), 23 | pybop.Parameter( 24 | "Positive electrode active material volume fraction", 25 | prior=pybop.Gaussian(0.55, 0.05), 26 | ), 27 | ) 28 | 29 | # Import the synthetic dataset, set model initial state 30 | csv_data = np.loadtxt(dataset_path, delimiter=",", skiprows=1) 31 | initial_state = {"Initial open-circuit voltage [V]": csv_data[0, 2]} 32 | model.set_initial_state(initial_state=initial_state) 33 | 34 | # Form dataset 35 | dataset = pybop.Dataset( 36 | { 37 | "Time [s]": csv_data[:, 0], 38 | "Current function [A]": csv_data[:, 1], 39 | "Voltage [V]": csv_data[:, 2], 40 | "Bulk open-circuit voltage [V]": csv_data[:, 3], 41 | } 42 | ) 43 | 44 | 45 | signal = ["Voltage [V]", "Bulk open-circuit voltage [V]"] 46 | # Construct the problem and cost classes 47 | problem = pybop.FittingProblem( 48 | model, 49 | parameters, 50 | dataset, 51 | signal=signal, 52 | ) 53 | cost = pybop.Minkowski(problem, p=2) 54 | 55 | # We construct the optimiser class the same as normal 56 | # but will be using the `optimiser` attribute directly 57 | # for this example. This interface works for all 58 | # non SciPy-based optimisers. 59 | # Warning: not all arguments are supported via this 60 | # interface. 61 | optim = pybop.AdamW(cost) 62 | 63 | # Create storage vars 64 | x_best = [] 65 | f_best = [] 66 | 67 | # Run optimisation 68 | for i in range(50): 69 | x = optim.optimiser.ask() 70 | f = [cost(x[0], calculate_grad=True)] 71 | optim.optimiser.tell(f) 72 | 73 | # Store best solution so far 74 | x_best.append(optim.optimiser.x_best()) 75 | f_best.append(optim.optimiser.x_best()) 76 | 77 | if i % 10 == 0: 78 | print( 79 | f"Iteration: {i} | Cost: {optim.optimiser.f_best()} | Parameters: {optim.optimiser.x_best()}" 80 | ) 81 | 82 | # Plot the timeseries output 83 | pybop.plot.problem( 84 | problem, problem_inputs=optim.optimiser.x_best(), title="Optimised Comparison" 85 | ) 86 | -------------------------------------------------------------------------------- /examples/scripts/getting_started/functional_parameters.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pybamm 3 | 4 | import pybop 5 | 6 | # This example demonstrates how to use a pybamm.FunctionalParameter to 7 | # optimise functional parameters using PyBOP. 8 | 9 | # Method: Define a new scalar parameter for use in a functional parameter 10 | # that already exists in the model, for example an exchange current density. 11 | 12 | 13 | # Load parameter set 14 | parameter_set = pybop.ParameterSet("Chen2020") 15 | 16 | 17 | # Define a new function using pybamm parameters 18 | def positive_electrode_exchange_current_density(c_e, c_s_surf, c_s_max, T): 19 | # New parameters 20 | j0_ref = pybamm.Parameter( 21 | "Positive electrode reference exchange-current density [A.m-2]" 22 | ) 23 | alpha = pybamm.Parameter("Positive electrode charge transfer coefficient") 24 | 25 | # Existing parameters 26 | c_e_init = pybamm.Parameter("Initial concentration in electrolyte [mol.m-3]") 27 | 28 | return ( 29 | j0_ref 30 | * ((c_e / c_e_init) * (c_s_surf / c_s_max) * (1 - c_s_surf / c_s_max)) ** alpha 31 | ) 32 | 33 | 34 | # Give default values to the new scalar parameters and pass the new function 35 | parameter_set.update( 36 | { 37 | "Positive electrode reference exchange-current density [A.m-2]": 1, 38 | "Positive electrode charge transfer coefficient": 0.5, 39 | }, 40 | check_already_exists=False, 41 | ) 42 | parameter_set["Positive electrode exchange-current density [A.m-2]"] = ( 43 | positive_electrode_exchange_current_density 44 | ) 45 | 46 | # Model definition 47 | model = pybop.lithium_ion.SPM( 48 | parameter_set=parameter_set, options={"contact resistance": "true"} 49 | ) 50 | 51 | # Fitting parameters 52 | parameters = pybop.Parameters( 53 | pybop.Parameter( 54 | "Positive electrode reference exchange-current density [A.m-2]", 55 | prior=pybop.Gaussian(1, 0.1), 56 | ), 57 | pybop.Parameter( 58 | "Positive electrode charge transfer coefficient", 59 | prior=pybop.Gaussian(0.5, 0.1), 60 | ), 61 | ) 62 | 63 | # Generate data 64 | sigma = 0.001 65 | t_eval = np.arange(0, 900, 3) 66 | values = model.predict(t_eval=t_eval) 67 | corrupt_values = values["Voltage [V]"].data + np.random.normal(0, sigma, len(t_eval)) 68 | 69 | # Form dataset 70 | dataset = pybop.Dataset( 71 | { 72 | "Time [s]": t_eval, 73 | "Current function [A]": values["Current [A]"].data, 74 | "Voltage [V]": corrupt_values, 75 | } 76 | ) 77 | 78 | # Generate problem, cost function, and optimisation class 79 | problem = pybop.FittingProblem(model, parameters, dataset) 80 | cost = pybop.RootMeanSquaredError(problem) 81 | optim = pybop.SciPyMinimize(cost, sigma0=0.1, max_iterations=125, verbose=True) 82 | 83 | # Run optimisation 84 | results = optim.run() 85 | 86 | # Plot the timeseries output 87 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison") 88 | 89 | # Plot convergence 90 | pybop.plot.convergence(optim) 91 | 92 | # Plot the parameter traces 93 | pybop.plot.parameters(optim) 94 | 95 | # Plot the cost landscape with optimisation path 96 | pybop.plot.surface(optim) 97 | -------------------------------------------------------------------------------- /examples/scripts/getting_started/jax-solver-example.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import pybop 4 | 5 | # Parameter set and model definition 6 | parameter_set = pybop.ParameterSet("Chen2020") 7 | 8 | # The Jaxified IDAKLU performs very well on high iteration 9 | # identification tasks, due to the just-in-time compilation 10 | model = pybop.lithium_ion.SPMe(parameter_set=parameter_set) 11 | 12 | # Fitting parameters 13 | parameters = pybop.Parameters( 14 | pybop.Parameter( 15 | "Negative electrode active material volume fraction", 16 | initial_value=0.55, 17 | prior=pybop.Gaussian(0.6, 0.03), 18 | bounds=[0.5, 0.8], 19 | ), 20 | pybop.Parameter( 21 | "Positive electrode active material volume fraction", 22 | initial_value=0.55, 23 | prior=pybop.Gaussian(0.6, 0.03), 24 | ), 25 | ) 26 | 27 | # Generate data 28 | sigma = 0.002 29 | experiment = pybop.Experiment( 30 | [ 31 | ( 32 | "Charge at 0.5C for 3 minutes (3 second period)", 33 | "Discharge at 0.5C for 3 minutes (3 second period)", 34 | ), 35 | ] 36 | ) 37 | values = model.predict(initial_state={"Initial SoC": 0.5}, experiment=experiment) 38 | 39 | 40 | def noisy(data, sigma): 41 | return data + np.random.normal(0, sigma, len(data)) 42 | 43 | 44 | # Form dataset 45 | dataset = pybop.Dataset( 46 | { 47 | "Time [s]": values["Time [s]"].data, 48 | "Current function [A]": values["Current [A]"].data, 49 | "Voltage [V]": noisy(values["Voltage [V]"].data, sigma), 50 | } 51 | ) 52 | 53 | # Construct the Problem 54 | problem = pybop.FittingProblem(model, parameters, dataset) 55 | 56 | # By selecting a Jax based cost function, the IDAKLU solver will be 57 | # jaxified (wrapped in a Jax compiled expression) and used for optimisation 58 | cost = pybop.JaxLogNormalLikelihood(problem, sigma0=sigma) 59 | 60 | # Test gradient-based optimiser 61 | optim = pybop.IRPropMin( 62 | cost, sigma0=0.02, max_unchanged_iterations=35, max_iterations=100, verbose=True 63 | ) 64 | 65 | results = optim.run() 66 | 67 | # Plot convergence 68 | pybop.plot.convergence(optim) 69 | 70 | # Plot parameter trace 71 | pybop.plot.parameters(optim) 72 | 73 | # Plot voronoi surface 74 | pybop.plot.surface(optim) 75 | -------------------------------------------------------------------------------- /examples/scripts/getting_started/linked_parameters.py: -------------------------------------------------------------------------------- 1 | from pybamm import Parameter 2 | 3 | import pybop 4 | 5 | # The aim of this script is to show how to systematically update 6 | # design parameters which depend on the optimisation parameters. 7 | 8 | # Define parameter set and additional parameters needed for the cost function 9 | parameter_set = pybop.ParameterSet("Chen2020", formation_concentrations=True) 10 | parameter_set.update( 11 | { 12 | "Electrolyte density [kg.m-3]": Parameter("Separator density [kg.m-3]"), 13 | "Negative electrode active material density [kg.m-3]": Parameter( 14 | "Negative electrode density [kg.m-3]" 15 | ), 16 | "Negative electrode carbon-binder density [kg.m-3]": Parameter( 17 | "Negative electrode density [kg.m-3]" 18 | ), 19 | "Positive electrode active material density [kg.m-3]": Parameter( 20 | "Positive electrode density [kg.m-3]" 21 | ), 22 | "Positive electrode carbon-binder density [kg.m-3]": Parameter( 23 | "Positive electrode density [kg.m-3]" 24 | ), 25 | }, 26 | check_already_exists=False, 27 | ) 28 | 29 | # Link parameters 30 | parameter_set.update( 31 | { 32 | "Positive electrode porosity": 1 33 | - Parameter("Positive electrode active material volume fraction") 34 | } 35 | ) 36 | 37 | # Define model 38 | model = pybop.lithium_ion.SPMe(parameter_set=parameter_set) 39 | 40 | # Fitting parameters 41 | parameters = pybop.Parameters( 42 | pybop.Parameter( 43 | "Positive electrode thickness [m]", 44 | prior=pybop.Gaussian(7.56e-05, 0.1e-05), 45 | bounds=[65e-06, 10e-05], 46 | ), 47 | pybop.Parameter( 48 | "Positive electrode active material volume fraction", 49 | prior=pybop.Gaussian(0.6, 0.15), 50 | bounds=[0.1, 0.9], 51 | ), 52 | ) 53 | 54 | # Define test protocol 55 | experiment = pybop.Experiment( 56 | [ 57 | "Discharge at 1C until 2.5 V (10 seconds period)", 58 | "Hold at 2.5 V for 30 minutes or until 10 mA (10 seconds period)", 59 | ], 60 | ) 61 | signal = ["Voltage [V]", "Current [A]"] 62 | 63 | # Generate problem 64 | problem = pybop.DesignProblem( 65 | model, 66 | parameters, 67 | experiment, 68 | signal=signal, 69 | initial_state={"Initial SoC": 1.0}, 70 | update_capacity=True, 71 | ) 72 | 73 | # Define the cost 74 | cost = pybop.GravimetricEnergyDensity(problem) 75 | 76 | # Run optimisation 77 | optim = pybop.XNES( 78 | cost, verbose=True, allow_infeasible_solutions=False, max_iterations=10 79 | ) 80 | results = optim.run() 81 | print(f"Initial gravimetric energy density: {cost(optim.x0):.2f} Wh.kg-1") 82 | print(f"Optimised gravimetric energy density: {cost(results.x):.2f} Wh.kg-1") 83 | 84 | # Plot the timeseries output 85 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison") 86 | 87 | # Plot the cost landscape with optimisation path 88 | pybop.plot.surface(optim) 89 | -------------------------------------------------------------------------------- /examples/scripts/getting_started/mcmc_example.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import numpy as np 4 | 5 | import pybop 6 | 7 | # Set parallelization if on macOS / Unix 8 | parallel = True if sys.platform != "win32" else False 9 | 10 | # Parameter set and model definition 11 | parameter_set = pybop.ParameterSet("Chen2020") 12 | parameter_set.update( 13 | { 14 | "Negative electrode active material volume fraction": 0.63, 15 | "Positive electrode active material volume fraction": 0.71, 16 | } 17 | ) 18 | synth_model = pybop.lithium_ion.SPMe(parameter_set=parameter_set) 19 | 20 | # Fitting parameters 21 | parameters = pybop.Parameters( 22 | pybop.Parameter( 23 | "Negative electrode active material volume fraction", 24 | prior=pybop.Gaussian(0.68, 0.02), 25 | transformation=pybop.LogTransformation(), 26 | ), 27 | pybop.Parameter( 28 | "Positive electrode active material volume fraction", 29 | prior=pybop.Gaussian(0.65, 0.02), 30 | transformation=pybop.LogTransformation(), 31 | ), 32 | ) 33 | 34 | # Generate data 35 | init_soc = 0.5 36 | sigma = 0.005 37 | experiment = pybop.Experiment( 38 | [ 39 | ("Discharge at 0.5C for 3 minutes (5 second period)",), 40 | ] 41 | ) 42 | values = synth_model.predict( 43 | initial_state={"Initial SoC": init_soc}, experiment=experiment 44 | ) 45 | 46 | 47 | def noisy(data, sigma): 48 | return data + np.random.normal(0, sigma, len(data)) 49 | 50 | 51 | # Form dataset 52 | dataset = pybop.Dataset( 53 | { 54 | "Time [s]": values["Time [s]"].data, 55 | "Current function [A]": values["Current [A]"].data, 56 | "Voltage [V]": noisy(values["Voltage [V]"].data, sigma), 57 | } 58 | ) 59 | 60 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set) 61 | signal = ["Voltage [V]"] 62 | 63 | # Generate problem, likelihood, and sampler 64 | problem = pybop.FittingProblem( 65 | model, parameters, dataset, signal=signal, initial_state={"Initial SoC": init_soc} 66 | ) 67 | likelihood = pybop.GaussianLogLikelihood(problem) 68 | posterior = pybop.LogPosterior(likelihood) 69 | 70 | sampler = pybop.DifferentialEvolutionMCMC( 71 | posterior, 72 | chains=3, 73 | max_iterations=250, # Reduced for CI, increase for improved posteriors 74 | warm_up=100, 75 | verbose=True, 76 | parallel=parallel, # (macOS/WSL/Linux only) 77 | ) 78 | chains = sampler.run() 79 | 80 | # Summary statistics 81 | posterior_summary = pybop.PosteriorSummary(chains) 82 | print(posterior_summary.get_summary_statistics()) 83 | posterior_summary.plot_trace() 84 | posterior_summary.summary_table() 85 | posterior_summary.plot_posterior() 86 | posterior_summary.plot_chains() 87 | posterior_summary.effective_sample_size() 88 | print(f"rhat: {posterior_summary.rhat()}") 89 | -------------------------------------------------------------------------------- /examples/scripts/getting_started/multi_fitting.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import pybop 4 | 5 | # Parameter set and model definition 6 | parameter_set = pybop.ParameterSet("Chen2020") 7 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set) 8 | 9 | # Create initial SOC, experiment objects 10 | init_soc = [{"Initial SoC": 0.8}, {"Initial SoC": 0.6}] 11 | experiment = [ 12 | pybop.Experiment([("Discharge at 0.5C for 2 minutes (4 second period)")]), 13 | pybop.Experiment([("Discharge at 1C for 1 minutes (4 second period)")]), 14 | ] 15 | 16 | # Fitting parameters 17 | parameters = pybop.Parameters( 18 | pybop.Parameter( 19 | "Negative electrode active material volume fraction", 20 | prior=pybop.Gaussian(0.68, 0.05), 21 | true_value=parameter_set["Negative electrode active material volume fraction"], 22 | ), 23 | pybop.Parameter( 24 | "Positive electrode active material volume fraction", 25 | prior=pybop.Gaussian(0.58, 0.05), 26 | true_value=parameter_set["Positive electrode active material volume fraction"], 27 | ), 28 | ) 29 | 30 | # Generate a dataset and a fitting problem 31 | sigma = 0.002 32 | values = model.predict(initial_state=init_soc[0], experiment=experiment[0]) 33 | dataset_1 = pybop.Dataset( 34 | { 35 | "Time [s]": values["Time [s]"].data, 36 | "Current function [A]": values["Current [A]"].data, 37 | "Voltage [V]": values["Voltage [V]"].data 38 | + np.random.normal(0, sigma, len(values["Voltage [V]"].data)), 39 | } 40 | ) 41 | problem_1 = pybop.FittingProblem(model, parameters, dataset_1) 42 | 43 | # Generate a second dataset and problem 44 | model = model.new_copy() 45 | values = model.predict(initial_state=init_soc[1], experiment=experiment[1]) 46 | dataset_2 = pybop.Dataset( 47 | { 48 | "Time [s]": values["Time [s]"].data, 49 | "Current function [A]": values["Current [A]"].data, 50 | "Voltage [V]": values["Voltage [V]"].data 51 | + np.random.normal(0, sigma, len(values["Voltage [V]"].data)), 52 | } 53 | ) 54 | problem_2 = pybop.FittingProblem(model, parameters, dataset_2) 55 | 56 | # Combine the problems into one 57 | problem = pybop.MultiFittingProblem(problem_1, problem_2) 58 | 59 | # Generate the cost function and optimisation class 60 | cost = pybop.SumSquaredError(problem) 61 | optim = pybop.CuckooSearch( 62 | cost, 63 | verbose=True, 64 | sigma0=0.05, 65 | max_unchanged_iterations=20, 66 | max_iterations=100, 67 | ) 68 | 69 | # Run optimisation 70 | results = optim.run() 71 | print("True parameters:", parameters.true_value()) 72 | 73 | # Plot the timeseries output 74 | pybop.plot.problem(problem_1, problem_inputs=results.x, title="Optimised Comparison") 75 | pybop.plot.problem(problem_2, problem_inputs=results.x, title="Optimised Comparison") 76 | 77 | # Plot convergence 78 | pybop.plot.convergence(optim) 79 | 80 | # Plot the parameter traces 81 | pybop.plot.parameters(optim) 82 | 83 | # Plot the cost landscape with optimisation path 84 | pybop.plot.surface(optim) 85 | -------------------------------------------------------------------------------- /examples/scripts/getting_started/multi_start_optimisation.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import pybop 4 | 5 | # Define model and use the high-performant IDAKLU solver for sensitivities 6 | parameter_set = pybop.ParameterSet("Chen2020") 7 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set) 8 | 9 | # Fitting parameters 10 | parameters = pybop.Parameters( 11 | pybop.Parameter( 12 | "Negative electrode active material volume fraction", 13 | prior=pybop.Gaussian(0.6, 0.1), 14 | ), 15 | pybop.Parameter( 16 | "Positive electrode active material volume fraction", 17 | prior=pybop.Gaussian(0.6, 0.1), 18 | ), 19 | ) 20 | 21 | # Generate data 22 | sigma = 0.001 23 | t_eval = np.arange(0, 900, 3) 24 | values = model.predict(t_eval=t_eval) 25 | corrupt_values = values["Voltage [V]"].data + np.random.normal( 26 | 0, sigma, len(values["Voltage [V]"].data) 27 | ) 28 | 29 | # Form dataset 30 | dataset = pybop.Dataset( 31 | { 32 | "Time [s]": values["Time [s]"].data, 33 | "Current function [A]": values["Current [A]"].data, 34 | "Voltage [V]": corrupt_values, 35 | } 36 | ) 37 | 38 | # Generate problem, cost function classes 39 | problem = pybop.FittingProblem(model, parameters, dataset) 40 | cost = pybop.RootMeanSquaredError(problem) 41 | 42 | # Construct the optimiser with 10 multistart runs 43 | # Each of these runs has a random starting position sampled 44 | # from the parameter priors 45 | optim = pybop.GradientDescent( 46 | cost, sigma0=[0.6, 0.02], max_iterations=50, multistart=10, verbose=True 47 | ) 48 | 49 | # Run optimisation 50 | results = optim.run() 51 | 52 | # We can plot the timeseries output, for the best run 53 | # using the results.x attribute 54 | pybop.plot.problem(problem, problem_inputs=results.x_best, title="Optimised Comparison") 55 | 56 | # Plot convergence 57 | pybop.plot.convergence(optim) 58 | 59 | # Plot the parameter traces 60 | pybop.plot.parameters(optim) 61 | 62 | # Plot the cost landscape with optimisation path 63 | bounds = np.asarray([[0.5, 0.8], [0.4, 0.7]]) 64 | pybop.plot.surface(optim, bounds=bounds) 65 | -------------------------------------------------------------------------------- /examples/scripts/getting_started/simple_BPX.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import pybop 4 | 5 | # Define model 6 | parameter_set = pybop.ParameterSet(json_path="examples/parameters/example_BPX.json") 7 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set) 8 | 9 | # Fitting parameters 10 | parameters = pybop.Parameters( 11 | pybop.Parameter( 12 | "Negative particle radius [m]", 13 | prior=pybop.Gaussian(6e-06, 0.1e-6), 14 | bounds=[1e-6, 9e-6], 15 | true_value=parameter_set["Negative particle radius [m]"], 16 | ), 17 | pybop.Parameter( 18 | "Positive particle radius [m]", 19 | prior=pybop.Gaussian(4.5e-07, 0.1e-6), 20 | bounds=[1e-7, 9e-7], 21 | true_value=parameter_set["Positive particle radius [m]"], 22 | ), 23 | ) 24 | 25 | # Generate data 26 | sigma = 0.001 27 | t_eval = np.arange(0, 900, 5) 28 | values = model.predict(t_eval=t_eval) 29 | corrupt_values = values["Voltage [V]"].data + np.random.normal(0, sigma, len(t_eval)) 30 | 31 | # Form dataset 32 | dataset = pybop.Dataset( 33 | { 34 | "Time [s]": t_eval, 35 | "Current function [A]": values["Current [A]"].data, 36 | "Voltage [V]": corrupt_values, 37 | } 38 | ) 39 | 40 | # Generate problem, cost function, and optimisation class 41 | problem = pybop.FittingProblem(model, parameters, dataset) 42 | cost = pybop.SumSquaredError(problem) 43 | optim = pybop.CMAES(cost, max_iterations=40, verbose=True) 44 | 45 | # Run the optimisation 46 | results = optim.run() 47 | print("True parameters:", parameters.true_value()) 48 | 49 | # Plot the timeseries output 50 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison") 51 | 52 | # Plot convergence 53 | pybop.plot.convergence(optim) 54 | 55 | # Plot the parameter traces 56 | pybop.plot.parameters(optim) 57 | 58 | # Plot the cost landscape with optimisation path 59 | pybop.plot.surface(optim) 60 | -------------------------------------------------------------------------------- /examples/scripts/getting_started/simple_dfn.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import numpy as np 4 | 5 | import pybop 6 | 7 | # Get the current directory location and convert to absolute path 8 | current_dir = os.path.dirname(os.path.abspath(__file__)) 9 | dataset_path = os.path.join( 10 | current_dir, "../../data/synthetic/dfn_charge_discharge_75.csv" 11 | ) 12 | 13 | # Define model 14 | parameter_set = pybop.ParameterSet("Chen2020") 15 | model = pybop.lithium_ion.DFN(parameter_set=parameter_set) 16 | 17 | # Fitting parameters 18 | parameters = pybop.Parameters( 19 | pybop.Parameter( 20 | "Negative electrode active material volume fraction", 21 | prior=pybop.Gaussian(0.68, 0.05), 22 | initial_value=0.65, 23 | bounds=[0.4, 0.9], 24 | ), 25 | pybop.Parameter( 26 | "Positive electrode active material volume fraction", 27 | prior=pybop.Gaussian(0.58, 0.05), 28 | initial_value=0.65, 29 | bounds=[0.4, 0.9], 30 | ), 31 | ) 32 | 33 | # Import the synthetic dataset, set model initial state 34 | csv_data = np.loadtxt(dataset_path, delimiter=",", skiprows=1) 35 | initial_state = {"Initial open-circuit voltage [V]": csv_data[0, 2]} 36 | model.set_initial_state(initial_state=initial_state) 37 | 38 | # Form dataset 39 | dataset = pybop.Dataset( 40 | { 41 | "Time [s]": csv_data[:, 0], 42 | "Current function [A]": csv_data[:, 1], 43 | "Voltage [V]": csv_data[:, 2], 44 | "Bulk open-circuit voltage [V]": csv_data[:, 3], 45 | } 46 | ) 47 | 48 | signal = ["Voltage [V]", "Bulk open-circuit voltage [V]"] 49 | # Generate problem, cost function, and optimisation class 50 | problem = pybop.FittingProblem( 51 | model, 52 | parameters, 53 | dataset, 54 | signal=signal, 55 | ) 56 | cost = pybop.RootMeanSquaredError(problem) 57 | 58 | optim = pybop.IRPropPlus( 59 | cost, 60 | verbose=True, 61 | max_iterations=60, 62 | max_unchanged_iterations=15, 63 | compute_sensitivities=True, 64 | n_sensitivity_samples=64, # Decrease samples for CI (increase for higher accuracy) 65 | ) 66 | 67 | # Run optimisation 68 | results = optim.run() 69 | 70 | # Plot the timeseries output 71 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison") 72 | 73 | # Plot convergence 74 | pybop.plot.convergence(optim) 75 | 76 | # Plot the parameter traces 77 | pybop.plot.parameters(optim) 78 | 79 | # Plot the cost landscape with optimisation path 80 | pybop.plot.surface(optim) 81 | -------------------------------------------------------------------------------- /examples/scripts/getting_started/weighted_cost.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import pybop 4 | 5 | # Parameter set and model definition 6 | parameter_set = pybop.ParameterSet("Chen2020") 7 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set) 8 | 9 | # Fitting parameters 10 | parameters = pybop.Parameters( 11 | pybop.Parameter( 12 | "Negative electrode active material volume fraction", 13 | prior=pybop.Gaussian(0.68, 0.05), 14 | bounds=[0.5, 0.8], 15 | true_value=parameter_set["Negative electrode active material volume fraction"], 16 | ), 17 | pybop.Parameter( 18 | "Positive electrode active material volume fraction", 19 | prior=pybop.Gaussian(0.58, 0.05), 20 | bounds=[0.4, 0.7], 21 | true_value=parameter_set["Positive electrode active material volume fraction"], 22 | ), 23 | ) 24 | 25 | # Generate data 26 | sigma = 0.001 27 | experiment = pybop.Experiment( 28 | [ 29 | ( 30 | "Discharge at 0.5C for 3 minutes (3 second period)", 31 | "Charge at 0.5C for 3 minutes (3 second period)", 32 | ), 33 | ] 34 | * 2 35 | ) 36 | values = model.predict(experiment=experiment, initial_state={"Initial SoC": 0.5}) 37 | 38 | 39 | def noisy(data, sigma): 40 | return data + np.random.normal(0, sigma, len(data)) 41 | 42 | 43 | # Form dataset 44 | dataset = pybop.Dataset( 45 | { 46 | "Time [s]": values["Time [s]"].data, 47 | "Current function [A]": values["Current [A]"].data, 48 | "Voltage [V]": noisy(values["Voltage [V]"].data, sigma), 49 | } 50 | ) 51 | 52 | # Generate problem, cost function, and optimisation class 53 | problem = pybop.FittingProblem(model, parameters, dataset) 54 | cost1 = pybop.SumSquaredError(problem) 55 | cost2 = pybop.RootMeanSquaredError(problem) 56 | weighted_cost = pybop.WeightedCost(cost1, cost2, weights=[0.1, 1]) 57 | 58 | for cost in [weighted_cost, cost1, cost2]: 59 | optim = pybop.IRPropMin(cost, max_iterations=60) 60 | 61 | # Run the optimisation 62 | results = optim.run() 63 | print("True parameters:", parameters.true_value()) 64 | 65 | # Plot the timeseries output 66 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison") 67 | 68 | # Plot convergence 69 | pybop.plot.convergence(optim) 70 | 71 | # Plot the cost landscape with optimisation path 72 | pybop.plot.surface(optim) 73 | -------------------------------------------------------------------------------- /examples/standalone/cost.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import pybop 4 | 5 | 6 | class StandaloneCost(pybop.BaseCost): 7 | """ 8 | A standalone cost function example that inherits from pybop.BaseCost. 9 | 10 | This class represents a simple cost function without a problem object, used for demonstration purposes. 11 | It is a quadratic function of one variable with a constant term, defined by 12 | the formula: cost(x) = x^2 + 42. 13 | 14 | Parameters 15 | ---------- 16 | problem : object, optional 17 | A dummy problem instance used to initialize the superclass. This is not 18 | used in the current class but is accepted for compatibility with the 19 | BaseCost interface. 20 | parameters : pybop.Parameters 21 | A pybop.Parameters object storing a dictionary of parameters and their 22 | properties, for example their initial value and bounds. 23 | 24 | Methods 25 | ------- 26 | __call__(x) 27 | Calculate the cost for a given parameter value. 28 | """ 29 | 30 | def __init__(self, problem=None): 31 | """ 32 | Initialise the StandaloneCost class with optional problem instance. 33 | 34 | The problem object is not utilised in this subclass. The parameters, including 35 | their initial value and bounds, are defined within this standalone cost object. 36 | """ 37 | super().__init__(problem) 38 | 39 | self._parameters = pybop.Parameters( 40 | pybop.Parameter( 41 | "x", 42 | initial_value=4.2, 43 | bounds=[-1, 10], 44 | ), 45 | ) 46 | self.x0 = self._parameters.initial_value() 47 | 48 | def compute( 49 | self, y: dict = None, dy: np.ndarray = None, calculate_grad: bool = False 50 | ): 51 | """ 52 | Compute the cost for a given parameter value. 53 | 54 | The cost function is defined as cost(x) = x^2 + 42, where x is the 55 | parameter value. 56 | 57 | Returns 58 | ------- 59 | float 60 | The calculated cost value for the given parameter. 61 | """ 62 | return np.asarray(self._parameters["x"].value ** 2 + 42) 63 | -------------------------------------------------------------------------------- /examples/standalone/model.py: -------------------------------------------------------------------------------- 1 | import pybamm 2 | 3 | from pybop.models.base_model import BaseModel 4 | 5 | 6 | class ExponentialDecay(BaseModel): 7 | """ 8 | Exponential decay model with two parameters y0 and k 9 | 10 | dy/dt = -ky 11 | y(0) = y0 12 | 13 | """ 14 | 15 | def __init__( 16 | self, 17 | name: str = "Constant Model", 18 | parameter_set: pybamm.ParameterValues = None, 19 | n_states: int = 1, 20 | ): 21 | super().__init__(name=name, parameter_set=parameter_set) 22 | 23 | self.n_states = n_states 24 | if n_states < 1: 25 | raise ValueError("The number of states (n_states) must be greater than 0") 26 | self.pybamm_model = pybamm.BaseModel() 27 | ys = [pybamm.Variable(f"y_{i}") for i in range(n_states)] 28 | k = pybamm.Parameter("k") 29 | y0 = pybamm.Parameter("y0") 30 | self.pybamm_model.rhs = {y: -k * y for y in ys} 31 | self.pybamm_model.initial_conditions = {y: y0 for y in ys} 32 | self.pybamm_model.variables = {"y_0": ys[0], "2y": 2 * ys[0]} 33 | 34 | default_parameter_values = pybamm.ParameterValues( 35 | { 36 | "k": 0.1, 37 | "y0": 1, 38 | } 39 | ) 40 | 41 | self._unprocessed_model = self.pybamm_model 42 | 43 | self.default_parameter_values = ( 44 | default_parameter_values 45 | if self._parameter_set is None 46 | else self._parameter_set 47 | ) 48 | self._parameter_set = self.default_parameter_values 49 | self._unprocessed_parameter_set = self._parameter_set 50 | 51 | self._geometry = self.pybamm_model.default_geometry 52 | self._submesh_types = self.pybamm_model.default_submesh_types 53 | self._var_pts = self.pybamm_model.default_var_pts 54 | self._spatial_methods = self.pybamm_model.default_spatial_methods 55 | self._solver = pybamm.CasadiSolver(mode="fast") 56 | self._model_with_set_params = None 57 | self._built_model = None 58 | self._built_initial_soc = None 59 | self._mesh = None 60 | self._disc = None 61 | self.geometric_parameters = {} 62 | -------------------------------------------------------------------------------- /examples/standalone/optimiser.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.optimize import minimize 3 | 4 | from pybop import BaseOptimiser, OptimisationResult 5 | 6 | 7 | class StandaloneOptimiser(BaseOptimiser): 8 | """ 9 | Defines an example standalone optimiser without a Cost. 10 | """ 11 | 12 | def __init__(self, cost=None, **optimiser_kwargs): 13 | # Define cost function 14 | def cost(x): 15 | x1, x2 = x 16 | return (x1 - 2) ** 2 + (x2 - 4) ** 4 17 | 18 | # Set initial values and other options 19 | optimiser_options = dict( 20 | x0=np.array([0, 0]), 21 | bounds=None, 22 | method="Nelder-Mead", 23 | jac=False, 24 | maxiter=100, 25 | ) 26 | optimiser_options.update(optimiser_kwargs) 27 | super().__init__(cost, **optimiser_options) 28 | 29 | def _set_up_optimiser(self): 30 | """ 31 | Parse optimiser options. 32 | """ 33 | # Reformat bounds 34 | if isinstance(self.bounds, dict): 35 | self._scipy_bounds = [ 36 | (lower, upper) 37 | for lower, upper in zip(self.bounds["lower"], self.bounds["upper"]) 38 | ] 39 | else: 40 | self._scipy_bounds = self.bounds 41 | 42 | # Parse additional options and remove them from the options dictionary 43 | self._options = self.unset_options 44 | self.unset_options = dict() 45 | self._options["options"] = self._options.pop("options", dict()) 46 | if "maxiter" in self._options.keys(): 47 | # Nest this option within an options dictionary for SciPy minimize 48 | self._options["options"]["maxiter"] = self._options.pop("maxiter") 49 | 50 | def _run(self): 51 | """ 52 | Executes the optimisation process using SciPy's minimize function. 53 | 54 | Returns 55 | ------- 56 | x : numpy.ndarray 57 | The best parameter set found by the optimisation. 58 | final_cost : float 59 | The final cost associated with the best parameters. 60 | """ 61 | self.log = [[self.x0]] 62 | 63 | # Add callback storing history of parameter values 64 | def callback(x): 65 | self.log.append([x]) 66 | 67 | # Run optimiser 68 | result = minimize( 69 | self.cost, 70 | self.x0, 71 | bounds=self._scipy_bounds, 72 | callback=callback, 73 | **self._options, 74 | ) 75 | 76 | return OptimisationResult( 77 | optim=self, 78 | x=result.x, 79 | n_iterations=result.nit, 80 | scipy_result=result, 81 | ) 82 | 83 | def name(self): 84 | """ 85 | Provides the name of the optimisation strategy. 86 | 87 | Returns 88 | ------- 89 | str 90 | The name 'StandaloneOptimiser'. 91 | """ 92 | return "StandaloneOptimiser" 93 | -------------------------------------------------------------------------------- /examples/standalone/problem.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from pybop import BaseProblem, Inputs 4 | 5 | 6 | class StandaloneProblem(BaseProblem): 7 | """ 8 | Defines an example standalone problem without a Model. 9 | """ 10 | 11 | def __init__( 12 | self, 13 | parameters, 14 | dataset, 15 | model=None, 16 | check_model=True, 17 | signal=None, 18 | additional_variables=None, 19 | initial_state=None, 20 | ): 21 | super().__init__(parameters, model, check_model, signal, additional_variables) 22 | self._dataset = dataset.data 23 | 24 | # Check that the dataset contains time and current 25 | for name in ["Time [s]", *self.signal]: 26 | if name not in self._dataset: 27 | raise ValueError(f"expected {name} in list of dataset") 28 | 29 | self._domain_data = self._dataset[self.domain] 30 | self.n_data = len(self._domain_data) 31 | if np.any(self._domain_data < 0): 32 | raise ValueError("Times can not be negative.") 33 | if np.any(self._domain_data[:-1] >= self._domain_data[1:]): 34 | raise ValueError("Times must be increasing.") 35 | 36 | for signal in self.signal: 37 | if len(self._dataset[signal]) != self.n_data: 38 | raise ValueError( 39 | f"Time data and {signal} data must be the same length." 40 | ) 41 | self._target = {signal: self._dataset[signal] for signal in self.signal} 42 | 43 | def evaluate(self, inputs: Inputs): 44 | """ 45 | Evaluate the model with the given parameters and return the signal. 46 | 47 | Parameters 48 | ---------- 49 | inputs : Inputs 50 | Parameters for evaluation of the model. 51 | 52 | Returns 53 | ------- 54 | dict[str, np.ndarray[np.float64]] 55 | The model output y(t) simulated with the given inputs. 56 | """ 57 | return { 58 | signal: inputs["Gradient"] * self._domain_data + inputs["Intercept"] 59 | for signal in self.signal 60 | } 61 | 62 | def evaluateS1(self, inputs): 63 | """ 64 | Evaluate the model with the given parameters and return the signal and its derivatives. 65 | 66 | Parameters 67 | ---------- 68 | inputs : Inputs 69 | Parameters for evaluation of the model. 70 | 71 | Returns 72 | ------- 73 | tuple[dict[str, np.ndarray[np.float64]], dict[str, dict[str, np.ndarray]]] 74 | A tuple containing the simulation result y(t) and the sensitivities dy/dx(t) for each 75 | parameter x and signal y. 76 | """ 77 | 78 | y = self.evaluate(inputs) 79 | 80 | dy = dict.fromkeys(inputs.keys()) 81 | dy["Gradient"] = {signal: self._domain_data for signal in self.signal} 82 | dy["Intercept"] = { 83 | signal: np.zeros((self.n_outputs, self.n_data)) for signal in self.signal 84 | } 85 | 86 | return (y, dy) 87 | -------------------------------------------------------------------------------- /papers/Hallemans et al/Data/LGM50LT/Notes on LG M50LT OCP.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/papers/Hallemans et al/Data/LGM50LT/Notes on LG M50LT OCP.pdf -------------------------------------------------------------------------------- /papers/Hallemans et al/Data/LGM50LT/OCP_LGM50LT.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/papers/Hallemans et al/Data/LGM50LT/OCP_LGM50LT.mat -------------------------------------------------------------------------------- /papers/Hallemans et al/Data/LGM50LT/impedanceLGM50LT_Hybrid_4h.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/papers/Hallemans et al/Data/LGM50LT/impedanceLGM50LT_Hybrid_4h.mat -------------------------------------------------------------------------------- /papers/Hallemans et al/Data/Z_SPMegrouped_SOC_chen2020.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/papers/Hallemans et al/Data/Z_SPMegrouped_SOC_chen2020.mat -------------------------------------------------------------------------------- /papers/Hallemans et al/Data/timeDomainSimulation_SPMegrouped.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/papers/Hallemans et al/Data/timeDomainSimulation_SPMegrouped.mat -------------------------------------------------------------------------------- /papers/Hallemans et al/Fig11_tauSensitivity.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import numpy as np 3 | from scipy.io import savemat 4 | 5 | import pybop 6 | from pybop.models.lithium_ion.basic_SPMe import convert_physical_to_grouped_parameters 7 | 8 | SOC = 0.2 9 | 10 | factor = 2 11 | Nparams = 11 12 | Nfreq = 60 13 | fmin = 2e-4 14 | fmax = 1e3 15 | 16 | # Get grouped parameters 17 | R0 = 0.01 18 | 19 | parameter_set = pybop.ParameterSet.pybamm("Chen2020") 20 | parameter_set["Electrolyte diffusivity [m2.s-1]"] = 1.769e-10 21 | parameter_set["Electrolyte conductivity [S.m-1]"] = 1e16 22 | parameter_set["Negative electrode conductivity [S.m-1]"] = 1e16 23 | parameter_set["Positive electrode conductivity [S.m-1]"] = 1e16 24 | 25 | grouped_parameters = convert_physical_to_grouped_parameters(parameter_set) 26 | grouped_parameters["Series resistance [Ohm]"] = R0 27 | model_options = {"surface form": "differential", "contact resistance": "true"} 28 | var_pts = {"x_n": 100, "x_s": 20, "x_p": 100, "r_n": 100, "r_p": 100} 29 | 30 | ## Change parameters 31 | parameter_name = "Negative particle diffusion time scale [s]" 32 | param0 = grouped_parameters[parameter_name] 33 | params = np.logspace(np.log10(param0 / factor), np.log10(param0 * factor), Nparams) 34 | 35 | # Simulate impedance at these parameter values 36 | frequencies = np.logspace(np.log10(fmin), np.log10(fmax), Nfreq) 37 | 38 | impedances = 1j * np.zeros((Nfreq, Nparams)) 39 | for ii, param in enumerate(params): 40 | grouped_parameters[parameter_name] = param 41 | model = pybop.lithium_ion.GroupedSPMe( 42 | parameter_set=grouped_parameters, 43 | eis=True, 44 | options=model_options, 45 | var_pts=var_pts, 46 | ) 47 | model.build( 48 | initial_state={"Initial SoC": SOC}, 49 | ) 50 | simulation = model.simulateEIS(inputs=None, f_eval=frequencies) 51 | impedances[:, ii] = simulation["Impedance"] 52 | 53 | fig, ax = plt.subplots() 54 | for ii in range(Nparams): 55 | ax.plot( 56 | np.real(impedances[:, ii]), 57 | -np.imag(impedances[:, ii]), 58 | ) 59 | ax.set(xlabel=r"$Z_r(\omega)$ [$\Omega$]", ylabel=r"$-Z_j(\omega)$ [$\Omega$]") 60 | ax.grid() 61 | ax.set_aspect("equal", "box") 62 | plt.show() 63 | 64 | mdic = {"Z": impedances, "f": frequencies, "name": parameter_name} 65 | savemat("Data/Z_SPMegrouped_taudn_20.mat", mdic) 66 | -------------------------------------------------------------------------------- /papers/Hallemans et al/Fig2_timeDomainSimulation.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import numpy as np 3 | from scipy.io import savemat 4 | 5 | import pybop 6 | from pybop.models.lithium_ion.basic_SPMe import convert_physical_to_grouped_parameters 7 | 8 | ## Grouped parameter set 9 | R0 = 0.01 10 | 11 | parameter_set = pybop.ParameterSet.pybamm("Chen2020") 12 | parameter_set["Electrolyte diffusivity [m2.s-1]"] = 1.769e-10 13 | parameter_set["Electrolyte conductivity [S.m-1]"] = 1e16 14 | parameter_set["Negative electrode conductivity [S.m-1]"] = 1e16 15 | parameter_set["Positive electrode conductivity [S.m-1]"] = 1e16 16 | 17 | grouped_parameters = convert_physical_to_grouped_parameters(parameter_set) 18 | grouped_parameters["Series resistance [Ohm]"] = R0 19 | model_options = {"surface form": "differential", "contact resistance": "true"} 20 | var_pts = {"x_n": 100, "x_s": 20, "x_p": 100, "r_n": 100, "r_p": 100} 21 | 22 | ## Create model 23 | model = pybop.lithium_ion.GroupedSPMe( 24 | parameter_set=grouped_parameters, eis=True, var_pts=var_pts, options=model_options 25 | ) 26 | 27 | ## Test model in the time domain 28 | SOC0 = 0.9 29 | model.build(initial_state={"Initial SoC": SOC0}) 30 | 31 | Ts = 10 # Sampling period 32 | T = 2 * 60 * 60 # 2h 33 | N = int(T / Ts) 34 | time = np.linspace(0, T - Ts, N) 35 | 36 | i_relax7 = np.zeros([7 * int(60 / Ts)]) # 7 min 37 | i_relax20 = np.zeros([20 * int(60 / Ts)]) # 20 min 38 | i_discharge = 5 * np.ones([53 * int(60 / Ts)]) # 53 min 39 | i_charge = -5 * np.ones([20 * int(60 / Ts)]) # 20 min 40 | current = np.concatenate((i_relax7, i_discharge, i_relax20, i_charge, i_relax20)) 41 | 42 | experiment = pybop.Dataset( 43 | { 44 | "Time [s]": time, 45 | "Current function [A]": current, 46 | } 47 | ) 48 | model.set_current_function(dataset=experiment) 49 | simulation = model.predict(t_eval=time) 50 | 51 | # Plot traces 52 | fig, ax = plt.subplots() 53 | ax.plot(simulation["Time [s]"].data, simulation["Current [A]"].data) 54 | ax.set(xlabel="time [s]", ylabel="Current [A]") 55 | ax.grid() 56 | plt.show() 57 | 58 | fig, ax = plt.subplots() 59 | ax.plot(simulation["Time [s]"].data, simulation["Voltage [V]"].data) 60 | ax.set(xlabel="time [s]", ylabel="Voltage [V]") 61 | ax.grid() 62 | plt.show() 63 | 64 | ## Save data 65 | t = simulation["Time [s]"].data 66 | i = simulation["Current [A]"].data 67 | v = simulation["Voltage [V]"].data 68 | 69 | mdic = {"t": t, "i": i, "v": v, "SOC0": SOC0} 70 | savemat("Data/timeDomainSimulation_SPMegrouped.mat", mdic) 71 | -------------------------------------------------------------------------------- /papers/Hallemans et al/Fig5_impedance_models.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import numpy as np 3 | from scipy.io import savemat 4 | 5 | import pybop 6 | 7 | Nfreq = 60 8 | SOC = 0.5 9 | fmin = 2e-4 10 | fmax = 1e3 11 | 12 | frequencies = np.logspace(np.log10(fmin), np.log10(fmax), Nfreq) 13 | impedances = 1j * np.zeros((Nfreq, 3)) 14 | 15 | # Define model 16 | parameter_set = pybop.ParameterSet.pybamm("Chen2020") 17 | parameter_set["Contact resistance [Ohm]"] = 0.01 18 | 19 | model_options = {"surface form": "differential", "contact resistance": "true"} 20 | var_pts = {"x_n": 100, "x_s": 20, "x_p": 100, "r_n": 100, "r_p": 100} 21 | 22 | 23 | ## SPM 24 | model = pybop.lithium_ion.SPM( 25 | parameter_set=parameter_set, options=model_options, eis=True, var_pts=var_pts 26 | ) 27 | model.build(initial_state={"Initial SoC": SOC}) 28 | 29 | simulation = model.simulateEIS(inputs=None, f_eval=frequencies) 30 | impedances[:, 0] = simulation["Impedance"] 31 | 32 | ## SPMe 33 | model = pybop.lithium_ion.SPMe( 34 | parameter_set=parameter_set, options=model_options, eis=True, var_pts=var_pts 35 | ) 36 | model.build(initial_state={"Initial SoC": SOC}) 37 | simulation = model.simulateEIS(inputs=None, f_eval=frequencies) 38 | impedances[:, 1] = simulation["Impedance"] 39 | 40 | ## DFN 41 | model = pybop.lithium_ion.DFN( 42 | parameter_set=parameter_set, options=model_options, eis=True, var_pts=var_pts 43 | ) 44 | model.build(initial_state={"Initial SoC": SOC}) 45 | simulation = model.simulateEIS(inputs=None, f_eval=frequencies) 46 | impedances[:, 2] = simulation["Impedance"] 47 | 48 | ## Plot 49 | fig, ax = plt.subplots() 50 | for ii in range(3): 51 | ax.plot( 52 | np.real(impedances[:, ii]), 53 | -np.imag(impedances[:, ii]), 54 | ) 55 | ax.set(xlabel=r"$Z_r(\omega)$ [$\Omega$]", ylabel=r"$-Z_j(\omega)$ [$\Omega$]") 56 | ax.grid() 57 | ax.set_aspect("equal", "box") 58 | plt.show() 59 | 60 | ## Save 61 | mdic = {"Z": impedances, "f": frequencies} 62 | savemat("Data/Z_SPM_SPMe_DFN_Pybop_chen2020.mat", mdic) 63 | -------------------------------------------------------------------------------- /papers/Hallemans et al/Fig7_impedance_SOC_SPMegrouped.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import numpy as np 3 | 4 | import pybop 5 | from pybop.models.lithium_ion.basic_SPMe import convert_physical_to_grouped_parameters 6 | 7 | ## Group parameter set 8 | R0 = 0.01 9 | 10 | parameter_set = pybop.ParameterSet.pybamm("Chen2020") 11 | parameter_set["Electrolyte diffusivity [m2.s-1]"] = 1.769e-10 12 | parameter_set["Electrolyte conductivity [S.m-1]"] = 1e16 13 | parameter_set["Negative electrode conductivity [S.m-1]"] = 1e16 14 | parameter_set["Positive electrode conductivity [S.m-1]"] = 1e16 15 | 16 | grouped_parameters = convert_physical_to_grouped_parameters(parameter_set) 17 | grouped_parameters["Series resistance [Ohm]"] = R0 18 | 19 | ## Create model 20 | model_options = {"surface form": "differential", "contact resistance": "true"} 21 | var_pts = {"x_n": 100, "x_s": 20, "x_p": 100, "r_n": 100, "r_p": 100} 22 | model = pybop.lithium_ion.GroupedSPMe( 23 | parameter_set=grouped_parameters, eis=True, var_pts=var_pts, options=model_options 24 | ) 25 | 26 | ## Simulate impedance 27 | Nfreq = 60 28 | fmin = 2e-4 29 | fmax = 1e3 30 | NSOC = 9 31 | frequencies = np.logspace(np.log10(fmin), np.log10(fmax), Nfreq) 32 | SOCs = np.linspace(0.1, 0.9, NSOC) 33 | 34 | impedances = 1j * np.zeros((Nfreq, NSOC)) 35 | for ii, SOC in enumerate(SOCs): 36 | model.build(initial_state={"Initial SoC": SOC}) 37 | simulation = model.simulateEIS(inputs=None, f_eval=frequencies) 38 | impedances[:, ii] = simulation["Impedance"] 39 | 40 | fig, ax = plt.subplots() 41 | for ii in range(len(SOCs)): 42 | ax.plot( 43 | np.real(impedances[:, ii]), 44 | -np.imag(impedances[:, ii]), 45 | ) 46 | ax.set(xlabel=r"$Z_r(\omega)$ [$\Omega$]", ylabel=r"$-Z_j(\omega)$ [$\Omega$]") 47 | ax.grid() 48 | ax.set_aspect("equal", "box") 49 | plt.show() 50 | 51 | ## Save data 52 | # mdic = {"Z": impedances, "f": frequencies, "SOC": SOCs} 53 | # savemat("Data/Z_SPMegrouped_SOC_chen2020.mat", mdic) 54 | -------------------------------------------------------------------------------- /papers/Hallemans et al/Fig8_groupedParamSensitivity.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import numpy as np 3 | from scipy.io import savemat 4 | 5 | import pybop 6 | from pybop.models.lithium_ion.basic_SPMe import convert_physical_to_grouped_parameters 7 | 8 | # 9 | factor = 2 10 | Nparams = 11 11 | SOC = 0.5 12 | Nfreq = 60 13 | fmin = 2e-4 14 | fmax = 1e3 15 | 16 | # Get grouped parameters 17 | R0 = 0.01 18 | 19 | parameter_set = pybop.ParameterSet.pybamm("Chen2020") 20 | parameter_set["Electrolyte diffusivity [m2.s-1]"] = 1.769e-10 21 | parameter_set["Electrolyte conductivity [S.m-1]"] = 1e16 22 | parameter_set["Negative electrode conductivity [S.m-1]"] = 1e16 23 | parameter_set["Positive electrode conductivity [S.m-1]"] = 1e16 24 | 25 | grouped_parameters = convert_physical_to_grouped_parameters(parameter_set) 26 | grouped_parameters["Series resistance [Ohm]"] = R0 27 | model_options = {"surface form": "differential", "contact resistance": "true"} 28 | 29 | var_pts = {"x_n": 100, "x_s": 20, "x_p": 100, "r_n": 100, "r_p": 100} 30 | 31 | ## Change parameters 32 | parameter_name = "Negative electrode relative porosity" 33 | 34 | # "Positive particle diffusion time scale [s]" 35 | # "Positive electrode electrolyte diffusion time scale [s]" 36 | # "Separator electrolyte diffusion time scale [s]" 37 | # "Positive electrode charge transfer time scale [s]" 38 | # "Series resistance [Ohm]" 39 | # "Positive electrode relative porosity" 40 | # "Cation transference number" 41 | # "Reference electrolyte capacity [A.s]" 42 | # "Positive electrode capacitance [F]" 43 | # "Positive theoretical electrode capacity [As]" 44 | # "Positive electrode relative thickness" 45 | # "Measured cell capacity [A.s]" 46 | 47 | param0 = grouped_parameters[parameter_name] 48 | 49 | params = np.logspace(np.log10(param0 / factor), np.log10(param0 * factor), Nparams) 50 | 51 | # Simulate impedance at these parameter values 52 | frequencies = np.logspace(np.log10(fmin), np.log10(fmax), Nfreq) 53 | 54 | impedances = 1j * np.zeros((Nfreq, Nparams)) 55 | for ii, param in enumerate(params): 56 | grouped_parameters[parameter_name] = param 57 | model = pybop.lithium_ion.GroupedSPMe( 58 | parameter_set=grouped_parameters, 59 | eis=True, 60 | options=model_options, 61 | var_pts=var_pts, 62 | ) 63 | model.build( 64 | initial_state={"Initial SoC": SOC}, 65 | ) 66 | simulation = model.simulateEIS(inputs=None, f_eval=frequencies) 67 | impedances[:, ii] = simulation["Impedance"] 68 | 69 | fig, ax = plt.subplots() 70 | for ii in range(Nparams): 71 | ax.plot( 72 | np.real(impedances[:, ii]), 73 | -np.imag(impedances[:, ii]), 74 | ) 75 | ax.set(xlabel=r"$Z_r(\omega)$ [$\Omega$]", ylabel=r"$-Z_j(\omega)$ [$\Omega$]") 76 | ax.grid() 77 | ax.set_aspect("equal", "box") 78 | plt.show() 79 | 80 | mdic = {"Z": impedances, "f": frequencies, "name": parameter_name} 81 | savemat("Data/Sensitivity/Z_SPMegrouped_zetan.mat", mdic) 82 | -------------------------------------------------------------------------------- /papers/Hallemans et al/README.md: -------------------------------------------------------------------------------- 1 | # Physics-based battery model parametrisation from impedance data 2 | 3 | This directory contains the code used for [Hallemans et al.](http://arxiv.org/abs/2412.10896) article. 4 | 5 | Note: These scripts were ran using `PyBOP` v24.12, and are not maintained for future PyBOP releases. 6 | -------------------------------------------------------------------------------- /pybop/_evaluation.py: -------------------------------------------------------------------------------- 1 | import jax.numpy as jnp 2 | import numpy as np 3 | from pints import Evaluator as PintsEvaluator 4 | 5 | 6 | class SequentialJaxEvaluator(PintsEvaluator): 7 | """ 8 | Sequential evaluates a function (or callable object) 9 | for either a single or multiple positions. This class is based 10 | off the PintsSequentialEvaluator class, with additions for 11 | PyBOP's JAX cost classes. 12 | 13 | Parameters 14 | ---------- 15 | function : callable 16 | The function to evaluate. This function should accept an input and 17 | optionally additional arguments, returning either a single value or a tuple. 18 | args : sequence, optional 19 | A sequence containing extra arguments to be passed to the function. 20 | If specified, the function will be called as `function(x, *args)`. 21 | """ 22 | 23 | def _evaluate(self, positions): 24 | scores = [self._function(x, *self._args) for x in positions] 25 | 26 | # If gradient provided, convert jnp to np and return 27 | if isinstance(scores[0], tuple): 28 | return [(score[0].item(), score[1]) for score in scores] 29 | 30 | return np.asarray(scores) 31 | 32 | 33 | class SciPyEvaluator(PintsEvaluator): 34 | """ 35 | Evaluates a function (or callable object) for the SciPy optimisers 36 | for either a single or multiple positions. 37 | 38 | Parameters 39 | ---------- 40 | function : callable 41 | The function to evaluate. This function should accept an input and 42 | optionally additional arguments, returning either a single value or a tuple. 43 | args : sequence, optional 44 | A sequence containing extra arguments to be passed to the function. 45 | If specified, the function will be called as `function(x, *args)`. 46 | """ 47 | 48 | def _evaluate(self, positions): 49 | scores = [self._function(x, *self._args) for x in [positions]] 50 | 51 | if not isinstance(scores[0], tuple): 52 | return np.asarray(scores)[0] 53 | 54 | # If gradient provided, convert jnp to np and return 55 | if isinstance(scores[0][0], jnp.ndarray): 56 | return [(score[0].item(), score[1]) for score in scores][0] 57 | return [(score[0], score[1]) for score in scores][0] 58 | -------------------------------------------------------------------------------- /pybop/_experiment.py: -------------------------------------------------------------------------------- 1 | from pybamm import Experiment 2 | 3 | 4 | class Experiment(Experiment): 5 | """ 6 | Light wrapper of the PyBaMM Experiment class for generating experiment conditions for PyBaMM models. 7 | Credit: PyBaMM 8 | 9 | Base class for experimental conditions under which to run the model. In general, a 10 | list of operating conditions should be passed in. Each operating condition should 11 | be either a `pybamm.step._Step` class, created using one of the methods 12 | `pybamm.step.current`, `pybamm.step.c_rate`, `pybamm.step.voltage` 13 | , `pybamm.step.power`, `pybamm.step.resistance`, or 14 | `pybamm.step.string`, or a string, in which case the string is passed to 15 | `pybamm.step.string`. 16 | 17 | Parameters 18 | ---------- 19 | operating_conditions : list 20 | List of operating conditions 21 | period : string, optional 22 | Period (1/frequency) at which to record outputs. Default is 1 minute. Can be 23 | overwritten by individual operating conditions. 24 | temperature: float, optional 25 | The ambient air temperature in degrees Celsius at which to run the experiment. 26 | Default is None whereby the ambient temperature is taken from the parameter set. 27 | This value is overwritten if the temperature is specified in a step. 28 | termination : list, optional 29 | List of conditions under which to terminate the experiment. Default is None. 30 | This is different from the termination for individual steps. Termination for 31 | individual steps is specified in the step itself, and the simulation moves to 32 | the next step when the termination condition is met 33 | (e.g. 2.5V discharge cut-off). Termination for the 34 | experiment as a whole is specified here, and the simulation stops when the 35 | termination condition is met (e.g. 80% capacity). 36 | """ 37 | 38 | def __init__( 39 | self, 40 | operating_conditions, 41 | period=None, 42 | temperature=None, 43 | termination=None, 44 | ): 45 | super().__init__( 46 | operating_conditions, 47 | period, 48 | temperature, 49 | termination, 50 | ) 51 | -------------------------------------------------------------------------------- /pybop/_version.py: -------------------------------------------------------------------------------- 1 | import importlib.metadata 2 | 3 | __version__ = importlib.metadata.version("pybop") 4 | -------------------------------------------------------------------------------- /pybop/applications/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/pybop/applications/__init__.py -------------------------------------------------------------------------------- /pybop/costs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/pybop/costs/__init__.py -------------------------------------------------------------------------------- /pybop/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/pybop/models/__init__.py -------------------------------------------------------------------------------- /pybop/models/_exponential_decay.py: -------------------------------------------------------------------------------- 1 | import pybamm 2 | 3 | from pybop.models.base_model import BaseModel 4 | 5 | 6 | class ExponentialDecayModel(BaseModel): 7 | """ 8 | Exponential decay model defined by the equation: 9 | 10 | dy/dt = -k * y, y(0) = y0 11 | 12 | Note: The output variables are named "y_{i}" for each state. 13 | For example, the first state is "y_0", the second is "y_1", etc. 14 | Attributes: 15 | n_states (int): Number of states in the system (default is 1). 16 | pybamm_model (pybamm.BaseModel): PyBaMM model representation. 17 | default_parameter_values (pybamm.ParameterValues): Default parameter values 18 | for the model, with "k" (decay rate) and "y0" (initial condition). 19 | 20 | Parameters: 21 | name (str): Name of the model (default: "Experimental Decay Model"). 22 | parameter_set (pybamm.ParameterValues): Parameter values for the model. 23 | n_states (int): Number of states in the system. Must be >= 1. 24 | """ 25 | 26 | def __init__( 27 | self, 28 | name: str = "Experimental Decay Model", 29 | parameter_set: pybamm.ParameterValues = None, 30 | n_states: int = 1, 31 | solver=None, 32 | ): 33 | if n_states < 1: 34 | raise ValueError("The number of states (n_states) must be at least 1.") 35 | 36 | super().__init__(name=name, parameter_set=parameter_set) 37 | 38 | self.n_states = n_states 39 | if solver is None: 40 | self._solver = pybamm.CasadiSolver 41 | self._solver.mode = "fast with events" 42 | self._solver.max_step_decrease_count = 1 43 | else: 44 | self._solver = solver 45 | 46 | # Initialise the PyBaMM model, variables, parameters 47 | self.pybamm_model = pybamm.BaseModel() 48 | ys = [pybamm.Variable(f"y_{i}") for i in range(n_states)] 49 | k = pybamm.Parameter("k") 50 | y0 = pybamm.Parameter("y0") 51 | 52 | # Set up the right-hand side and initial conditions 53 | self.pybamm_model.rhs = {y: -k * y for y in ys} 54 | self.pybamm_model.initial_conditions = {y: y0 for y in ys} 55 | 56 | # Define model outputs and set default values 57 | self.pybamm_model.variables = {f"y_{en}": i for en, i in enumerate(ys)} | { 58 | "Time [s]": pybamm.t 59 | } 60 | self.default_parameter_values = parameter_set or pybamm.ParameterValues( 61 | {"k": 0.1, "y0": 1} 62 | ) 63 | 64 | # Store model attributes to be used by the solver 65 | self._unprocessed_model = self.pybamm_model 66 | self._parameter_set = self.default_parameter_values 67 | self._unprocessed_parameter_set = self._parameter_set 68 | 69 | # Geometry and solver setup 70 | self._geometry = self.pybamm_model.default_geometry 71 | self._submesh_types = self.pybamm_model.default_submesh_types 72 | self._var_pts = self.pybamm_model.default_var_pts 73 | self._spatial_methods = self.pybamm_model.default_spatial_methods 74 | self._solver = pybamm.CasadiSolver(mode="fast") 75 | 76 | # Additional attributes for solver and discretisation 77 | self._model_with_set_params = None 78 | self._built_model = None 79 | self._built_initial_soc = None 80 | self._mesh = None 81 | self._disc = None 82 | self.geometric_parameters = {} 83 | -------------------------------------------------------------------------------- /pybop/models/empirical/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Import lithium ion based models 3 | # 4 | from .base_ecm import ECircuitModel 5 | from .ecm import Thevenin 6 | -------------------------------------------------------------------------------- /pybop/models/empirical/ecm.py: -------------------------------------------------------------------------------- 1 | from pybamm import equivalent_circuit as pybamm_equivalent_circuit 2 | 3 | from pybop.models.empirical.base_ecm import ECircuitModel 4 | 5 | 6 | class Thevenin(ECircuitModel): 7 | """ 8 | The Thevenin class represents an equivalent circuit model based on the Thevenin model in PyBaMM. 9 | 10 | This class encapsulates the PyBaMM equivalent circuit Thevenin model, providing an interface 11 | to define the parameters, geometry, submesh types, variable points, spatial methods, and solver 12 | to be used for simulations. 13 | 14 | Parameters 15 | ---------- 16 | name : str, optional 17 | A name for the model instance. Defaults to "Equivalent Circuit Thevenin Model". 18 | **model_kwargs : optional 19 | Valid PyBaMM model option keys and their values, for example: 20 | parameter_set : pybamm.ParameterValues or dict, optional 21 | The parameters for the model. If None, default parameters provided by PyBaMM are used. 22 | geometry : dict, optional 23 | The geometry definitions for the model. If None, default geometry from PyBaMM is used. 24 | submesh_types : dict, optional 25 | The types of submeshes to use. If None, default submesh types from PyBaMM are used. 26 | var_pts : dict, optional 27 | The discretization points for each variable in the model. If None, default points from PyBaMM are used. 28 | spatial_methods : dict, optional 29 | The spatial methods used for discretization. If None, default spatial methods from PyBaMM are used. 30 | solver : pybamm.Solver, optional 31 | The solver to use for simulating the model. If None, the default solver from PyBaMM is used. 32 | build : bool, optional 33 | If True, the model is built upon creation (default: False). 34 | options : dict, optional 35 | A dictionary of options to customise the behaviour of the PyBaMM model. 36 | """ 37 | 38 | def __init__( 39 | self, 40 | name="Equivalent Circuit Thevenin Model", 41 | eis=False, 42 | **model_kwargs, 43 | ): 44 | super().__init__( 45 | pybamm_model=pybamm_equivalent_circuit.Thevenin, 46 | name=name, 47 | eis=eis, 48 | **model_kwargs, 49 | ) 50 | -------------------------------------------------------------------------------- /pybop/models/lithium_ion/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Import lithium ion based models 3 | # 4 | from .base_echem import EChemBaseModel 5 | from .echem import SPM, SPMe, DFN, MPM, MSMR, WeppnerHuggins, SPDiffusion, GroupedSPMe 6 | -------------------------------------------------------------------------------- /pybop/observers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/pybop/observers/__init__.py -------------------------------------------------------------------------------- /pybop/optimisers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/pybop/optimisers/__init__.py -------------------------------------------------------------------------------- /pybop/optimisers/optimisation.py: -------------------------------------------------------------------------------- 1 | from pybop import XNES, BasePintsOptimiser, BaseSciPyOptimiser 2 | 3 | 4 | class Optimisation: 5 | """ 6 | A high-level class for optimisation using PyBOP or PINTS optimisers. 7 | 8 | This class provides an alternative API to the `PyBOP.Optimiser()` API, 9 | specifically allowing for single user-friendly interface for the 10 | optimisation process.The class can be used with either PyBOP or PINTS 11 | optimisers. 12 | 13 | Parameters 14 | ---------- 15 | cost : pybop.BaseCost or pints.ErrorMeasure 16 | An objective function to be optimized, which can be either a pybop.Cost 17 | optimiser : pybop.Optimiser or subclass of pybop.BaseOptimiser, optional 18 | An optimiser from either the PINTS or PyBOP framework to perform the optimisation (default: None). 19 | sigma0 : float or sequence, optional 20 | Initial step size or standard deviation for the optimiser (default: None). 21 | verbose : bool, optional 22 | If True, the optimisation progress is printed (default: False). 23 | physical_viability : bool, optional 24 | If True, the feasibility of the optimised parameters is checked (default: True). 25 | allow_infeasible_solutions : bool, optional 26 | If True, infeasible parameter values will be allowed in the optimisation (default: True). 27 | 28 | Attributes 29 | ---------- 30 | All attributes from the pybop.optimiser() class 31 | 32 | """ 33 | 34 | def __init__(self, cost, optimiser=None, **optimiser_kwargs): 35 | self.__dict__["optim"] = ( 36 | None # Pre-define optimiser to avoid recursion during initialisation 37 | ) 38 | if optimiser is None: 39 | self.optim = XNES(cost, **optimiser_kwargs) 40 | elif issubclass(optimiser, BasePintsOptimiser): 41 | self.optim = optimiser(cost, **optimiser_kwargs) 42 | elif issubclass(optimiser, BaseSciPyOptimiser): 43 | self.optim = optimiser(cost, **optimiser_kwargs) 44 | else: 45 | raise ValueError("Unknown optimiser type") 46 | 47 | def run(self): 48 | return self.optim.run() 49 | 50 | def __getattr__(self, attr): 51 | if "optim" in self.__dict__ and hasattr(self.optim, attr): 52 | return getattr(self.optim, attr) 53 | raise AttributeError( 54 | f"'{self.__class__.__name__}' object has no attribute '{attr}'" 55 | ) 56 | 57 | def __setattr__(self, name: str, value) -> None: 58 | if ( 59 | name in self.__dict__ 60 | or "optim" not in self.__dict__ 61 | or not hasattr(self.optim, name) 62 | ): 63 | object.__setattr__(self, name, value) 64 | else: 65 | setattr(self.optim, name, value) 66 | -------------------------------------------------------------------------------- /pybop/parameters/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/pybop/parameters/__init__.py -------------------------------------------------------------------------------- /pybop/plot/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Import plots 3 | # 4 | from .plotly_manager import PlotlyManager 5 | from .standard_plots import StandardPlot, StandardSubplot, trajectories 6 | from .contour import contour 7 | from .dataset import dataset 8 | from .convergence import convergence 9 | from .parameters import parameters 10 | from .problem import problem 11 | from .nyquist import nyquist 12 | from .voronoi import surface 13 | -------------------------------------------------------------------------------- /pybop/plot/convergence.py: -------------------------------------------------------------------------------- 1 | from pybop.plot.standard_plots import StandardPlot 2 | 3 | 4 | def convergence(optim, show=True, **layout_kwargs): 5 | """ 6 | Plot the convergence of the optimisation algorithm. 7 | 8 | Parameters 9 | ----------- 10 | optim : object 11 | Optimisation object containing the cost function and optimiser. 12 | show : bool, optional 13 | If True, the figure is shown upon creation (default: True). 14 | **layout_kwargs : optional 15 | Valid Plotly layout keys and their values, 16 | e.g. `xaxis_title="Time [s]"` or 17 | `xaxis={"title": "Time [s]", font={"size":14}}` 18 | 19 | Returns 20 | --------- 21 | fig : plotly.graph_objs.Figure 22 | The Plotly figure object for the convergence plot. 23 | """ 24 | 25 | # Extract log from the optimisation object 26 | cost_log = optim.log.cost_best 27 | 28 | # Generate a list of iteration numbers 29 | iteration_numbers = list(range(1, len(cost_log) + 1)) 30 | 31 | # Create a plot dictionary 32 | plot_dict = StandardPlot( 33 | x=iteration_numbers, 34 | y=cost_log, 35 | layout_options=dict( 36 | xaxis_title="Iteration", 37 | yaxis_title="Cost", 38 | title="Convergence", 39 | ), 40 | trace_names=optim.name(), 41 | ) 42 | 43 | # Generate and display the figure 44 | fig = plot_dict(show=False) 45 | fig.update_layout(**layout_kwargs) 46 | if show: 47 | fig.show() 48 | 49 | return fig 50 | -------------------------------------------------------------------------------- /pybop/plot/dataset.py: -------------------------------------------------------------------------------- 1 | from pybop.plot.standard_plots import StandardPlot, trajectories 2 | 3 | 4 | def dataset(dataset, signal=None, trace_names=None, show=True, **layout_kwargs): 5 | """ 6 | Quickly plot a PyBOP Dataset using Plotly. 7 | 8 | Parameters 9 | ---------- 10 | dataset : object 11 | A PyBOP dataset. 12 | signal : list or str, optional 13 | The name of the time series to plot (default: "Voltage [V]"). 14 | trace_names : list or str, optional 15 | Name(s) for the trace(s) (default: "Data"). 16 | show : bool, optional 17 | If True, the figure is shown upon creation (default: True). 18 | **layout_kwargs : optional 19 | Valid Plotly layout keys and their values, 20 | e.g. `xaxis_title="Time / s"` or 21 | `xaxis={"title": "Time [s]", font={"size":14}}` 22 | 23 | Returns 24 | ------- 25 | plotly.graph_objs.Figure 26 | The Plotly figure object for the scatter plot. 27 | """ 28 | 29 | # Get data dictionary 30 | if signal is None: 31 | signal = ["Voltage [V]"] 32 | dataset.check(signal=signal) 33 | 34 | # Compile ydata and labels or legend 35 | y = [dataset[s] for s in signal] 36 | if len(signal) == 1: 37 | yaxis_title = signal[0] 38 | if trace_names is None: 39 | trace_names = ["Data"] 40 | else: 41 | yaxis_title = "Output" 42 | if trace_names is None: 43 | trace_names = StandardPlot.remove_brackets(signal) 44 | 45 | # Create the figure 46 | fig = trajectories( 47 | x=dataset[dataset.domain], 48 | y=y, 49 | trace_names=trace_names, 50 | show=False, 51 | xaxis_title=StandardPlot.remove_brackets(dataset.domain), 52 | yaxis_title=yaxis_title, 53 | ) 54 | fig.update_layout(**layout_kwargs) 55 | if show: 56 | fig.show() 57 | 58 | return fig 59 | -------------------------------------------------------------------------------- /pybop/plot/parameters.py: -------------------------------------------------------------------------------- 1 | from pybop import GaussianLogLikelihood 2 | from pybop.plot.standard_plots import StandardSubplot 3 | 4 | 5 | def parameters(optim, show=True, **layout_kwargs): 6 | """ 7 | Plot the evolution of parameters during the optimization process using Plotly. 8 | 9 | Parameters 10 | ---------- 11 | optim : object 12 | Optimisation object containing the history of parameter values and associated cost. 13 | show : bool, optional 14 | If True, the figure is shown upon creation (default: True). 15 | **layout_kwargs : optional 16 | Valid Plotly layout keys and their values, 17 | e.g. `xaxis_title="Time [s]"` or 18 | `xaxis={"title": "Time [s]", font={"size":14}}` 19 | 20 | Returns 21 | ------- 22 | plotly.graph_objs.Figure 23 | A Plotly figure object showing the parameter evolution over iterations. 24 | """ 25 | 26 | # Extract parameters and log from the optimisation object 27 | parameters = optim.cost.parameters 28 | x = list(range(len(optim.log.x))) 29 | y = [list(item) for item in zip(*optim.log.x)] 30 | 31 | # Create lists of axis titles and trace names 32 | axis_titles = [] 33 | trace_names = parameters.keys() 34 | for name in trace_names: 35 | axis_titles.append(("Function Call", name)) 36 | 37 | if isinstance(optim.cost, GaussianLogLikelihood): 38 | axis_titles.append(("Function Call", "Sigma")) 39 | trace_names.append("Sigma") 40 | 41 | # Set subplot layout options 42 | layout_options = dict( 43 | title="Parameter Convergence", 44 | width=1024, 45 | height=576, 46 | legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1), 47 | ) 48 | 49 | # Create a plot dictionary 50 | plot_dict = StandardSubplot( 51 | x=x, 52 | y=y, 53 | axis_titles=axis_titles, 54 | layout_options=layout_options, 55 | trace_names=trace_names, 56 | trace_name_width=50, 57 | ) 58 | 59 | # Generate the figure and update the layout 60 | fig = plot_dict(show=False) 61 | fig.update_layout(**layout_kwargs) 62 | if show: 63 | fig.show() 64 | 65 | return fig 66 | -------------------------------------------------------------------------------- /pybop/problems/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/pybop/problems/__init__.py -------------------------------------------------------------------------------- /pybop/samplers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/pybop/samplers/__init__.py -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=64"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "pybop" 7 | version = "25.3" 8 | authors = [ 9 | {name = "The PyBOP Team"}, 10 | ] 11 | maintainers = [ 12 | {name = "The PyBOP Team"}, 13 | ] 14 | description = "Python Battery Optimisation and Parameterisation" 15 | readme = {file = "README.md", content-type = "text/markdown"} 16 | license = { file = "LICENSE" } 17 | classifiers = [ 18 | "Development Status :: 3 - Alpha", 19 | "License :: OSI Approved :: BSD License", 20 | "Programming Language :: Python :: 3.9", 21 | "Programming Language :: Python :: 3.10", 22 | "Programming Language :: Python :: 3.11", 23 | "Programming Language :: Python :: 3.12", 24 | "Intended Audience :: Science/Research", 25 | "Topic :: Scientific/Engineering", 26 | ] 27 | requires-python = ">=3.9, <3.13" 28 | dependencies = [ 29 | "pybamm[jax]>=24.5.1", 30 | "numpy>=1.16, <2.0", 31 | "scipy>=1.3", 32 | "pints>=0.5", 33 | "SALib>=1.5", 34 | ] 35 | 36 | [project.optional-dependencies] 37 | plot = ["plotly>=5.0"] 38 | docs = [ 39 | "pydata-sphinx-theme", 40 | "sphinx>=6", 41 | "sphinx-autobuild", 42 | "sphinx-autoapi", 43 | "sphinx_copybutton", 44 | "sphinx_favicon", 45 | "sphinx_design", 46 | "myst-parser", 47 | ] 48 | dev = [ 49 | "nox[uv]", 50 | "nbmake", 51 | "pre-commit", 52 | "pytest>=6", 53 | "pytest-cov", 54 | "pytest-mock", 55 | "pytest-xdist", 56 | "ruff", 57 | ] 58 | scifem = [ 59 | "scikit-fem>=8.1.0" # scikit-fem is a dependency for the multi-dimensional pybamm models 60 | ] 61 | bpx = [ 62 | "bpx>=0.5, <0.6", 63 | ] 64 | all = ["pybop[plot,scifem,bpx]"] 65 | 66 | [tool.setuptools.packages.find] 67 | include = ["pybop", "pybop.*"] 68 | 69 | [project.urls] 70 | Homepage = "https://github.com/pybop-team/PyBOP" 71 | Documentation = "https://pybop-docs.readthedocs.io" 72 | Repository = "https://github.com/pybop-team/PyBOP" 73 | Releases = "https://github.com/pybop-team/PyBOP/releases" 74 | Changelog = "https://github.com/pybop-team/PyBOP/blob/develop/CHANGELOG.md" 75 | 76 | [tool.pytest.ini_options] 77 | addopts = "--showlocals -v -n auto" 78 | 79 | [tool.ruff] 80 | extend-include = ["*.ipynb"] 81 | extend-exclude = ["__init__.py"] 82 | fix = true 83 | 84 | [tool.ruff.lint] 85 | select = [ 86 | "A", # flake8-builtins: Check for Python builtins being used as variables or parameters 87 | "B", # flake8-bugbear: Find likely bugs and design problems 88 | "E", # pycodestyle errors 89 | "W", # pycodestyle warnings 90 | "F", # pyflakes: Detect various errors by parsing the source file 91 | "I", # isort: Check and enforce import ordering 92 | "ISC", # flake8-implicit-str-concat: Check for implicit string concatenation 93 | "TID", # flake8-tidy-imports: Validate import hygiene 94 | "UP", # pyupgrade: Automatically upgrade syntax for newer versions of Python 95 | "SLF001", # flake8-string-format: Check for private object name access 96 | ] 97 | 98 | ignore = ["E501","E741"] 99 | 100 | [tool.ruff.lint.per-file-ignores] 101 | "tests/*" = ["SLF001"] 102 | "**.ipynb" = ["E402", "E703"] 103 | 104 | [tool.ruff.lint.flake8-tidy-imports] 105 | ban-relative-imports = "all" 106 | -------------------------------------------------------------------------------- /readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-20.04 5 | tools: 6 | python: "3.11" 7 | 8 | # Build documentation in the "docs/" directory with Sphinx 9 | sphinx: 10 | configuration: docs/conf.py 11 | 12 | formats: 13 | - htmlzip 14 | - pdf 15 | - epub 16 | 17 | python: 18 | install: 19 | - method: pip 20 | path: .[docs] 21 | -------------------------------------------------------------------------------- /scripts/ci/build_matrix.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This helper script generates a matrix for further use in the 4 | # scheduled/nightly builds for PyBOP, i.e., in scheduled_tests.yaml 5 | # It generates a matrix of all combinations of the following variables: 6 | # - python_version: 3.X 7 | # - os: ubuntu-latest, windows-latest, macos-13 (amd64), macos-14 (arm64) 8 | # - pybamm_version: the last X versions of PyBaMM from PyPI, excluding release candidates 9 | 10 | # To update the matrix, the variables below can be modified as needed. 11 | 12 | python_version=("3.9" "3.10" "3.11" "3.12") 13 | os=("ubuntu-latest" "windows-latest" "macos-13" "macos-14") 14 | # This command fetches the last PyBaMM version excluding release candidates from PyPI 15 | pybamm_version=($(curl -s https://pypi.org/pypi/pybamm/json | jq -r '.releases | keys[]' | grep -v rc | sort -V | tail -n 1 | paste -sd " " -)) 16 | 17 | # This command fetches the last PyBaMM versions including release candidates from PyPI 18 | #pybamm_version=($(curl -s https://pypi.org/pypi/pybamm/json | jq -r '.releases | keys[]' | sort -V | tail -n 1 | paste -sd " " -)) 19 | 20 | # open dict 21 | json='{ 22 | "include": [ 23 | ' 24 | 25 | # loop through each combination of variables to generate matrix components 26 | for py_ver in "${python_version[@]}"; do 27 | for os_type in "${os[@]}"; do 28 | for pybamm_ver in "${pybamm_version[@]}"; do 29 | json+='{ 30 | "os": "'$os_type'", 31 | "python_version": "'$py_ver'", 32 | "pybamm_version": "'$pybamm_ver'" 33 | },' 34 | done 35 | done 36 | done 37 | 38 | # fix structure, removing trailing comma 39 | json=${json%,} 40 | 41 | # close dict 42 | json+=' 43 | ] 44 | }' 45 | 46 | # Example for filtering out incompatible combinations 47 | #json=$(echo "$json" | jq -c 'del(.include[] | select(.pybamm_version == "23.9" and .python_version == "3.12"))') 48 | 49 | echo "$json" | jq -c . 50 | -------------------------------------------------------------------------------- /tests/docs/test_docs.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | import subprocess 3 | import sys 4 | from pathlib import Path 5 | 6 | import pytest 7 | 8 | 9 | class TestDocs: 10 | """A class to test the PyBOP documentation.""" 11 | 12 | pytestmark = pytest.mark.docs 13 | 14 | def test_docs(self): 15 | """ 16 | Check if the documentation can be built and run any doctests (currently not used). 17 | 18 | Credit: PyBaMM Team 19 | """ 20 | print("Checking if docs can be built.") 21 | docs_path = Path("docs") 22 | build_path = docs_path / "_build" / "html" 23 | 24 | try: 25 | subprocess.run( 26 | [ 27 | "sphinx-build", 28 | "-j", 29 | "auto", 30 | "-b", 31 | "html", 32 | str(docs_path), 33 | str(build_path), 34 | "--keep-going", 35 | ], 36 | check=True, 37 | capture_output=True, 38 | ) 39 | except subprocess.CalledProcessError as e: 40 | print(f"FAILED with exit code {e.returncode}") 41 | print(f"stdout: {e.stdout.decode()}") 42 | print(f"stderr: {e.stderr.decode()}") 43 | sys.exit(e.returncode) 44 | finally: 45 | # Regardless of whether the doctests pass or fail, attempt to remove the built files. 46 | print("Deleting built files.") 47 | try: 48 | shutil.rmtree(build_path) 49 | except Exception as e: 50 | print(f"Error deleting built files: {e}") 51 | -------------------------------------------------------------------------------- /tests/examples/test_examples.py: -------------------------------------------------------------------------------- 1 | import os 2 | import runpy 3 | 4 | import pytest 5 | 6 | import pybop 7 | 8 | 9 | class TestExamples: 10 | """ 11 | A class to test the example scripts. 12 | """ 13 | 14 | def list_of_examples(): 15 | examples_list = [] 16 | path_to_example_scripts = os.path.join( 17 | pybop.script_path, "..", "examples", "scripts" 18 | ) 19 | for dirpath, _, filenames in os.walk(path_to_example_scripts): 20 | for file in filenames: 21 | if file.endswith(".py"): 22 | examples_list.append(os.path.join(dirpath, file)) 23 | return examples_list 24 | 25 | @pytest.mark.parametrize("example", list_of_examples()) 26 | @pytest.mark.examples 27 | def test_example_scripts(self, example): 28 | runpy.run_path(example) 29 | -------------------------------------------------------------------------------- /tests/unit/test_classifier.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | 4 | import pybop 5 | 6 | 7 | class TestClassifier: 8 | """ 9 | A class to test the classification of different optimisation results. 10 | """ 11 | 12 | pytestmark = pytest.mark.unit 13 | 14 | @pytest.fixture 15 | def problem(self): 16 | model = pybop.empirical.Thevenin() 17 | experiment = pybop.Experiment( 18 | [ 19 | "Discharge at 0.5C for 2 minutes (4 seconds period)", 20 | "Charge at 0.5C for 2 minutes (4 seconds period)", 21 | ] 22 | ) 23 | solution = model.predict(experiment=experiment) 24 | dataset = pybop.Dataset( 25 | { 26 | "Time [s]": solution["Time [s]"].data, 27 | "Current function [A]": solution["Current [A]"].data, 28 | "Voltage [V]": solution["Voltage [V]"].data, 29 | } 30 | ) 31 | parameters = pybop.Parameters( 32 | pybop.Parameter( 33 | "R0 [Ohm]", 34 | prior=pybop.Uniform(0.001, 0.1), 35 | bounds=[1e-4, 0.1], 36 | ), 37 | ) 38 | return pybop.FittingProblem(model, parameters, dataset) 39 | 40 | def test_classify_using_hessian_invalid(self, problem): 41 | cost = pybop.SumSquaredError(problem) 42 | optim = pybop.Optimisation(cost=cost) 43 | x = np.asarray([0.001]) 44 | results = pybop.OptimisationResult(x=x, optim=optim) 45 | 46 | with pytest.raises( 47 | ValueError, 48 | match="The function classify_using_hessian currently only works" 49 | " in the case of 2 parameters, and dx must have the same length as x.", 50 | ): 51 | pybop.classify_using_hessian(results) 52 | -------------------------------------------------------------------------------- /tests/unit/test_dataset.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | 4 | import pybop 5 | 6 | 7 | class TestDataset: 8 | """ 9 | Class to test dataset construction. 10 | """ 11 | 12 | pytestmark = pytest.mark.unit 13 | 14 | def test_dataset(self): 15 | # Construct and simulate model 16 | model = pybop.lithium_ion.SPM() 17 | solution = model.predict(t_eval=np.linspace(0, 10, 100)) 18 | 19 | # Form dataset 20 | data_dictionary = { 21 | "Time [s]": solution["Time [s]"].data, 22 | "Current [A]": solution["Current [A]"].data, 23 | "Voltage [V]": solution["Voltage [V]"].data, 24 | } 25 | dataset = pybop.Dataset(data_dictionary) 26 | 27 | # Test repr 28 | print(dataset) 29 | 30 | # Test data structure 31 | assert dataset.data == data_dictionary 32 | assert np.all(dataset["Time [s]"] == solution["Time [s]"].data) 33 | 34 | # Test exception for non-dictionary inputs 35 | with pytest.raises( 36 | TypeError, match="The input to pybop.Dataset must be a dictionary." 37 | ): 38 | pybop.Dataset(["StringInputShouldNotWork"]) 39 | with pytest.raises( 40 | TypeError, match="The input to pybop.Dataset must be a dictionary." 41 | ): 42 | pybop.Dataset(solution["Time [s]"].data) 43 | 44 | # Test conversion of pybamm solution into dictionary 45 | assert dataset.data == pybop.Dataset(solution).data 46 | 47 | # Test set and get item 48 | test_current = solution["Current [A]"].data + np.ones_like( 49 | solution["Current [A]"].data 50 | ) 51 | dataset["Current [A]"] = test_current 52 | assert np.all(dataset["Current [A]"] == test_current) 53 | with pytest.raises(ValueError): 54 | dataset["Time"] 55 | 56 | # Test conversion of single signal to list 57 | assert dataset.check() 58 | 59 | # Test get subset 60 | dataset = dataset.get_subset(list(range(5))) 61 | assert len(dataset[dataset.domain]) == 5 62 | 63 | # Form frequency dataset 64 | data_dictionary = { 65 | "Frequency [Hz]": np.linspace(-10, 0, 10), 66 | "Current [A]": np.zeros(10), 67 | "Impedance": np.zeros(10), 68 | } 69 | frequency_dataset = pybop.Dataset(data_dictionary) 70 | 71 | with pytest.raises(ValueError, match="Frequencies cannot be negative."): 72 | frequency_dataset.check(domain="Frequency [Hz]", signal="Impedance") 73 | -------------------------------------------------------------------------------- /tests/unit/test_experiment.py: -------------------------------------------------------------------------------- 1 | import pybamm 2 | import pytest 3 | 4 | import pybop 5 | 6 | 7 | class TestExperiment: 8 | """ 9 | Class to test the experiment class. 10 | """ 11 | 12 | pytestmark = pytest.mark.unit 13 | 14 | def test_experiment(self): 15 | # Define example protocol 16 | protocol = [("Discharge at 1 C for 20 seconds")] 17 | 18 | # Construct matching experiments 19 | pybop_experiment = pybop.Experiment(protocol) 20 | pybamm_experiment = pybamm.Experiment(protocol) 21 | 22 | assert [step.to_dict() for step in pybop_experiment.steps] == [ 23 | step.to_dict() for step in pybamm_experiment.steps 24 | ] 25 | 26 | assert pybop_experiment.cycle_lengths == pybamm_experiment.cycle_lengths 27 | 28 | assert str(pybop_experiment) == str(pybamm_experiment) 29 | 30 | assert repr(pybop_experiment) == repr(pybamm_experiment) 31 | 32 | assert pybop_experiment.termination == pybamm_experiment.termination 33 | -------------------------------------------------------------------------------- /tests/unit/test_import.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import sys 3 | from unittest.mock import patch 4 | 5 | import pytest 6 | 7 | 8 | class TestImport: 9 | pytestmark = pytest.mark.unit 10 | 11 | def test_multiprocessing_init_non_win32(self, monkeypatch): 12 | """Test multiprocessing init on non-Windows platforms""" 13 | monkeypatch.setattr(sys, "platform", "linux") 14 | # Unload pybop and its sub-modules 15 | self.unload_pybop() 16 | with patch("multiprocessing.set_start_method") as mock_set_start_method: 17 | importlib.import_module("pybop") 18 | mock_set_start_method.assert_called_once_with("fork") 19 | 20 | def test_multiprocessing_init_win32(self, monkeypatch): 21 | """Test multiprocessing init on Windows""" 22 | monkeypatch.setattr(sys, "platform", "win32") 23 | self.unload_pybop() 24 | with patch("multiprocessing.set_start_method") as mock_set_start_method: 25 | importlib.import_module("pybop") 26 | mock_set_start_method.assert_called_once_with("spawn") 27 | 28 | def unload_pybop(self): 29 | """ 30 | Unload pybop and its sub-modules. Credit PyBaMM team: 31 | https://github.com/pybamm-team/PyBaMM/blob/90c1c357a97dfd5c8c6a9092a70dddf0dac978db/tests/unit/test_util.py 32 | """ 33 | # Unload pybop and its sub-modules 34 | for module_name in list(sys.modules.keys()): 35 | base_module_name = module_name.split(".")[0] 36 | if base_module_name == "pybop": 37 | sys.modules.pop(module_name) 38 | -------------------------------------------------------------------------------- /tests/unit/test_solvers.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pybamm 3 | import pytest 4 | 5 | import pybop 6 | 7 | 8 | class TestSolvers: 9 | """ 10 | A class to test the forward model solver interface 11 | """ 12 | 13 | pytestmark = pytest.mark.unit 14 | 15 | @pytest.fixture( 16 | params=[ 17 | pybamm.IDAKLUSolver(atol=1e-4, rtol=1e-4), 18 | pybamm.CasadiSolver(atol=1e-4, rtol=1e-4, mode="safe"), 19 | pybamm.CasadiSolver(atol=1e-4, rtol=1e-4, mode="fast with events"), 20 | ] 21 | ) 22 | def solver(self, request): 23 | solver = request.param 24 | return solver.copy() 25 | 26 | @pytest.fixture 27 | def model(self, solver): 28 | parameter_set = pybop.ParameterSet.pybamm("Marquis2019") 29 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set, solver=solver) 30 | return model 31 | 32 | def test_solvers_with_model_predict(self, model, solver): 33 | assert model.solver == solver 34 | assert model.solver.atol == 1e-4 35 | assert model.solver.rtol == 1e-4 36 | 37 | # Ensure solver is functional 38 | sol = model.predict(t_eval=np.linspace(0, 1, 100)) 39 | assert np.isfinite(sol["Voltage [V]"].data).all() 40 | 41 | signals = ["Voltage [V]", "Bulk open-circuit voltage [V]"] 42 | additional_vars = [ 43 | "Maximum negative particle concentration", 44 | "Positive electrode volume-averaged concentration [mol.m-3]", 45 | ] 46 | 47 | parameters = pybop.Parameters( 48 | pybop.Parameter( 49 | "Negative electrode conductivity [S.m-1]", prior=pybop.Uniform(0.1, 100) 50 | ) 51 | ) 52 | dataset = pybop.Dataset( 53 | { 54 | "Time [s]": sol["Time [s]"].data, 55 | "Current function [A]": sol["Current [A]"].data, 56 | "Voltage [V]": sol["Voltage [V]"].data, 57 | "Bulk open-circuit voltage [V]": sol[ 58 | "Bulk open-circuit voltage [V]" 59 | ].data, 60 | } 61 | ) 62 | problem = pybop.FittingProblem( 63 | model, 64 | parameters=parameters, 65 | dataset=dataset, 66 | signal=signals, 67 | additional_variables=additional_vars, 68 | ) 69 | 70 | y = problem.evaluate(inputs={"Negative electrode conductivity [S.m-1]": 10}) 71 | 72 | for signal in signals: 73 | assert np.isfinite(y[signal].data).all() 74 | 75 | if isinstance(model.solver, pybamm.IDAKLUSolver): 76 | assert model.solver.output_variables is not None 77 | --------------------------------------------------------------------------------