Skip to content

Commit 49cd4a8

Browse files
committed
Merge pull request #3475 from arjxn-py/fix-default-imports
Resolve default imports for optional dependencies
1 parent 32ec458 commit 49cd4a8

39 files changed

+254
-84
lines changed

CHANGELOG.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
1+
# [Unreleased](https://github.com/pybamm-team/PyBaMM/)
2+
3+
## Bug fixes
4+
5+
- Fixed bug in calculation of theoretical energy that made it very slow ([#3506](https://github.com/pybamm-team/PyBaMM/pull/3506))
6+
17
# [v23.9rc1](https://github.com/pybamm-team/PyBaMM/tree/v23.9rc0) - 2023-10-31
28

9+
- Fixed a bug where the JaxSolver would fails when using GPU support with no input parameters ([#3423](https://github.com/pybamm-team/PyBaMM/pull/3423))
10+
- Make pybamm importable with minimal dependencies ([#3044](https://github.com/pybamm-team/PyBaMM/pull/3044), [#3475](https://github.com/pybamm-team/PyBaMM/pull/3475))
11+
- Fixed a bug where supplying an initial soc did not work with half cell models ([#3456](https://github.com/pybamm-team/PyBaMM/pull/3456))
12+
13+
14+
# [v23.9rc0](https://github.com/pybamm-team/PyBaMM/tree/v23.9rc0) - 2023-10-31
15+
316
## Features
417

518
- The parameter "Ambient temperature [K]" can now be given as a function of position `(y,z)` and time `t`. The "edge" and "current collector" heat transfer coefficient parameters can also depend on `(y,z)` ([#3257](https://github.com/pybamm-team/PyBaMM/pull/3257))
@@ -17,7 +30,6 @@
1730

1831
## Bug fixes
1932

20-
- Fixed a bug where the JaxSolver would fails when using GPU support with no input parameters ([#3423](https://github.com/pybamm-team/PyBaMM/pull/3423))
2133
- Fixed a bug where empty lists passed to QuickPlot resulted in an IndexError and did not return a meaningful error message ([#3359](https://github.com/pybamm-team/PyBaMM/pull/3359))
2234
- Fixed a bug where there was a missing thermal conductivity in the thermal pouch cell models ([#3330](https://github.com/pybamm-team/PyBaMM/pull/3330))
2335
- Fixed a bug that caused incorrect results of “{Domain} electrode thickness change [m]” due to the absence of dimension for the variable `electrode_thickness_change`([#3329](https://github.com/pybamm-team/PyBaMM/pull/3329)).
@@ -36,7 +48,6 @@
3648
- Error generated when invalid parameter values are passed ([#3132](https://github.com/pybamm-team/PyBaMM/pull/3132))
3749
- Parameters in `Prada2013` have been updated to better match those given in the paper, which is a 2.3 Ah cell, instead of the mix-and-match with the 1.1 Ah cell from Lain2019 ([#3096](https://github.com/pybamm-team/PyBaMM/pull/3096))
3850
- The `OneDimensionalX` thermal model has been updated to account for edge/tab cooling and account for the current collector volumetric heat capacity. It now gives the correct behaviour compared with a lumped model with the correct total heat transfer coefficient and surface area for cooling. ([#3042](https://github.com/pybamm-team/PyBaMM/pull/3042))
39-
- Fixed a bug where supplying an initial soc did not work with half cell models ([#3456](https://github.com/pybamm-team/PyBaMM/pull/3456))
4051

4152
## Optimizations
4253

@@ -56,7 +67,7 @@
5667
- Added option to use an empirical hysteresis model for the diffusivity and exchange-current density ([#3194](https://github.com/pybamm-team/PyBaMM/pull/3194))
5768
- Double-layer capacity can now be provided as a function of temperature ([#3174](https://github.com/pybamm-team/PyBaMM/pull/3174))
5869
- `pybamm_install_jax` is deprecated. It is now replaced with `pip install pybamm[jax]` ([#3163](https://github.com/pybamm-team/PyBaMM/pull/3163))
59-
- PyBaMM now has optional dependencies that can be installed with the pattern `pip install pybamm[option]` e.g. `pybamm[plot]` ([#3044](https://github.com/pybamm-team/PyBaMM/pull/3044))
70+
- PyBaMM now has optional dependencies that can be installed with the pattern `pip install pybamm[option]` e.g. `pybamm[plot]` ([#3044](https://github.com/pybamm-team/PyBaMM/pull/3044), [#3475](https://github.com/pybamm-team/PyBaMM/pull/3475))
6071

6172
# [v23.5](https://github.com/pybamm-team/PyBaMM/tree/v23.5) - 2023-06-18
6273

CONTRIBUTING.md

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -100,21 +100,52 @@ On the other hand... We _do_ want to compare several tools, to generate document
100100

101101
Only 'core pybamm' is installed by default. The others have to be specified explicitly when running the installation command.
102102

103-
### Matplotlib
103+
### Managing Optional Dependencies and Their Imports
104104

105-
We use Matplotlib in PyBaMM, but with two caveats:
105+
PyBaMM utilizes optional dependencies to allow users to choose which additional libraries they want to use. Managing these optional dependencies and their imports is essential to provide flexibility to PyBaMM users.
106106

107-
First, Matplotlib should only be used in plotting methods, and these should _never_ be called by other PyBaMM methods. So users who don't like Matplotlib will not be forced to use it in any way. Use in notebooks is OK and encouraged.
107+
PyBaMM provides a utility function `have_optional_dependency`, to check for the availability of optional dependencies within methods. This function can be used to conditionally import optional dependencies only if they are available. Here's how to use it:
108108

109-
Second, Matplotlib should never be imported at the module level, but always inside methods. For example:
109+
Optional dependencies should never be imported at the module level, but always inside methods. For example:
110110

111111
```
112-
def plot_great_things(self, x, y, z):
113-
import matplotlib.pyplot as pl
112+
def use_pybtex(x,y,z):
113+
pybtex = have_optional_dependency("pybtex")
114114
...
115115
```
116116

117-
This allows people to (1) use PyBaMM without ever importing Matplotlib and (2) configure Matplotlib's back-end in their scripts, which _must_ be done before e.g. `pyplot` is first imported.
117+
While importing a specific module instead of an entire package/library:
118+
119+
```python
120+
def use_parse_file(x, y, z):
121+
parse_file = have_optional_dependency("pybtex.database", "parse_file")
122+
...
123+
```
124+
125+
This allows people to (1) use PyBaMM without importing optional dependencies by default and (2) configure module-dependent functionalities in their scripts, which _must_ be done before e.g. `print_citations` method is first imported.
126+
127+
**Writing Tests for Optional Dependencies**
128+
129+
Whenever a new optional dependency is added for optional functionality, it is recommended to write a corresponding unit test in `test_util.py`. This ensures that an error is raised upon the absence of said dependency. Here's an example:
130+
131+
```python
132+
from tests import TestCase
133+
import pybamm
134+
135+
136+
class TestUtil(TestCase):
137+
def test_optional_dependency(self):
138+
# Test that an error is raised when pybtex is not available
139+
with self.assertRaisesRegex(
140+
ModuleNotFoundError, "Optional dependency pybtex is not available"
141+
):
142+
sys.modules["pybtex"] = None
143+
pybamm.function_using_pybtex(x, y, z)
144+
145+
# Test that the function works when pybtex is available
146+
sys.modules["pybtex"] = pybamm.util.have_optional_dependency("pybtex")
147+
pybamm.function_using_pybtex(x, y, z)
148+
```
118149

119150
## Testing
120151

@@ -266,7 +297,6 @@ This also means that, if you can't fix the bug yourself, it will be much easier
266297
```
267298

268299
This will start the debugger at the point where the `ValueError` was raised, and allow you to investigate further. Sometimes, it is more informative to put the try-except block further up the call stack than exactly where the error is raised.
269-
270300
2. Warnings. If functions are raising warnings instead of errors, it can be hard to pinpoint where this is coming from. Here, you can use the `warnings` module to convert warnings to errors:
271301

272302
```python
@@ -276,19 +306,15 @@ This also means that, if you can't fix the bug yourself, it will be much easier
276306
```
277307

278308
Then you can use a try-except block, as in a., but with, for example, `RuntimeWarning` instead of `ValueError`.
279-
280309
3. Stepping through the expression tree. Most calls in PyBaMM are operations on [expression trees](https://github.com/pybamm-team/PyBaMM/blob/develop/docs/source/examples/notebooks/expression_tree/expression-tree.ipynb). To view an expression tree in ipython, you can use the `render` command:
281310

282311
```python
283312
expression_tree.render()
284313
```
285314

286315
You can then step through the expression tree, using the `children` attribute, to pinpoint exactly where a bug is coming from. For example, if `expression_tree.jac(y)` is failing, you can check `expression_tree.children[0].jac(y)`, then `expression_tree.children[0].children[0].jac(y)`, etc.
287-
288316
3. To isolate whether a bug is in a model, its Jacobian or its simplified version, you can set the `use_jacobian` and/or `use_simplify` attributes of the model to `False` (they are both `True` by default for most models).
289-
290317
4. If a model isn't giving the answer you expect, you can try comparing it to other models. For example, you can investigate parameter limits in which two models should give the same answer by setting some parameters to be small or zero. The `StandardOutputComparison` class can be used to compare some standard outputs from battery models.
291-
292318
5. To get more information about what is going on under the hood, and hence understand what is causing the bug, you can set the [logging](https://realpython.com/python-logging/) level to `DEBUG` by adding the following line to your test or script:
293319

294320
```python3

docs/source/user_guide/installation/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ Package Minimum support
6666
`SciPy <https://docs.scipy.org/doc/scipy/>`__ 2.8.2
6767
`CasADi <https://web.casadi.org/docs/>`__ 3.6.0
6868
`Xarray <https://docs.xarray.dev/en/stable/>`__ 2023.04.0
69+
`Anytree <https://anytree.readthedocs.io/en/stable/>`__ 2.4.3
6970
================================================================ ==========================
7071

7172
.. _install.optional_dependencies:

pybamm/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,13 @@
4747
get_parameters_filepath,
4848
have_jax,
4949
install_jax,
50+
have_optional_dependency,
5051
is_jax_compatible,
5152
get_git_commit_info,
5253
)
5354
from .logger import logger, set_logging_level, get_new_logger
5455
from .settings import settings
5556
from .citations import Citations, citations, print_citations
56-
5757
#
5858
# Classes for the Expression Tree
5959
#

pybamm/citations.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,8 @@
66
import pybamm
77
import os
88
import warnings
9-
import pybtex
109
from sys import _getframe
11-
from pybtex.database import parse_file, parse_string, Entry
12-
from pybtex.scanner import PybtexError
10+
from pybamm.util import have_optional_dependency
1311

1412

1513
class Citations:
@@ -76,6 +74,7 @@ def read_citations(self):
7674
"""Reads the citations in `pybamm.CITATIONS.bib`. Other works can be cited
7775
by passing a BibTeX citation to :meth:`register`.
7876
"""
77+
parse_file = have_optional_dependency("pybtex.database", "parse_file")
7978
citations_file = os.path.join(pybamm.root_dir(), "pybamm", "CITATIONS.bib")
8079
bib_data = parse_file(citations_file, bib_format="bibtex")
8180
for key, entry in bib_data.entries.items():
@@ -86,6 +85,7 @@ def _add_citation(self, key, entry):
8685
previous entry is overwritten
8786
"""
8887

88+
Entry = have_optional_dependency("pybtex.database", "Entry")
8989
# Check input types are correct
9090
if not isinstance(key, str) or not isinstance(entry, Entry):
9191
raise TypeError()
@@ -151,6 +151,8 @@ def _parse_citation(self, key):
151151
key: str
152152
A BibTeX formatted citation
153153
"""
154+
PybtexError = have_optional_dependency("pybtex.scanner", "PybtexError")
155+
parse_string = have_optional_dependency("pybtex.database", "parse_string")
154156
try:
155157
# Parse string as a bibtex citation, and check that a citation was found
156158
bib_data = parse_string(key, bib_format="bibtex")
@@ -217,6 +219,7 @@ def print(self, filename=None, output_format="text", verbose=False):
217219
"""
218220
# Parse citations that were not known keys at registration, but do not
219221
# fail if they cannot be parsed
222+
pybtex = have_optional_dependency("pybtex")
220223
try:
221224
for key in self._unknown_citations:
222225
self._parse_citation(key)

pybamm/expression_tree/array.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
# NumpyArray class
33
#
44
import numpy as np
5-
import sympy
65
from scipy.sparse import csr_matrix, issparse
76

87
import pybamm
8+
from pybamm.util import have_optional_dependency
99

1010

1111
class Array(pybamm.Symbol):
@@ -125,6 +125,7 @@ def is_constant(self):
125125

126126
def to_equation(self):
127127
"""Returns the value returned by the node when evaluated."""
128+
sympy = have_optional_dependency("sympy")
128129
entries_list = self.entries.tolist()
129130
return sympy.Array(entries_list)
130131

pybamm/expression_tree/binary_operators.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
import numbers
55

66
import numpy as np
7-
import sympy
87
from scipy.sparse import csr_matrix, issparse
98
import functools
109

1110
import pybamm
11+
from pybamm.util import have_optional_dependency
1212

1313

1414
def _preprocess_binary(left, right):
@@ -147,6 +147,7 @@ def _sympy_operator(self, left, right):
147147

148148
def to_equation(self):
149149
"""Convert the node and its subtree into a SymPy equation."""
150+
sympy = have_optional_dependency("sympy")
150151
if self.print_name is not None:
151152
return sympy.Symbol(self.print_name)
152153
else:
@@ -323,6 +324,7 @@ def _binary_evaluate(self, left, right):
323324

324325
def _sympy_operator(self, left, right):
325326
"""Override :meth:`pybamm.BinaryOperator._sympy_operator`"""
327+
sympy = have_optional_dependency("sympy")
326328
left = sympy.Matrix(left)
327329
right = sympy.Matrix(right)
328330
return left * right
@@ -626,6 +628,7 @@ def _binary_new_copy(self, left, right):
626628

627629
def _sympy_operator(self, left, right):
628630
"""Override :meth:`pybamm.BinaryOperator._sympy_operator`"""
631+
sympy = have_optional_dependency("sympy")
629632
return sympy.Min(left, right)
630633

631634

@@ -662,6 +665,7 @@ def _binary_new_copy(self, left, right):
662665

663666
def _sympy_operator(self, left, right):
664667
"""Override :meth:`pybamm.BinaryOperator._sympy_operator`"""
668+
sympy = have_optional_dependency("sympy")
665669
return sympy.Max(left, right)
666670

667671

pybamm/expression_tree/concatenations.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
from collections import defaultdict
66

77
import numpy as np
8-
import sympy
98
from scipy.sparse import issparse, vstack
109

1110
import pybamm
11+
from pybamm.util import have_optional_dependency
1212

1313

1414
class Concatenation(pybamm.Symbol):
@@ -135,6 +135,7 @@ def is_constant(self):
135135

136136
def _sympy_operator(self, *children):
137137
"""Apply appropriate SymPy operators."""
138+
sympy = have_optional_dependency("sympy")
138139
self.concat_latex = tuple(map(sympy.latex, children))
139140

140141
if self.print_name is not None:

pybamm/expression_tree/functions.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,11 @@
33
#
44
import numbers
55

6-
import autograd
76
import numpy as np
8-
import sympy
97
from scipy import special
108

119
import pybamm
12-
10+
from pybamm.util import have_optional_dependency
1311

1412
class Function(pybamm.Symbol):
1513
"""
@@ -96,6 +94,7 @@ def _function_diff(self, children, idx):
9694
Derivative with respect to child number 'idx'.
9795
See :meth:`pybamm.Symbol._diff()`.
9896
"""
97+
autograd = have_optional_dependency("autograd")
9998
# Store differentiated function, needed in case we want to convert to CasADi
10099
if self.derivative == "autograd":
101100
return Function(
@@ -202,6 +201,7 @@ def _sympy_operator(self, child):
202201

203202
def to_equation(self):
204203
"""Convert the node and its subtree into a SymPy equation."""
204+
sympy = have_optional_dependency("sympy")
205205
if self.print_name is not None:
206206
return sympy.Symbol(self.print_name)
207207
else:
@@ -250,6 +250,7 @@ def _function_new_copy(self, children):
250250

251251
def _sympy_operator(self, child):
252252
"""Apply appropriate SymPy operators."""
253+
sympy = have_optional_dependency("sympy")
253254
class_name = self.__class__.__name__.lower()
254255
sympy_function = getattr(sympy, class_name)
255256
return sympy_function(child)
@@ -267,6 +268,7 @@ def _function_diff(self, children, idx):
267268

268269
def _sympy_operator(self, child):
269270
"""Override :meth:`pybamm.Function._sympy_operator`"""
271+
sympy = have_optional_dependency("sympy")
270272
return sympy.asinh(child)
271273

272274

@@ -287,6 +289,7 @@ def _function_diff(self, children, idx):
287289

288290
def _sympy_operator(self, child):
289291
"""Override :meth:`pybamm.Function._sympy_operator`"""
292+
sympy = have_optional_dependency("sympy")
290293
return sympy.atan(child)
291294

292295

pybamm/expression_tree/independent_variable.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
#
22
# IndependentVariable class
33
#
4-
import sympy
5-
64
import pybamm
5+
from pybamm.util import have_optional_dependency
76

87
KNOWN_COORD_SYS = ["cartesian", "cylindrical polar", "spherical polar"]
98

@@ -44,6 +43,7 @@ def _jac(self, variable):
4443

4544
def to_equation(self):
4645
"""Convert the node and its subtree into a SymPy equation."""
46+
sympy = have_optional_dependency("sympy")
4747
if self.print_name is not None:
4848
return sympy.Symbol(self.print_name)
4949
else:
@@ -77,6 +77,7 @@ def _evaluate_for_shape(self):
7777

7878
def to_equation(self):
7979
"""Convert the node and its subtree into a SymPy equation."""
80+
sympy = have_optional_dependency("sympy")
8081
return sympy.Symbol("t")
8182

8283

0 commit comments

Comments
 (0)