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