diff --git a/.github/workflows/lychee_links.yaml b/.github/workflows/lychee_links.yaml new file mode 100644 index 000000000..a5d181f94 --- /dev/null +++ b/.github/workflows/lychee_links.yaml @@ -0,0 +1,58 @@ +# Lychee Link Checking + +name: Links +on: + workflow_dispatch: + pull_request: + push: + branches: + - main + schedule: + - cron: '0 6 * * 0' # Run weekly on Sundays at 06:00 UTC + +jobs: + Lychee: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Restore lychee cache + uses: actions/cache@v4 + with: + path: .lycheecache + key: cache-lychee-${{ github.sha }} + restore-keys: cache-lychee- + + - name: Set up Lychee + uses: lycheeverse/lychee-action@v1.10.0 + with: + args: >- + --cache + --no-progress + --max-cache-age 2d + --timeout 10 + --max-retries 5 + --skip-missing + --exclude-loopback + --accept 200,429 + --exclude "https://tiles.stadiamaps.com/*|https://b.tile.openstreetmap.org/*" + --exclude "https://cartodb-basemaps-c.global.ssl.fastly.net/*" + --exclude "https://events.mapbox.com/*|https://events.mapbox.cn/*|https://api.mapbox.cn/*" + --exclude "https://github.com/mikolalysenko/glsl-read-float/*" + --exclude "https://fonts.openmaptiles.org/*" + --exclude "https://a.tile.openstreetmap.org/*" + --exclude "https://cdn.plot.ly/*" + --exclude "https://doi.org/*" + --exclude-path ./CHANGELOG.md + --exclude-path asv.conf.json + --exclude-path docs/conf.py + './**/*.rst' + './**/*.md' + './**/*.py' + './**/*.ipynb' + './**/*.json' + './**/*.toml' + fail: true + jobSummary: true + format: markdown diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 47ef467c5..82299a924 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.5.0" + rev: "v0.5.1" hooks: - id: ruff args: [--fix, --show-fixes] diff --git a/CHANGELOG.md b/CHANGELOG.md index 108a48dfd..8fcf76005 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Features - [#327](https://github.com/pybop-team/PyBOP/issues/327) - Adds the `WeightedCost` subclass, defines when to evaluate a problem and adds the `spm_weighted_cost` example script. +- [#403](https://github.com/pybop-team/PyBOP/pull/403/) - Adds lychee link checking action. ## Bug Fixes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 727c5c510..110debbb2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -62,7 +62,7 @@ You now have everything you need to start making changes! ### B. Writing your code -6. PyBOP is developed in [Python](https://en.wikipedia.org/wiki/Python_(programming_language)), and makes heavy use of [NumPy](https://en.wikipedia.org/wiki/NumPy) (see also [NumPy for MatLab users](https://numpy.org/doc/stable/user/numpy-for-matlab-users.html) and [Python for R users](http://blog.hackerearth.com/how-can-r-users-learn-python-for-data-science)). +6. PyBOP is developed in [Python](https://en.wikipedia.org/wiki/Python_(programming_language)), and makes heavy use of [NumPy](https://en.wikipedia.org/wiki/NumPy) (see also [NumPy for MatLab users](https://numpy.org/doc/stable/user/numpy-for-matlab-users.html) and [Python for R users](https://rebeccabarter.com/blog/2023-09-11-from_r_to_python)). 7. Make sure to follow our [coding style guidelines](#coding-style-guidelines). 8. Commit your changes to your branch with [useful, descriptive commit messages](https://chris.beams.io/posts/git-commit/): Remember these are publicly visible and should still make sense a few months ahead in time. While developing, you can keep using the GitHub issue you're working on as a place for discussion. [Refer to your commits](https://stackoverflow.com/questions/8910271/how-can-i-reference-a-commit-in-an-issue-comment-on-github) when discussing specific lines of code. 9. If you want to add a dependency on another library, or re-use code you found somewhere else, have a look at [these guidelines](#dependencies-and-reusing-code). diff --git a/README.md b/README.md index 99f7a031e..9080dc9b6 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ [![Contributors](https://img.shields.io/github/contributors/pybop-team/PyBOP)](https://github.com/pybop-team/PyBOP/graphs/contributors) [![Last Commit](https://img.shields.io/github/last-commit/pybop-team/PyBOP/develop?color=purple)](https://github.com/pybop-team/PyBOP/commits/develop) [![Python Versions from PEP 621 TOML](https://img.shields.io/python/required-version-toml?tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2Fpybop-team%2FPyBOP%2Fdevelop%2Fpyproject.toml&label=Python)](https://pypi.org/project/pybop/) - [![Forks](https://img.shields.io/github/forks/pybop-team/PyBOP?style=flat)](https://github.com/pybop-team/PyBOPe/network/members) + [![Forks](https://img.shields.io/github/forks/pybop-team/PyBOP?style=flat)](https://github.com/pybop-team/PyBOP/network/members) [![Stars](https://img.shields.io/github/stars/pybop-team/PyBOP?style=flat&color=gold)](https://github.com/pybop-team/PyBOP/stargazers) [![Codecov](https://codecov.io/gh/pybop-team/PyBOP/branch/develop/graph/badge.svg)](https://codecov.io/gh/pybop-team/PyBOP) [![Open Issues](https://img.shields.io/github/issues/pybop-team/PyBOP)](https://github.com/pybop-team/PyBOP/issues/) @@ -74,7 +74,7 @@ Additional script-based examples can be found in the [examples directory](https: - [Unscented Kalman filter parameter identification of a SPM](https://github.com/pybop-team/PyBOP/blob/develop/examples/scripts/spm_UKF.py) - [Import and export parameters using Faraday's BPX format](https://github.com/pybop-team/PyBOP/blob/develop/examples/scripts/BPX_spm.py) - [Maximum a posteriori parameter identification of a SPM](https://github.com/pybop-team/PyBOP/blob/develop/examples/scripts/BPX_spm.py) -- [Gradient based parameter identification of a SPM](https://github.com/pybop-team/PyBOP/blob/develop/examples/scripts/spm_adam.py) +- [Gradient based parameter identification of a SPM](https://github.com/pybop-team/PyBOP/blob/develop/examples/scripts/spm_AdamW.py) ### Supported Methods The table below lists the currently supported [models](https://github.com/pybop-team/PyBOP/tree/develop/pybop/models), [optimisers](https://github.com/pybop-team/PyBOP/tree/develop/pybop/optimisers), and [cost functions](https://github.com/pybop-team/PyBOP/tree/develop/pybop/costs) in PyBOP. diff --git a/benchmarks/benchmark_model.py b/benchmarks/benchmark_model.py index 843b03bcc..5d496215b 100644 --- a/benchmarks/benchmark_model.py +++ b/benchmarks/benchmark_model.py @@ -1,8 +1,7 @@ import numpy as np import pybop - -from .benchmark_utils import set_random_seed +from benchmarks.benchmark_utils import set_random_seed class BenchmarkModel: diff --git a/benchmarks/benchmark_optim_construction.py b/benchmarks/benchmark_optim_construction.py index fee5f0789..75bb28b3c 100644 --- a/benchmarks/benchmark_optim_construction.py +++ b/benchmarks/benchmark_optim_construction.py @@ -1,8 +1,7 @@ import numpy as np import pybop - -from .benchmark_utils import set_random_seed +from benchmarks.benchmark_utils import set_random_seed class BenchmarkOptimisationConstruction: diff --git a/benchmarks/benchmark_parameterisation.py b/benchmarks/benchmark_parameterisation.py index a64116a48..681502387 100644 --- a/benchmarks/benchmark_parameterisation.py +++ b/benchmarks/benchmark_parameterisation.py @@ -1,8 +1,7 @@ import numpy as np import pybop - -from .benchmark_utils import set_random_seed +from benchmarks.benchmark_utils import set_random_seed class BenchmarkParameterisation: diff --git a/benchmarks/benchmark_track_parameterisation.py b/benchmarks/benchmark_track_parameterisation.py index 9180ffecb..a420dd3b9 100644 --- a/benchmarks/benchmark_track_parameterisation.py +++ b/benchmarks/benchmark_track_parameterisation.py @@ -1,8 +1,7 @@ import numpy as np import pybop - -from .benchmark_utils import set_random_seed +from benchmarks.benchmark_utils import set_random_seed class BenchmarkTrackParameterisation: diff --git a/docs/_extension/gallery_directive.py b/docs/_extension/gallery_directive.py index 3579ffcd8..4ab88d996 100644 --- a/docs/_extension/gallery_directive.py +++ b/docs/_extension/gallery_directive.py @@ -12,7 +12,7 @@ """ from pathlib import Path -from typing import Any, Dict, List +from typing import Any from docutils import nodes from docutils.parsers.rst import directives @@ -68,7 +68,7 @@ class GalleryGridDirective(SphinxDirective): "class-card": directives.unchanged, } - def run(self) -> List[nodes.Node]: + def run(self) -> list[nodes.Node]: """Create the gallery grid.""" if self.arguments: # If an argument is given, assume it's a path to a YAML file @@ -129,7 +129,7 @@ def run(self) -> List[nodes.Node]: return [container.children[0]] -def setup(app: Sphinx) -> Dict[str, Any]: +def setup(app: Sphinx) -> dict[str, Any]: """Add custom configuration to sphinx app. Args: diff --git a/docs/conf.py b/docs/conf.py index df93a8fa4..d7c54b116 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -7,11 +7,11 @@ from pathlib import Path sys.path.append(str(Path(".").resolve())) -from pybop._version import __version__ # noqa: E402 +from pybop._version import __version__ # -- Project information ----------------------------------------------------- project = "PyBOP" -copyright = "2023, The PyBOP Team" +copyright = "2023, The PyBOP Team" # noqa A001 author = "The PyBOP Team" release = f"v{__version__}" diff --git a/examples/notebooks/LG_M50_ECM/1-single-pulse-circuit-model.ipynb b/examples/notebooks/LG_M50_ECM/1-single-pulse-circuit-model.ipynb index 9fd084dd6..9030596d8 100644 --- a/examples/notebooks/LG_M50_ECM/1-single-pulse-circuit-model.ipynb +++ b/examples/notebooks/LG_M50_ECM/1-single-pulse-circuit-model.ipynb @@ -7,7 +7,7 @@ "source": [ "## LG M50 Single Pulse Parameter Identification\n", "\n", - "This example presents an experimental parameter identification method for a two-RC circuit model. The data for this notebook is located within the same directory and was obtained from [[1]](https://github.com/WDWidanage/Simscape-Battery-Library/tree/main/Examples/parameterEstimation_TECMD/Data).\n", + "This example presents an experimental parameter identification method for a two-RC circuit model. The data for this notebook is located within the same directory and was obtained from WDWidanage/Simscape-Battery-Library [[1]](https://github.com/WDWidanage/Simscape-Battery-Library/tree/a3842b91b3ccda006bc9be5d59c8bcbd167ceef7/Examples/parameterEstimation_TECMD/Data).\n", "\n", "\n", "### Setting up the Environment\n", @@ -261,7 +261,7 @@ { "type": "scatter", "x": [ - 0.0, + 0, 0.01800000004004687, 0.12100000004284084, 0.25100000004749745, @@ -571,7 +571,7 @@ }, "colorscale": [ [ - 0.0, + 0, "#0d0887" ], [ @@ -607,7 +607,7 @@ "#fdca26" ], [ - 1.0, + 1, "#f0f921" ] ], @@ -631,7 +631,7 @@ }, "colorscale": [ [ - 0.0, + 0, "#0d0887" ], [ @@ -667,7 +667,7 @@ "#fdca26" ], [ - 1.0, + 1, "#f0f921" ] ], @@ -682,7 +682,7 @@ }, "colorscale": [ [ - 0.0, + 0, "#0d0887" ], [ @@ -718,7 +718,7 @@ "#fdca26" ], [ - 1.0, + 1, "#f0f921" ] ], @@ -745,7 +745,7 @@ }, "colorscale": [ [ - 0.0, + 0, "#0d0887" ], [ @@ -781,7 +781,7 @@ "#fdca26" ], [ - 1.0, + 1, "#f0f921" ] ], @@ -796,7 +796,7 @@ }, "colorscale": [ [ - 0.0, + 0, "#0d0887" ], [ @@ -832,7 +832,7 @@ "#fdca26" ], [ - 1.0, + 1, "#f0f921" ] ], @@ -977,7 +977,7 @@ }, "colorscale": [ [ - 0.0, + 0, "#0d0887" ], [ @@ -1013,7 +1013,7 @@ "#fdca26" ], [ - 1.0, + 1, "#f0f921" ] ], @@ -1104,7 +1104,7 @@ ], "sequential": [ [ - 0.0, + 0, "#0d0887" ], [ @@ -1140,13 +1140,13 @@ "#fdca26" ], [ - 1.0, + 1, "#f0f921" ] ], "sequentialminus": [ [ - 0.0, + 0, "#0d0887" ], [ @@ -1182,7 +1182,7 @@ "#fdca26" ], [ - 1.0, + 1, "#f0f921" ] ] @@ -1888,7 +1888,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.2" + "version": "3.11.7" } }, "nbformat": 4, diff --git a/examples/notebooks/pouch_cell_identification.ipynb b/examples/notebooks/pouch_cell_identification.ipynb index d952e22c7..11c87993c 100644 --- a/examples/notebooks/pouch_cell_identification.ipynb +++ b/examples/notebooks/pouch_cell_identification.ipynb @@ -8,7 +8,7 @@ "source": [ "## Pouch Cell Model Parameter Identification\n", "\n", - "In this notebook, we present the single particle model with a two dimensional current collector. This is achieved via the potential-pair models introduced in [[1]](10.1149/1945-7111/abbce4) as implemented in PyBaMM. At a high-level this is accomplished as a potential-pair model which is resolved across the discretised spatial locations.\n", + "In this notebook, we present the single particle model with a two dimensional current collector. This is achieved via the potential-pair models introduced in Marquis et al. [[1]](https://doi.org/10.1149/1945-7111/abbce4) as implemented in PyBaMM. At a high-level this is accomplished as a potential-pair model which is resolved across the discretised spatial locations.\n", "\n", "### Setting up the Environment\n", "\n", @@ -36,58 +36,38 @@ "name": "stdout", "output_type": "stream", "text": [ - "Requirement already satisfied: pip in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (24.0)\r\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Requirement already satisfied: ipywidgets in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (8.1.2)\r\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Requirement already satisfied: comm>=0.1.3 in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (from ipywidgets) (0.2.2)\r\n", - "Requirement already satisfied: ipython>=6.1.0 in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (from ipywidgets) (8.23.0)\r\n", - "Requirement already satisfied: traitlets>=4.3.1 in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (from ipywidgets) (5.14.2)\r\n", - "Requirement already satisfied: widgetsnbextension~=4.0.10 in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (from ipywidgets) (4.0.10)\r\n", - "Requirement already satisfied: jupyterlab-widgets~=3.0.10 in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (from ipywidgets) (3.0.10)\r\n", - "Requirement already satisfied: decorator in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (from ipython>=6.1.0->ipywidgets) (5.1.1)\r\n", - "Requirement already satisfied: jedi>=0.16 in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (from ipython>=6.1.0->ipywidgets) (0.19.1)\r\n", - "Requirement already satisfied: matplotlib-inline in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (from ipython>=6.1.0->ipywidgets) (0.1.6)\r\n", - "Requirement already satisfied: prompt-toolkit<3.1.0,>=3.0.41 in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (from ipython>=6.1.0->ipywidgets) (3.0.43)\r\n", - "Requirement already satisfied: pygments>=2.4.0 in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (from ipython>=6.1.0->ipywidgets) (2.17.2)\r\n", - "Requirement already satisfied: stack-data in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (from ipython>=6.1.0->ipywidgets) (0.6.3)\r\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Requirement already satisfied: pexpect>4.3 in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (from ipython>=6.1.0->ipywidgets) (4.9.0)\r\n", - "Requirement already satisfied: parso<0.9.0,>=0.8.3 in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (from jedi>=0.16->ipython>=6.1.0->ipywidgets) (0.8.4)\r\n", - "Requirement already satisfied: ptyprocess>=0.5 in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (from pexpect>4.3->ipython>=6.1.0->ipywidgets) (0.7.0)\r\n", - "Requirement already satisfied: wcwidth in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (from prompt-toolkit<3.1.0,>=3.0.41->ipython>=6.1.0->ipywidgets) (0.2.13)\r\n", - "Requirement already satisfied: executing>=1.2.0 in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (from stack-data->ipython>=6.1.0->ipywidgets) (2.0.1)\r\n", - "Requirement already satisfied: asttokens>=2.1.0 in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (from stack-data->ipython>=6.1.0->ipywidgets) (2.4.1)\r\n", - "Requirement already satisfied: pure-eval in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (from stack-data->ipython>=6.1.0->ipywidgets) (0.2.2)\r\n", - "Requirement already satisfied: six>=1.12.0 in /Users/engs2510/.pyenv/versions/pybop-3.12/lib/python3.12/site-packages (from asttokens>=2.1.0->stack-data->ipython>=6.1.0->ipywidgets) (1.16.0)\r\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Note: you may need to restart the kernel to use updated packages.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ + "Requirement already satisfied: pip in /Users/engs2510/.pyenv/versions/3.12.2/envs/pybop-3.12/lib/python3.12/site-packages (24.1.1)\n", + "Collecting pip\n", + " Downloading pip-24.1.2-py3-none-any.whl.metadata (3.6 kB)\n", + "Requirement already satisfied: ipywidgets in /Users/engs2510/.pyenv/versions/3.12.2/envs/pybop-3.12/lib/python3.12/site-packages (8.1.3)\n", + "Requirement already satisfied: comm>=0.1.3 in /Users/engs2510/.pyenv/versions/3.12.2/envs/pybop-3.12/lib/python3.12/site-packages (from ipywidgets) (0.2.2)\n", + "Requirement already satisfied: ipython>=6.1.0 in /Users/engs2510/.pyenv/versions/3.12.2/envs/pybop-3.12/lib/python3.12/site-packages (from ipywidgets) (8.23.0)\n", + "Requirement already satisfied: traitlets>=4.3.1 in /Users/engs2510/.pyenv/versions/3.12.2/envs/pybop-3.12/lib/python3.12/site-packages (from ipywidgets) (5.14.2)\n", + "Requirement already satisfied: widgetsnbextension~=4.0.11 in /Users/engs2510/.pyenv/versions/3.12.2/envs/pybop-3.12/lib/python3.12/site-packages (from ipywidgets) (4.0.11)\n", + "Requirement already satisfied: jupyterlab-widgets~=3.0.11 in /Users/engs2510/.pyenv/versions/3.12.2/envs/pybop-3.12/lib/python3.12/site-packages (from ipywidgets) (3.0.11)\n", + "Requirement already satisfied: decorator in /Users/engs2510/.pyenv/versions/3.12.2/envs/pybop-3.12/lib/python3.12/site-packages (from ipython>=6.1.0->ipywidgets) (5.1.1)\n", + "Requirement already satisfied: jedi>=0.16 in /Users/engs2510/.pyenv/versions/3.12.2/envs/pybop-3.12/lib/python3.12/site-packages (from ipython>=6.1.0->ipywidgets) (0.19.1)\n", + "Requirement already satisfied: matplotlib-inline in /Users/engs2510/.pyenv/versions/3.12.2/envs/pybop-3.12/lib/python3.12/site-packages (from ipython>=6.1.0->ipywidgets) (0.1.6)\n", + "Requirement already satisfied: prompt-toolkit<3.1.0,>=3.0.41 in /Users/engs2510/.pyenv/versions/3.12.2/envs/pybop-3.12/lib/python3.12/site-packages (from ipython>=6.1.0->ipywidgets) (3.0.43)\n", + "Requirement already satisfied: pygments>=2.4.0 in /Users/engs2510/.pyenv/versions/3.12.2/envs/pybop-3.12/lib/python3.12/site-packages (from ipython>=6.1.0->ipywidgets) (2.17.2)\n", + "Requirement already satisfied: stack-data in /Users/engs2510/.pyenv/versions/3.12.2/envs/pybop-3.12/lib/python3.12/site-packages (from ipython>=6.1.0->ipywidgets) (0.6.3)\n", + "Requirement already satisfied: pexpect>4.3 in /Users/engs2510/.pyenv/versions/3.12.2/envs/pybop-3.12/lib/python3.12/site-packages (from ipython>=6.1.0->ipywidgets) (4.9.0)\n", + "Requirement already satisfied: parso<0.9.0,>=0.8.3 in /Users/engs2510/.pyenv/versions/3.12.2/envs/pybop-3.12/lib/python3.12/site-packages (from jedi>=0.16->ipython>=6.1.0->ipywidgets) (0.8.4)\n", + "Requirement already satisfied: ptyprocess>=0.5 in /Users/engs2510/.pyenv/versions/3.12.2/envs/pybop-3.12/lib/python3.12/site-packages (from pexpect>4.3->ipython>=6.1.0->ipywidgets) (0.7.0)\n", + "Requirement already satisfied: wcwidth in /Users/engs2510/.pyenv/versions/3.12.2/envs/pybop-3.12/lib/python3.12/site-packages (from prompt-toolkit<3.1.0,>=3.0.41->ipython>=6.1.0->ipywidgets) (0.2.13)\n", + "Requirement already satisfied: executing>=1.2.0 in /Users/engs2510/.pyenv/versions/3.12.2/envs/pybop-3.12/lib/python3.12/site-packages (from stack-data->ipython>=6.1.0->ipywidgets) (2.0.1)\n", + "Requirement already satisfied: asttokens>=2.1.0 in /Users/engs2510/.pyenv/versions/3.12.2/envs/pybop-3.12/lib/python3.12/site-packages (from stack-data->ipython>=6.1.0->ipywidgets) (2.4.1)\n", + "Requirement already satisfied: pure-eval in /Users/engs2510/.pyenv/versions/3.12.2/envs/pybop-3.12/lib/python3.12/site-packages (from stack-data->ipython>=6.1.0->ipywidgets) (0.2.2)\n", + "Requirement already satisfied: six>=1.12.0 in /Users/engs2510/.pyenv/versions/3.12.2/envs/pybop-3.12/lib/python3.12/site-packages (from asttokens>=2.1.0->stack-data->ipython>=6.1.0->ipywidgets) (1.16.0)\n", + "Downloading pip-24.1.2-py3-none-any.whl (1.8 MB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m1.8/1.8 MB\u001b[0m \u001b[31m14.9 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m00:01\u001b[0m00:01\u001b[0m\n", + "\u001b[?25hInstalling collected packages: pip\n", + " Attempting uninstall: pip\n", + " Found existing installation: pip 24.1.1\n", + " Uninstalling pip-24.1.1:\n", + " Successfully uninstalled pip-24.1.1\n", + "Successfully installed pip-24.1.2\n", + "Note: you may need to restart the kernel to use updated packages.\n", "Note: you may need to restart the kernel to use updated packages.\n" ] } @@ -454,7 +434,7 @@ { "data": { "text/plain": [ - "array([0.47496537, 0.61140011])" + "array([0.50112972, 0.60822849])" ] }, "execution_count": 12, @@ -509,7 +489,7 @@ { "data": { "image/svg+xml": [ - "02004006008003.73.723.743.763.78ReferenceModelOptimised ComparisonTime / sVoltage / V" + "02004006008003.723.743.763.78ReferenceModelOptimised ComparisonTime / sVoltage / V" ] }, "metadata": {}, @@ -541,35 +521,6 @@ } }, "outputs": [ - { - "data": { - "text/html": [ - " \n", - " " - ] - }, - "metadata": {}, - "output_type": "display_data" - }, { "data": { "application/vnd.plotly.v1+json": { @@ -635,39 +586,39 @@ ], "z": [ [ - -0.00022055019400039357, - -0.000208179915115477, - -0.00017733967875981405, - -0.00013382475248593996, - -0.00009744280975100401 + -0.00022054368296133442, + -0.00020817417477179742, + -0.00017733555998179898, + -0.0001338224058942881, + -0.00009744128489582522 ], [ - -0.00022006829217595638, - -0.00020771646089835422, - -0.00017025065441761346, - -0.00010411185465723509, - -1.4831730728913642e-30 + -0.0002200624215295616, + -0.00020771124661048325, + -0.00017024715510881273, + -0.00010411043723798278, + 2.0121777293998183e-28 ], [ - -0.00023289339271513726, - -0.00022148992831206213, - -0.0001854997008341146, - -0.00011792954324326614, - 1.7126139220369943e-29 + -0.00023288833495346782, + -0.0002214855296927363, + -0.00018549704922968932, + -0.0001179289043801153, + 1.0313790127184039e-29 ], [ - -0.00025468769038466233, - -0.0002482874721993978, - -0.00022970881918716953, - -0.00020352723357102716, - -0.0001826834881022567 + -0.0002546830904738895, + -0.0002482834858238037, + -0.00022970648308007864, + -0.00020352686864885834, + -0.00018268420575956896 ], [ - -0.0002626049278499214, - -0.00025977998017220894, - -0.00024814221133557976, - -0.00023376411978352077, - -0.00022697336267452442 + -0.00026260052021642185, + -0.00025977603234850937, + -0.0002481397380647309, + -0.0002337634294784395, + -0.00022697371294696725 ] ] } @@ -1505,34 +1456,7 @@ } } } - }, - "text/html": [ - "
" - ] + } }, "metadata": {}, "output_type": "display_data" @@ -1644,39 +1568,39 @@ ], "z": [ [ - 3.7078876177014553, - 3.7078763478472188, - 3.707853323934633, - 3.7078260164047094, - 3.707808564119712 + 3.7080242488013124, + 3.708012980167902, + 3.7079899587671075, + 3.707962653872268, + 3.7079452026255506 ], [ - 3.7078651244026894, - 3.7078535908947208, - 3.7078220221902427, - 3.7077785523089566, - 3.7077445721396733 + 3.7080017566721923, + 3.7079902242011094, + 3.7079586581556887, + 3.707915191282287, + 3.7078812123884246 ], [ - 3.7078229486184475, - 3.707804218479802, - 3.7077454950451716, - 3.7076369622920593, - 3.7074529354653967 + 3.7079595825355014, + 3.7079408535029024, + 3.7078821330256533, + 3.7077736038127984, + 3.7075895787025472 ], [ - 3.7077939368814126, - 3.707773778768286, - 3.7077102612800763, - 3.7075952142136557, - 3.707407186787348 + 3.707930571743512, + 3.707910414663849, + 3.7078468999564493, + 3.707731856210595, + 3.7075438305495547 ], [ - 3.7077846062570954, - 3.70776995420107, - 3.707720253036007, - 3.707647593021552, - 3.707589085925729 + 3.707921241493712, + 3.7079065902163797, + 3.7078568915338876, + 3.707784234516676, + 3.707725729161621 ] ] } @@ -2514,34 +2438,7 @@ } } } - }, - "text/html": [ - "
" - ] + } }, "metadata": {}, "output_type": "display_data" @@ -2592,7 +2489,7 @@ { "data": { "image/svg+xml": [ - "510152025300.00050.00060.00070.00080.00090.001ConvergenceIterationCost" + "510152025300.00050.0010.00150.0020.0025ConvergenceIterationCost" ] }, "metadata": {}, @@ -2601,7 +2498,7 @@ { "data": { "image/svg+xml": [ - "0501001500.450.50.550.60.650.70.750.80.850.90501001500.50.520.540.560.580.60.620.640.660.68Negative electrode active material volume fractionPositive electrode active material volume fractionParameter ConvergenceFunction CallFunction CallNegative electrode active material volume fractionPositive electrode active material volume fraction" + "501001500.50.550.60.650.70.75501001500.550.60.650.7Negative electrode active material volume fractionPositive electrode active material volume fractionParameter ConvergenceFunction CallFunction CallNegative electrode active material volume fractionPositive electrode active material volume fraction" ] }, "metadata": {}, @@ -2637,7 +2534,7 @@ { "data": { "image/svg+xml": [ - "0.50.60.70.80.90.50.550.60.650.70.750.80.040.080.120.160.20.24Cost LandscapeNegative electrode active material volume fractionPositive electrode active material volume fraction" + "0.50.60.70.80.90.50.550.60.650.70.750.80.020.040.060.080.10.120.140.16Cost LandscapeNegative electrode active material volume fractionPositive electrode active material volume fraction" ] }, "metadata": {}, diff --git a/examples/notebooks/spm_electrode_design.ipynb b/examples/notebooks/spm_electrode_design.ipynb index e1fd58204..90571c993 100644 --- a/examples/notebooks/spm_electrode_design.ipynb +++ b/examples/notebooks/spm_electrode_design.ipynb @@ -10,7 +10,7 @@ "\n", "NOTE: This is a brittle example, the classes and methods below will be integrated into PyBOP in a future release.\n", "\n", - "A design optimisation example loosely based on work by L.D. Couto available at https://doi.org/10.1016/j.energy.2022.125966.\n", + "A design optimisation example loosely based on work by L.D. Couto available at [[1]](https://doi.org/10.1016/j.energy.2022.125966).\n", "\n", "The target is to maximise the gravimetric energy density over a range of possible design parameter values, including for example:\n", "\n", @@ -396,7 +396,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.2" + "version": "3.11.7" }, "widgets": { "application/vnd.jupyter.widget-state+json": { diff --git a/examples/standalone/problem.py b/examples/standalone/problem.py index d76f9dca5..18bf1f7d4 100644 --- a/examples/standalone/problem.py +++ b/examples/standalone/problem.py @@ -24,7 +24,7 @@ def __init__( self._dataset = dataset.data # Check that the dataset contains time and current - for name in ["Time [s]"] + self.signal: + for name in ["Time [s]", *self.signal]: if name not in self._dataset: raise ValueError(f"expected {name} in list of dataset") diff --git a/pybop/_dataset.py b/pybop/_dataset.py index 0da8be4be..66bcb1f10 100644 --- a/pybop/_dataset.py +++ b/pybop/_dataset.py @@ -1,6 +1,5 @@ import numpy as np -from pybamm import Interpolant, solvers -from pybamm import t as pybamm_t +from pybamm import solvers class Dataset: @@ -77,26 +76,7 @@ def __getitem__(self, key): return self.data[key] - def Interpolant(self): - """ - Create an interpolation function of the dataset based on the independent variable. - - Currently, only time-based interpolation is supported. This method modifies - the instance's Interpolant attribute to be an interpolation function that - can be evaluated at different points in time. - - Raises - ------ - NotImplementedError - If the independent variable for interpolation is not supported. - """ - - if self.variable == "time": - self.Interpolant = Interpolant(self.x, self.y, pybamm_t) - else: - NotImplementedError("Only time interpolation is supported") - - def check(self, signal=["Voltage [V]"]): + def check(self, signal=None): """ Check the consistency of a PyBOP Dataset against the expected format. @@ -110,11 +90,13 @@ def check(self, signal=["Voltage [V]"]): ValueError If the time series and the data series are not consistent. """ + if signal is None: + signal = ["Voltage [V]"] if isinstance(signal, str): signal = [signal] # Check that the dataset contains time and chosen signal - for name in ["Time [s]"] + signal: + for name in ["Time [s]", *signal]: if name not in self.names: raise ValueError(f"expected {name} in list of dataset") diff --git a/pybop/costs/_likelihoods.py b/pybop/costs/_likelihoods.py index 42fdf40df..d8a66360f 100644 --- a/pybop/costs/_likelihoods.py +++ b/pybop/costs/_likelihoods.py @@ -1,4 +1,4 @@ -from typing import List, Tuple, Union +from typing import Union import numpy as np @@ -14,7 +14,7 @@ class BaseLikelihood(BaseCost): """ def __init__(self, problem: BaseProblem): - super(BaseLikelihood, self).__init__(problem) + super().__init__(problem) self.n_time_data = problem.n_time_data @@ -32,8 +32,8 @@ class GaussianLogLikelihoodKnownSigma(BaseLikelihood): per dimension. """ - def __init__(self, problem: BaseProblem, sigma0: Union[List[float], float]): - super(GaussianLogLikelihoodKnownSigma, self).__init__(problem) + def __init__(self, problem: BaseProblem, sigma0: Union[list[float], float]): + super().__init__(problem) sigma0 = self.check_sigma0(sigma0) self.sigma2 = sigma0**2.0 self._offset = -0.5 * self.n_time_data * np.log(2 * np.pi * self.sigma2) @@ -65,7 +65,7 @@ def _evaluate(self, inputs: Inputs, grad: Union[None, np.ndarray] = None) -> flo return e if self.n_outputs != 1 else e.item() - def _evaluateS1(self, inputs: Inputs) -> Tuple[float, np.ndarray]: + def _evaluateS1(self, inputs: Inputs) -> tuple[float, np.ndarray]: """ Calculates the log-likelihood and gradient. """ @@ -99,7 +99,7 @@ def check_sigma0(self, sigma0: Union[np.ndarray, float]): if np.shape(sigma0) not in [(), (1,), (self.n_outputs,)]: raise ValueError( "sigma0 must be either a scalar value (one standard deviation for " - + "all coordinates) or an array with one entry per dimension." + "all coordinates) or an array with one entry per dimension." ) return sigma0 @@ -124,10 +124,10 @@ class GaussianLogLikelihood(BaseLikelihood): def __init__( self, problem: BaseProblem, - sigma0: Union[float, List[float], List[Parameter]] = 0.002, + sigma0: Union[float, list[float], list[Parameter]] = 0.002, dsigma_scale: float = 1.0, ): - super(GaussianLogLikelihood, self).__init__(problem) + super().__init__(problem) self._dsigma_scale = dsigma_scale self._logpi = -0.5 * self.n_time_data * np.log(2 * np.pi) self._fixed_problem = False # keep problem evaluation within _evaluate @@ -138,7 +138,7 @@ def __init__( self._dl = np.ones(self.n_parameters) def _add_sigma_parameters(self, sigma0): - sigma0 = [sigma0] if not isinstance(sigma0, List) else sigma0 + sigma0 = [sigma0] if not isinstance(sigma0, list) else sigma0 sigma0 = self._pad_sigma0(sigma0) for i, value in enumerate(sigma0): @@ -229,7 +229,7 @@ def _evaluate(self, inputs: Inputs, grad: Union[None, np.ndarray] = None) -> flo return e if self.n_outputs != 1 else e.item() - def _evaluateS1(self, inputs: Inputs) -> Tuple[float, np.ndarray]: + def _evaluateS1(self, inputs: Inputs) -> tuple[float, np.ndarray]: """ Calculates the log-likelihood and sensitivities. @@ -290,7 +290,7 @@ class MAP(BaseLikelihood): """ def __init__(self, problem, likelihood, sigma0=None, gradient_step=1e-3): - super(MAP, self).__init__(problem) + super().__init__(problem) self.sigma0 = sigma0 self.gradient_step = gradient_step if self.sigma0 is None: @@ -303,7 +303,7 @@ def __init__(self, problem, likelihood, sigma0=None, gradient_step=1e-3): except Exception as e: raise ValueError( f"An error occurred when constructing the Likelihood class: {e}" - ) + ) from e if hasattr(self, "likelihood") and not isinstance( self.likelihood, BaseLikelihood @@ -338,7 +338,7 @@ def _evaluate(self, inputs: Inputs, grad=None) -> float: posterior = log_likelihood + log_prior return posterior - def _evaluateS1(self, inputs: Inputs) -> Tuple[float, np.ndarray]: + def _evaluateS1(self, inputs: Inputs) -> tuple[float, np.ndarray]: """ Compute the maximum a posteriori with respect to the parameters. The method passes the likelihood gradient to the optimiser without modification. diff --git a/pybop/costs/base_cost.py b/pybop/costs/base_cost.py index bd11f8a35..322d082fc 100644 --- a/pybop/costs/base_cost.py +++ b/pybop/costs/base_cost.py @@ -79,7 +79,7 @@ def evaluate(self, x, grad=None): raise e except Exception as e: - raise ValueError(f"Error in cost calculation: {e}") + raise ValueError(f"Error in cost calculation: {e}") from e def _evaluate(self, inputs: Inputs, grad=None): """ @@ -141,7 +141,7 @@ def evaluateS1(self, x): raise e except Exception as e: - raise ValueError(f"Error in cost calculation: {e}") + raise ValueError(f"Error in cost calculation: {e}") from e def _evaluateS1(self, inputs: Inputs): """ diff --git a/pybop/costs/design_costs.py b/pybop/costs/design_costs.py index 056c348fe..738dfe61e 100644 --- a/pybop/costs/design_costs.py +++ b/pybop/costs/design_costs.py @@ -31,7 +31,7 @@ def __init__(self, problem, update_capacity=False): problem : object The problem instance containing the model and data. """ - super(DesignCost, self).__init__(problem) + super().__init__(problem) self.problem = problem if update_capacity is True: nominal_capacity_warning = ( @@ -41,7 +41,7 @@ def __init__(self, problem, update_capacity=False): nominal_capacity_warning = ( "The nominal capacity is fixed at the initial model value." ) - warnings.warn(nominal_capacity_warning, UserWarning) + warnings.warn(nominal_capacity_warning, UserWarning, stacklevel=2) self.update_capacity = update_capacity self.parameter_set = problem.model.parameter_set self.update_simulation_data(self.parameters.as_dict("initial")) @@ -97,7 +97,7 @@ class GravimetricEnergyDensity(DesignCost): """ def __init__(self, problem, update_capacity=False): - super(GravimetricEnergyDensity, self).__init__(problem, update_capacity) + super().__init__(problem, update_capacity) self._fixed_problem = False # keep problem evaluation within _evaluate def _evaluate(self, inputs: Inputs, grad=None): @@ -154,7 +154,7 @@ class VolumetricEnergyDensity(DesignCost): """ def __init__(self, problem, update_capacity=False): - super(VolumetricEnergyDensity, self).__init__(problem, update_capacity) + super().__init__(problem, update_capacity) self._fixed_problem = False # keep problem evaluation within _evaluate def _evaluate(self, inputs: Inputs, grad=None): diff --git a/pybop/costs/fitting_costs.py b/pybop/costs/fitting_costs.py index da4584242..13673287a 100644 --- a/pybop/costs/fitting_costs.py +++ b/pybop/costs/fitting_costs.py @@ -18,7 +18,7 @@ class RootMeanSquaredError(BaseCost): """ def __init__(self, problem): - super(RootMeanSquaredError, self).__init__(problem) + super().__init__(problem) # Default fail gradient self._de = 1.0 @@ -142,7 +142,7 @@ class SumSquaredError(BaseCost): """ def __init__(self, problem): - super(SumSquaredError, self).__init__(problem) + super().__init__(problem) # Default fail gradient self._de = 1.0 @@ -172,7 +172,7 @@ def _evaluate(self, inputs: Inputs, grad=None): e = np.asarray( [ - np.sum(((self._current_prediction[signal] - self._target[signal]) ** 2)) + np.sum((self._current_prediction[signal] - self._target[signal]) ** 2) for signal in self.signal ] ) diff --git a/pybop/models/base_model.py b/pybop/models/base_model.py index e9ba3d6c4..461989086 100644 --- a/pybop/models/base_model.py +++ b/pybop/models/base_model.py @@ -1,6 +1,6 @@ import copy from dataclasses import dataclass -from typing import Any, Dict, Optional, Union +from typing import Any, Optional, Union import casadi import numpy as np @@ -11,7 +11,7 @@ @dataclass -class TimeSeriesState(object): +class TimeSeriesState: """ The current state of a time series model that is a pybamm model. """ @@ -76,9 +76,9 @@ def __init__(self, name="Base Model", parameter_set=None): def build( self, dataset: Dataset = None, - parameters: Union[Parameters, Dict] = None, + parameters: Union[Parameters, dict] = None, check_model: bool = True, - init_soc: float = None, + init_soc: Optional[float] = None, ) -> None: """ Construct the PyBaMM model if not already built, and set parameters. @@ -191,10 +191,10 @@ def set_params(self, rebuild=False): def rebuild( self, dataset: Dataset = None, - parameters: Union[Parameters, Dict] = None, + parameters: Union[Parameters, dict] = None, parameter_set: ParameterSet = None, check_model: bool = True, - init_soc: float = None, + init_soc: Optional[float] = None, ) -> None: """ Rebuild the PyBaMM model for a given parameter set. @@ -329,7 +329,7 @@ def step(self, state: TimeSeriesState, time: np.ndarray) -> TimeSeriesState: def simulate( self, inputs: Inputs, t_eval: np.array - ) -> Dict[str, np.ndarray[np.float64]]: + ) -> dict[str, np.ndarray[np.float64]]: """ Execute the forward model simulation and return the result. @@ -457,8 +457,8 @@ def predict( t_eval: np.array = None, parameter_set: ParameterSet = None, experiment: Experiment = None, - init_soc: float = None, - ) -> Dict[str, np.ndarray[np.float64]]: + init_soc: Optional[float] = None, + ) -> dict[str, np.ndarray[np.float64]]: """ Solve the model using PyBaMM's simulation framework and return the solution. @@ -676,7 +676,7 @@ def submesh_types(self): return self._submesh_types @submesh_types.setter - def submesh_types(self, submesh_types: Optional[Dict[str, Any]]): + def submesh_types(self, submesh_types: Optional[dict[str, Any]]): self._submesh_types = ( submesh_types.copy() if submesh_types is not None else None ) @@ -690,7 +690,7 @@ def var_pts(self): return self._var_pts @var_pts.setter - def var_pts(self, var_pts: Optional[Dict[str, int]]): + def var_pts(self, var_pts: Optional[dict[str, int]]): self._var_pts = var_pts.copy() if var_pts is not None else None @property @@ -698,7 +698,7 @@ def spatial_methods(self): return self._spatial_methods @spatial_methods.setter - def spatial_methods(self, spatial_methods: Optional[Dict[str, Any]]): + def spatial_methods(self, spatial_methods: Optional[dict[str, Any]]): self._spatial_methods = ( spatial_methods.copy() if spatial_methods is not None else None ) diff --git a/pybop/models/lithium_ion/base_echem.py b/pybop/models/lithium_ion/base_echem.py index fd4aa2682..f60f500d3 100644 --- a/pybop/models/lithium_ion/base_echem.py +++ b/pybop/models/lithium_ion/base_echem.py @@ -136,7 +136,7 @@ def _check_params( ): if self.param_check_counter <= len(electrode_params): infeasibility_warning = "Non-physical point encountered - [{material_vol_fraction} + {porosity}] > 1.0!" - warnings.warn(infeasibility_warning, UserWarning) + warnings.warn(infeasibility_warning, UserWarning, stacklevel=2) self.param_check_counter += 1 return allow_infeasible_solutions @@ -308,7 +308,7 @@ def approximate_capacity(self, inputs: Inputs): mean_sto_pos ) - negative_electrode_ocp(mean_sto_neg) except Exception as e: - raise ValueError(f"Error in average voltage calculation: {e}") + raise ValueError(f"Error in average voltage calculation: {e}") from e # Calculate and update nominal capacity theoretical_capacity = theoretical_energy / average_voltage diff --git a/pybop/models/lithium_ion/echem.py b/pybop/models/lithium_ion/echem.py index 8bd8ab636..9fdd308e3 100644 --- a/pybop/models/lithium_ion/echem.py +++ b/pybop/models/lithium_ion/echem.py @@ -1,8 +1,7 @@ from pybamm import lithium_ion as pybamm_lithium_ion from pybop.models.lithium_ion.base_echem import EChemBaseModel - -from .weppner_huggins import BaseWeppnerHuggins +from pybop.models.lithium_ion.weppner_huggins import BaseWeppnerHuggins class SPM(EChemBaseModel): diff --git a/pybop/models/lithium_ion/weppner_huggins.py b/pybop/models/lithium_ion/weppner_huggins.py index 5d8d626a4..74c42c70e 100644 --- a/pybop/models/lithium_ion/weppner_huggins.py +++ b/pybop/models/lithium_ion/weppner_huggins.py @@ -36,7 +36,7 @@ def __init__(self, name="Weppner & Huggins model", **model_kwargs): # Model kwargs (build, options) are not implemented, keeping here for consistent interface if model_kwargs is not dict(build=True): unused_kwargs_warning = "The input model_kwargs are not currently used by the Weppner & Huggins model." - warnings.warn(unused_kwargs_warning, UserWarning) + warnings.warn(unused_kwargs_warning, UserWarning, stacklevel=2) super().__init__({}, name) diff --git a/pybop/observers/observer.py b/pybop/observers/observer.py index 1c35c25df..f7d6f25f3 100644 --- a/pybop/observers/observer.py +++ b/pybop/observers/observer.py @@ -38,10 +38,14 @@ def __init__( parameters: Parameters, model: BaseModel, check_model=True, - signal=["Voltage [V]"], - additional_variables=[], + signal=None, + additional_variables=None, init_soc=None, ) -> None: + if additional_variables is None: + additional_variables = [] + if signal is None: + signal = ["Voltage [V]"] super().__init__( parameters, model, check_model, signal, additional_variables, init_soc ) diff --git a/pybop/observers/unscented_kalman.py b/pybop/observers/unscented_kalman.py index afbc2a010..60f4f0949 100644 --- a/pybop/observers/unscented_kalman.py +++ b/pybop/observers/unscented_kalman.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import List, Tuple, Union +from typing import Union import numpy as np import scipy.linalg as linalg @@ -41,17 +41,21 @@ class UnscentedKalmanFilterObserver(Observer): def __init__( self, - parameters: List[Parameter], + parameters: list[Parameter], model: BaseModel, sigma0: Union[Covariance, float], process: Union[Covariance, float], measure: Union[Covariance, float], dataset=None, check_model=True, - signal=["Voltage [V]"], - additional_variables=[], + signal=None, + additional_variables=None, init_soc=None, ) -> None: + if additional_variables is None: + additional_variables = [] + if signal is None: + signal = ["Voltage [V]"] super().__init__( parameters, model, check_model, signal, additional_variables, init_soc ) @@ -59,7 +63,7 @@ def __init__( self._dataset = dataset.data # Check that the dataset contains time and current - dataset.check(self.signal + ["Current function [A]"]) + dataset.check([*self.signal, "Current function [A]"]) self._time_data = self._dataset["Time [s]"] self.n_time_data = len(self._time_data) @@ -152,7 +156,7 @@ def get_current_covariance(self) -> Covariance: @dataclass -class SigmaPoint(object): +class SigmaPoint: """ A sigma point is a point in the state space that is used to estimate the mean and covariance of a random variable. """ @@ -162,7 +166,7 @@ class SigmaPoint(object): w_c: float -class SquareRootUKF(object): +class SquareRootUKF: """ van der Menve, R., & Wan, E. A. (2001). THE SQUARE-ROOT UNSCENTED KALMAN FILTER FOR STATE AND PARAMETER-ESTIMATION. https://doi.org/10.1109/ICASSP.2001.940586 @@ -235,7 +239,7 @@ def reset(self, x: np.ndarray, S: np.ndarray) -> None: @staticmethod def gen_sigma_points( x: np.ndarray, S: np.ndarray, alpha: float, beta: float, states: np.ndarray - ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: """ Generates 2L+1 sigma points for the unscented transform, where L is the number of states. @@ -291,7 +295,7 @@ def unscented_transform( w_c: np.ndarray, sqrtR: np.ndarray, states: Union[np.ndarray, None] = None, - ) -> Tuple[np.ndarray, np.ndarray]: + ) -> tuple[np.ndarray, np.ndarray]: """ Performs the unscented transform diff --git a/pybop/optimisers/_adamw.py b/pybop/optimisers/_adamw.py index 24e5ec982..817d8f290 100644 --- a/pybop/optimisers/_adamw.py +++ b/pybop/optimisers/_adamw.py @@ -157,7 +157,7 @@ def tell(self, reply): # Check ask-tell pattern if not self._ready_for_tell: - raise Exception("ask() not called before tell()") + raise RuntimeError("ask() not called before tell()") self._ready_for_tell = False # Unpack reply diff --git a/pybop/optimisers/base_optimiser.py b/pybop/optimisers/base_optimiser.py index 4693468f2..2b00b2345 100644 --- a/pybop/optimisers/base_optimiser.py +++ b/pybop/optimisers/base_optimiser.py @@ -1,4 +1,5 @@ import warnings +from typing import Optional import numpy as np @@ -75,8 +76,9 @@ def __init__( cost_test = cost(self.x0) warnings.warn( "The cost is not an instance of pybop.BaseCost, but let's continue " - + "assuming that it is a callable function to be minimised.", + "assuming that it is a callable function to be minimised.", UserWarning, + stacklevel=2, ) self.cost = cost for i, value in enumerate(self.x0): @@ -85,8 +87,10 @@ def __init__( ) self.minimising = True - except Exception: - raise Exception("The cost is not a recognised cost object or function.") + except Exception as e: + raise Exception( + "The cost is not a recognised cost object or function." + ) from e if not np.isscalar(cost_test) or not np.isreal(cost_test): raise TypeError( @@ -211,7 +215,7 @@ def check_optimal_parameters(self, x): else: warnings.warn( "Optimised parameters are not physically viable! \nConsider retrying the optimisation" - + " with a non-gradient-based optimiser and the option allow_infeasible_solutions=False", + " with a non-gradient-based optimiser and the option allow_infeasible_solutions=False", UserWarning, stacklevel=2, ) @@ -269,8 +273,8 @@ class Result: def __init__( self, x: np.ndarray = None, - final_cost: float = None, - n_iterations: int = None, + final_cost: Optional[float] = None, + n_iterations: Optional[int] = None, scipy_result=None, ): self.x = x diff --git a/pybop/optimisers/pints_optimisers.py b/pybop/optimisers/pints_optimisers.py index 64b77b674..83f13aa00 100644 --- a/pybop/optimisers/pints_optimisers.py +++ b/pybop/optimisers/pints_optimisers.py @@ -272,7 +272,7 @@ def __init__(self, cost, **optimiser_kwargs): if len(x0) == 1 or len(cost.parameters) == 1: raise ValueError( "CMAES requires optimisation of >= 2 parameters at once. " - + "Please choose another optimiser." + "Please choose another optimiser." ) super().__init__(cost, PintsCMAES, **optimiser_kwargs) diff --git a/pybop/optimisers/scipy_optimisers.py b/pybop/optimisers/scipy_optimisers.py index 544abfc88..30499cdb9 100644 --- a/pybop/optimisers/scipy_optimisers.py +++ b/pybop/optimisers/scipy_optimisers.py @@ -161,7 +161,7 @@ def callback(intermediate_result: OptimizeResult): # Compute the absolute initial cost and resample if required self._cost0 = np.abs(self.cost(self.x0)) if np.isinf(self._cost0): - for i in range(1, self.num_resamples): + for _i in range(1, self.num_resamples): self.x0 = self.parameters.rvs(1)[0] self._cost0 = np.abs(self.cost(self.x0)) if not np.isinf(self._cost0): diff --git a/pybop/parameters/parameter.py b/pybop/parameters/parameter.py index 27836fc80..093abaed9 100644 --- a/pybop/parameters/parameter.py +++ b/pybop/parameters/parameter.py @@ -1,12 +1,12 @@ import warnings from collections import OrderedDict -from typing import Dict, List, Union +from typing import Union import numpy as np from pybop._utils import is_numeric -Inputs = Dict[str, float] +Inputs = dict[str, float] class Parameter: @@ -219,7 +219,7 @@ def __getitem__(self, key: str) -> Parameter: def __len__(self) -> int: return len(self.param) - def keys(self) -> List: + def keys(self) -> list: """ A list of parameter names """ @@ -245,7 +245,7 @@ def add(self, parameter): if parameter.name in self.param.keys(): raise ValueError( f"There is already a parameter with the name {parameter.name} " - + "in the Parameters object. Please remove the duplicate entry." + "in the Parameters object. Please remove the duplicate entry." ) self.param[parameter.name] = parameter elif isinstance(parameter, dict): @@ -255,7 +255,7 @@ def add(self, parameter): if name in self.param.keys(): raise ValueError( f"There is already a parameter with the name {name} " - + "in the Parameters object. Please remove the duplicate entry." + "in the Parameters object. Please remove the duplicate entry." ) self.param[name] = Parameter(**parameter) else: @@ -287,7 +287,7 @@ def join(self, parameters=None): else: print(f"Discarding duplicate {param.name}.") - def get_bounds(self) -> Dict: + def get_bounds(self) -> dict: """ Get bounds, for either all or no parameters. """ @@ -317,12 +317,12 @@ def update(self, initial_values=None, values=None, bounds=None): if values is not None: param.update(value=values[i]) if bounds is not None: - if isinstance(bounds, Dict): + if isinstance(bounds, dict): param.set_bounds(bounds=[bounds["lower"][i], bounds["upper"][i]]) else: param.set_bounds(bounds=bounds[i]) - def rvs(self, n_samples: int) -> List: + def rvs(self, n_samples: int) -> list: """ Draw random samples from each parameter's prior distribution. @@ -355,7 +355,7 @@ def rvs(self, n_samples: int) -> List: return all_samples - def get_sigma0(self) -> List: + def get_sigma0(self) -> list: """ Get the standard deviation, for either all or no parameters. """ @@ -434,7 +434,7 @@ def get_bounds_for_plotly(self): return bounds - def as_dict(self, values=None) -> Dict: + def as_dict(self, values=None) -> dict: """ Parameters ---------- @@ -465,7 +465,7 @@ def verify(self, inputs: Union[Inputs, None] = None): ---------- inputs : Inputs or numeric """ - if inputs is None or isinstance(inputs, Dict): + if inputs is None or isinstance(inputs, dict): return inputs elif (isinstance(inputs, list) and all(is_numeric(x) for x in inputs)) or all( is_numeric(x) for x in list(inputs) diff --git a/pybop/parameters/parameter_set.py b/pybop/parameters/parameter_set.py index 43f3e999b..c1b9505b5 100644 --- a/pybop/parameters/parameter_set.py +++ b/pybop/parameters/parameter_set.py @@ -1,6 +1,5 @@ import json import types -from typing import List from pybamm import LithiumIonParameters, ParameterValues, parameter_sets @@ -35,7 +34,7 @@ def __setitem__(self, key, value): def __getitem__(self, key): return self.params[key] - def keys(self) -> List: + def keys(self) -> list: """ A list of parameter names """ @@ -67,7 +66,7 @@ def import_parameters(self, json_path=None): # Read JSON file if not self.params and self.json_path: - with open(self.json_path, "r") as file: + with open(self.json_path) as file: self.params = json.load(file) else: raise ValueError( @@ -139,7 +138,7 @@ def export_parameters(self, output_json_path, fit_params=None): # Update parameter set if fit_params is not None: - for i, param in enumerate(fit_params): + for _i, param in enumerate(fit_params): exportable_params.update({param.name: param.value}) # Replace non-serializable values diff --git a/pybop/plotting/plot2d.py b/pybop/plotting/plot2d.py index ee8d70573..781b697ba 100644 --- a/pybop/plotting/plot2d.py +++ b/pybop/plotting/plot2d.py @@ -71,8 +71,9 @@ def plot2d( if len(cost.parameters) > 2: warnings.warn( "This cost function requires more than 2 parameters. " - + "Plotting in 2d with fixed values for the additional parameters.", + "Plotting in 2d with fixed values for the additional parameters.", UserWarning, + stacklevel=2, ) for ( i, diff --git a/pybop/plotting/plot_dataset.py b/pybop/plotting/plot_dataset.py index 70573e476..ecc84aa6e 100644 --- a/pybop/plotting/plot_dataset.py +++ b/pybop/plotting/plot_dataset.py @@ -3,9 +3,7 @@ from pybop import StandardPlot, plot_trajectories -def plot_dataset( - dataset, signal=["Voltage [V]"], trace_names=None, show=True, **layout_kwargs -): +def plot_dataset(dataset, signal=None, trace_names=None, show=True, **layout_kwargs): """ Quickly plot a PyBOP Dataset using Plotly. @@ -31,6 +29,8 @@ def plot_dataset( """ # Get data dictionary + if signal is None: + signal = ["Voltage [V]"] dataset.check(signal) # Compile ydata and labels or legend diff --git a/pybop/plotting/plotly_manager.py b/pybop/plotting/plotly_manager.py index 7b4b079a4..5554a2af5 100644 --- a/pybop/plotting/plotly_manager.py +++ b/pybop/plotting/plotly_manager.py @@ -119,7 +119,7 @@ def check_browser_availability(self): if self.pio and self.pio.renderers.default == "browser": try: webbrowser.get() - except webbrowser.Error: + except webbrowser.Error as e: raise Exception( "\n **Browser Not Found** \nFor Windows users, in order to view figures in the browser using Plotly, " "you need to set the environment variable BROWSER equal to the " @@ -129,4 +129,4 @@ def check_browser_availability(self): "\n\nThen reactivate your virtual environment. Alternatively, you can use a " "different Plotly renderer. For more information see: " "https://plotly.com/python/renderers/#setting-the-default-renderer" - ) + ) from e diff --git a/pybop/plotting/quick_plot.py b/pybop/plotting/quick_plot.py index 5be353a62..1ef4e3ffe 100644 --- a/pybop/plotting/quick_plot.py +++ b/pybop/plotting/quick_plot.py @@ -57,16 +57,16 @@ def __init__( x, y, layout=None, - layout_options=DEFAULT_LAYOUT_OPTIONS.copy(), - trace_options=DEFAULT_TRACE_OPTIONS.copy(), + layout_options=DEFAULT_LAYOUT_OPTIONS, + trace_options=DEFAULT_TRACE_OPTIONS, trace_names=None, trace_name_width=40, ): self.x = x self.y = y self.layout = layout - self.layout_options = layout_options - self.trace_options = DEFAULT_TRACE_OPTIONS.copy() + self.layout_options = layout_options.copy() + self.trace_options = trace_options.copy() if trace_options is not None: for arg, value in trace_options.items(): self.trace_options[arg] = value @@ -246,9 +246,9 @@ def __init__( num_cols=None, axis_titles=None, layout=None, - layout_options=DEFAULT_LAYOUT_OPTIONS.copy(), - subplot_options=DEFAULT_SUBPLOT_OPTIONS.copy(), - trace_options=DEFAULT_SUBPLOT_TRACE_OPTIONS.copy(), + layout_options=DEFAULT_LAYOUT_OPTIONS, + subplot_options=DEFAULT_SUBPLOT_OPTIONS, + trace_options=DEFAULT_SUBPLOT_TRACE_OPTIONS, trace_names=None, trace_name_width=40, ): @@ -267,7 +267,7 @@ def __init__( elif self.num_cols is None: self.num_cols = int(math.ceil(self.num_traces / self.num_rows)) self.axis_titles = axis_titles - self.subplot_options = DEFAULT_SUBPLOT_OPTIONS.copy() + self.subplot_options = subplot_options.copy() if subplot_options is not None: for arg, value in subplot_options.items(): self.subplot_options[arg] = value diff --git a/pybop/problems/base_problem.py b/pybop/problems/base_problem.py index 4d9d85194..ee36a6bb4 100644 --- a/pybop/problems/base_problem.py +++ b/pybop/problems/base_problem.py @@ -27,11 +27,15 @@ def __init__( parameters, model=None, check_model=True, - signal=["Voltage [V]"], - additional_variables=[], + signal=None, + additional_variables=None, init_soc=None, ): # Check if parameters is a list of pybop.Parameter objects + if additional_variables is None: + additional_variables = [] + if signal is None: + signal = ["Voltage [V]"] if isinstance(parameters, list): if all(isinstance(param, Parameter) for param in parameters): parameters = Parameters(*parameters) diff --git a/pybop/problems/design_problem.py b/pybop/problems/design_problem.py index a1efa22fd..30e2d9c3a 100644 --- a/pybop/problems/design_problem.py +++ b/pybop/problems/design_problem.py @@ -34,11 +34,15 @@ def __init__( parameters, experiment, check_model=True, - signal=["Voltage [V]"], - additional_variables=[], + signal=None, + additional_variables=None, init_soc=None, ): # Add time and current and remove duplicates + if additional_variables is None: + additional_variables = [] + if signal is None: + signal = ["Voltage [V]"] additional_variables.extend(["Time [s]", "Current [A]"]) additional_variables = list(set(additional_variables)) diff --git a/pybop/problems/fitting_problem.py b/pybop/problems/fitting_problem.py index b27955479..58d59e9ee 100644 --- a/pybop/problems/fitting_problem.py +++ b/pybop/problems/fitting_problem.py @@ -32,11 +32,15 @@ def __init__( parameters, dataset, check_model=True, - signal=["Voltage [V]"], - additional_variables=[], + signal=None, + additional_variables=None, init_soc=None, ): # Add time and remove duplicates + if additional_variables is None: + additional_variables = [] + if signal is None: + signal = ["Voltage [V]"] additional_variables.extend(["Time [s]"]) additional_variables = list(set(additional_variables)) @@ -47,7 +51,7 @@ def __init__( self.parameters.initial_value() # Check that the dataset contains time and current - dataset.check(self.signal + ["Current function [A]"]) + dataset.check([*self.signal, "Current function [A]"]) # Unpack time and target data self._time_data = self._dataset["Time [s]"] diff --git a/pyproject.toml b/pyproject.toml index 99067e24e..0ec781e58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ Homepage = "https://github.com/pybop-team/PyBOP" Documentation = "https://pybop-docs.readthedocs.io" Repository = "https://github.com/pybop-team/PyBOP" Releases = "https://github.com/pybop-team/PyBOP/releases" -Changelog = "https://github.com/pybop-team/PyBOP/CHANGELOG.md" +Changelog = "https://github.com/pybop-team/PyBOP/blob/develop/CHANGELOG.md" [tool.pytest.ini_options] addopts = "--showlocals -v -n auto" @@ -81,10 +81,23 @@ addopts = "--showlocals -v -n auto" [tool.ruff] extend-include = ["*.ipynb"] extend-exclude = ["__init__.py"] +fix = true [tool.ruff.lint] -extend-select = ["I"] +select = [ + "A", # flake8-builtins: Check for Python builtins being used as variables or parameters + "B", # flake8-bugbear: Find likely bugs and design problems + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes: Detect various errors by parsing the source file + "I", # isort: Check and enforce import ordering + "ISC", # flake8-implicit-str-concat: Check for implicit string concatenation + "TID", # flake8-tidy-imports: Validate import hygiene + "UP", # pyupgrade: Automatically upgrade syntax for newer versions of Python +] + ignore = ["E501","E741"] +per-file-ignores = {"**.ipynb" = ["E402", "E703"]} -[tool.ruff.lint.per-file-ignores] -"**.ipynb" = ["E402", "E703"] +[tool.ruff.lint.flake8-tidy-imports] +ban-relative-imports = "all" diff --git a/tests/examples/test_examples.py b/tests/examples/test_examples.py index 1c45be840..2bebc6fc6 100644 --- a/tests/examples/test_examples.py +++ b/tests/examples/test_examples.py @@ -12,14 +12,14 @@ class TestExamples: """ def list_of_examples(): - list = [] + examples_list = [] path_to_example_scripts = os.path.join( pybop.script_path, "..", "examples", "scripts" ) for example in os.listdir(path_to_example_scripts): if example.endswith(".py"): - list.append(os.path.join(path_to_example_scripts, example)) - return list + examples_list.append(os.path.join(path_to_example_scripts, example)) + return examples_list @pytest.mark.parametrize("example", list_of_examples()) @pytest.mark.examples diff --git a/tests/unit/test_dataset.py b/tests/unit/test_dataset.py index 9c4eac13d..618b8ad53 100644 --- a/tests/unit/test_dataset.py +++ b/tests/unit/test_dataset.py @@ -20,7 +20,7 @@ def test_dataset(self): data_dictionary = { "Time [s]": solution["Time [s]"].data, "Current [A]": solution["Current [A]"].data, - "Terminal voltage [V]": solution["Terminal voltage [V]"].data, + "Voltage [V]": solution["Voltage [V]"].data, } dataset = pybop.Dataset(data_dictionary) @@ -55,4 +55,4 @@ def test_dataset(self): dataset["Time"] # Test conversion of single signal to list - assert dataset.check(signal="Terminal voltage [V]") + assert dataset.check() diff --git a/tests/unit/test_observer_unscented_kalman.py b/tests/unit/test_observer_unscented_kalman.py index ce60abbc0..0b5d3067b 100644 --- a/tests/unit/test_observer_unscented_kalman.py +++ b/tests/unit/test_observer_unscented_kalman.py @@ -156,3 +156,22 @@ def test_wrong_input_shapes(self, model, parameters): pybop.UnscentedKalmanFilterObserver( parameters, model, sigma0, process, measure, signal=signal ) + + @pytest.mark.unit + def test_without_signal(self): + model = pybop.lithium_ion.SPM() + parameters = pybop.Parameters( + pybop.Parameter( + "Negative electrode active material volume fraction", + prior=pybop.Gaussian(0.5, 0.05), + ) + ) + model.build(parameters=parameters) + n = model.n_states + sigma0 = np.diag([1e-4] * n) + process = np.diag([1e-4] * n) + measure = np.diag([1e-4]) + observer = pybop.UnscentedKalmanFilterObserver( + parameters, model, sigma0, process, measure + ) + assert observer.signal == ["Voltage [V]"] diff --git a/tests/unit/test_optimisation.py b/tests/unit/test_optimisation.py index 8444159b9..c61d2fae2 100644 --- a/tests/unit/test_optimisation.py +++ b/tests/unit/test_optimisation.py @@ -236,7 +236,7 @@ def test_optimiser_kwargs(self, cost, optimiser): assert optim.pints_optimiser._lambda == 0.1 # Incorrect values - for i, match in (("Value", -1),): + for i, _match in (("Value", -1),): with pytest.raises( Exception, match="must be a numeric value between 0 and 1." ): @@ -253,7 +253,7 @@ def test_optimiser_kwargs(self, cost, optimiser): # Check defaults assert optim.pints_optimiser.n_hyper_parameters() == 5 assert optim.pints_optimiser.x_guessed() == optim.pints_optimiser._x0 - with pytest.raises(Exception): + with pytest.raises(RuntimeError): optim.pints_optimiser.tell([0.1]) else: @@ -326,12 +326,8 @@ def test_default_optimiser(self, cost): optim = pybop.Optimisation(cost=cost) assert optim.name() == "Exponential Natural Evolution Strategy (xNES)" - # Test incorrect setting attribute - with pytest.raises( - AttributeError, - match="'Optimisation' object has no attribute 'not_a_valid_attribute'", - ): - optim.not_a_valid_attribute + # Test getting incorrect attribute + assert not hasattr(optim, "not_a_valid_attribute") @pytest.mark.unit def test_incorrect_optimiser_class(self, cost): diff --git a/tests/unit/test_parameters.py b/tests/unit/test_parameters.py index ebfccea12..90c43622c 100644 --- a/tests/unit/test_parameters.py +++ b/tests/unit/test_parameters.py @@ -130,8 +130,8 @@ def test_parameters_construction(self, parameter): with pytest.raises( ValueError, match="There is already a parameter with the name " - + "Negative electrode active material volume fraction" - + " in the Parameters object. Please remove the duplicate entry.", + "Negative electrode active material volume fraction" + " in the Parameters object. Please remove the duplicate entry.", ): params.add(parameter) @@ -158,8 +158,8 @@ def test_parameters_construction(self, parameter): with pytest.raises( ValueError, match="There is already a parameter with the name " - + "Negative electrode active material volume fraction" - + " in the Parameters object. Please remove the duplicate entry.", + "Negative electrode active material volume fraction" + " in the Parameters object. Please remove the duplicate entry.", ): params.add( dict( diff --git a/tests/unit/test_plots.py b/tests/unit/test_plots.py index 4c7e14d42..79015c006 100644 --- a/tests/unit/test_plots.py +++ b/tests/unit/test_plots.py @@ -66,7 +66,7 @@ def test_dataset_plots(self, dataset): dataset["Voltage [V]"], trace_names=["Time [s]", "Voltage [V]"], ) - pybop.plot_dataset(dataset, signal=["Voltage [V]"]) + pybop.plot_dataset(dataset) @pytest.fixture def fitting_problem(self, model, parameters, dataset):