From d6b76cacb92e543c2ca765cf0766c6d06d02ff66 Mon Sep 17 00:00:00 2001 From: NicolaCourtier <45851982+NicolaCourtier@users.noreply.github.com> Date: Fri, 10 Apr 2026 16:24:18 +0100 Subject: [PATCH 1/4] Rename Sobol sensitivity method --- pybop/analysis/sensitivity_analysis.py | 9 +++++++-- pybop/problems/problem.py | 6 +++--- tests/unit/test_problem.py | 20 +++++++++----------- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/pybop/analysis/sensitivity_analysis.py b/pybop/analysis/sensitivity_analysis.py index 49370bd20..67f40ee12 100644 --- a/pybop/analysis/sensitivity_analysis.py +++ b/pybop/analysis/sensitivity_analysis.py @@ -1,5 +1,6 @@ from typing import TYPE_CHECKING +import numpy as np from SALib.analyze import sobol from SALib.sample.sobol import sample @@ -7,7 +8,7 @@ from pybop.problems.problem import Problem -def sensitivity_analysis( +def get_sobol_sensitivities( problem: "Problem", n_samples: int = 256, calc_second_order: bool = False ) -> dict: """ @@ -36,9 +37,13 @@ def sensitivity_analysis( Sensitivities : dict """ + bounds_array = problem.parameters.get_bounds_array() + if not np.isfinite(bounds_array).all(): + raise ValueError("SOBOL analysis requires finite bounds.") + salib_dict = { "names": problem.parameters.names, - "bounds": problem.parameters.get_bounds_array(), + "bounds": bounds_array, "num_vars": len(problem.parameters), } diff --git a/pybop/problems/problem.py b/pybop/problems/problem.py index c8f3e1ba1..4952192a3 100644 --- a/pybop/problems/problem.py +++ b/pybop/problems/problem.py @@ -1,6 +1,6 @@ import numpy as np -from pybop.analysis.sensitivity_analysis import sensitivity_analysis +from pybop.analysis.sensitivity_analysis import get_sobol_sensitivities from pybop.costs.base_cost import BaseCost from pybop.costs.evaluation import Evaluation from pybop.parameters.parameter import Inputs, Parameters @@ -260,7 +260,7 @@ def get_finite_initial_cost(self): raise ValueError("The initial parameter values return an infinite cost.") return cost0 - def sensitivity_analysis( + def get_sobol_sensitivities( self, n_samples: int = 256, calc_second_order: bool = False ) -> dict: """ @@ -275,7 +275,7 @@ def sensitivity_analysis( calc_second_order : bool, optional Whether to calculate second-order sensitivities. """ - return sensitivity_analysis( + return get_sobol_sensitivities( problem=self, n_samples=n_samples, calc_second_order=calc_second_order ) diff --git a/tests/unit/test_problem.py b/tests/unit/test_problem.py index 18f75bb9a..0449361e7 100644 --- a/tests/unit/test_problem.py +++ b/tests/unit/test_problem.py @@ -22,18 +22,10 @@ def model(self): def parameters(self): return { "Negative particle radius [m]": pybop.Parameter( - distribution=pybop.Gaussian( - 2e-05, - 0.1e-5, - truncated_at=[1e-6, 5e-5], - ) + distribution=pybop.Gaussian(2e-05, 0.1e-5, truncated_at=[1e-6, 5e-5]) ), "Positive particle radius [m]": pybop.Parameter( - distribution=pybop.Gaussian( - 0.5e-05, - 0.1e-5, - truncated_at=[1e-6, 5e-5], - ) + distribution=pybop.Gaussian(0.5e-05, 0.1e-5, truncated_at=[1e-6, 5e-5]) ), } @@ -239,7 +231,7 @@ def test_parameter_sensitivities(self, simulator, dataset): cost = pybop.MeanAbsoluteError(dataset) problem = pybop.Problem(simulator, cost) n_params = len(problem.parameters) - result = problem.sensitivity_analysis(4, calc_second_order=True) + result = problem.get_sobol_sensitivities(4, calc_second_order=True) # Assertions assert isinstance(result, dict) @@ -253,3 +245,9 @@ def test_parameter_sensitivities(self, simulator, dataset): assert isinstance(result["S2_conf"], np.ndarray) assert result["S1"].shape == (n_params,) assert result["ST"].shape == (n_params,) + + problem.parameters["Negative particle radius [m]"] = pybop.Parameter( + distribution=pybop.Gaussian(2e-05, 0.1e-5) # unbounded + ) + with pytest.raises(ValueError, match="SOBOL analysis requires finite bounds."): + problem.get_sobol_sensitivities(4, calc_second_order=True) From 49d73d2beff98f6eed76b8314cef40180f4fa7f0 Mon Sep 17 00:00:00 2001 From: NicolaCourtier <45851982+NicolaCourtier@users.noreply.github.com> Date: Fri, 10 Apr 2026 16:58:03 +0100 Subject: [PATCH 2/4] Make SALib optional --- CHANGELOG.md | 2 + pybop/analysis/sensitivity_analysis.py | 54 -------------------------- pybop/problems/problem.py | 20 ---------- pyproject.toml | 4 +- tests/unit/test_problem.py | 25 ------------ 5 files changed, 4 insertions(+), 101 deletions(-) delete mode 100644 pybop/analysis/sensitivity_analysis.py diff --git a/CHANGELOG.md b/CHANGELOG.md index de8015701..7fdf62d93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ ## Breaking Changes +- [#938](https://github.com/pybop-team/PyBOP/pull/938) - Make SALib an optional dependency and remove `sensitivity_analysis` in favour of using SALib directly. + # [v26.3](https://github.com/pybop-team/PyBOP/tree/v26.3) - 2026-03-05 ## Features diff --git a/pybop/analysis/sensitivity_analysis.py b/pybop/analysis/sensitivity_analysis.py deleted file mode 100644 index 67f40ee12..000000000 --- a/pybop/analysis/sensitivity_analysis.py +++ /dev/null @@ -1,54 +0,0 @@ -from typing import TYPE_CHECKING - -import numpy as np -from SALib.analyze import sobol -from SALib.sample.sobol import sample - -if TYPE_CHECKING: - from pybop.problems.problem import Problem - - -def get_sobol_sensitivities( - problem: "Problem", n_samples: int = 256, calc_second_order: bool = False -) -> dict: - """ - Computes the parameter sensitivities on the combined cost function using - SOBOL analysis from the SALib module [1]. - - Parameters - ---------- - problem : pybop.Problem - The optimisation problem. - n_samples : int, optional - Number of samples for SOBOL sensitivity analysis, - performs best as order of 2, i.e. 128, 256, etc. - calc_second_order : bool, optional - Whether to calculate second-order sensitivities. - - References - ---------- - .. [1] Iwanaga, T., Usher, W., & Herman, J. (2022). Toward SALib 2.0: - Advancing the accessibility and interpretability of global sensitivity - analyses. Socio-Environmental Systems Modelling, 4, 18155. - doi:10.18174/sesmo.18155 - - Returns - ------- - Sensitivities : dict - """ - - bounds_array = problem.parameters.get_bounds_array() - if not np.isfinite(bounds_array).all(): - raise ValueError("SOBOL analysis requires finite bounds.") - - salib_dict = { - "names": problem.parameters.names, - "bounds": bounds_array, - "num_vars": len(problem.parameters), - } - - # Create samples, compute cost - param_values = sample(salib_dict, n_samples) - costs = problem.evaluate(param_values).values - - return sobol.analyze(salib_dict, costs, calc_second_order=calc_second_order) diff --git a/pybop/problems/problem.py b/pybop/problems/problem.py index 4952192a3..2cdcd1e46 100644 --- a/pybop/problems/problem.py +++ b/pybop/problems/problem.py @@ -1,6 +1,5 @@ import numpy as np -from pybop.analysis.sensitivity_analysis import get_sobol_sensitivities from pybop.costs.base_cost import BaseCost from pybop.costs.evaluation import Evaluation from pybop.parameters.parameter import Inputs, Parameters @@ -260,25 +259,6 @@ def get_finite_initial_cost(self): raise ValueError("The initial parameter values return an infinite cost.") return cost0 - def get_sobol_sensitivities( - self, n_samples: int = 256, calc_second_order: bool = False - ) -> dict: - """ - Computes the parameter sensitivities on the combined cost function using - SOBOL analysis. See pybop.analysis.sensitivity_analysis for more details. - - Parameters - ---------- - n_samples : int, optional - Number of samples for SOBOL sensitivity analysis, performs best as a - power of 2, i.e. 128, 256, etc. - calc_second_order : bool, optional - Whether to calculate second-order sensitivities. - """ - return get_sobol_sensitivities( - problem=self, n_samples=n_samples, calc_second_order=calc_second_order - ) - @property def cost(self): return self._cost diff --git a/pyproject.toml b/pyproject.toml index b730726db..7e4ac9618 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,11 +32,11 @@ dependencies = [ "numpy>=1.26", "scipy>=1.12", "pints>=0.6.0", - "SALib>=1.5", ] [project.optional-dependencies] plot = ["plotly>=6"] +salib = ["SALib>=1.5"] scifem = [ "scikit-fem>=8.1.0" # scikit-fem is a dependency for the multi-dimensional pybamm models ] @@ -50,7 +50,7 @@ ep-bolfi = [ pyprobe = [ "PyProBE-Data>=2.5.0;python_version >= '3.11' and python_version < '3.13'" ] -all = ["pybop[plot,scifem,bpx,pyprobe,ep-bolfi]"] +all = ["pybop[plot,salib,scifem,bpx,pyprobe,ep-bolfi]"] [dependency-groups] docs = [ diff --git a/tests/unit/test_problem.py b/tests/unit/test_problem.py index 0449361e7..7a9eb5987 100644 --- a/tests/unit/test_problem.py +++ b/tests/unit/test_problem.py @@ -226,28 +226,3 @@ def test_problem_construct_with_model_predict(self, parameters, model, dataset): problem_output["Voltage [V]"].data, atol=1e-6, ) - - def test_parameter_sensitivities(self, simulator, dataset): - cost = pybop.MeanAbsoluteError(dataset) - problem = pybop.Problem(simulator, cost) - n_params = len(problem.parameters) - result = problem.get_sobol_sensitivities(4, calc_second_order=True) - - # Assertions - assert isinstance(result, dict) - assert "S1" in result - assert "ST" in result - assert isinstance(result["S1"], np.ndarray) - assert isinstance(result["S2"], np.ndarray) - assert isinstance(result["ST"], np.ndarray) - assert isinstance(result["S1_conf"], np.ndarray) - assert isinstance(result["ST_conf"], np.ndarray) - assert isinstance(result["S2_conf"], np.ndarray) - assert result["S1"].shape == (n_params,) - assert result["ST"].shape == (n_params,) - - problem.parameters["Negative particle radius [m]"] = pybop.Parameter( - distribution=pybop.Gaussian(2e-05, 0.1e-5) # unbounded - ) - with pytest.raises(ValueError, match="SOBOL analysis requires finite bounds."): - problem.get_sobol_sensitivities(4, calc_second_order=True) From 5bc5c323c5eb88c4ad56656a040ad021aa22028e Mon Sep 17 00:00:00 2001 From: NicolaCourtier <45851982+NicolaCourtier@users.noreply.github.com> Date: Thu, 14 May 2026 13:30:56 +0100 Subject: [PATCH 3/4] Update installation.rst --- docs/installation.rst | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 50138d81e..9f02032dd 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -43,25 +43,43 @@ For those who prefer to install PyBOP from a local clone of the repository or wi In editable mode, changes you make to the source code will immediately affect the PyBOP installation without the need for reinstallation. Optional Dependencies ------------------ -``plotly`` - For plotting, PyBOP uses plotly. It can be installed with: +--------------------- +``plotly`` - For plotting, PyBOP uses `plotly `. It can be installed with: .. code-block:: console pip install pybop[plot] -``scikit-fem`` - This is a dependency for the multi-dimensional pybamm models, and can be installed using: +``salib`` - To compute sensitivities, PyBOP can be paired with the `Sensitivity Analysis Library (SALib) `: + +.. code-block:: console + + pip install pybop[salib] + +``scikit-fem`` - This is a dependency for the multi-dimensional PyBaMM models, and can be installed using: .. code-block:: console pip install pybop[scifem] -``bpx`` - To use the Faraday Institution's Battery Parameter eXchange (BPX) package install the optional requirement: +``bpx`` - To use the Faraday Institution's `Battery Parameter eXchange (BPX) package `: .. code-block:: console pip install pybop[bpx] +``ep-bolfi`` - To use Expectation Propagation with Bayesian Optimization for Likelihood-Free Inference (`EP-BOLFI `): + +.. code-block:: console + + pip install pybop[ep-bolfi] + +``pyprobe`` - To import data from battery cyclers, use `Python Processing for Battery Experiments (PyProBE) `: + +.. code-block:: console + + pip install pybop[pyprobe] + To install all the optional dependencies, the command ``pip install pybop[all]`` is available. For more information on the optional packages, users are directed towards the `pyproject.toml `_. Verifying Installation From 9e62c3895203003c11c07c70b415189e1d00048c Mon Sep 17 00:00:00 2001 From: NicolaCourtier <45851982+NicolaCourtier@users.noreply.github.com> Date: Thu, 14 May 2026 13:36:01 +0100 Subject: [PATCH 4/4] pre-commit --- docs/installation.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 9f02032dd..990db5047 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -44,13 +44,13 @@ In editable mode, changes you make to the source code will immediately affect th Optional Dependencies --------------------- -``plotly`` - For plotting, PyBOP uses `plotly `. It can be installed with: +``plotly`` - For plotting, PyBOP uses `plotly `_. It can be installed with: .. code-block:: console pip install pybop[plot] -``salib`` - To compute sensitivities, PyBOP can be paired with the `Sensitivity Analysis Library (SALib) `: +``salib`` - To compute sensitivities, PyBOP can be paired with the `Sensitivity Analysis Library (SALib) `_: .. code-block:: console @@ -62,19 +62,19 @@ Optional Dependencies pip install pybop[scifem] -``bpx`` - To use the Faraday Institution's `Battery Parameter eXchange (BPX) package `: +``bpx`` - To use the Faraday Institution's `Battery Parameter eXchange (BPX) package `_: .. code-block:: console pip install pybop[bpx] -``ep-bolfi`` - To use Expectation Propagation with Bayesian Optimization for Likelihood-Free Inference (`EP-BOLFI `): +``ep-bolfi`` - To use Expectation Propagation with Bayesian Optimization for Likelihood-Free Inference (`EP-BOLFI `_): .. code-block:: console pip install pybop[ep-bolfi] -``pyprobe`` - To import data from battery cyclers, use `Python Processing for Battery Experiments (PyProBE) `: +``pyprobe`` - To import data from battery cyclers, use `Python Processing for Battery Experiments (PyProBE) `_: .. code-block:: console