From 74381f784cb50ae8a2b1cf436c1e264c7762d9f1 Mon Sep 17 00:00:00 2001 From: kratman Date: Wed, 26 Jun 2024 09:50:13 -0400 Subject: [PATCH 01/82] Replace tinyurl --- pybamm/expression_tree/array.py | 12 ++++++------ pybamm/parameters/parameter_values.py | 3 +-- pybamm/plotting/plot.py | 6 ++---- pybamm/plotting/plot2D.py | 6 ++---- pybamm/solvers/algebraic_solver.py | 5 +++-- pybamm/solvers/scipy_solver.py | 5 +++-- 6 files changed, 17 insertions(+), 20 deletions(-) diff --git a/pybamm/expression_tree/array.py b/pybamm/expression_tree/array.py index e16b7d17aa..d3021bb9f5 100644 --- a/pybamm/expression_tree/array.py +++ b/pybamm/expression_tree/array.py @@ -189,7 +189,8 @@ def linspace(start: float, stop: float, num: int = 50, **kwargs) -> pybamm.Array """ Creates a linearly spaced array by calling `numpy.linspace` with keyword arguments 'kwargs'. For a list of 'kwargs' see the - `numpy linspace documentation `_ + `numpy linspace documentation + `_ """ return pybamm.Array(np.linspace(start, stop, num, **kwargs)) @@ -200,9 +201,8 @@ def meshgrid( """ Return coordinate matrices as from coordinate vectors by calling `numpy.meshgrid` with keyword arguments 'kwargs'. For a list of 'kwargs' - see the `numpy meshgrid documentation `_ + see the `numpy meshgrid documentation + `_ """ - [X, Y] = np.meshgrid(x.entries, y.entries) - X = pybamm.Array(X) - Y = pybamm.Array(Y) - return X, Y + [x_grid, y_grid] = np.meshgrid(x.entries, y.entries) + return pybamm.Array(x_grid), pybamm.Array(y_grid) diff --git a/pybamm/parameters/parameter_values.py b/pybamm/parameters/parameter_values.py index 57ebf65058..815dbedcc0 100644 --- a/pybamm/parameters/parameter_values.py +++ b/pybamm/parameters/parameter_values.py @@ -256,7 +256,6 @@ def update(self, values, check_conflict=False, check_already_exists=True, path=" + "sure you want to update this parameter, use " + "param.update({{name: value}}, check_already_exists=False)" ) from err - # if no conflicts, update if isinstance(value, str): if ( value.startswith("[function]") @@ -269,7 +268,7 @@ def update(self, values, check_conflict=False, check_already_exists=True, path=" "or [2D data] is no longer supported. For functions, pass in a " "python function object. For data, pass in a python function " "that returns a pybamm Interpolant object. " - "See https://tinyurl.com/merv43ss for an example with both." + "See the Ai2020 parameter set for an example with both." ) elif value == "[input]": diff --git a/pybamm/plotting/plot.py b/pybamm/plotting/plot.py index 4037ab8fbf..ca145f9831 100644 --- a/pybamm/plotting/plot.py +++ b/pybamm/plotting/plot.py @@ -1,6 +1,3 @@ -# -# Method for creating a 1D plot of pybamm arrays -# import pybamm from .quick_plot import ax_min, ax_max from pybamm.util import import_optional_dependency @@ -10,7 +7,8 @@ def plot(x, y, ax=None, show_plot=True, **kwargs): """ Generate a simple 1D plot. Calls `matplotlib.pyplot.plot` with keyword arguments 'kwargs'. For a list of 'kwargs' see the - `matplotlib plot documentation `_ + `matplotlib plot documentation + `_ Parameters ---------- diff --git a/pybamm/plotting/plot2D.py b/pybamm/plotting/plot2D.py index 7d1f3c6bae..aa169ac39f 100644 --- a/pybamm/plotting/plot2D.py +++ b/pybamm/plotting/plot2D.py @@ -1,6 +1,3 @@ -# -# Method for creating a filled contour plot of pybamm arrays -# import pybamm from .quick_plot import ax_min, ax_max from pybamm.util import import_optional_dependency @@ -10,7 +7,8 @@ def plot2D(x, y, z, ax=None, show_plot=True, **kwargs): """ Generate a simple 2D plot. Calls `matplotlib.pyplot.contourf` with keyword arguments 'kwargs'. For a list of 'kwargs' see the - `matplotlib contourf documentation `_ + `matplotlib contourf documentation + `_ Parameters ---------- diff --git a/pybamm/solvers/algebraic_solver.py b/pybamm/solvers/algebraic_solver.py index bc711ff02a..5811e3b16d 100644 --- a/pybamm/solvers/algebraic_solver.py +++ b/pybamm/solvers/algebraic_solver.py @@ -26,8 +26,9 @@ class AlgebraicSolver(pybamm.BaseSolver): The tolerance for the solver (default is 1e-6). extra_options : dict, optional Any options to pass to the rootfinder. Vary depending on which method is chosen. - Please consult `SciPy documentation `_ for - details. + Please consult `SciPy documentation + `_ + for details. """ def __init__(self, method="lm", tol=1e-6, extra_options=None): diff --git a/pybamm/solvers/scipy_solver.py b/pybamm/solvers/scipy_solver.py index fb320f558d..9a66f5bc01 100644 --- a/pybamm/solvers/scipy_solver.py +++ b/pybamm/solvers/scipy_solver.py @@ -23,8 +23,9 @@ class ScipySolver(pybamm.BaseSolver): The tolerance to assert whether extrapolation occurs or not (default is 0). extra_options : dict, optional Any options to pass to the solver. - Please consult `SciPy documentation `_ for - details. + Please consult `SciPy documentation + `_ + for details. """ def __init__( From 91b1d50a0afd7f872e382069528e345b3f46511e Mon Sep 17 00:00:00 2001 From: "Eric G. Kratz" Date: Fri, 28 Jun 2024 10:30:15 -0400 Subject: [PATCH 02/82] Sync periodic tests with the push workflow (#4172) * Sync workflow * Remove caching * Switch order of commands * Fix lychee * Remove self-hosted runners * Apply suggestions from code review Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> --------- Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> --- .github/workflows/run_periodic_tests.yml | 147 ++---------------- pybamm/expression_tree/operations/latexify.py | 3 - 2 files changed, 12 insertions(+), 138 deletions(-) diff --git a/.github/workflows/run_periodic_tests.yml b/.github/workflows/run_periodic_tests.yml index 2083086ba6..6f79df76b2 100644 --- a/.github/workflows/run_periodic_tests.yml +++ b/.github/workflows/run_periodic_tests.yml @@ -24,18 +24,15 @@ concurrency: cancel-in-progress: true jobs: - run_unit_tests: - name: Unit tests (${{ matrix.os }} / Python ${{ matrix.python-version }}) + run_tests: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-12, macos-14, windows-latest] - python-version: ["3.9", "3.10", "3.11", "3.12"] - # Exclude Python 3.12 from unit tests since we run it in the coverage jobs - exclude: - - os: ubuntu-latest - python-version: "3.12" + os: [ ubuntu-latest, macos-12, macos-14, windows-latest ] + python-version: [ "3.9", "3.10", "3.11", "3.12" ] + name: Tests (${{ matrix.os }} / Python ${{ matrix.python-version }}) + steps: - name: Check out PyBaMM repository uses: actions/checkout@v4 @@ -78,91 +75,19 @@ jobs: run: python -m nox -s pybamm-requires - name: Run unit tests for ${{ matrix.os }} with Python ${{ matrix.python-version }} + if: matrix.os != 'ubuntu-latest' || matrix.python-version != '3.12' run: python -m nox -s unit - check_coverage: - runs-on: ubuntu-latest - name: Coverage tests (ubuntu-latest / Python 3.12) - - steps: - - name: Check out PyBaMM repository - uses: actions/checkout@v4 - - - name: Install Linux system dependencies - run: | - sudo apt-get update - sudo apt-get install gfortran gcc graphviz pandoc libopenblas-dev texlive-latex-extra dvipng - - - name: Set up Python 3.12 - uses: actions/setup-python@v5 - with: - python-version: 3.12 - - - name: Install nox - run: python -m pip install nox - - - name: Install SuiteSparse and SUNDIALS on GNU/Linux - timeout-minutes: 10 - run: python -m nox -s pybamm-requires - - - name: Run unit tests for Ubuntu with Python 3.12 and generate coverage report + - name: Run coverage tests for ${{ matrix.os }} with Python ${{ matrix.python-version }} + if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12' run: python -m nox -s coverage - name: Upload coverage report + if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12' uses: codecov/codecov-action@v4.5.0 - if: github.repository == 'pybamm-team/PyBaMM' with: token: ${{ secrets.CODECOV_TOKEN }} - run_integration_tests: - name: Integration tests (${{ matrix.os }} / Python ${{ matrix.python-version }}) - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-12, macos-14, windows-latest] - python-version: ["3.9", "3.10", "3.11", "3.12"] - steps: - - name: Check out PyBaMM repository - uses: actions/checkout@v4 - - - name: Install Linux system dependencies - if: matrix.os == 'ubuntu-latest' - run: | - sudo apt-get update - sudo apt-get install gfortran gcc graphviz pandoc libopenblas-dev texlive-latex-extra dvipng - - - name: Install macOS system dependencies - if: matrix.os == 'macos-12' || matrix.os == 'macos-14' - env: - HOMEBREW_NO_INSTALL_CLEANUP: 1 - HOMEBREW_NO_AUTO_UPDATE: 1 - HOMEBREW_NO_COLOR: 1 - # Speed up CI - NONINTERACTIVE: 1 - # sometimes gfortran cannot be found, so reinstall gcc just to be sure - run: | - brew analytics off - brew install graphviz - brew reinstall gcc - - - name: Install Windows system dependencies - if: matrix.os == 'windows-latest' - run: choco install graphviz --version=8.0.5 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - - name: Install nox - run: python -m pip install nox - - - name: Install SuiteSparse and SUNDIALS on GNU/Linux and macOS - timeout-minutes: 10 - if: matrix.os != 'windows-latest' - run: python -m nox -s pybamm-requires - - name: Run integration tests for ${{ matrix.os }} with Python ${{ matrix.python-version }} run: python -m nox -s integration @@ -170,6 +95,7 @@ jobs: run_doctests: runs-on: ubuntu-latest name: Doctests (ubuntu-latest / Python 3.11) + steps: - name: Check out PyBaMM repository uses: actions/checkout@v4 @@ -179,9 +105,9 @@ jobs: - name: Install Linux system dependencies run: | sudo apt-get update - sudo apt-get install graphviz pandoc libopenblas-dev texlive-latex-extra dvipng + sudo apt-get install graphviz pandoc texlive-latex-extra dvipng - - name: Set up Python 3.11 + - name: Set up Python uses: actions/setup-python@v5 with: python-version: 3.11 @@ -250,52 +176,3 @@ jobs: - name: Run example scripts tests for GNU/Linux with Python 3.12 run: python -m nox -s scripts - - # M-series Mac Mini - build-apple-mseries: - if: github.repository_owner == 'pybamm-team' - runs-on: [self-hosted, macOS, ARM64] - env: - GITHUB_PATH: ${PYENV_ROOT/bin:$PATH} - LD_LIBRARY_PATH: $HOME/.local/lib - strategy: - fail-fast: false - matrix: - python-version: ["3.9", "3.10", "3.11", "3.12"] - - steps: - - name: Check out PyBaMM repository - uses: actions/checkout@v4 - - - name: Install Python & create virtualenv - shell: bash - run: | - eval "$(pyenv init -)" - pyenv install ${{ matrix.python-version }} -s - pyenv virtualenv ${{ matrix.python-version }} pybamm-${{ matrix.python-version }} - - - name: Install build-time dependencies & run unit tests for M-series macOS runner - shell: bash - env: - HOMEBREW_NO_INSTALL_CLEANUP: 1 - NONINTERACTIVE: 1 - run: | - eval "$(pyenv init -)" - pyenv activate pybamm-${{ matrix.python-version }} - python -m pip install --upgrade pip nox - python -m nox -s pybamm-requires -- --force - python -m nox -s unit - - - name: Run integration tests for M-series macOS runner - run: | - eval "$(pyenv init -)" - pyenv activate pybamm-${{ matrix.python-version }} - python -m nox -s integration - - - name: Uninstall pyenv-virtualenv & Python - if: always() - shell: bash - run: | - eval "$(pyenv init -)" - pyenv activate pybamm-${{ matrix.python-version }} - pyenv uninstall -f $( python --version ) diff --git a/pybamm/expression_tree/operations/latexify.py b/pybamm/expression_tree/operations/latexify.py index 04b1a72e41..293570def8 100644 --- a/pybamm/expression_tree/operations/latexify.py +++ b/pybamm/expression_tree/operations/latexify.py @@ -267,14 +267,12 @@ def latexify(self, output_variables=None): # Split list with new lines eqn_new_line = sympy.Symbol(r"\\\\".join(map(custom_print_func, eqn_list))) - # Return latex of equations if self.filename is None: if self.newline is True: return eqn_new_line else: return eqn_list - # # Formats - tex elif self.filename.endswith(".tex"): # pragma: no cover return sympy.preview(eqn_new_line, outputTexFile=self.filename) @@ -289,7 +287,6 @@ def latexify(self, output_variables=None): euler=False, ) - # For more dvioptions see https://www.nongnu.org/dvipng/dvipng_4.html else: try: return sympy.preview( From b50dfa595dd93a2943aa2a5a8aa14ac9468aea8e Mon Sep 17 00:00:00 2001 From: Marc Berliner <34451391+MarcBerliner@users.noreply.github.com> Date: Fri, 28 Jun 2024 11:24:11 -0400 Subject: [PATCH 03/82] Add functions of time in experiment step (#4222) * time varying experiment steps * Update CHANGELOG.md * fix code coverage * rearrange function order * remove `is_drive_cycle` attribute * break out `record_tags` method --------- Co-authored-by: Eric G. Kratz --- CHANGELOG.md | 1 + pybamm/experiment/step/base_step.py | 122 ++++++++++++++++++++++------ pybamm/experiment/step/steps.py | 30 +------ tests/unit/test_simulation.py | 43 ++++++++++ 4 files changed, 143 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe78866a3d..9b7f98f450 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Features +- Added functionality to pass in arbitrary functions of time as the argument for a (`pybamm.step`). ([#4222](https://github.com/pybamm-team/PyBaMM/pull/4222)) - Added new parameters `"f{pref]Initial inner SEI on cracks thickness [m]"` and `"f{pref]Initial outer SEI on cracks thickness [m]"`, instead of hardcoding these to `L_inner_0 / 10000` and `L_outer_0 / 10000`. ([#4168](https://github.com/pybamm-team/PyBaMM/pull/4168)) - Added `pybamm.DataLoader` class to fetch data files from [pybamm-data](https://github.com/pybamm-team/pybamm-data/releases/tag/v1.0.0) and store it under local cache. ([#4098](https://github.com/pybamm-team/PyBaMM/pull/4098)) - Added `time` as an option for `Experiment.termination`. Now allows solving up to a user-specified time while also allowing different cycles and steps in an experiment to be handled normally. ([#4073](https://github.com/pybamm-team/PyBaMM/pull/4073)) diff --git a/pybamm/experiment/step/base_step.py b/pybamm/experiment/step/base_step.py index 626094ba54..16224a6afa 100644 --- a/pybamm/experiment/step/base_step.py +++ b/pybamm/experiment/step/base_step.py @@ -37,7 +37,7 @@ class BaseStep: ---------- value : float The value of the step, corresponding to the type of step. Can be a number, a - 2-tuple (for cccv_ode), or a 2-column array (for drive cycles) + 2-tuple (for cccv_ode), a 2-column array (for drive cycles), or a 1-argument function of t duration : float, optional The duration of the step in seconds. termination : str or list, optional @@ -72,8 +72,9 @@ def __init__( direction=None, ): # Check if drive cycle - self.is_drive_cycle = isinstance(value, np.ndarray) - if self.is_drive_cycle: + is_drive_cycle = isinstance(value, np.ndarray) + is_python_function = callable(value) + if is_drive_cycle: if value.ndim != 2 or value.shape[1] != 2: raise ValueError( "Drive cycle must be a 2-column array with time in the first column" @@ -83,36 +84,30 @@ def __init__( t = value[:, 0] if t[0] != 0: raise ValueError("Drive cycle must start at t=0") + elif is_python_function: + t0 = 0 + # Check if the function is only a function of t + try: + value_t0 = value(t0) + except TypeError: + raise TypeError( + "Input function must have only 1 positional argument for time" + ) from None + + # Check if the value at t0 is feasible + if not (np.isfinite(value_t0) and np.isscalar(value_t0)): + raise ValueError( + f"Input function must return a real number output at t = {t0}" + ) # Set duration if duration is None: duration = self.default_duration(value) self.duration = _convert_time_to_seconds(duration) - # Record all the args for repr and hash - self.repr_args = f"{value}, duration={duration}" - self.hash_args = f"{value}" - if termination: - self.repr_args += f", termination={termination}" - self.hash_args += f", termination={termination}" - if period: - self.repr_args += f", period={period}" - if temperature: - self.repr_args += f", temperature={temperature}" - self.hash_args += f", temperature={temperature}" - if tags: - self.repr_args += f", tags={tags}" - if start_time: - self.repr_args += f", start_time={start_time}" - if description: - self.repr_args += f", description={description}" - if direction: - self.repr_args += f", direction={direction}" - self.hash_args += f", direction={direction}" - # If drive cycle, repeat the drive cycle until the end of the experiment, # and create an interpolant - if self.is_drive_cycle: + if is_drive_cycle: t_max = self.duration if t_max > value[-1, 0]: # duration longer than drive cycle values so loop @@ -135,10 +130,33 @@ def __init__( name="Drive Cycle", ) self.period = np.diff(t).min() + elif is_python_function: + t = pybamm.t - pybamm.InputParameter("start time") + self.value = value(t) + self.period = _convert_time_to_seconds(period) else: self.value = value self.period = _convert_time_to_seconds(period) + if ( + hasattr(self, "calculate_charge_or_discharge") + and self.calculate_charge_or_discharge + ): + direction = self.value_based_charge_or_discharge() + self.direction = direction + + self.repr_args, self.hash_args = self.record_tags( + value, + duration, + termination, + period, + temperature, + tags, + start_time, + description, + direction, + ) + self.description = description if termination is None: @@ -167,8 +185,6 @@ def __init__( self.next_start_time = None self.end_time = None - self.direction = direction - def copy(self): """ Return a copy of the step. @@ -277,6 +293,58 @@ def update_model_events(self, new_model): event.name, event.expression + 1, event.event_type ) + def value_based_charge_or_discharge(self): + """ + Determine whether the step is a charge or discharge step based on the value of the + step + """ + if isinstance(self.value, pybamm.Symbol): + inpt = {"start time": 0} + init_curr = self.value.evaluate(t=0, inputs=inpt).flatten()[0] + else: + init_curr = self.value + sign = np.sign(init_curr) + if sign == 0: + return "Rest" + elif sign > 0: + return "Discharge" + else: + return "Charge" + + def record_tags( + self, + value, + duration, + termination, + period, + temperature, + tags, + start_time, + description, + direction, + ): + """Record all the args for repr and hash""" + repr_args = f"{value}, duration={duration}" + hash_args = f"{value}" + if termination: + repr_args += f", termination={termination}" + hash_args += f", termination={termination}" + if period: + repr_args += f", period={period}" + if temperature: + repr_args += f", temperature={temperature}" + hash_args += f", temperature={temperature}" + if tags: + repr_args += f", tags={tags}" + if start_time: + repr_args += f", start_time={start_time}" + if description: + repr_args += f", description={description}" + if direction: + repr_args += f", direction={direction}" + hash_args += f", direction={direction}" + return repr_args, hash_args + class BaseStepExplicit(BaseStep): def __init__(self, *args, **kwargs): diff --git a/pybamm/experiment/step/steps.py b/pybamm/experiment/step/steps.py index 62dc91cf01..e3322104bf 100644 --- a/pybamm/experiment/step/steps.py +++ b/pybamm/experiment/step/steps.py @@ -1,4 +1,3 @@ -import numpy as np import pybamm from .base_step import ( BaseStepExplicit, @@ -130,7 +129,7 @@ class Current(BaseStepExplicit): """ def __init__(self, value, **kwargs): - kwargs["direction"] = value_based_charge_or_discharge(value) + self.calculate_charge_or_discharge = True super().__init__(value, **kwargs) def current_value(self, variables): @@ -151,7 +150,7 @@ class CRate(BaseStepExplicit): """ def __init__(self, value, **kwargs): - kwargs["direction"] = value_based_charge_or_discharge(value) + self.calculate_charge_or_discharge = True super().__init__(value, **kwargs) def current_value(self, variables): @@ -201,7 +200,7 @@ class Power(BaseStepImplicit): """ def __init__(self, value, **kwargs): - kwargs["direction"] = value_based_charge_or_discharge(value) + self.calculate_charge_or_discharge = True super().__init__(value, **kwargs) def get_parameter_values(self, variables): @@ -232,7 +231,7 @@ class Resistance(BaseStepImplicit): """ def __init__(self, value, **kwargs): - kwargs["direction"] = value_based_charge_or_discharge(value) + self.calculate_charge_or_discharge = True super().__init__(value, **kwargs) def get_parameter_values(self, variables): @@ -414,24 +413,3 @@ def copy(self): return CustomStepImplicit( self.current_rhs_function, self.control, **self.kwargs ) - - -def value_based_charge_or_discharge(step_value): - """ - Determine whether the step is a charge or discharge step based on the value of the - step - """ - if isinstance(step_value, np.ndarray): - init_curr = step_value[0, 1] - elif isinstance(step_value, pybamm.Symbol): - inpt = {"start time": 0} - init_curr = step_value.evaluate(t=0, inputs=inpt).flatten()[0] - else: - init_curr = step_value - sign = np.sign(init_curr) - if sign == 0: - return "Rest" - elif sign > 0: - return "Discharge" - else: - return "Charge" diff --git a/tests/unit/test_simulation.py b/tests/unit/test_simulation.py index 63c15a91fc..368c84607c 100644 --- a/tests/unit/test_simulation.py +++ b/tests/unit/test_simulation.py @@ -365,6 +365,49 @@ def test_step_with_inputs(self): sim.solution.all_inputs[1]["Current function [A]"], 2 ) + def test_time_varying_input_function(self): + tf = 20.0 + + def oscillating(t): + return 3.6 + 0.1 * np.sin(2 * np.pi * t / tf) + + model = pybamm.lithium_ion.SPM() + + operating_modes = { + "Current [A]": pybamm.step.current, + "C-rate": pybamm.step.c_rate, + "Voltage [V]": pybamm.step.voltage, + "Power [W]": pybamm.step.power, + } + for name in operating_modes: + operating_mode = operating_modes[name] + step = operating_mode(oscillating, duration=tf / 2) + experiment = pybamm.Experiment([step, step], period=f"{tf / 100} seconds") + + solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) + sim = pybamm.Simulation(model, experiment=experiment, solver=solver) + sim.solve() + for sol in sim.solution.sub_solutions: + t0 = sol.t[0] + np.testing.assert_array_almost_equal( + sol[name].entries, np.array(oscillating(sol.t - t0)) + ) + + # check improper inputs + for x in (np.nan, np.inf): + + def f(t, x=x): + return x + t + + with self.assertRaises(ValueError): + operating_mode(f) + + def g(t, y): + return t + + with self.assertRaises(TypeError): + operating_mode(g) + def test_save_load(self): with TemporaryDirectory() as dir_name: test_name = os.path.join(dir_name, "tests.pickle") From a786a5eda671f3857b45ddad824199b3a6ee9843 Mon Sep 17 00:00:00 2001 From: Pip Liggins Date: Fri, 28 Jun 2024 16:51:31 +0100 Subject: [PATCH 04/82] Bug fix for cracking options (#4221) * Add OptionError if SEI in cracks selected but no cracks in mechanics * Add test --------- Co-authored-by: Robert Timms <43040151+rtimms@users.noreply.github.com> Co-authored-by: Eric G. Kratz --- .../full_battery_models/base_battery_model.py | 14 ++++++++++++++ .../test_base_battery_model.py | 4 ++++ 2 files changed, 18 insertions(+) diff --git a/pybamm/models/full_battery_models/base_battery_model.py b/pybamm/models/full_battery_models/base_battery_model.py index fb8d001f93..8a2e443338 100644 --- a/pybamm/models/full_battery_models/base_battery_model.py +++ b/pybamm/models/full_battery_models/base_battery_model.py @@ -633,6 +633,20 @@ def __init__(self, extra_options): "be 'none': 'particle mechanics', 'loss of active material'" ) + if "true" in options["SEI on cracks"]: + sei_on_cr = options["SEI on cracks"] + p_mechanics = options["particle mechanics"] + if isinstance(p_mechanics, str) and isinstance(sei_on_cr, tuple): + p_mechanics = (p_mechanics, p_mechanics) + if any( + sei == "true" and mech != "swelling and cracking" + for mech, sei in zip(p_mechanics, sei_on_cr) + ): + raise pybamm.OptionError( + "If 'SEI on cracks' is 'true' then 'particle mechanics' must be " + "'swelling and cracking'." + ) + # Check options are valid for option, value in options.items(): if isinstance(value, str) or option in [ diff --git a/tests/unit/test_models/test_full_battery_models/test_base_battery_model.py b/tests/unit/test_models/test_full_battery_models/test_base_battery_model.py index caff0cda5d..057d6d2ad9 100644 --- a/tests/unit/test_models/test_full_battery_models/test_base_battery_model.py +++ b/tests/unit/test_models/test_full_battery_models/test_base_battery_model.py @@ -310,6 +310,10 @@ def test_options(self): # SEI on cracks with self.assertRaisesRegex(pybamm.OptionError, "SEI on cracks"): pybamm.BaseBatteryModel({"SEI on cracks": "bad SEI on cracks"}) + with self.assertRaisesRegex(pybamm.OptionError, "'SEI on cracks' is 'true'"): + pybamm.BaseBatteryModel( + {"SEI on cracks": "true", "particle mechanics": "swelling only"} + ) # plating model with self.assertRaisesRegex(pybamm.OptionError, "lithium plating"): From 68f71f278501b8bc670aed1514d4b32a817f42d4 Mon Sep 17 00:00:00 2001 From: Pradyot Ranjan <99216956+prady0t@users.noreply.github.com> Date: Mon, 1 Jul 2024 19:28:40 +0530 Subject: [PATCH 05/82] Moving `test_util.py` to pytest (#4214) * Moving test_util.py to pytest Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * style: pre-commit fixes * Update pyproject.toml Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> * Update pyproject.toml Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> * removed usless coment and entrypoint Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * style: pre-commit fixes * Update tests/unit/test_util.py Co-authored-by: Arjun Verma * Update tests/unit/test_util.py Co-authored-by: Arjun Verma * Update tests/unit/test_util.py Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> * Added suggestions and class structure back Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * style: pre-commit fixes --------- Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> Co-authored-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Co-authored-by: Arjun Verma --- pyproject.toml | 8 ++-- tests/unit/test_util.py | 99 ++++++++++++++++++----------------------- 2 files changed, 47 insertions(+), 60 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7de16accaf..0f5c19987b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -107,9 +107,11 @@ dev = [ "pytest-doctestplus", # For test parameterization "parameterized>=0.9", - # For testing Jupyter notebooks + # pytest and its plugins "pytest>=6", "pytest-xdist", + "pytest-mock", + # For testing Jupyter notebooks "nbmake", # To access the metadata for python packages "importlib-metadata; python_version < '3.10'", @@ -217,13 +219,11 @@ ignore = [ "**.ipynb" = ["E402", "E703"] "docs/source/examples/notebooks/models/lithium-plating.ipynb" = ["F821"] -# NOTE: currently used only for notebook tests with the nbmake plugin. [tool.pytest.ini_options] minversion = "6" -# Use pytest-xdist to run tests in parallel by default, exit with -# error if not installed required_plugins = [ "pytest-xdist", + "pytest-mock", ] addopts = [ "-nauto", diff --git a/tests/unit/test_util.py b/tests/unit/test_util.py index 603af50560..21673a44fe 100644 --- a/tests/unit/test_util.py +++ b/tests/unit/test_util.py @@ -1,11 +1,9 @@ +import pytest import importlib -from tests import TestCase import os import sys import pybamm import tempfile -import unittest -from unittest.mock import patch from io import StringIO from tests import ( @@ -15,18 +13,18 @@ ) -class TestUtil(TestCase): +class TestUtil: """ Test the functionality in util.py """ def test_is_constant_and_can_evaluate(self): symbol = pybamm.PrimaryBroadcast(0, "negative electrode") - self.assertEqual(False, pybamm.is_constant_and_can_evaluate(symbol)) + assert not pybamm.is_constant_and_can_evaluate(symbol) symbol = pybamm.StateVector(slice(0, 1)) - self.assertEqual(False, pybamm.is_constant_and_can_evaluate(symbol)) + assert not pybamm.is_constant_and_can_evaluate(symbol) symbol = pybamm.Scalar(0) - self.assertEqual(True, pybamm.is_constant_and_can_evaluate(symbol)) + assert pybamm.is_constant_and_can_evaluate(symbol) def test_fuzzy_dict(self): d = pybamm.FuzzyDict( @@ -39,53 +37,50 @@ def test_fuzzy_dict(self): "Positive electrode diffusivity [m2.s-1]": 6, } ) - self.assertEqual(d["test"], 1) - with self.assertRaisesRegex(KeyError, "'test3' not found. Best matches are "): + assert d["test"] == 1 + with pytest.raises(KeyError, match="'test3' not found. Best matches are "): d.__getitem__("test3") - with self.assertRaisesRegex(KeyError, "stoichiometry"): + with pytest.raises(KeyError, match="stoichiometry"): d.__getitem__("Negative electrode SOC") - with self.assertRaisesRegex(KeyError, "dimensional version"): + with pytest.raises(KeyError, match="dimensional version"): d.__getitem__("A dimensional variable") - with self.assertRaisesRegex(KeyError, "open circuit voltage"): + with pytest.raises(KeyError, match="open circuit voltage"): d.__getitem__("Measured open circuit voltage [V]") - with self.assertRaisesRegex(KeyError, "Lower voltage"): + with pytest.raises(KeyError, match="Lower voltage"): d.__getitem__("Open-circuit voltage at 0% SOC [V]") - with self.assertRaisesRegex(KeyError, "Upper voltage"): + with pytest.raises(KeyError, match="Upper voltage"): d.__getitem__("Open-circuit voltage at 100% SOC [V]") - with self.assertWarns(DeprecationWarning): - self.assertEqual( - d["Positive electrode diffusivity [m2.s-1]"], - d["Positive particle diffusivity [m2.s-1]"], + with pytest.warns(DeprecationWarning): + assert ( + d["Positive electrode diffusivity [m2.s-1]"] + == d["Positive particle diffusivity [m2.s-1]"] ) def test_get_parameters_filepath(self): - tempfile_obj = tempfile.NamedTemporaryFile("w", dir=".") - self.assertTrue( - pybamm.get_parameters_filepath(tempfile_obj.name) == tempfile_obj.name - ) - tempfile_obj.close() + with tempfile.NamedTemporaryFile("w", dir=".") as tempfile_obj: + assert ( + pybamm.get_parameters_filepath(tempfile_obj.name) == tempfile_obj.name + ) package_dir = os.path.join(pybamm.root_dir(), "pybamm") - tempfile_obj = tempfile.NamedTemporaryFile("w", dir=package_dir) - path = os.path.join(package_dir, tempfile_obj.name) - self.assertTrue(pybamm.get_parameters_filepath(tempfile_obj.name) == path) - tempfile_obj.close() + with tempfile.NamedTemporaryFile("w", dir=package_dir) as tempfile_obj: + path = os.path.join(package_dir, tempfile_obj.name) + assert pybamm.get_parameters_filepath(tempfile_obj.name) == path + @pytest.mark.skipif(pybamm.have_jax(), reason="The JAX solver is not installed") def test_is_jax_compatible(self): - if pybamm.have_jax(): - compatible = pybamm.is_jax_compatible() - self.assertTrue(compatible) + assert True def test_git_commit_info(self): git_commit_info = pybamm.get_git_commit_info() - self.assertIsInstance(git_commit_info, str) - self.assertEqual(git_commit_info[:2], "v2") + assert isinstance(git_commit_info, str) + assert git_commit_info[:2] == "v2" def test_import_optional_dependency(self): optional_distribution_deps = get_optional_distribution_deps("pybamm") @@ -101,9 +96,9 @@ def test_import_optional_dependency(self): # Test import optional dependency for import_pkg in present_optional_import_deps: - with self.assertRaisesRegex( + with pytest.raises( ModuleNotFoundError, - f"Optional dependency {import_pkg} is not available.", + match=f"Optional dependency {import_pkg} is not available.", ): pybamm.util.import_optional_dependency(import_pkg) @@ -135,7 +130,7 @@ def test_pybamm_import(self): try: importlib.import_module("pybamm") except ModuleNotFoundError as error: - self.fail( + pytest.fail( f"Import of 'pybamm' shouldn't require optional dependencies. Error: {error}" ) finally: @@ -154,47 +149,39 @@ def test_optional_dependencies(self): ) # Check that optional dependencies are not present in the core PyBaMM installation - optional_present_deps = optional_distribution_deps & required_distribution_deps - self.assertFalse( - bool(optional_present_deps), + optional_present_deps = bool( + optional_distribution_deps & required_distribution_deps + ) + assert not optional_present_deps, ( f"Optional dependencies installed: {optional_present_deps}.\n" "Please ensure that optional dependencies are not present in the core PyBaMM installation, " - "or list them as required.", + "or list them as required." ) -class TestSearch(TestCase): - def test_url_gets_to_stdout(self): +class TestSearch: + def test_url_gets_to_stdout(self, mocker): model = pybamm.BaseModel() model.variables = {"Electrolyte concentration": 1, "Electrode potential": 0} param = pybamm.ParameterValues({"test": 10, "b": 2}) # Test variables search (default returns key) - with patch("sys.stdout", new=StringIO()) as fake_out: + with mocker.patch("sys.stdout", new=StringIO()) as fake_out: model.variables.search("Electrode") - self.assertEqual(fake_out.getvalue(), "Electrode potential\n") + assert fake_out.getvalue() == "Electrode potential\n" # Test bad var search (returns best matches) - with patch("sys.stdout", new=StringIO()) as fake_out: + with mocker.patch("sys.stdout", new=StringIO()) as fake_out: model.variables.search("Electrolyte cot") out = ( "No results for search using 'Electrolyte cot'. " "Best matches are ['Electrolyte concentration', " "'Electrode potential']\n" ) - self.assertEqual(fake_out.getvalue(), out) + assert fake_out.getvalue() == out # Test param search (default returns key, value) - with patch("sys.stdout", new=StringIO()) as fake_out: + with mocker.patch("sys.stdout", new=StringIO()) as fake_out: param.search("test") - self.assertEqual(fake_out.getvalue(), "test\t10\n") - - -if __name__ == "__main__": - print("Add -v for more debug output") - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() + assert fake_out.getvalue() == "test\t10\n" From deca7e2bc404d94a8c944507fb2adee71546b5b0 Mon Sep 17 00:00:00 2001 From: mleot <140573653+mleot@users.noreply.github.com> Date: Mon, 1 Jul 2024 12:17:17 -0700 Subject: [PATCH 06/82] Make function for handling time or reuse an existing one (#4209) * Make function for handling time or reuse an existing one Fixes #4113 * style: pre-commit fixes * fixing raises RegEx Error Experiment Test * fix pre-commit error * Update pybamm/callbacks.py Co-authored-by: Eric G. Kratz --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Co-authored-by: Valentin Sulzer Co-authored-by: Eric G. Kratz --- pybamm/callbacks.py | 7 +++---- pybamm/experiment/experiment.py | 29 ++++++++++++++++++++--------- pybamm/simulation.py | 4 ++-- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/pybamm/callbacks.py b/pybamm/callbacks.py index 56ea7aaf43..57f2426d0f 100644 --- a/pybamm/callbacks.py +++ b/pybamm/callbacks.py @@ -178,15 +178,14 @@ def on_step_end(self, logs): time_stop = logs["stopping conditions"]["time"] if time_stop is not None: time_now = logs["experiment time"] - if time_now < time_stop[0]: + if time_now < time_stop: self.logger.notice( - f"Time is now {time_now:.3f} s, " - f"will stop at {time_stop[0]:.3f} s." + f"Time is now {time_now:.3f} s, will stop at {time_stop:.3f} s." ) else: self.logger.notice( f"Stopping experiment since time ({time_now:.3f} s) " - f"has reached stopping time ({time_stop[0]:.3f} s)." + f"has reached stopping time ({time_stop:.3f} s)." ) def on_cycle_end(self, logs): diff --git a/pybamm/experiment/experiment.py b/pybamm/experiment/experiment.py index c83c2d3781..39c49780e4 100644 --- a/pybamm/experiment/experiment.py +++ b/pybamm/experiment/experiment.py @@ -169,15 +169,26 @@ def read_termination(termination): elif term.endswith("V"): end_discharge_V = term.split("V")[0] termination_dict["voltage"] = (float(end_discharge_V), "V") - elif term.endswith("s"): - end_time_s = term.split("s")[0] - termination_dict["time"] = (float(end_time_s), "s") - elif term.endswith("min"): - end_time_s = term.split("min")[0] - termination_dict["time"] = (float(end_time_s) * 60, "s") - elif term.endswith("h"): - end_time_s = term.split("h")[0] - termination_dict["time"] = (float(end_time_s) * 3600, "s") + elif any( + [ + term.endswith(key) + for key in [ + "hour", + "hours", + "h", + "hr", + "minute", + "minutes", + "m", + "min", + "second", + "seconds", + "s", + "sec", + ] + ] + ): + termination_dict["time"] = _convert_time_to_seconds(term) else: raise ValueError( "Only capacity or voltage can be provided as a termination reason, " diff --git a/pybamm/simulation.py b/pybamm/simulation.py index e61e9e2f68..8ec85d67d4 100644 --- a/pybamm/simulation.py +++ b/pybamm/simulation.py @@ -671,7 +671,7 @@ def solve( # if dt + starttime is larger than time_stop, set dt to time_stop - starttime if time_stop is not None: - dt = min(dt, time_stop[0] - start_time) + dt = min(dt, time_stop - start_time) step_str = str(step) model = self.steps_to_built_models[step.basic_repr()] @@ -779,7 +779,7 @@ def solve( if time_stop is not None: max_time = cycle_solution.t[-1] - if max_time >= time_stop[0]: + if max_time >= time_stop: break # Increment index for next iteration From 6c22e36d42204622472290390fc235c72fd775be Mon Sep 17 00:00:00 2001 From: "Caitlin D. Parke" Date: Mon, 1 Jul 2024 17:29:42 -0400 Subject: [PATCH 07/82] Added contact resistance to model. (#4192) * Added contact resistance to model. Needs to be tested. * style: pre-commit fixes * Fixed code syntax * style: pre-commit fixes * Contact resistance added to lumped model, taken out of base_thermal * style: pre-commit fixes * Minor fix * Added unit and integration tests * style: pre-commit fixes * Updated the thermal mdoels jupyter notebook with contact resistance example * style: pre-commit fixes * Updated to test total array rather than average of array --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Eric G. Kratz --- .../notebooks/models/thermal-models.ipynb | 195 +++++++++++++++++- pybamm/models/submodels/thermal/lumped.py | 26 ++- .../test_lithium_ion/test_thermal_models.py | 48 +++++ .../base_lithium_ion_tests.py | 5 +- 4 files changed, 261 insertions(+), 13 deletions(-) diff --git a/docs/source/examples/notebooks/models/thermal-models.ipynb b/docs/source/examples/notebooks/models/thermal-models.ipynb index 599a362b4b..4120ac2891 100644 --- a/docs/source/examples/notebooks/models/thermal-models.ipynb +++ b/docs/source/examples/notebooks/models/thermal-models.ipynb @@ -14,16 +14,13 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "\n", - "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m23.3.1\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m23.3.2\u001b[0m\n", - "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpip install --upgrade pip\u001b[0m\n", "Note: you may need to restart the kernel to use updated packages.\n" ] } @@ -57,19 +54,21 @@ "\n", "where $\\rho_k$ is the density, $c_{p,k}$ is the specific heat, and $L_k$ is the thickness of each component. The subscript $k \\in \\{cn, n, s, p, cp\\}$ is used to refer to the components negative current collector, negative electrode, separator, positive electrode, and positive current collector.\n", "\n", - "The heat source term accounts for Ohmic heating $Q_{Ohm,k}$ due to resistance in the solid and electrolyte, irreverisble heating due to electrochemical reactions $Q_{rxn,k}$, and reversible heating due to entropic changes in the the electrode $Q_{rev,k}$:\n", + "The heat source term accounts for Ohmic heating $Q_{Ohm,k}$ due to resistance in the solid and electrolyte, irreverisble heating due to electrochemical reactions $Q_{rxn,k}$, reversible heating due to entropic changes in the the electrode $Q_{rev,k}$, and heating due to contact resistance $Q_{cr}$:\n", "\n", "$$\n", - "Q = Q_{Ohm,k}+Q_{rxn,k}+Q_{rev,k},\n", + "Q = Q_{Ohm,k}+Q_{rxn,k}+Q_{rev,k}+Q_{cr},\n", "$$\n", "\n", "with\n", "\n", "$$ \n", - "Q_{Ohm,k} = -i_k \\nabla \\phi_k, \\quad Q_{rxn,k} = a_k j_k \\eta_k, \\quad Q_{rev,k} = a_k j_k T_k \\frac{\\partial U}{\\partial T} \\bigg|_{T=T_{\\infty}}.\n", + "Q_{Ohm,k} = -i_k \\nabla \\phi_k, \\quad Q_{rxn,k} = a_k j_k \\eta_k, \\quad Q_{rev,k} = a_k j_k T_k \\frac{\\partial U}{\\partial T} \\bigg|_{T=T_{\\infty}}, Q_{cr} = \\frac{R_{cr}}{V_{cell}}i_k^2.\n", "$$\n", "\n", - "Here $i_k$ is the current, $\\phi_k$ the potential, $a_k$ the surface area to volume ratio, $j_k$ the interfacial current density, $\\eta_k$ the overpotential, and $U$ the open-circuit potential. The averaged heat source term $\\bar{Q}$ is computed by taking the volume-average of $Q$.\n" + "Here $i_k$ is the current, $\\phi_k$ the potential, $a_k$ the surface area to volume ratio, $j_k$ the interfacial current density, $\\eta_k$ the overpotential, $U$ the open-circuit potential, $R_{cr}$ is the contact resistance, and $V_{cell}$ is the total cell volume. The averaged heat source term $\\bar{Q}$ is computed by taking the volume-average of $Q$.\n", + "\n", + "\n" ] }, { @@ -87,12 +86,14 @@ "\n", "When using the option `{\"cell geometry\": \"pouch\"}` the parameter $A$ and $V$ are computed automatically from the pouch dimensions, assuming a single-layer pouch cell, i.e. $A$ is the total surface area of a single-layer pouch cell and $V$ is the volume. The parameter $h$ is still set by the \"Total heat transfer coefficient [W.m-2.K-1]\" parameter.\n", "\n", + "When using the option `{\"contact resistance\": \"true\"}` the parameter \"Contact resistance [Ohm]\" must be specified to calculate the heating from contact resistance, which corresponds to $R_{cr}$. \"Cell volume [m3]\" is $V_{cell}$ within the governing equation. The default lumped model option is `{\"contact resistance\": \"false\"}`.\n", + "\n", "The lumped thermal option can be selected as follows\n" ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -113,7 +114,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 3, "metadata": {}, "outputs": [ { @@ -130,6 +131,49 @@ "print(\"Cell geometry:\", model.options[\"cell geometry\"])" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The contact resistance option can be turned on via the following" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "options = {\"thermal\": \"lumped\", \"contact resistance\": \"true\"}\n", + "model = pybamm.lithium_ion.DFN(options)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The default option for the lumped model does not include contact resistance." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "contact resistance: false\n" + ] + } + ], + "source": [ + "options = {\"thermal\": \"lumped\"}\n", + "model = pybamm.lithium_ion.DFN(options)\n", + "print(\"contact resistance:\", model.options[\"contact resistance\"])" + ] + }, { "attachments": {}, "cell_type": "markdown", @@ -431,6 +475,135 @@ ")" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For the lumped model, we can compare how the contact resistance affects the heating. To do so, we must set the `\"contact resistance\"` option to `\"true\"` and update the `\"Contact resistance [Ohm]\"` parameter." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "model_no_contact_resistance = pybamm.lithium_ion.SPMe(\n", + " {\"cell geometry\": \"arbitrary\", \"thermal\": \"lumped\", \"contact resistance\": \"false\"},\n", + " name=\"lumped thermal model\",\n", + ")\n", + "model_contact_resistance = pybamm.lithium_ion.SPMe(\n", + " {\"cell geometry\": \"arbitrary\", \"thermal\": \"lumped\", \"contact resistance\": \"true\"},\n", + " name=\"lumped thermal model with contact resistance\",\n", + ")\n", + "models = [model_no_contact_resistance, model_contact_resistance]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We then choose a parameter set." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "parameter_values = pybamm.ParameterValues(\"Marquis2019\")\n", + "lumped_params = parameter_values.copy()\n", + "lumped_params_contact_resistance = parameter_values.copy()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For the contact resistance model, we must specify a contact resistance greater than zero. The default is zero." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "lumped_params_contact_resistance.update(\n", + " {\n", + " \"Contact resistance [Ohm]\": 0.05,\n", + " }\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The models and parameters are then used to solve for a 1C discharge." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "params = [lumped_params, lumped_params_contact_resistance]\n", + "sols = []\n", + "for model, param in zip(models, params):\n", + " sim = pybamm.Simulation(model, parameter_values=param)\n", + " sim.solve([0, 3600])\n", + " sols.append(sim.solution)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can then compare the voltage and cell temperature and see the impact of the contact resistance." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "1254ec88a920400096b25541577ee948", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "interactive(children=(FloatSlider(value=0.0, description='t', max=1.0, step=0.01), Output()), _dom_classes=('w…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "output_variables = [\n", + " \"Voltage [V]\",\n", + " \"X-averaged cell temperature [K]\",\n", + " \"Cell temperature [K]\",\n", + "]\n", + "pybamm.dynamic_plot(sols, output_variables)" + ] + }, { "attachments": {}, "cell_type": "markdown", @@ -481,7 +654,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.6" + "version": "3.11.9" }, "toc": { "base_numbering": 1, diff --git a/pybamm/models/submodels/thermal/lumped.py b/pybamm/models/submodels/thermal/lumped.py index 4afde4fa57..76af2904bc 100644 --- a/pybamm/models/submodels/thermal/lumped.py +++ b/pybamm/models/submodels/thermal/lumped.py @@ -53,11 +53,25 @@ def get_coupled_variables(self, variables): V = variables["Cell thermal volume [m3]"] Q_cool_W = -self.param.h_total * (T_vol_av - T_amb) * self.param.A_cooling Q_cool_vol_av = Q_cool_W / V + + # Contact resistance heating Q_cr + if self.options["contact resistance"] == "true": + I = variables["Current [A]"] + Q_cr_W = self.calculate_Q_cr_W(I, self.param.R_contact) + V = self.param.V_cell + Q_cr_vol_av = self.calculate_Q_cr_vol_av(I, self.param.R_contact, V) + else: + Q_cr_W = pybamm.Scalar(0) + Q_cr_vol_av = Q_cr_W + variables.update( { # Lumped cooling "Lumped total cooling [W.m-3]": Q_cool_vol_av, "Lumped total cooling [W]": Q_cool_W, + # Contact resistance + "Lumped contact resistance heating [W.m-3]": Q_cr_vol_av, + "Lumped contact resistance heating [W]": Q_cr_W, } ) return variables @@ -66,12 +80,22 @@ def set_rhs(self, variables): T_vol_av = variables["Volume-averaged cell temperature [K]"] Q_vol_av = variables["Volume-averaged total heating [W.m-3]"] Q_cool_vol_av = variables["Lumped total cooling [W.m-3]"] + Q_cr_vol_av = variables["Lumped contact resistance heating [W.m-3]"] rho_c_p_eff_av = variables[ "Volume-averaged effective heat capacity [J.K-1.m-3]" ] - self.rhs = {T_vol_av: (Q_vol_av + Q_cool_vol_av) / rho_c_p_eff_av} + self.rhs = {T_vol_av: (Q_vol_av + Q_cr_vol_av + Q_cool_vol_av) / rho_c_p_eff_av} def set_initial_conditions(self, variables): T_vol_av = variables["Volume-averaged cell temperature [K]"] self.initial_conditions = {T_vol_av: self.param.T_init} + + def calculate_Q_cr_W(self, current, contact_resistance): + Q_cr_W = current**2 * contact_resistance + return Q_cr_W + + def calculate_Q_cr_vol_av(self, current, contact_resistance, volume): + Q_cr_W = self.calculate_Q_cr_W(current, contact_resistance) + Q_cr_vol_av = Q_cr_W / volume + return Q_cr_vol_av diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_thermal_models.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_thermal_models.py index 4d2f50e8d5..7de36da042 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_thermal_models.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_thermal_models.py @@ -103,6 +103,54 @@ def err(a, b): self.assertGreater(1e-5, err(solutions["SPMe 1+1D"], solutions["SPMe 2+1D"])) + def test_lumped_contact_resistance(self): + # Test that the heating with contact resistance is greater than without + + # load models + model_no_contact_resistance = pybamm.lithium_ion.SPMe( + { + "cell geometry": "arbitrary", + "thermal": "lumped", + "contact resistance": "false", + } + ) + model_contact_resistance = pybamm.lithium_ion.SPMe( + { + "cell geometry": "arbitrary", + "thermal": "lumped", + "contact resistance": "true", + } + ) + models = [model_no_contact_resistance, model_contact_resistance] + + # parameters + parameter_values = pybamm.ParameterValues("Marquis2019") + lumped_params = parameter_values.copy() + lumped_params_contact_resistance = parameter_values.copy() + + lumped_params_contact_resistance.update( + { + "Contact resistance [Ohm]": 0.05, + } + ) + + # solve the models + params = [lumped_params, lumped_params_contact_resistance] + sols = [] + for model, param in zip(models, params): + sim = pybamm.Simulation(model, parameter_values=param) + sim.solve([0, 3600]) + sols.append(sim.solution) + + # get the average temperature from each model + avg_cell_temp = sols[0]["X-averaged cell temperature [K]"].entries + avg_cell_temp_cr = sols[1]["X-averaged cell temperature [K]"].entries + + # check that the cell temperature of the lumped thermal model + # with contact resistance is higher than without contact resistance + # skip the first entry because they are the same due to initial conditions + np.testing.assert_array_less(avg_cell_temp[1:], avg_cell_temp_cr[1:]) + if __name__ == "__main__": print("Add -v for more debug output") diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_tests.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_tests.py index 7d190875a6..7e1f2d5cac 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_tests.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_tests.py @@ -138,7 +138,10 @@ def test_well_posed_thermal_2plus1D_hom(self): self.check_well_posedness(options) def test_well_posed_contact_resistance(self): - options = {"contact resistance": "true"} + options = { + "contact resistance": "true", + "thermal": "lumped", + } self.check_well_posedness(options) def test_well_posed_particle_uniform(self): From 6a0cd9a4f4da1fbabad256f1a90134f8b301f6d1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Jul 2024 18:09:03 -0400 Subject: [PATCH 08/82] chore: update pre-commit hooks (#4234) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.4.10 → v0.5.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.10...v0.5.0) - [github.com/adamchainz/blacken-docs: 1.16.0 → 1.18.0](https://github.com/adamchainz/blacken-docs/compare/1.16.0...1.18.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d308a5893d..ccd6274823 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.4.10" + rev: "v0.5.0" hooks: - id: ruff args: [--fix, --show-fixes] @@ -13,7 +13,7 @@ repos: types_or: [python, pyi, jupyter] - repo: https://github.com/adamchainz/blacken-docs - rev: "1.16.0" + rev: "1.18.0" hooks: - id: blacken-docs additional_dependencies: [black==23.*] From e625a09b2558f72d37934761726684be04dd13c0 Mon Sep 17 00:00:00 2001 From: Saransh Chopra Date: Wed, 3 Jul 2024 17:56:07 +0200 Subject: [PATCH 09/82] V24.5 (#4215) * Bump to v24.5rc0 * Update publish_pypi.yml * Remove T flag --------- Co-authored-by: Saransh-cpp Co-authored-by: Eric G. Kratz --- .github/workflows/publish_pypi.yml | 2 +- CHANGELOG.md | 2 ++ CITATION.cff | 2 +- pybamm/version.py | 2 +- pyproject.toml | 2 +- vcpkg.json | 2 +- 6 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publish_pypi.yml b/.github/workflows/publish_pypi.yml index 76958ddb6b..6616ae1a64 100644 --- a/.github/workflows/publish_pypi.yml +++ b/.github/workflows/publish_pypi.yml @@ -305,7 +305,7 @@ jobs: merge-multiple: true - name: Sanity check downloaded artifacts - run: ls -lTA artifacts/ + run: ls -lA artifacts/ - name: Publish to PyPI if: github.event.inputs.target == 'pypi' || github.event_name == 'release' diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b7f98f450..9addd13346 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # [Unreleased](https://github.com/pybamm-team/PyBaMM/) +# [v24.5rc0](https://github.com/pybamm-team/PyBaMM/tree/v24.5rc0) - 2024-05-01 + ## Features - Added functionality to pass in arbitrary functions of time as the argument for a (`pybamm.step`). ([#4222](https://github.com/pybamm-team/PyBaMM/pull/4222)) diff --git a/CITATION.cff b/CITATION.cff index 10e942667c..43fa574cdd 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -24,6 +24,6 @@ keywords: - "expression tree" - "python" - "symbolic differentiation" -version: "24.1" +version: "24.5rc0" repository-code: "https://github.com/pybamm-team/PyBaMM" title: "Python Battery Mathematical Modelling (PyBaMM)" diff --git a/pybamm/version.py b/pybamm/version.py index 61641b1fbe..bc0f2f5d12 100644 --- a/pybamm/version.py +++ b/pybamm/version.py @@ -1 +1 @@ -__version__ = "24.1" +__version__ = "24.5rc0" diff --git a/pyproject.toml b/pyproject.toml index 0f5c19987b..890f884769 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ build-backend = "setuptools.build_meta" [project] name = "pybamm" -version = "24.1" +version = "24.5rc0" license = { file = "LICENSE.txt" } description = "Python Battery Mathematical Modelling" authors = [{name = "The PyBaMM Team", email = "pybamm@pybamm.org"}] diff --git a/vcpkg.json b/vcpkg.json index 23cdcb3f58..4e2fb4fe7e 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,6 +1,6 @@ { "name": "pybamm", - "version-string": "24.1", + "version-string": "24.5rc0", "dependencies": [ "casadi", { From 3ed14052361220883d6340580eca3aed01908c4b Mon Sep 17 00:00:00 2001 From: Pradyot Ranjan <99216956+prady0t@users.noreply.github.com> Date: Thu, 4 Jul 2024 02:43:29 +0530 Subject: [PATCH 10/82] Moving a bunch of integration test files to pytest. (#4238) * Moving a bunch of integration test files to pytest Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * style: pre-commit fixes --------- Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> Co-authored-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Arjun Verma Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> --- .../test_equivalent_circuit/test_thevenin.py | 13 +------ .../test_asymptotics_convergence.py | 13 +------ .../test_compare_basic_models.py | 13 +------ .../test_lead_acid/test_compare_outputs.py | 13 +------ .../test_lead_acid/test_full.py | 15 ++------ .../test_lead_acid/test_loqs.py | 13 +------ .../test_lead_acid/test_loqs_surface_form.py | 18 ++-------- .../test_full_side_reactions.py | 13 +------ .../test_loqs_side_reactions.py | 13 +------ .../test_lithium_ion/test_basic_models.py | 25 +++++-------- .../test_compare_basic_models.py | 13 +------ .../test_lithium_ion/test_dfn_half_cell.py | 15 ++------ .../test_external_temperature.py | 13 +------ .../test_lithium_ion/test_initial_soc.py | 35 +++++++------------ .../test_lithium_ion/test_newman_tobias.py | 15 ++------ .../test_lithium_ion/test_spm.py | 15 ++------ .../test_lithium_ion/test_spm_half_cell.py | 15 ++------ .../test_lithium_ion/test_spme.py | 15 ++------ .../test_lithium_ion/test_spme_half_cell.py | 15 ++------ .../test_lithium_ion/test_yang2017.py | 13 +------ 20 files changed, 55 insertions(+), 258 deletions(-) diff --git a/tests/integration/test_models/test_full_battery_models/test_equivalent_circuit/test_thevenin.py b/tests/integration/test_models/test_full_battery_models/test_equivalent_circuit/test_thevenin.py index 54530222cf..e01cf1a7d7 100644 --- a/tests/integration/test_models/test_full_battery_models/test_equivalent_circuit/test_thevenin.py +++ b/tests/integration/test_models/test_full_battery_models/test_equivalent_circuit/test_thevenin.py @@ -1,20 +1,9 @@ import pybamm -import unittest import tests -from tests import TestCase -class TestThevenin(TestCase): +class TestThevenin: def test_basic_processing(self): model = pybamm.equivalent_circuit.Thevenin() modeltest = tests.StandardModelTest(model) modeltest.test_all() - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - unittest.main() diff --git a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_asymptotics_convergence.py b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_asymptotics_convergence.py index c78e7f9223..ec3004185a 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_asymptotics_convergence.py +++ b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_asymptotics_convergence.py @@ -1,14 +1,12 @@ # # Tests for the asymptotic convergence of the simplified models # -from tests import TestCase import pybamm import numpy as np -import unittest -class TestAsymptoticConvergence(TestCase): +class TestAsymptoticConvergence: def test_leading_order_convergence(self): """ Check that the leading-order model solution converges linearly in C_e to the @@ -72,12 +70,3 @@ def get_max_error(current): loqs_rates = np.log2(loqs_errs[:-1] / loqs_errs[1:]) np.testing.assert_array_less(0.99 * np.ones_like(loqs_rates), loqs_rates) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - unittest.main() diff --git a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_compare_basic_models.py b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_compare_basic_models.py index 76099c1f5b..3e2158ee9e 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_compare_basic_models.py +++ b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_compare_basic_models.py @@ -1,14 +1,12 @@ # # Compare basic models with full models # -from tests import TestCase import pybamm import numpy as np -import unittest -class TestCompareBasicModels(TestCase): +class TestCompareBasicModels: def test_compare_full(self): basic_full = pybamm.lead_acid.BasicFull() full = pybamm.lead_acid.Full() @@ -39,12 +37,3 @@ def test_compare_full(self): np.testing.assert_allclose( basic_sol[name].entries, sol[name].entries, rtol=1e-4, atol=1e-8 ) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - unittest.main() diff --git a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_compare_outputs.py b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_compare_outputs.py index 1bca3bf7ee..fab76b6df3 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_compare_outputs.py +++ b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_compare_outputs.py @@ -1,14 +1,12 @@ # # Tests for the asymptotic convergence of the simplified models # -from tests import TestCase import pybamm import numpy as np -import unittest from tests import StandardOutputComparison -class TestCompareOutputs(TestCase): +class TestCompareOutputs: def test_compare_averages_asymptotics(self): """ Check that the average value of certain variables is constant across submodels @@ -87,12 +85,3 @@ def test_compare_outputs_surface_form(self): # compare outputs comparison = StandardOutputComparison(solutions) comparison.test_all(skip_first_timestep=True) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - unittest.main() diff --git a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_full.py b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_full.py index 640b3da5f6..1c5e93615d 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_full.py +++ b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_full.py @@ -1,15 +1,13 @@ # # Tests for the lead-acid Full model # -from tests import TestCase import pybamm import tests -import unittest import numpy as np -class TestLeadAcidFull(TestCase): +class TestLeadAcidFull: def test_basic_processing(self): options = {"thermal": "isothermal"} model = pybamm.lead_acid.Full(options) @@ -56,7 +54,7 @@ def test_basic_processing_1plus1D(self): modeltest.test_all(skip_output_tests=True) -class TestLeadAcidFullSurfaceForm(TestCase): +class TestLeadAcidFullSurfaceForm: def test_basic_processing_differential(self): options = {"surface form": "differential"} model = pybamm.lead_acid.Full(options) @@ -88,12 +86,3 @@ def test_thermal(self): model = pybamm.lead_acid.Full(options) modeltest = tests.StandardModelTest(model) modeltest.test_all() - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - unittest.main() diff --git a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_loqs.py b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_loqs.py index f800aa0a4e..b9893224da 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_loqs.py +++ b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_loqs.py @@ -1,15 +1,13 @@ # # Tests for the lead-acid LOQS model # -from tests import TestCase import pybamm import tests -import unittest import numpy as np -class TestLOQS(TestCase): +class TestLOQS: def test_basic_processing(self): model = pybamm.lead_acid.LOQS() modeltest = tests.StandardModelTest(model) @@ -74,12 +72,3 @@ def test_basic_processing_1plus1D(self): model = pybamm.lead_acid.LOQS(options) modeltest = tests.StandardModelTest(model, var_pts=var_pts) modeltest.test_all(skip_output_tests=True) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - unittest.main() diff --git a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_loqs_surface_form.py b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_loqs_surface_form.py index b646031907..a99e42d93e 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_loqs_surface_form.py +++ b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_loqs_surface_form.py @@ -1,16 +1,13 @@ # # Tests for the lead-acid LOQS model with capacitance # -from tests import TestCase import pybamm import tests - -import unittest - +import pytest import numpy as np -class TestLeadAcidLoqsSurfaceForm(TestCase): +class TestLeadAcidLoqsSurfaceForm: def test_basic_processing(self): options = {"surface form": "algebraic"} model = pybamm.lead_acid.LOQS(options) @@ -23,7 +20,7 @@ def test_basic_processing_with_capacitance(self): modeltest = tests.StandardModelTest(model) modeltest.test_all() - @unittest.skip("model not working for 1+1D differential") + @pytest.mark.skip(reason="model not working for 1+1D differential") def test_basic_processing_1p1D_differential(self): options = { "surface form": "differential", @@ -59,12 +56,3 @@ def test_set_up(self): optimtest = tests.OptimisationsTest(model) optimtest.set_up_model(to_python=True) optimtest.set_up_model(to_python=False) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - unittest.main() diff --git a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_full_side_reactions.py b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_full_side_reactions.py index 41bd47dd4b..f873fbccd4 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_full_side_reactions.py +++ b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_full_side_reactions.py @@ -1,15 +1,13 @@ # # Tests for the lead-acid Full model # -from tests import TestCase import pybamm import tests -import unittest import numpy as np -class TestLeadAcidFullSideReactions(TestCase): +class TestLeadAcidFullSideReactions: def test_basic_processing(self): options = {"hydrolysis": "true"} model = pybamm.lead_acid.Full(options) @@ -45,12 +43,3 @@ def test_basic_processing_zero_current(self): parameter_values.update({"Current function [A]": 0}) modeltest = tests.StandardModelTest(model, parameter_values=parameter_values) modeltest.test_all(skip_output_tests=True) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - unittest.main() diff --git a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_loqs_side_reactions.py b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_loqs_side_reactions.py index 501689af69..056528e5a8 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_loqs_side_reactions.py +++ b/tests/integration/test_models/test_full_battery_models/test_lead_acid/test_side_reactions/test_loqs_side_reactions.py @@ -1,13 +1,11 @@ # # Tests for the lead-acid LOQS model # -from tests import TestCase import pybamm import tests -import unittest -class TestLeadAcidLOQSWithSideReactions(TestCase): +class TestLeadAcidLOQSWithSideReactions: def test_discharge_differential(self): options = {"surface form": "differential", "hydrolysis": "true"} model = pybamm.lead_acid.LOQS(options) @@ -46,12 +44,3 @@ def test_zero_current(self): parameter_values.update({"Current function [A]": 0}) modeltest = tests.StandardModelTest(model, parameter_values=parameter_values) modeltest.test_all(skip_output_tests=True) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - unittest.main() diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_basic_models.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_basic_models.py index 932405dc67..83f8dee318 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_basic_models.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_basic_models.py @@ -1,10 +1,8 @@ # # Test basic model classes # -from tests import TestCase import pybamm - -import unittest +import pytest class BaseBasicModelTest: @@ -21,31 +19,26 @@ def test_with_experiment(self): sim.solve(calc_esoh=False) -class TestBasicSPM(BaseBasicModelTest, TestCase): +class TestBasicSPM(BaseBasicModelTest): + @pytest.fixture(autouse=True) def setUp(self): self.model = pybamm.lithium_ion.BasicSPM() -class TestBasicDFN(BaseBasicModelTest, TestCase): +class TestBasicDFN(BaseBasicModelTest): + @pytest.fixture(autouse=True) def setUp(self): self.model = pybamm.lithium_ion.BasicDFN() -class TestBasicDFNComposite(BaseBasicModelTest, TestCase): +class TestBasicDFNComposite(BaseBasicModelTest): + @pytest.fixture(autouse=True) def setUp(self): self.model = pybamm.lithium_ion.BasicDFNComposite() -class TestBasicDFNHalfCell(BaseBasicModelTest, TestCase): +class TestBasicDFNHalfCell(BaseBasicModelTest): + @pytest.fixture(autouse=True) def setUp(self): options = {"working electrode": "positive"} self.model = pybamm.lithium_ion.BasicDFNHalfCell(options) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - unittest.main() diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_compare_basic_models.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_compare_basic_models.py index 58d6f1c202..89ccefee36 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_compare_basic_models.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_compare_basic_models.py @@ -1,14 +1,12 @@ # # Compare basic models with full models # -from tests import TestCase import pybamm import numpy as np -import unittest -class TestCompareBasicModels(TestCase): +class TestCompareBasicModels: def test_compare_dfns(self): parameter_values = pybamm.ParameterValues("Ecker2015") basic_dfn = pybamm.lithium_ion.BasicDFN() @@ -88,12 +86,3 @@ def test_compare_spms(self): np.testing.assert_allclose( basic_sol[name].entries, sol[name].entries, rtol=1e-4 ) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - unittest.main() diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_dfn_half_cell.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_dfn_half_cell.py index 8e5b0ae0a5..4c64a35dff 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_dfn_half_cell.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_dfn_half_cell.py @@ -1,21 +1,12 @@ # # Tests for the lithium-ion DFN half-cell model # -from tests import TestCase import pybamm -import unittest from tests import BaseIntegrationTestLithiumIonHalfCell +import pytest -class TestDFNHalfCell(BaseIntegrationTestLithiumIonHalfCell, TestCase): +class TestDFNHalfCell(BaseIntegrationTestLithiumIonHalfCell): + @pytest.fixture(autouse=True) def setUp(self): self.model = pybamm.lithium_ion.DFN - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - unittest.main() diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_external/test_external_temperature.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_external/test_external_temperature.py index e03a38cfe9..70ec18e9de 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_external/test_external_temperature.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_external/test_external_temperature.py @@ -1,13 +1,11 @@ # # Tests for inputting a temperature profile # -from tests import TestCase import pybamm -import unittest import numpy as np -class TestInputLumpedTemperature(TestCase): +class TestInputLumpedTemperature: def test_input_lumped_temperature(self): model = pybamm.lithium_ion.SPMe() parameter_values = model.default_parameter_values @@ -27,12 +25,3 @@ def test_input_lumped_temperature(self): inputs = {"Volume-averaged cell temperature [K]": T_av} T_av += 1 sim.step(dt, inputs=inputs) # works - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - unittest.main() diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_initial_soc.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_initial_soc.py index 42973aa9a9..8a03b4a3b8 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_initial_soc.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_initial_soc.py @@ -1,15 +1,14 @@ # # Test edge cases for initial SOC # -from tests import TestCase import pybamm -import unittest +import pytest -class TestInitialSOC(TestCase): - def test_interpolant_parameter_sets(self): - model = pybamm.lithium_ion.SPM() - params = [ +class TestInitialSOC: + @pytest.mark.parametrize( + "param", + [ "Ai2020", "Chen2020", "Ecker2015", @@ -17,19 +16,11 @@ def test_interpolant_parameter_sets(self): "Mohtat2020", "OKane2022", "ORegan2022", - ] - for param in params: - with self.subTest(param=param): - parameter_values = pybamm.ParameterValues(param) - sim = pybamm.Simulation(model=model, parameter_values=parameter_values) - sim.solve([0, 600], initial_soc=0.2) - sim.solve([0, 600], initial_soc="3.7 V") - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - unittest.main() + ], + ) + def test_interpolant_parameter_sets(self, param): + model = pybamm.lithium_ion.SPM() + parameter_values = pybamm.ParameterValues(param) + sim = pybamm.Simulation(model=model, parameter_values=parameter_values) + sim.solve([0, 600], initial_soc=0.2) + sim.solve([0, 600], initial_soc="3.7 V") diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_newman_tobias.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_newman_tobias.py index 44fbaa25d4..e9eead9773 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_newman_tobias.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_newman_tobias.py @@ -1,13 +1,13 @@ # # Tests for the lithium-ion Newman-Tobias model # -from tests import TestCase import pybamm -import unittest from tests import BaseIntegrationTestLithiumIon +import pytest -class TestNewmanTobias(BaseIntegrationTestLithiumIon, TestCase): +class TestNewmanTobias(BaseIntegrationTestLithiumIon): + @pytest.fixture(autouse=True) def setUp(self): self.model = pybamm.lithium_ion.NewmanTobias @@ -26,12 +26,3 @@ def test_composite_graphite_silicon(self): def test_composite_graphite_silicon_sei(self): pass # skip this test - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - unittest.main() diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm.py index 6caa1cc3eb..ecbccf9c3e 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm.py @@ -1,21 +1,12 @@ # # Tests for the lithium-ion SPM model # -from tests import TestCase import pybamm -import unittest from tests import BaseIntegrationTestLithiumIon +import pytest -class TestSPM(BaseIntegrationTestLithiumIon, TestCase): +class TestSPM(BaseIntegrationTestLithiumIon): + @pytest.fixture(autouse=True) def setUp(self): self.model = pybamm.lithium_ion.SPM - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - unittest.main() diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm_half_cell.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm_half_cell.py index b8b2d2f0dd..cf5c1cbb67 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm_half_cell.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm_half_cell.py @@ -1,21 +1,12 @@ # # Tests for the half-cell lithium-ion SPM model # -from tests import TestCase import pybamm -import unittest from tests import BaseIntegrationTestLithiumIonHalfCell +import pytest -class TestSPMHalfCell(BaseIntegrationTestLithiumIonHalfCell, TestCase): +class TestSPMHalfCell(BaseIntegrationTestLithiumIonHalfCell): + @pytest.fixture(autouse=True) def setUp(self): self.model = pybamm.lithium_ion.SPM - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - unittest.main() diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spme.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spme.py index 7ab4dc12ec..6b8096dc8b 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spme.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spme.py @@ -1,25 +1,16 @@ # # Tests for the lithium-ion SPMe model # -from tests import TestCase import pybamm -import unittest from tests import BaseIntegrationTestLithiumIon +import pytest -class TestSPMe(BaseIntegrationTestLithiumIon, TestCase): +class TestSPMe(BaseIntegrationTestLithiumIon): + @pytest.fixture(autouse=True) def setUp(self): self.model = pybamm.lithium_ion.SPMe def test_integrated_conductivity(self): options = {"electrolyte conductivity": "integrated"} self.run_basic_processing_test(options) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - unittest.main() diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spme_half_cell.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spme_half_cell.py index c8098ff3d4..4b4ae8e997 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spme_half_cell.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spme_half_cell.py @@ -1,21 +1,12 @@ # # Tests for the half-cell lithium-ion SPMe model # -from tests import TestCase import pybamm -import unittest from tests import BaseIntegrationTestLithiumIonHalfCell +import pytest -class TestSPMeHalfCell(BaseIntegrationTestLithiumIonHalfCell, TestCase): +class TestSPMeHalfCell(BaseIntegrationTestLithiumIonHalfCell): + @pytest.fixture(autouse=True) def setUp(self): self.model = pybamm.lithium_ion.SPMe - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - unittest.main() diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_yang2017.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_yang2017.py index ae4208117f..938580a387 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_yang2017.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_yang2017.py @@ -1,21 +1,10 @@ import pybamm -import unittest import tests -from tests import TestCase -class TestYang2017(TestCase): +class TestYang2017: def test_basic_processing(self): model = pybamm.lithium_ion.Yang2017() parameter_values = pybamm.ParameterValues("OKane2022") modeltest = tests.StandardModelTest(model, parameter_values=parameter_values) modeltest.test_all() - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - unittest.main() From f270232e7700c7e0b1789dbc67d44ea8142bce8b Mon Sep 17 00:00:00 2001 From: PatriceJada <59295119+PatriceJada@users.noreply.github.com> Date: Wed, 3 Jul 2024 17:52:31 -0500 Subject: [PATCH 11/82] Adding Scorecard.yml and Scorecard Badge, GOSST (#4144) * Create scorecard.yml for continuous fuzzing with OSS-Fuzz Integrating Google's OSS-Fuzz platform. GSoC * Update README.md Add scorecard badge to display security score * Update README.md Add scorecard badge * Update .github/workflows/scorecard.yml Run on the main branch. * Update .github/workflows/scorecard.yml Run tests at 3:35 AM every Friday --- .github/workflows/scorecard.yml | 73 +++++++++++++++++++++++++++++++++ README.md | 1 + 2 files changed, 74 insertions(+) create mode 100644 .github/workflows/scorecard.yml diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 0000000000..47b7fb6e15 --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,73 @@ +# This workflow uses actions that are not certified by GitHub. They are provided +# by a third-party and are governed by separate terms of service, privacy +# policy, and support documentation. + +name: Scorecard supply-chain security +on: + # For Branch-Protection check. Only the default branch is supported. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection + branch_protection_rule: + # To guarantee Maintained check is occasionally updated. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained + schedule: + - cron: '25 3 * * 5' + push: + branches: [ "develop", "main" ] + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + # Needed to publish results and get a badge (see publish_results below). + id-token: write + # Uncomment the permissions below if installing in a private repository. + # contents: read + # actions: read + + steps: + - name: "Checkout code" + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 + with: + results_file: results.sarif + results_format: sarif + # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: + # - you want to enable the Branch-Protection check on a *public* repository, or + # - you are installing Scorecard on a *private* repository + # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional. + # repo_token: ${{ secrets.SCORECARD_TOKEN }} + + # Public repositories: + # - Publish results to OpenSSF REST API for easy access by consumers + # - Allows the repository to include the Scorecard badge. + # - See https://github.com/ossf/scorecard-action#publishing-results. + # For private repositories: + # - `publish_results` will always be set to `false`, regardless + # of the value entered here. + publish_results: true + + # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF + # format to the repository Actions tab. + - name: "Upload artifact" + uses: actions/upload-artifact@97a0fba1372883ab732affbe8f94b823f91727db # v3.pre.node20 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard (optional). + # Commenting out will disable upload of results to your repo's Code Scanning dashboard + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9 + with: + sarif_file: results.sarif diff --git a/README.md b/README.md index e09e21e26c..85fc55c2c3 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ [![DOI](https://zenodo.org/badge/DOI/10.5334/jors.309.svg)](https://doi.org/10.5334/jors.309) [![release](https://img.shields.io/github/v/release/pybamm-team/PyBaMM?color=yellow)](https://github.com/pybamm-team/PyBaMM/releases) [![code style](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) +[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/pybamm-team/PyBaMM/badge)](https://scorecard.dev/viewer/?uri=github.com/pybamm-team/PyBaMM) [![All Contributors](https://img.shields.io/badge/all_contributors-88-orange.svg)](#-contributors) From 359dec633f893e4f583120c62181042f8c85be87 Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Fri, 5 Jul 2024 11:21:54 -0400 Subject: [PATCH 12/82] move batch study setup into test so it isn't run first by other tests --- tests/unit/test_batch_study.py | 61 +++++++++++++++++++--------------- 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/tests/unit/test_batch_study.py b/tests/unit/test_batch_study.py index ab7370a193..2713a3f35b 100644 --- a/tests/unit/test_batch_study.py +++ b/tests/unit/test_batch_study.py @@ -8,34 +8,38 @@ import unittest from tempfile import TemporaryDirectory -spm = pybamm.lithium_ion.SPM() -spm_uniform = pybamm.lithium_ion.SPM({"particle": "uniform profile"}) -casadi_safe = pybamm.CasadiSolver(mode="safe") -casadi_fast = pybamm.CasadiSolver(mode="fast") -exp1 = pybamm.Experiment([("Discharge at C/5 for 10 minutes", "Rest for 1 hour")]) -exp2 = pybamm.Experiment([("Discharge at C/20 for 10 minutes", "Rest for 1 hour")]) - -bs_false_only_models = pybamm.BatchStudy( - models={"SPM": spm, "SPM uniform": spm_uniform} -) -bs_true_only_models = pybamm.BatchStudy( - models={"SPM": spm, "SPM uniform": spm_uniform}, permutations=True -) -bs_false = pybamm.BatchStudy( - models={"SPM": spm, "SPM uniform": spm_uniform}, - solvers={"casadi safe": casadi_safe, "casadi fast": casadi_fast}, - experiments={"exp1": exp1, "exp2": exp2}, -) -bs_true = pybamm.BatchStudy( - models={"SPM": spm, "SPM uniform": spm_uniform}, - solvers={"casadi safe": casadi_safe, "casadi fast": casadi_fast}, - experiments={"exp2": exp2}, - permutations=True, -) - class TestBatchStudy(TestCase): def test_solve(self): + spm = pybamm.lithium_ion.SPM() + spm_uniform = pybamm.lithium_ion.SPM({"particle": "uniform profile"}) + casadi_safe = pybamm.CasadiSolver(mode="safe") + casadi_fast = pybamm.CasadiSolver(mode="fast") + exp1 = pybamm.Experiment( + [("Discharge at C/5 for 10 minutes", "Rest for 1 hour")] + ) + exp2 = pybamm.Experiment( + [("Discharge at C/20 for 10 minutes", "Rest for 1 hour")] + ) + + bs_false_only_models = pybamm.BatchStudy( + models={"SPM": spm, "SPM uniform": spm_uniform} + ) + bs_true_only_models = pybamm.BatchStudy( + models={"SPM": spm, "SPM uniform": spm_uniform}, permutations=True + ) + bs_false = pybamm.BatchStudy( + models={"SPM": spm, "SPM uniform": spm_uniform}, + solvers={"casadi safe": casadi_safe, "casadi fast": casadi_fast}, + experiments={"exp1": exp1, "exp2": exp2}, + ) + bs_true = pybamm.BatchStudy( + models={"SPM": spm, "SPM uniform": spm_uniform}, + solvers={"casadi safe": casadi_safe, "casadi fast": casadi_fast}, + experiments={"exp2": exp2}, + permutations=True, + ) + # Tests for exceptions for name in pybamm.BatchStudy.INPUT_LIST: with self.assertRaises(ValueError): @@ -96,7 +100,12 @@ def test_create_gif(self): ValueError, "The simulations have not been solved yet." ): pybamm.BatchStudy( - models={"SPM": spm, "SPM uniform": spm_uniform} + models={ + "SPM": pybamm.lithium_ion.SPM(), + "SPM uniform": pybamm.lithium_ion.SPM( + {"particle": "uniform profile"} + ), + } ).create_gif() bs.solve([0, 10]) From 65f2c825715827321556cf5cebe60773bb30bd09 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 8 Jul 2024 16:33:41 -0400 Subject: [PATCH 13/82] chore: update pre-commit hooks (#4251) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.5.0 → v0.5.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.0...v0.5.1) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ccd6274823..8effca2b07 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] From 0cc59d9297c7d0ba850d2bef5e0141ee5279917b Mon Sep 17 00:00:00 2001 From: "Eric G. Kratz" Date: Tue, 9 Jul 2024 12:04:08 -0400 Subject: [PATCH 14/82] Fixes for sympy (#4253) --- tests/unit/test_expression_tree/test_functions.py | 14 ++++++++------ .../test_expression_tree/test_unary_operators.py | 7 ++++--- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/tests/unit/test_expression_tree/test_functions.py b/tests/unit/test_expression_tree/test_functions.py index 16626f7512..c4b5fb4368 100644 --- a/tests/unit/test_expression_tree/test_functions.py +++ b/tests/unit/test_expression_tree/test_functions.py @@ -76,22 +76,24 @@ def test_to_equation(self): self.assertEqual(func.to_equation(), sympy.Symbol("test")) # Test Arcsinh - self.assertEqual(pybamm.Arcsinh(a).to_equation(), sympy.asinh(a)) + self.assertEqual(pybamm.Arcsinh(a).to_equation(), sympy.asinh("a")) # Test Arctan - self.assertEqual(pybamm.Arctan(a).to_equation(), sympy.atan(a)) + self.assertEqual(pybamm.Arctan(a).to_equation(), sympy.atan("a")) # Test Exp - self.assertEqual(pybamm.Exp(a).to_equation(), sympy.exp(a)) + self.assertEqual(pybamm.Exp(a).to_equation(), sympy.exp("a")) # Test log - self.assertEqual(pybamm.Log(54.0).to_equation(), sympy.log(54.0)) + value = 54.0 + self.assertEqual(pybamm.Log(value).to_equation(), sympy.log(value)) # Test sinh - self.assertEqual(pybamm.Sinh(a).to_equation(), sympy.sinh(a)) + self.assertEqual(pybamm.Sinh(a).to_equation(), sympy.sinh("a")) # Test Function - self.assertEqual(pybamm.Function(np.log, 10).to_equation(), 10.0) + value = 10 + self.assertEqual(pybamm.Function(np.log, value).to_equation(), value) def test_to_from_json_error(self): a = pybamm.Symbol("a") diff --git a/tests/unit/test_expression_tree/test_unary_operators.py b/tests/unit/test_expression_tree/test_unary_operators.py index 06f2e1fa5e..2f476e3d09 100644 --- a/tests/unit/test_expression_tree/test_unary_operators.py +++ b/tests/unit/test_expression_tree/test_unary_operators.py @@ -699,10 +699,11 @@ def test_to_equation(self): self.assertEqual(pybamm.Floor(-2.5).to_equation(), sympy.Symbol("test")) # Test Negate - self.assertEqual(pybamm.Negate(4).to_equation(), -4.0) + value = 4 + self.assertEqual(pybamm.Negate(value).to_equation(), -value) # Test AbsoluteValue - self.assertEqual(pybamm.AbsoluteValue(-4).to_equation(), 4.0) + self.assertEqual(pybamm.AbsoluteValue(-value).to_equation(), value) # Test Gradient self.assertEqual(pybamm.Gradient(a).to_equation(), sympy_Gradient("a")) @@ -710,7 +711,7 @@ def test_to_equation(self): # Test Divergence self.assertEqual( pybamm.Divergence(pybamm.Gradient(a)).to_equation(), - sympy_Divergence(sympy_Gradient(a)), + sympy_Divergence(sympy_Gradient("a")), ) # Test BoundaryValue From b4eaa3f32679fb42d34aa3286a8a4dfa4b117a1e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Jul 2024 13:06:11 -0400 Subject: [PATCH 15/82] Bump the actions group with 3 updates (#4250) Bumps the actions group with 3 updates: [actions/upload-artifact](https://github.com/actions/upload-artifact), [ossf/scorecard-action](https://github.com/ossf/scorecard-action) and [github/codeql-action](https://github.com/github/codeql-action). Updates `actions/upload-artifact` from 3.pre.node20 to 4.3.4 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v3-node20...v4.3.4) Updates `ossf/scorecard-action` from 2.3.1 to 2.3.3 - [Release notes](https://github.com/ossf/scorecard-action/releases) - [Changelog](https://github.com/ossf/scorecard-action/blob/main/RELEASE.md) - [Commits](https://github.com/ossf/scorecard-action/compare/0864cf19026789058feabb7e87baa5f140aac736...dc50aa9510b46c811795eb24b2f1ba02a914e534) Updates `github/codeql-action` from 3.24.9 to 3.25.11 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/1b1aada464948af03b950897e5eb522f92603cc2...b611370bb5703a7efb587f9d136a52ea24c5c38c) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production dependency-group: actions - dependency-name: ossf/scorecard-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/periodic_benchmarks.yml | 2 +- .github/workflows/publish_pypi.yml | 8 ++++---- .github/workflows/run_benchmarks_over_history.yml | 2 +- .github/workflows/scorecard.yml | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/periodic_benchmarks.yml b/.github/workflows/periodic_benchmarks.yml index 33d7bc0bbe..fe40bb0248 100644 --- a/.github/workflows/periodic_benchmarks.yml +++ b/.github/workflows/periodic_benchmarks.yml @@ -48,7 +48,7 @@ jobs: LD_LIBRARY_PATH: $HOME/.local/lib - name: Upload results as artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v4.3.4 with: name: asv_periodic_results path: results diff --git a/.github/workflows/publish_pypi.yml b/.github/workflows/publish_pypi.yml index 6616ae1a64..7a1566a964 100644 --- a/.github/workflows/publish_pypi.yml +++ b/.github/workflows/publish_pypi.yml @@ -88,7 +88,7 @@ jobs: CIBW_TEST_COMMAND: python -c "import pybamm; print(pybamm.IDAKLUSolver())" - name: Upload Windows wheels - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v4.3.4 with: name: wheels_windows path: ./wheelhouse/*.whl @@ -123,7 +123,7 @@ jobs: python -c "import pybamm; print(pybamm.IDAKLUSolver())" - name: Upload wheels for Linux - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v4.3.4 with: name: wheels_manylinux path: ./wheelhouse/*.whl @@ -254,7 +254,7 @@ jobs: python -c "import pybamm; print(pybamm.IDAKLUSolver())" - name: Upload wheels for macOS (amd64, arm64) - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v4.3.4 with: name: wheels_${{ matrix.os }} path: ./wheelhouse/*.whl @@ -274,7 +274,7 @@ jobs: run: pipx run build --sdist - name: Upload SDist - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v4.3.4 with: name: sdist path: ./dist/*.tar.gz diff --git a/.github/workflows/run_benchmarks_over_history.yml b/.github/workflows/run_benchmarks_over_history.yml index a281380f7f..9af7b5a755 100644 --- a/.github/workflows/run_benchmarks_over_history.yml +++ b/.github/workflows/run_benchmarks_over_history.yml @@ -46,7 +46,7 @@ jobs: ${{ github.event.inputs.commit_start }}..${{ github.event.inputs.commit_end }} - name: Upload results as artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v4.3.4 with: name: asv_over_history_results path: results diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 47b7fb6e15..0c81f71bde 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -37,7 +37,7 @@ jobs: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 + uses: ossf/scorecard-action@dc50aa9510b46c811795eb24b2f1ba02a914e534 # v2.3.3 with: results_file: results.sarif results_format: sarif @@ -59,7 +59,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@97a0fba1372883ab732affbe8f94b823f91727db # v3.pre.node20 + uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 with: name: SARIF file path: results.sarif @@ -68,6 +68,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9 + uses: github/codeql-action/upload-sarif@b611370bb5703a7efb587f9d136a52ea24c5c38c # v3.25.11 with: sarif_file: results.sarif From 5e04abdd13fdebb1d8d01f7ba53c4bd02f8a7dfd Mon Sep 17 00:00:00 2001 From: "Eric G. Kratz" Date: Wed, 10 Jul 2024 10:14:20 -0400 Subject: [PATCH 16/82] Fix test pypi worflow (#4217) * Update test pypi * Style fix --- .github/workflows/publish_pypi.yml | 2 -- scripts/install_KLU_Sundials.py | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish_pypi.yml b/.github/workflows/publish_pypi.yml index 7a1566a964..402bf9e859 100644 --- a/.github/workflows/publish_pypi.yml +++ b/.github/workflows/publish_pypi.yml @@ -317,8 +317,6 @@ jobs: if: github.event.inputs.target == 'testpypi' uses: pypa/gh-action-pypi-publish@release/v1 with: - user: __token__ - password: ${{ secrets.TESTPYPI_TOKEN }} packages-dir: files/ repository-url: https://test.pypi.org/legacy/ diff --git a/scripts/install_KLU_Sundials.py b/scripts/install_KLU_Sundials.py index 4bb1cf2458..7a5466f467 100755 --- a/scripts/install_KLU_Sundials.py +++ b/scripts/install_KLU_Sundials.py @@ -112,7 +112,8 @@ def install_sundials(download_dir, install_dir): OpenMP_omp_LIBRARY = "/usr/local/opt/libomp/lib/libomp.dylib" else: raise NotImplementedError( - f"Unsupported processor architecture: {platform.processor()}. Only 'arm' and 'i386' architectures are supported." + f"Unsupported processor architecture: {platform.processor()}. " + "Only 'arm' and 'i386' architectures are supported." ) # Don't pass the following args to CMake when building wheels. We set a custom From ab7348fe05a42510e3e0fe3be0a606eae7f5364e Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Thu, 11 Jul 2024 09:50:29 -0400 Subject: [PATCH 17/82] Issue 4224 duration bug (#4239) * make longer default duration and calculate it for C-rate * add tests * typo * #4224 add warning for time termination and add abs * fix tests * #4224 keep non-C-rate default at 24h for performance reasons * trying to fix experiment * fix example * #4224 eric comments * fix bug --- .../tutorial-5-run-experiments.ipynb | 118 +++++++++++++----- pybamm/callbacks.py | 40 ++++-- pybamm/experiment/step/base_step.py | 23 +++- pybamm/experiment/step/steps.py | 5 + pybamm/simulation.py | 29 +++-- tests/unit/test_callbacks.py | 7 +- .../test_experiments/test_experiment_steps.py | 11 +- .../test_simulation_with_experiment.py | 19 +++ 8 files changed, 193 insertions(+), 59 deletions(-) diff --git a/docs/source/examples/notebooks/getting_started/tutorial-5-run-experiments.ipynb b/docs/source/examples/notebooks/getting_started/tutorial-5-run-experiments.ipynb index eb82f59719..85be34e421 100644 --- a/docs/source/examples/notebooks/getting_started/tutorial-5-run-experiments.ipynb +++ b/docs/source/examples/notebooks/getting_started/tutorial-5-run-experiments.ipynb @@ -25,18 +25,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "\n", - "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m23.3.1\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m24.0\u001b[0m\n", - "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpip install --upgrade pip\u001b[0m\n", "Note: you may need to restart the kernel to use updated packages.\n" ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "An NVIDIA GPU may be present on this machine, but a CUDA-enabled jaxlib is not installed. Falling back to cpu.\n" - ] } ], "source": [ @@ -163,18 +153,19 @@ "name": "stderr", "output_type": "stream", "text": [ - "At t = 522.66 and h = 1.1556e-13, the corrector convergence failed repeatedly or with |h| = hmin.\n" + "At t = 339.952 and h = 1.4337e-18, the corrector convergence failed repeatedly or with |h| = hmin.\n", + "At t = 522.687 and h = 4.04917e-14, the corrector convergence failed repeatedly or with |h| = hmin.\n" ] }, { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "5ab1f22de6af4878b6ca43d27ffc01c5", + "model_id": "93feca98298f4111909ae487e2a1e273", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "interactive(children=(FloatSlider(value=0.0, description='t', max=40.132949019384355, step=0.40132949019384356…" + "interactive(children=(FloatSlider(value=0.0, description='t', max=40.13268704803602, step=0.4013268704803602),…" ] }, "metadata": {}, @@ -183,7 +174,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 5, @@ -211,12 +202,12 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "7cdac234d74241a2814918053454f6a6", + "model_id": "4d6e43032f4e4aa6be5843c4916b4b50", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "interactive(children=(FloatSlider(value=0.0, description='t', max=13.076977041121545, step=0.13076977041121546…" + "interactive(children=(FloatSlider(value=0.0, description='t', max=13.076887099589111, step=0.1307688709958911)…" ] }, "metadata": {}, @@ -225,7 +216,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 6, @@ -255,7 +246,7 @@ { "data": { "text/plain": [ - "_Step(C-rate, 1.0, duration=1 hour, period=1 minute, temperature=25oC, tags=['tag1'], description=Discharge at 1C for 1 hour)" + "Step(1.0, duration=1 hour, period=1 minute, temperature=25oC, tags=['tag1'], description=Discharge at 1C for 1 hour, direction=Discharge)" ] }, "execution_count": 7, @@ -293,7 +284,7 @@ { "data": { "text/plain": [ - "_Step(current, 1, duration=1 hour, termination=2.5 V)" + "Step(1, duration=1 hour, termination=2.5 V, direction=Discharge)" ] }, "execution_count": 8, @@ -321,7 +312,7 @@ { "data": { "text/plain": [ - "_Step(current, 1.0, duration=1 hour, termination=2.5V, description=Discharge at 1A for 1 hour or until 2.5V)" + "Step(1.0, duration=1 hour, termination=2.5V, description=Discharge at 1A for 1 hour or until 2.5V, direction=Discharge)" ] }, "execution_count": 9, @@ -348,10 +339,78 @@ "execution_count": 10, "metadata": {}, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2024-07-10 14:41:02.625 - [WARNING] callbacks.on_experiment_infeasible_time(240): \n", + "\n", + "\tExperiment is infeasible: default duration (1.0 seconds) was reached during 'Step([[ 0.00000000e+00 0.00000000e+00]\n", + " [ 1.69491525e-02 5.31467428e-02]\n", + " [ 3.38983051e-02 1.05691312e-01]\n", + " [ 5.08474576e-02 1.57038356e-01]\n", + " [ 6.77966102e-02 2.06606093e-01]\n", + " [ 8.47457627e-02 2.53832900e-01]\n", + " [ 1.01694915e-01 2.98183679e-01]\n", + " [ 1.18644068e-01 3.39155918e-01]\n", + " [ 1.35593220e-01 3.76285385e-01]\n", + " [ 1.52542373e-01 4.09151388e-01]\n", + " [ 1.69491525e-01 4.37381542e-01]\n", + " [ 1.86440678e-01 4.60655989e-01]\n", + " [ 2.03389831e-01 4.78711019e-01]\n", + " [ 2.20338983e-01 4.91342062e-01]\n", + " [ 2.37288136e-01 4.98406004e-01]\n", + " [ 2.54237288e-01 4.99822806e-01]\n", + " [ 2.71186441e-01 4.95576416e-01]\n", + " [ 2.88135593e-01 4.85714947e-01]\n", + " [ 3.05084746e-01 4.70350133e-01]\n", + " [ 3.22033898e-01 4.49656065e-01]\n", + " [ 3.38983051e-01 4.23867214e-01]\n", + " [ 3.55932203e-01 3.93275778e-01]\n", + " [ 3.72881356e-01 3.58228370e-01]\n", + " [ 3.89830508e-01 3.19122092e-01]\n", + " [ 4.06779661e-01 2.76400033e-01]\n", + " [ 4.23728814e-01 2.30546251e-01]\n", + " [ 4.40677966e-01 1.82080288e-01]\n", + " [ 4.57627119e-01 1.31551282e-01]\n", + " [ 4.74576271e-01 7.95317480e-02]\n", + " [ 4.91525424e-01 2.66110874e-02]\n", + " [ 5.08474576e-01 -2.66110874e-02]\n", + " [ 5.25423729e-01 -7.95317480e-02]\n", + " [ 5.42372881e-01 -1.31551282e-01]\n", + " [ 5.59322034e-01 -1.82080288e-01]\n", + " [ 5.76271186e-01 -2.30546251e-01]\n", + " [ 5.93220339e-01 -2.76400033e-01]\n", + " [ 6.10169492e-01 -3.19122092e-01]\n", + " [ 6.27118644e-01 -3.58228370e-01]\n", + " [ 6.44067797e-01 -3.93275778e-01]\n", + " [ 6.61016949e-01 -4.23867214e-01]\n", + " [ 6.77966102e-01 -4.49656065e-01]\n", + " [ 6.94915254e-01 -4.70350133e-01]\n", + " [ 7.11864407e-01 -4.85714947e-01]\n", + " [ 7.28813559e-01 -4.95576416e-01]\n", + " [ 7.45762712e-01 -4.99822806e-01]\n", + " [ 7.62711864e-01 -4.98406004e-01]\n", + " [ 7.79661017e-01 -4.91342062e-01]\n", + " [ 7.96610169e-01 -4.78711019e-01]\n", + " [ 8.13559322e-01 -4.60655989e-01]\n", + " [ 8.30508475e-01 -4.37381542e-01]\n", + " [ 8.47457627e-01 -4.09151388e-01]\n", + " [ 8.64406780e-01 -3.76285385e-01]\n", + " [ 8.81355932e-01 -3.39155918e-01]\n", + " [ 8.98305085e-01 -2.98183679e-01]\n", + " [ 9.15254237e-01 -2.53832900e-01]\n", + " [ 9.32203390e-01 -2.06606093e-01]\n", + " [ 9.49152542e-01 -1.57038356e-01]\n", + " [ 9.66101695e-01 -1.05691312e-01]\n", + " [ 9.83050847e-01 -5.31467428e-02]\n", + " [ 1.00000000e+00 -1.22464680e-16]], duration=1.0, period=0.016949152542372836, direction=Rest)'. The returned solution only contains up to step 1 of cycle 1. Please specify a duration in the step instructions.\n" + ] + }, { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "730d5e19b17e447ebde5679de68c46ef", + "model_id": "6364b4579fc447e2a607f2f8414172ba", "version_major": 2, "version_minor": 0 }, @@ -365,7 +424,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 10, @@ -419,13 +478,14 @@ "output_type": "stream", "text": [ "[1] Joel A. E. Andersson, Joris Gillis, Greg Horn, James B. Rawlings, and Moritz Diehl. CasADi – A software framework for nonlinear optimization and optimal control. Mathematical Programming Computation, 11(1):1–36, 2019. doi:10.1007/s12532-018-0139-4.\n", - "[2] Marc Doyle, Thomas F. Fuller, and John Newman. Modeling of galvanostatic charge and discharge of the lithium/polymer/insertion cell. Journal of the Electrochemical society, 140(6):1526–1533, 1993. doi:10.1149/1.2221597.\n", - "[3] Charles R. Harris, K. Jarrod Millman, Stéfan J. van der Walt, Ralf Gommers, Pauli Virtanen, David Cournapeau, Eric Wieser, Julian Taylor, Sebastian Berg, Nathaniel J. Smith, and others. Array programming with NumPy. Nature, 585(7825):357–362, 2020. doi:10.1038/s41586-020-2649-2.\n", - "[4] Scott G. Marquis, Valentin Sulzer, Robert Timms, Colin P. Please, and S. Jon Chapman. An asymptotic derivation of a single particle model with electrolyte. Journal of The Electrochemical Society, 166(15):A3693–A3706, 2019. doi:10.1149/2.0341915jes.\n", - "[5] Peyman Mohtat, Suhak Lee, Jason B Siegel, and Anna G Stefanopoulou. Towards better estimability of electrode-specific state of health: decoding the cell expansion. Journal of Power Sources, 427:101–111, 2019.\n", - "[6] Valentin Sulzer, Scott G. Marquis, Robert Timms, Martin Robinson, and S. Jon Chapman. Python Battery Mathematical Modelling (PyBaMM). Journal of Open Research Software, 9(1):14, 2021. doi:10.5334/jors.309.\n", - "[7] Pauli Virtanen, Ralf Gommers, Travis E. Oliphant, Matt Haberland, Tyler Reddy, David Cournapeau, Evgeni Burovski, Pearu Peterson, Warren Weckesser, Jonathan Bright, and others. SciPy 1.0: fundamental algorithms for scientific computing in Python. Nature Methods, 17(3):261–272, 2020. doi:10.1038/s41592-019-0686-2.\n", - "[8] Andrew Weng, Jason B Siegel, and Anna Stefanopoulou. Differential voltage analysis for battery manufacturing process control. arXiv preprint arXiv:2303.07088, 2023.\n", + "[2] Von DAG Bruggeman. Berechnung verschiedener physikalischer konstanten von heterogenen substanzen. i. dielektrizitätskonstanten und leitfähigkeiten der mischkörper aus isotropen substanzen. Annalen der physik, 416(7):636–664, 1935.\n", + "[3] Marc Doyle, Thomas F. Fuller, and John Newman. Modeling of galvanostatic charge and discharge of the lithium/polymer/insertion cell. Journal of the Electrochemical society, 140(6):1526–1533, 1993. doi:10.1149/1.2221597.\n", + "[4] Charles R. Harris, K. Jarrod Millman, Stéfan J. van der Walt, Ralf Gommers, Pauli Virtanen, David Cournapeau, Eric Wieser, Julian Taylor, Sebastian Berg, Nathaniel J. Smith, and others. Array programming with NumPy. Nature, 585(7825):357–362, 2020. doi:10.1038/s41586-020-2649-2.\n", + "[5] Scott G. Marquis, Valentin Sulzer, Robert Timms, Colin P. Please, and S. Jon Chapman. An asymptotic derivation of a single particle model with electrolyte. Journal of The Electrochemical Society, 166(15):A3693–A3706, 2019. doi:10.1149/2.0341915jes.\n", + "[6] Peyman Mohtat, Suhak Lee, Jason B Siegel, and Anna G Stefanopoulou. Towards better estimability of electrode-specific state of health: decoding the cell expansion. Journal of Power Sources, 427:101–111, 2019.\n", + "[7] Valentin Sulzer, Scott G. Marquis, Robert Timms, Martin Robinson, and S. Jon Chapman. Python Battery Mathematical Modelling (PyBaMM). Journal of Open Research Software, 9(1):14, 2021. doi:10.5334/jors.309.\n", + "[8] Pauli Virtanen, Ralf Gommers, Travis E. Oliphant, Matt Haberland, Tyler Reddy, David Cournapeau, Evgeni Burovski, Pearu Peterson, Warren Weckesser, Jonathan Bright, and others. SciPy 1.0: fundamental algorithms for scientific computing in Python. Nature Methods, 17(3):261–272, 2020. doi:10.1038/s41592-019-0686-2.\n", + "[9] Andrew Weng, Jason B Siegel, and Anna Stefanopoulou. Differential voltage analysis for battery manufacturing process control. arXiv preprint arXiv:2303.07088, 2023.\n", "\n" ] } diff --git a/pybamm/callbacks.py b/pybamm/callbacks.py index 57f2426d0f..4e8c67c8be 100644 --- a/pybamm/callbacks.py +++ b/pybamm/callbacks.py @@ -36,37 +36,37 @@ def on_experiment_start(self, logs): """ Called at the start of an experiment simulation. """ - pass + pass # pragma: no cover def on_cycle_start(self, logs): """ Called at the start of each cycle in an experiment simulation. """ - pass + pass # pragma: no cover def on_step_start(self, logs): """ Called at the start of each step in an experiment simulation. """ - pass + pass # pragma: no cover def on_step_end(self, logs): """ Called at the end of each step in an experiment simulation. """ - pass + pass # pragma: no cover def on_cycle_end(self, logs): """ Called at the end of each cycle in an experiment simulation. """ - pass + pass # pragma: no cover def on_experiment_end(self, logs): """ Called at the end of an experiment simulation. """ - pass + pass # pragma: no cover def on_experiment_error(self, logs): """ @@ -75,13 +75,19 @@ def on_experiment_error(self, logs): For example, this could be used to send an error alert with a bug report when running batch simulations in the cloud. """ - pass + pass # pragma: no cover - def on_experiment_infeasible(self, logs): + def on_experiment_infeasible_time(self, logs): """ - Called when an experiment simulation is infeasible. + Called when an experiment simulation is infeasible due to reaching maximum time. """ - pass + pass # pragma: no cover + + def on_experiment_infeasible_event(self, logs): + """ + Called when an experiment simulation is infeasible due to an event. + """ + pass # pragma: no cover ######################################################################################## @@ -226,7 +232,19 @@ def on_experiment_error(self, logs): error = logs["error"] pybamm.logger.error(f"Simulation error: {error}") - def on_experiment_infeasible(self, logs): + def on_experiment_infeasible_time(self, logs): + duration = logs["step duration"] + cycle_num = logs["cycle number"][0] + step_num = logs["step number"][0] + operating_conditions = logs["step operating conditions"] + self.logger.warning( + f"\n\n\tExperiment is infeasible: default duration ({duration} seconds) " + f"was reached during '{operating_conditions}'. The returned solution only " + f"contains up to step {step_num} of cycle {cycle_num}. " + "Please specify a duration in the step instructions." + ) + + def on_experiment_infeasible_event(self, logs): termination = logs["termination"] cycle_num = logs["cycle number"][0] step_num = logs["step number"][0] diff --git a/pybamm/experiment/step/base_step.py b/pybamm/experiment/step/base_step.py index 16224a6afa..6b77bed2cf 100644 --- a/pybamm/experiment/step/base_step.py +++ b/pybamm/experiment/step/base_step.py @@ -71,6 +71,8 @@ def __init__( description=None, direction=None, ): + self.input_duration = duration + self.input_value = value # Check if drive cycle is_drive_cycle = isinstance(value, np.ndarray) is_python_function = callable(value) @@ -100,8 +102,11 @@ def __init__( f"Input function must return a real number output at t = {t0}" ) + # Record whether the step uses the default duration + # This will be used by the experiment to check whether the step is feasible + self.uses_default_duration = duration is None # Set duration - if duration is None: + if self.uses_default_duration: duration = self.default_duration(value) self.duration = _convert_time_to_seconds(duration) @@ -195,8 +200,8 @@ def copy(self): A copy of the step. """ return self.__class__( - self.value, - duration=self.duration, + self.input_value, + duration=self.input_duration, termination=self.termination, period=self.period, temperature=self.temperature, @@ -259,7 +264,7 @@ def default_duration(self, value): t = value[:, 0] return t[-1] else: - return 24 * 3600 # 24 hours in seconds + return 24 * 3600 # one day in seconds def process_model(self, model, parameter_values): new_model = model.new_copy() @@ -411,10 +416,16 @@ def set_up(self, new_model, new_parameter_values): def _convert_time_to_seconds(time_and_units): """Convert a time in seconds, minutes or hours to a time in seconds""" - # If the time is a number, assume it is in seconds - if isinstance(time_and_units, numbers.Number) or time_and_units is None: + if time_and_units is None: return time_and_units + # If the time is a number, assume it is in seconds + if isinstance(time_and_units, numbers.Number): + if time_and_units <= 0: + raise ValueError("time must be positive") + else: + return time_and_units + # Split number and units units = time_and_units.lstrip("0123456789.- ") time = time_and_units[: -len(units)] diff --git a/pybamm/experiment/step/steps.py b/pybamm/experiment/step/steps.py index e3322104bf..e66178dc81 100644 --- a/pybamm/experiment/step/steps.py +++ b/pybamm/experiment/step/steps.py @@ -156,6 +156,11 @@ def __init__(self, value, **kwargs): def current_value(self, variables): return self.value * pybamm.Parameter("Nominal cell capacity [A.h]") + def default_duration(self, value): + # "value" is C-rate, so duration is "1 / value" hours in seconds + # with a 2x safety factor + return 1 / abs(value) * 3600 * 2 + def c_rate(value, **kwargs): """ diff --git a/pybamm/simulation.py b/pybamm/simulation.py index 8ec85d67d4..a55310870e 100644 --- a/pybamm/simulation.py +++ b/pybamm/simulation.py @@ -679,6 +679,7 @@ def solve( logs["step number"] = (step_num, cycle_length) logs["step operating conditions"] = step_str + logs["step duration"] = step.duration callbacks.on_step_start(logs) inputs = { @@ -767,23 +768,33 @@ def solve( callbacks.on_step_end(logs) logs["termination"] = step_solution.termination - # Only allow events specified by experiment - if not ( + + # Check for some cases that would make the experiment end early + if step_termination == "final time" and step.uses_default_duration: + # reached the default duration of a step (typically we should + # reach an event before the default duration) + callbacks.on_experiment_infeasible_time(logs) + feasible = False + break + + elif not ( isinstance(step_solution, pybamm.EmptySolution) or step_termination == "final time" or "[experiment]" in step_termination ): - callbacks.on_experiment_infeasible(logs) + # Step has reached an event that is not specified in the + # experiment + callbacks.on_experiment_infeasible_event(logs) feasible = False break - if time_stop is not None: - max_time = cycle_solution.t[-1] - if max_time >= time_stop: - break + elif time_stop is not None and logs["experiment time"] >= time_stop: + # reached the time limit of the experiment + break - # Increment index for next iteration - idx += 1 + else: + # Increment index for next iteration, then continue + idx += 1 if save_this_cycle or feasible is False: self._solution = self._solution + cycle_solution diff --git a/tests/unit/test_callbacks.py b/tests/unit/test_callbacks.py index b36fef9ec6..649c7d9ec8 100644 --- a/tests/unit/test_callbacks.py +++ b/tests/unit/test_callbacks.py @@ -81,6 +81,7 @@ def test_logging_callback(self): "cycle number": (5, 12), "step number": (1, 4), "elapsed time": 0.45, + "step duration": 1, "step operating conditions": "Charge", "termination": "event", } @@ -96,10 +97,14 @@ def test_logging_callback(self): with open("test_callback.log") as f: self.assertIn("Cycle 5/12, step 1/4", f.read()) - callback.on_experiment_infeasible(logs) + callback.on_experiment_infeasible_event(logs) with open("test_callback.log") as f: self.assertIn("Experiment is infeasible: 'event'", f.read()) + callback.on_experiment_infeasible_time(logs) + with open("test_callback.log") as f: + self.assertIn("Experiment is infeasible: default duration", f.read()) + callback.on_experiment_end(logs) with open("test_callback.log") as f: self.assertIn("took 0.45", f.read()) diff --git a/tests/unit/test_experiments/test_experiment_steps.py b/tests/unit/test_experiments/test_experiment_steps.py index 9d1abcc133..4bb686986f 100644 --- a/tests/unit/test_experiments/test_experiment_steps.py +++ b/tests/unit/test_experiments/test_experiment_steps.py @@ -43,15 +43,20 @@ def test_step(self): with self.assertRaisesRegex(ValueError, "temperature units"): step = pybamm.step.current(1, temperature="298T") + with self.assertRaisesRegex(ValueError, "time must be positive"): + pybamm.step.current(1, duration=0) + def test_specific_steps(self): current = pybamm.step.current(1) self.assertIsInstance(current, pybamm.step.Current) self.assertEqual(current.value, 1) self.assertEqual(str(current), repr(current)) + self.assertEqual(current.duration, 24 * 3600) c_rate = pybamm.step.c_rate(1) self.assertIsInstance(c_rate, pybamm.step.CRate) self.assertEqual(c_rate.value, 1) + self.assertEqual(c_rate.duration, 3600 * 2) voltage = pybamm.step.voltage(1) self.assertIsInstance(voltage, pybamm.step.Voltage) @@ -145,19 +150,19 @@ def test_step_string(self): { "type": "CRate", "value": -1, - "duration": 86400, + "duration": 7200, "termination": [pybamm.step.VoltageTermination(4.1)], }, { "value": 4.1, "type": "Voltage", - "duration": 86400, + "duration": 3600 * 24, "termination": [pybamm.step.CurrentTermination(0.05)], }, { "value": 3, "type": "Voltage", - "duration": 86400, + "duration": 3600 * 24, "termination": [pybamm.step.CrateTermination(0.02)], }, { diff --git a/tests/unit/test_experiments/test_simulation_with_experiment.py b/tests/unit/test_experiments/test_simulation_with_experiment.py index bfc5ad6dee..4b3fa3366a 100644 --- a/tests/unit/test_experiments/test_simulation_with_experiment.py +++ b/tests/unit/test_experiments/test_simulation_with_experiment.py @@ -10,6 +10,12 @@ from datetime import datetime +class ShortDurationCRate(pybamm.step.CRate): + def default_duration(self, value): + # Set a short default duration for testing early stopping due to infeasible time + return 1 + + class TestSimulationExperiment(TestCase): def test_set_up(self): experiment = pybamm.Experiment( @@ -272,6 +278,19 @@ def test_run_experiment_breaks_early_error(self): # Different callback - this is for coverage on the `Callback` class sol = sim.solve(callbacks=pybamm.callbacks.Callback()) + def test_run_experiment_infeasible_time(self): + experiment = pybamm.Experiment( + [ShortDurationCRate(1, termination="2.5V"), "Rest for 1 hour"] + ) + model = pybamm.lithium_ion.SPM() + parameter_values = pybamm.ParameterValues("Chen2020") + sim = pybamm.Simulation( + model, parameter_values=parameter_values, experiment=experiment + ) + sol = sim.solve() + self.assertEqual(len(sol.cycles), 1) + self.assertEqual(len(sol.cycles[0].steps), 1) + def test_run_experiment_termination_capacity(self): # with percent experiment = pybamm.Experiment( From 74432435144f5b6ceb4bb1ae4823d7fbe8c01afb Mon Sep 17 00:00:00 2001 From: jsbrittain <98161205+jsbrittain@users.noreply.github.com> Date: Mon, 15 Jul 2024 12:44:39 +0100 Subject: [PATCH 18/82] Add support for MLIR-based expression evaluation (#4199) * Fix CodeCov GHA workflow failure (#3845) This amends the tag for the CodeCov GitHub Action from `4.1.0` to `v4.1.0`. This was a Dependabot error * Begin refactor IDAKLU solver to support generalisable expressions [i.e. support more than just casadi functions] * Continue refactor; compiles but with dlopen error on load; committing to test on another machine * Refactor: Introduce Expression and ExpressionSet classes * Restructure Expressions with generics folder and implementation-specific subfolders * Separate Expression classes * Template Expression class * WIP: Subclass expressions (remove templates in order to generalise execution framework) * Subclass expressions and remove unnecessary template arguments * Isolate casadi functionality to Expression subclasses * Add IREE expressions class * Remove breakpoints * Map input arguments to (reduced) call signature in MLIR * Add support for inputs * Support sensitivities * Pre-commit * Support output_variables * Fix designator order error on linux; remove reference to ninja * OS-invariant library loading * Convert some pointer arrays to vectors; fixes sporadic crashes * Fix bad memory allocations * Tidy-up code * Tidy up code * Resolve jax/iree version numbers * Conditional compilation of iree code * Fix compiler variables * Update noxfile sources list * Pre-commit * Make IREE tests conditional on IREE install * Enable IREE in CI unit/integration testing * Make demotion optional in idaklu_solver.py (still unsupported by IDAKLU) * style: pre-commit fixes * Fix expression tree test given change to bracketed expressions * Codacy corrections and suppressions * Enable IREE in unit testing * style: pre-commit fixes * Enable IREE in coverage testing * Restrict IREE install to supported MacOS/Python versions * Restrict IREE supported macOS installs * Additional tests for IREE (demotion, output_variables, sensititivies) * style: pre-commit fixes * Additional tests (improve test coverage) * Fix IREE unit test * Fix sensitivities-sparsity bug; improve tests * Fix tests * Improve coverage * Improve coverage (indirect) * Suppress IREE warning on reload * style: pre-commit fixes * Fix codacy warning * Fix noxfile docstring * Update noxfile.py Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> * Update pyproject.toml Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> * style: pre-commit fixes * Update pybamm/solvers/c_solvers/idaklu.cpp Co-authored-by: Martin Robinson * Update pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunctions.cpp Co-authored-by: Martin Robinson * Try removing no-cover from expression_tree/function.py * Remove C from class name * Clarify Base Expression docstrings * Remove build-time iree.compiler search in CMakeLists.txt * Add install note to iree-compiler in pyproject.toml * Add IREE dependencies to docs * style: pre-commit fixes * Refactor MLIR parsing into ModuleParser class * style: pre-commit fixes * Add codacy hints * Codacy fix * Remove no-cover statements * style: pre-commit fixes * Coverage fix --------- Co-authored-by: Ferran Brosa Planella Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Co-authored-by: Martin Robinson --- .github/workflows/run_periodic_tests.yml | 2 + .github/workflows/test_on_push.yml | 2 + CMakeLists.txt | 99 ++++- .../source/examples/notebooks/models/spm1.png | Bin 24160 -> 24150 bytes .../user_guide/installation/gnu-linux-mac.rst | 13 + docs/source/user_guide/installation/index.rst | 14 +- noxfile.py | 100 ++++- pybamm/__init__.py | 5 +- .../operations/evaluate_python.py | 55 ++- pybamm/solvers/base_solver.py | 50 ++- pybamm/solvers/c_solvers/idaklu.cpp | 76 +++- .../solvers/c_solvers/idaklu/CasadiSolver.cpp | 1 - .../idaklu/CasadiSolverOpenMP_solvers.cpp | 1 - .../idaklu/CasadiSolverOpenMP_solvers.hpp | 125 ------ .../idaklu/Expressions/Base/Expression.hpp | 69 +++ .../idaklu/Expressions/Base/ExpressionSet.hpp | 86 ++++ .../Expressions/Base/ExpressionTypes.hpp | 6 + .../Expressions/Casadi/CasadiFunctions.cpp | 80 ++++ .../Expressions/Casadi/CasadiFunctions.hpp | 157 +++++++ .../idaklu/Expressions/Expressions.hpp | 6 + .../Expressions/IREE/IREEBaseFunction.hpp | 27 ++ .../idaklu/Expressions/IREE/IREEFunction.hpp | 59 +++ .../idaklu/Expressions/IREE/IREEFunctions.cpp | 230 ++++++++++ .../idaklu/Expressions/IREE/IREEFunctions.hpp | 146 +++++++ .../idaklu/Expressions/IREE/ModuleParser.cpp | 91 ++++ .../idaklu/Expressions/IREE/ModuleParser.hpp | 55 +++ .../idaklu/Expressions/IREE/iree_jit.cpp | 408 ++++++++++++++++++ .../idaklu/Expressions/IREE/iree_jit.hpp | 140 ++++++ .../solvers/c_solvers/idaklu/IDAKLUSolver.cpp | 1 + .../{CasadiSolver.hpp => IDAKLUSolver.hpp} | 14 +- ...olverOpenMP.hpp => IDAKLUSolverOpenMP.hpp} | 41 +- ...olverOpenMP.cpp => IDAKLUSolverOpenMP.inl} | 113 ++--- .../idaklu/IDAKLUSolverOpenMP_solvers.cpp | 1 + .../idaklu/IDAKLUSolverOpenMP_solvers.hpp | 131 ++++++ .../idaklu/{idaklu_jax.cpp => IdakluJax.cpp} | 2 +- .../idaklu/{idaklu_jax.hpp => IdakluJax.hpp} | 0 .../idaklu/{options.cpp => Options.cpp} | 2 +- .../idaklu/{options.hpp => Options.hpp} | 0 pybamm/solvers/c_solvers/idaklu/Solution.cpp | 1 + .../idaklu/{solution.hpp => Solution.hpp} | 0 .../c_solvers/idaklu/casadi_functions.cpp | 105 ----- .../c_solvers/idaklu/casadi_functions.hpp | 160 ------- .../c_solvers/idaklu/casadi_solver.hpp | 36 -- .../idaklu/casadi_sundials_functions.hpp | 27 -- pybamm/solvers/c_solvers/idaklu/common.hpp | 60 ++- .../{casadi_solver.cpp => idaklu_solver.hpp} | 61 +-- pybamm/solvers/c_solvers/idaklu/python.hpp | 2 +- pybamm/solvers/c_solvers/idaklu/solution.cpp | 0 .../c_solvers/idaklu/sundials_functions.hpp | 36 ++ ...s_functions.cpp => sundials_functions.inl} | 184 ++++---- pybamm/solvers/idaklu_solver.py | 406 ++++++++++++++--- pybamm/solvers/processed_variable_computed.py | 17 +- pyproject.toml | 11 +- setup.py | 52 ++- .../test_operations/test_evaluate_python.py | 72 +++- tests/unit/test_solvers/test_idaklu_solver.py | 344 +++++++++------ 56 files changed, 3057 insertions(+), 925 deletions(-) delete mode 100644 pybamm/solvers/c_solvers/idaklu/CasadiSolver.cpp delete mode 100644 pybamm/solvers/c_solvers/idaklu/CasadiSolverOpenMP_solvers.cpp delete mode 100644 pybamm/solvers/c_solvers/idaklu/CasadiSolverOpenMP_solvers.hpp create mode 100644 pybamm/solvers/c_solvers/idaklu/Expressions/Base/Expression.hpp create mode 100644 pybamm/solvers/c_solvers/idaklu/Expressions/Base/ExpressionSet.hpp create mode 100644 pybamm/solvers/c_solvers/idaklu/Expressions/Base/ExpressionTypes.hpp create mode 100644 pybamm/solvers/c_solvers/idaklu/Expressions/Casadi/CasadiFunctions.cpp create mode 100644 pybamm/solvers/c_solvers/idaklu/Expressions/Casadi/CasadiFunctions.hpp create mode 100644 pybamm/solvers/c_solvers/idaklu/Expressions/Expressions.hpp create mode 100644 pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEBaseFunction.hpp create mode 100644 pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunction.hpp create mode 100644 pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunctions.cpp create mode 100644 pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunctions.hpp create mode 100644 pybamm/solvers/c_solvers/idaklu/Expressions/IREE/ModuleParser.cpp create mode 100644 pybamm/solvers/c_solvers/idaklu/Expressions/IREE/ModuleParser.hpp create mode 100644 pybamm/solvers/c_solvers/idaklu/Expressions/IREE/iree_jit.cpp create mode 100644 pybamm/solvers/c_solvers/idaklu/Expressions/IREE/iree_jit.hpp create mode 100644 pybamm/solvers/c_solvers/idaklu/IDAKLUSolver.cpp rename pybamm/solvers/c_solvers/idaklu/{CasadiSolver.hpp => IDAKLUSolver.hpp} (75%) rename pybamm/solvers/c_solvers/idaklu/{CasadiSolverOpenMP.hpp => IDAKLUSolverOpenMP.hpp} (82%) rename pybamm/solvers/c_solvers/idaklu/{CasadiSolverOpenMP.cpp => IDAKLUSolverOpenMP.inl} (82%) create mode 100644 pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP_solvers.cpp create mode 100644 pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP_solvers.hpp rename pybamm/solvers/c_solvers/idaklu/{idaklu_jax.cpp => IdakluJax.cpp} (99%) rename pybamm/solvers/c_solvers/idaklu/{idaklu_jax.hpp => IdakluJax.hpp} (100%) rename pybamm/solvers/c_solvers/idaklu/{options.cpp => Options.cpp} (99%) rename pybamm/solvers/c_solvers/idaklu/{options.hpp => Options.hpp} (100%) create mode 100644 pybamm/solvers/c_solvers/idaklu/Solution.cpp rename pybamm/solvers/c_solvers/idaklu/{solution.hpp => Solution.hpp} (100%) delete mode 100644 pybamm/solvers/c_solvers/idaklu/casadi_functions.cpp delete mode 100644 pybamm/solvers/c_solvers/idaklu/casadi_functions.hpp delete mode 100644 pybamm/solvers/c_solvers/idaklu/casadi_solver.hpp delete mode 100644 pybamm/solvers/c_solvers/idaklu/casadi_sundials_functions.hpp rename pybamm/solvers/c_solvers/idaklu/{casadi_solver.cpp => idaklu_solver.hpp} (69%) delete mode 100644 pybamm/solvers/c_solvers/idaklu/solution.cpp create mode 100644 pybamm/solvers/c_solvers/idaklu/sundials_functions.hpp rename pybamm/solvers/c_solvers/idaklu/{casadi_sundials_functions.cpp => sundials_functions.inl} (67%) diff --git a/.github/workflows/run_periodic_tests.yml b/.github/workflows/run_periodic_tests.yml index 6f79df76b2..2dd9ef8a89 100644 --- a/.github/workflows/run_periodic_tests.yml +++ b/.github/workflows/run_periodic_tests.yml @@ -14,6 +14,8 @@ on: env: FORCE_COLOR: 3 + PYBAMM_IDAKLU_EXPR_CASADI: ON + PYBAMM_IDAKLU_EXPR_IREE: ON concurrency: # github.workflow: name of the workflow, so that we don't cancel other workflows diff --git a/.github/workflows/test_on_push.yml b/.github/workflows/test_on_push.yml index 97c37e8c28..adfb698a69 100644 --- a/.github/workflows/test_on_push.yml +++ b/.github/workflows/test_on_push.yml @@ -6,6 +6,8 @@ on: env: FORCE_COLOR: 3 + PYBAMM_IDAKLU_EXPR_CASADI: ON + PYBAMM_IDAKLU_EXPR_IREE: ON concurrency: # github.workflow: name of the workflow, so that we don't cancel other workflows diff --git a/CMakeLists.txt b/CMakeLists.txt index b9fe37c331..661f63457e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,32 +35,76 @@ add_compile_definitions(_GLIBCXX_USE_CXX11_ABI=0) if(NOT PYBIND11_DIR) set(PYBIND11_DIR pybind11) endif() - add_subdirectory(${PYBIND11_DIR}) -# The sources list should mirror the list in setup.py + +# Check Casadi build flag +if(NOT DEFINED PYBAMM_IDAKLU_EXPR_CASADI) + set(PYBAMM_IDAKLU_EXPR_CASADI ON) +endif() +message("PYBAMM_IDAKLU_EXPR_CASADI: ${PYBAMM_IDAKLU_EXPR_CASADI}") + +# Casadi PyBaMM source files +set(IDAKLU_EXPR_CASADI_SOURCE_FILES "") +if(${PYBAMM_IDAKLU_EXPR_CASADI} STREQUAL "ON" ) + add_compile_definitions(CASADI_ENABLE) + set(IDAKLU_EXPR_CASADI_SOURCE_FILES + pybamm/solvers/c_solvers/idaklu/Expressions/Casadi/CasadiFunctions.cpp + pybamm/solvers/c_solvers/idaklu/Expressions/Casadi/CasadiFunctions.hpp + ) +endif() + +# Check IREE build flag +if(NOT DEFINED PYBAMM_IDAKLU_EXPR_IREE) + set(PYBAMM_IDAKLU_EXPR_IREE OFF) +endif() +message("PYBAMM_IDAKLU_EXPR_IREE: ${PYBAMM_IDAKLU_EXPR_IREE}") + +# IREE (MLIR expression evaluation) PyBaMM source files +set(IDAKLU_EXPR_IREE_SOURCE_FILES "") +if(${PYBAMM_IDAKLU_EXPR_IREE} STREQUAL "ON" ) + add_compile_definitions(IREE_ENABLE) + # Source file list + set(IDAKLU_EXPR_IREE_SOURCE_FILES + pybamm/solvers/c_solvers/idaklu/Expressions/IREE/iree_jit.cpp + pybamm/solvers/c_solvers/idaklu/Expressions/IREE/iree_jit.hpp + pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunctions.cpp + pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunctions.hpp + pybamm/solvers/c_solvers/idaklu/Expressions/IREE/ModuleParser.cpp + pybamm/solvers/c_solvers/idaklu/Expressions/IREE/ModuleParser.hpp + ) +endif() + +# The complete (all dependencies) sources list should be mirrored in setup.py pybind11_add_module(idaklu - pybamm/solvers/c_solvers/idaklu/casadi_functions.cpp - pybamm/solvers/c_solvers/idaklu/casadi_functions.hpp - pybamm/solvers/c_solvers/idaklu/casadi_solver.cpp - pybamm/solvers/c_solvers/idaklu/casadi_solver.hpp - pybamm/solvers/c_solvers/idaklu/CasadiSolver.cpp - pybamm/solvers/c_solvers/idaklu/CasadiSolver.hpp - pybamm/solvers/c_solvers/idaklu/CasadiSolverOpenMP.cpp - pybamm/solvers/c_solvers/idaklu/CasadiSolverOpenMP.hpp - pybamm/solvers/c_solvers/idaklu/CasadiSolverOpenMP_solvers.cpp - pybamm/solvers/c_solvers/idaklu/CasadiSolverOpenMP_solvers.hpp - pybamm/solvers/c_solvers/idaklu/casadi_sundials_functions.cpp - pybamm/solvers/c_solvers/idaklu/casadi_sundials_functions.hpp - pybamm/solvers/c_solvers/idaklu/idaklu_jax.cpp - pybamm/solvers/c_solvers/idaklu/idaklu_jax.hpp + # pybind11 interface + pybamm/solvers/c_solvers/idaklu.cpp + # IDAKLU solver (SUNDIALS) + pybamm/solvers/c_solvers/idaklu/idaklu_solver.hpp + pybamm/solvers/c_solvers/idaklu/IDAKLUSolver.cpp + pybamm/solvers/c_solvers/idaklu/IDAKLUSolver.hpp + pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl + pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.hpp + pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP_solvers.cpp + pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP_solvers.hpp + pybamm/solvers/c_solvers/idaklu/sundials_functions.inl + pybamm/solvers/c_solvers/idaklu/sundials_functions.hpp + pybamm/solvers/c_solvers/idaklu/IdakluJax.cpp + pybamm/solvers/c_solvers/idaklu/IdakluJax.hpp pybamm/solvers/c_solvers/idaklu/common.hpp pybamm/solvers/c_solvers/idaklu/python.hpp pybamm/solvers/c_solvers/idaklu/python.cpp - pybamm/solvers/c_solvers/idaklu/solution.cpp - pybamm/solvers/c_solvers/idaklu/solution.hpp - pybamm/solvers/c_solvers/idaklu/options.hpp - pybamm/solvers/c_solvers/idaklu/options.cpp - pybamm/solvers/c_solvers/idaklu.cpp + pybamm/solvers/c_solvers/idaklu/Solution.cpp + pybamm/solvers/c_solvers/idaklu/Solution.hpp + pybamm/solvers/c_solvers/idaklu/Options.hpp + pybamm/solvers/c_solvers/idaklu/Options.cpp + # IDAKLU expressions / function evaluation [abstract] + pybamm/solvers/c_solvers/idaklu/Expressions/Expressions.hpp + pybamm/solvers/c_solvers/idaklu/Expressions/Base/Expression.hpp + pybamm/solvers/c_solvers/idaklu/Expressions/Base/ExpressionSet.hpp + pybamm/solvers/c_solvers/idaklu/Expressions/Base/ExpressionTypes.hpp + # IDAKLU expressions - concrete implementations + ${IDAKLU_EXPR_CASADI_SOURCE_FILES} + ${IDAKLU_EXPR_IREE_SOURCE_FILES} ) if (NOT DEFINED USE_PYTHON_CASADI) @@ -113,3 +157,16 @@ else() endif() include_directories(${SuiteSparse_INCLUDE_DIRS}) target_link_libraries(idaklu PRIVATE ${SuiteSparse_LIBRARIES}) + +# IREE (MLIR compiler and runtime library) build settings +if(${PYBAMM_IDAKLU_EXPR_IREE} STREQUAL "ON" ) + set(IREE_BUILD_COMPILER ON) + set(IREE_BUILD_TESTS OFF) + set(IREE_BUILD_SAMPLES OFF) + add_subdirectory(iree EXCLUDE_FROM_ALL) + set(IREE_COMPILER_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/iree/compiler") + target_include_directories(idaklu SYSTEM PRIVATE "${IREE_COMPILER_ROOT}/bindings/c/iree/compiler") + target_compile_options(idaklu PRIVATE ${IREE_DEFAULT_COPTS}) + target_link_libraries(idaklu PRIVATE iree_compiler_bindings_c_loader) + target_link_libraries(idaklu PRIVATE iree_runtime_runtime) +endif() diff --git a/docs/source/examples/notebooks/models/spm1.png b/docs/source/examples/notebooks/models/spm1.png index 7e0e9ea9cc09656856bc3884639c6c0b7cfef208..a8509b442a9028f5a58ac5de5367d2c85968ddf9 100644 GIT binary patch literal 24150 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 -based solver, see `Optional - JaxSolver `_. +* `jax `_ -based solver, see `Optional - JaxSolver `_. +* `IREE `_ (`MLIR `_) support, see `Optional - IREE / MLIR Support `_. Dependencies ------------ @@ -205,6 +206,17 @@ Dependency Minimu `jaxlib `__ 0.4.20 jax Support library for JAX ========================================================================= ================== ================== ======================= +IREE dependencies +^^^^^^^^^^^^^^^^^^ + +Installable with ``pip install "pybamm[iree]"`` (requires ``jax`` dependencies to be installed). + +========================================================================= ================== ================== ======================= +Dependency Minimum Version pip extra Notes +========================================================================= ================== ================== ======================= +`iree-compiler `__ 20240507.886 iree IREE compiler +========================================================================= ================== ================== ======================= + Full installation guide ----------------------- diff --git a/noxfile.py b/noxfile.py index 7237786ef6..373b77f71f 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1,6 +1,8 @@ import nox import os import sys +import warnings +import platform from pathlib import Path @@ -13,11 +15,54 @@ nox.options.sessions = ["pre-commit", "unit"] +def set_iree_state(): + """ + Check if IREE is enabled and set the environment variable accordingly. + + Returns + ------- + str + "ON" if IREE is enabled, "OFF" otherwise. + + """ + state = "ON" if os.getenv("PYBAMM_IDAKLU_EXPR_IREE", "OFF") == "ON" else "OFF" + if state == "ON": + if sys.platform == "win32": + warnings.warn( + ( + "IREE is not enabled on Windows yet. " + "Setting PYBAMM_IDAKLU_EXPR_IREE=OFF." + ), + stacklevel=2, + ) + return "OFF" + if sys.platform == "darwin": + # iree-compiler is currently only available as a wheel on macOS 13 (or + # higher) and Python version 3.11 + mac_ver = int(platform.mac_ver()[0].split(".")[0]) + if (not sys.version_info[:2] == (3, 11)) or mac_ver < 13: + warnings.warn( + ( + "IREE is only supported on MacOS 13 (or higher) and Python" + "version 3.11. Setting PYBAMM_IDAKLU_EXPR_IREE=OFF." + ), + stacklevel=2, + ) + return "OFF" + return state + + homedir = os.getenv("HOME") PYBAMM_ENV = { "SUNDIALS_INST": f"{homedir}/.local", "LD_LIBRARY_PATH": f"{homedir}/.local/lib", "PYTHONIOENCODING": "utf-8", + # Expression evaluators (...EXPR_CASADI cannot be fully disabled at this time) + "PYBAMM_IDAKLU_EXPR_CASADI": os.getenv("PYBAMM_IDAKLU_EXPR_CASADI", "ON"), + "PYBAMM_IDAKLU_EXPR_IREE": set_iree_state(), + "IREE_INDEX_URL": os.getenv( + "IREE_INDEX_URL", "https://iree.dev/pip-release-links.html" + ), } VENV_DIR = Path("./venv").resolve() @@ -59,6 +104,29 @@ def run_pybamm_requires(session): "advice.detachedHead=false", external=True, ) + if PYBAMM_ENV.get("PYBAMM_IDAKLU_EXPR_IREE") == "ON" and not os.path.exists( + "./iree" + ): + session.run( + "git", + "clone", + "--depth=1", + "--recurse-submodules", + "--shallow-submodules", + "--branch=candidate-20240507.886", + "https://github.com/openxla/iree", + "iree/", + external=True, + ) + with session.chdir("iree"): + session.run( + "git", + "submodule", + "update", + "--init", + "--recursive", + external=True, + ) else: session.error("nox -s pybamm-requires is only available on Linux & macOS.") @@ -70,6 +138,15 @@ def run_coverage(session): session.install("setuptools", silent=False) session.install("coverage", silent=False) session.install("-e", ".[all,dev,jax]", silent=False) + if PYBAMM_ENV.get("PYBAMM_IDAKLU_EXPR_IREE") == "ON": + # See comments in 'dev' session + session.install( + "-e", + ".[iree]", + "--find-links", + PYBAMM_ENV.get("IREE_INDEX_URL"), + silent=False, + ) session.run("pytest", "--cov=pybamm", "--cov-report=xml", "tests/unit") @@ -98,6 +175,15 @@ def run_unit(session): set_environment_variables(PYBAMM_ENV, session=session) session.install("setuptools", silent=False) session.install("-e", ".[all,dev,jax]", silent=False) + if PYBAMM_ENV.get("PYBAMM_IDAKLU_EXPR_IREE") == "ON": + # See comments in 'dev' session + session.install( + "-e", + ".[iree]", + "--find-links", + PYBAMM_ENV.get("IREE_INDEX_URL"), + silent=False, + ) session.run("python", "run-tests.py", "--unit") @@ -130,6 +216,17 @@ def set_dev(session): session.install("virtualenv", "cmake") session.run("virtualenv", os.fsdecode(VENV_DIR), silent=True) python = os.fsdecode(VENV_DIR.joinpath("bin/python")) + components = ["all", "dev", "jax"] + args = [] + if PYBAMM_ENV.get("PYBAMM_IDAKLU_EXPR_IREE") == "ON": + # Install IREE libraries for Jax-MLIR expression evaluation in the IDAKLU solver + # (optional). IREE is currently pre-release and relies on nightly jaxlib builds. + # When upgrading Jax/IREE ensure that the following are compatible with each other: + # - Jax and Jaxlib version [pyproject.toml] + # - IREE repository clone (use the matching nightly candidate) [noxfile.py] + # - IREE compiler matches Jaxlib (use the matching nightly build) [pyproject.toml] + components.append("iree") + args = ["--find-links", PYBAMM_ENV.get("IREE_INDEX_URL")] # Temporary fix for Python 3.12 CI. TODO: remove after # https://bitbucket.org/pybtex-devs/pybtex/issues/169/replace-pkg_resources-with # is fixed @@ -140,7 +237,8 @@ def set_dev(session): "pip", "install", "-e", - ".[all,dev,jax]", + ".[{}]".format(",".join(components)), + *args, external=True, ) diff --git a/pybamm/__init__.py b/pybamm/__init__.py index b3b7fafd3f..a371fdbc03 100644 --- a/pybamm/__init__.py +++ b/pybamm/__init__.py @@ -2,6 +2,9 @@ from pybamm.version import __version__ +# Demote expressions to 32-bit floats/ints - option used for IDAKLU-MLIR compilation +demote_expressions_to_32bit = False + # Utility classes and methods from .util import root_dir from .util import Timer, TimerTime, FuzzyDict @@ -168,7 +171,7 @@ from .solvers.jax_bdf_solver import jax_bdf_integrate from .solvers.idaklu_jax import IDAKLUJax -from .solvers.idaklu_solver import IDAKLUSolver, have_idaklu +from .solvers.idaklu_solver import IDAKLUSolver, have_idaklu, have_iree # Experiments from .experiment.experiment import Experiment diff --git a/pybamm/expression_tree/operations/evaluate_python.py b/pybamm/expression_tree/operations/evaluate_python.py index 6d13761756..20a6d4b4a2 100644 --- a/pybamm/expression_tree/operations/evaluate_python.py +++ b/pybamm/expression_tree/operations/evaluate_python.py @@ -281,7 +281,7 @@ def find_symbols( if isinstance(symbol, pybamm.Index): symbol_str = f"{children_vars[0]}[{symbol.slice.start}:{symbol.slice.stop}]" else: - symbol_str = symbol.name + children_vars[0] + symbol_str = symbol.name + "(" + children_vars[0] + ")" elif isinstance(symbol, pybamm.Function): children_str = "" @@ -596,6 +596,59 @@ def __init__(self, symbol: pybamm.Symbol): static_argnums=self._static_argnums, ) + def _demote_constants(self): + """Demote 64-bit constants (f64, i64) to 32-bit (f32, i32)""" + if not pybamm.demote_expressions_to_32bit: + return # pragma: no cover + self._constants = EvaluatorJax._demote_64_to_32(self._constants) + + @classmethod + def _demote_64_to_32(cls, c): + """Demote 64-bit operations (f64, i64) to 32-bit (f32, i32)""" + + if not pybamm.demote_expressions_to_32bit: + return c + if isinstance(c, float): + c = jax.numpy.float32(c) + if isinstance(c, int): + c = jax.numpy.int32(c) + if isinstance(c, np.int64): + c = c.astype(jax.numpy.int32) + if isinstance(c, np.ndarray): + if c.dtype == np.float64: + c = c.astype(jax.numpy.float32) + if c.dtype == np.int64: + c = c.astype(jax.numpy.int32) + if isinstance(c, jax.numpy.ndarray): + if c.dtype == jax.numpy.float64: + c = c.astype(jax.numpy.float32) + if c.dtype == jax.numpy.int64: + c = c.astype(jax.numpy.int32) + if isinstance( + c, pybamm.expression_tree.operations.evaluate_python.JaxCooMatrix + ): + if c.data.dtype == np.float64: + c.data = c.data.astype(jax.numpy.float32) + if c.row.dtype == np.int64: + c.row = c.row.astype(jax.numpy.int32) + if c.col.dtype == np.int64: + c.col = c.col.astype(jax.numpy.int32) + if isinstance(c, dict): + c = {key: EvaluatorJax._demote_64_to_32(value) for key, value in c.items()} + if isinstance(c, tuple): + c = tuple(EvaluatorJax._demote_64_to_32(value) for value in c) + if isinstance(c, list): + c = [EvaluatorJax._demote_64_to_32(value) for value in c] + return c + + @property + def _constants(self): + return tuple(map(EvaluatorJax._demote_64_to_32, self.__constants)) + + @_constants.setter + def _constants(self, value): + self.__constants = value + def get_jacobian(self): n = len(self._arg_list) diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index 1425bf0845..0eb573e87a 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -256,32 +256,30 @@ def set_up(self, model, inputs=None, t_eval=None, ics_only=False): model.casadi_sensitivities_rhs = jacp_rhs model.casadi_sensitivities_algebraic = jacp_algebraic - # if output_variables specified then convert functions to casadi - # expressions for evaluation within the respective solver - self.computed_var_fcns = {} - self.computed_dvar_dy_fcns = {} - self.computed_dvar_dp_fcns = {} - for key in self.output_variables: - # ExplicitTimeIntegral's are not computed as part of the solver and - # do not need to be converted - if isinstance( - model.variables_and_events[key], pybamm.ExplicitTimeIntegral - ): - continue - # Generate Casadi function to calculate variable and derivates - # to enable sensitivites to be computed within the solver - ( - self.computed_var_fcns[key], - self.computed_dvar_dy_fcns[key], - self.computed_dvar_dp_fcns[key], - _, - ) = process( - model.variables_and_events[key], - BaseSolver._wrangle_name(key), - vars_for_processing, - use_jacobian=True, - return_jacp_stacked=True, - ) + # if output_variables specified then convert functions to casadi + # expressions for evaluation within the respective solver + self.computed_var_fcns = {} + self.computed_dvar_dy_fcns = {} + self.computed_dvar_dp_fcns = {} + for key in self.output_variables: + # ExplicitTimeIntegral's are not computed as part of the solver and + # do not need to be converted + if isinstance(model.variables_and_events[key], pybamm.ExplicitTimeIntegral): + continue + # Generate Casadi function to calculate variable and derivates + # to enable sensitivites to be computed within the solver + ( + self.computed_var_fcns[key], + self.computed_dvar_dy_fcns[key], + self.computed_dvar_dp_fcns[key], + _, + ) = process( + model.variables_and_events[key], + BaseSolver._wrangle_name(key), + vars_for_processing, + use_jacobian=True, + return_jacp_stacked=True, + ) pybamm.logger.info("Finish solver set-up") diff --git a/pybamm/solvers/c_solvers/idaklu.cpp b/pybamm/solvers/c_solvers/idaklu.cpp index 9f99d4d3f4..3afed5faa8 100644 --- a/pybamm/solvers/c_solvers/idaklu.cpp +++ b/pybamm/solvers/c_solvers/idaklu.cpp @@ -8,14 +8,20 @@ #include #include -#include "idaklu/casadi_solver.hpp" -#include "idaklu/idaklu_jax.hpp" +#include "idaklu/idaklu_solver.hpp" +#include "idaklu/IdakluJax.hpp" #include "idaklu/common.hpp" #include "idaklu/python.hpp" +#include "idaklu/Expressions/Casadi/CasadiFunctions.hpp" -Function generate_function(const std::string &data) +#ifdef IREE_ENABLE +#include "idaklu/Expressions/IREE/IREEFunctions.hpp" +#endif + + +casadi::Function generate_casadi_function(const std::string &data) { - return Function::deserialize(data); + return casadi::Function::deserialize(data); } namespace py = pybind11; @@ -50,8 +56,8 @@ PYBIND11_MODULE(idaklu, m) py::arg("number_of_sensitivity_parameters"), py::return_value_policy::take_ownership); - py::class_(m, "CasadiSolver") - .def("solve", &CasadiSolver::solve, + py::class_(m, "IDAKLUSolver") + .def("solve", &IDAKLUSolver::solve, "perform a solve", py::arg("t"), py::arg("y0"), @@ -59,7 +65,7 @@ PYBIND11_MODULE(idaklu, m) py::arg("inputs"), py::return_value_policy::take_ownership); - m.def("create_casadi_solver", &create_casadi_solver, + m.def("create_casadi_solver", &create_idaklu_solver, "Create a casadi idaklu solver object", py::arg("number_of_states"), py::arg("number_of_parameters"), @@ -79,13 +85,41 @@ PYBIND11_MODULE(idaklu, m) py::arg("atol"), py::arg("rtol"), py::arg("inputs"), - py::arg("var_casadi_fcns"), + py::arg("var_fcns"), + py::arg("dvar_dy_fcns"), + py::arg("dvar_dp_fcns"), + py::arg("options"), + py::return_value_policy::take_ownership); + +#ifdef IREE_ENABLE + m.def("create_iree_solver", &create_idaklu_solver, + "Create a iree idaklu solver object", + py::arg("number_of_states"), + py::arg("number_of_parameters"), + py::arg("rhs_alg"), + py::arg("jac_times_cjmass"), + py::arg("jac_times_cjmass_colptrs"), + py::arg("jac_times_cjmass_rowvals"), + py::arg("jac_times_cjmass_nnz"), + py::arg("jac_bandwidth_lower"), + py::arg("jac_bandwidth_upper"), + py::arg("jac_action"), + py::arg("mass_action"), + py::arg("sens"), + py::arg("events"), + py::arg("number_of_events"), + py::arg("rhs_alg_id"), + py::arg("atol"), + py::arg("rtol"), + py::arg("inputs"), + py::arg("var_fcns"), py::arg("dvar_dy_fcns"), py::arg("dvar_dp_fcns"), py::arg("options"), py::return_value_policy::take_ownership); +#endif - m.def("generate_function", &generate_function, + m.def("generate_function", &generate_casadi_function, "Generate a casadi function", py::arg("string"), py::return_value_policy::take_ownership); @@ -133,11 +167,25 @@ PYBIND11_MODULE(idaklu, m) &Registrations ); - py::class_(m, "Function"); + py::class_(m, "Function"); + +#ifdef IREE_ENABLE + py::class_(m, "IREEBaseFunctionType") + .def(py::init<>()) + .def_readwrite("mlir", &IREEBaseFunctionType::mlir) + .def_readwrite("kept_var_idx", &IREEBaseFunctionType::kept_var_idx) + .def_readwrite("nnz", &IREEBaseFunctionType::nnz) + .def_readwrite("numel", &IREEBaseFunctionType::numel) + .def_readwrite("col", &IREEBaseFunctionType::col) + .def_readwrite("row", &IREEBaseFunctionType::row) + .def_readwrite("pytree_shape", &IREEBaseFunctionType::pytree_shape) + .def_readwrite("pytree_sizes", &IREEBaseFunctionType::pytree_sizes) + .def_readwrite("n_args", &IREEBaseFunctionType::n_args); +#endif py::class_(m, "solution") - .def_readwrite("t", &Solution::t) - .def_readwrite("y", &Solution::y) - .def_readwrite("yS", &Solution::yS) - .def_readwrite("flag", &Solution::flag); + .def_readwrite("t", &Solution::t) + .def_readwrite("y", &Solution::y) + .def_readwrite("yS", &Solution::yS) + .def_readwrite("flag", &Solution::flag); } diff --git a/pybamm/solvers/c_solvers/idaklu/CasadiSolver.cpp b/pybamm/solvers/c_solvers/idaklu/CasadiSolver.cpp deleted file mode 100644 index 16a04f8eb9..0000000000 --- a/pybamm/solvers/c_solvers/idaklu/CasadiSolver.cpp +++ /dev/null @@ -1 +0,0 @@ -#include "CasadiSolver.hpp" diff --git a/pybamm/solvers/c_solvers/idaklu/CasadiSolverOpenMP_solvers.cpp b/pybamm/solvers/c_solvers/idaklu/CasadiSolverOpenMP_solvers.cpp deleted file mode 100644 index 868d2b2138..0000000000 --- a/pybamm/solvers/c_solvers/idaklu/CasadiSolverOpenMP_solvers.cpp +++ /dev/null @@ -1 +0,0 @@ -#include "CasadiSolverOpenMP_solvers.hpp" diff --git a/pybamm/solvers/c_solvers/idaklu/CasadiSolverOpenMP_solvers.hpp b/pybamm/solvers/c_solvers/idaklu/CasadiSolverOpenMP_solvers.hpp deleted file mode 100644 index 3e39e5a303..0000000000 --- a/pybamm/solvers/c_solvers/idaklu/CasadiSolverOpenMP_solvers.hpp +++ /dev/null @@ -1,125 +0,0 @@ -#ifndef PYBAMM_IDAKLU_CASADI_SOLVER_OPENMP_HPP -#define PYBAMM_IDAKLU_CASADI_SOLVER_OPENMP_HPP - -#include "CasadiSolverOpenMP.hpp" -#include "casadi_solver.hpp" - -/** - * @brief CasadiSolver Dense implementation with OpenMP class - */ -class CasadiSolverOpenMP_Dense : public CasadiSolverOpenMP { -public: - template - CasadiSolverOpenMP_Dense(Args&& ... args) - : CasadiSolverOpenMP(std::forward(args) ...) - { - LS = SUNLinSol_Dense(yy, J, sunctx); - Initialize(); - } -}; - -/** - * @brief CasadiSolver KLU implementation with OpenMP class - */ -class CasadiSolverOpenMP_KLU : public CasadiSolverOpenMP { -public: - template - CasadiSolverOpenMP_KLU(Args&& ... args) - : CasadiSolverOpenMP(std::forward(args) ...) - { - LS = SUNLinSol_KLU(yy, J, sunctx); - Initialize(); - } -}; - -/** - * @brief CasadiSolver Banded implementation with OpenMP class - */ -class CasadiSolverOpenMP_Band : public CasadiSolverOpenMP { -public: - template - CasadiSolverOpenMP_Band(Args&& ... args) - : CasadiSolverOpenMP(std::forward(args) ...) - { - LS = SUNLinSol_Band(yy, J, sunctx); - Initialize(); - } -}; - -/** - * @brief CasadiSolver SPBCGS implementation with OpenMP class - */ -class CasadiSolverOpenMP_SPBCGS : public CasadiSolverOpenMP { -public: - template - CasadiSolverOpenMP_SPBCGS(Args&& ... args) - : CasadiSolverOpenMP(std::forward(args) ...) - { - LS = SUNLinSol_SPBCGS( - yy, - precon_type, - options.linsol_max_iterations, - sunctx - ); - Initialize(); - } -}; - -/** - * @brief CasadiSolver SPFGMR implementation with OpenMP class - */ -class CasadiSolverOpenMP_SPFGMR : public CasadiSolverOpenMP { -public: - template - CasadiSolverOpenMP_SPFGMR(Args&& ... args) - : CasadiSolverOpenMP(std::forward(args) ...) - { - LS = SUNLinSol_SPFGMR( - yy, - precon_type, - options.linsol_max_iterations, - sunctx - ); - Initialize(); - } -}; - -/** - * @brief CasadiSolver SPGMR implementation with OpenMP class - */ -class CasadiSolverOpenMP_SPGMR : public CasadiSolverOpenMP { -public: - template - CasadiSolverOpenMP_SPGMR(Args&& ... args) - : CasadiSolverOpenMP(std::forward(args) ...) - { - LS = SUNLinSol_SPGMR( - yy, - precon_type, - options.linsol_max_iterations, - sunctx - ); - Initialize(); - } -}; - -/** - * @brief CasadiSolver SPTFQMR implementation with OpenMP class - */ -class CasadiSolverOpenMP_SPTFQMR : public CasadiSolverOpenMP { -public: - template - CasadiSolverOpenMP_SPTFQMR(Args&& ... args) - : CasadiSolverOpenMP(std::forward(args) ...) - { - LS = SUNLinSol_SPTFQMR( - yy, - precon_type, - options.linsol_max_iterations, - sunctx - ); - Initialize(); - } -}; - -#endif // PYBAMM_IDAKLU_CASADI_SOLVER_OPENMP_HPP diff --git a/pybamm/solvers/c_solvers/idaklu/Expressions/Base/Expression.hpp b/pybamm/solvers/c_solvers/idaklu/Expressions/Base/Expression.hpp new file mode 100644 index 0000000000..bbf60b4568 --- /dev/null +++ b/pybamm/solvers/c_solvers/idaklu/Expressions/Base/Expression.hpp @@ -0,0 +1,69 @@ +#ifndef PYBAMM_EXPRESSION_HPP +#define PYBAMM_EXPRESSION_HPP + +#include "ExpressionTypes.hpp" +#include "../../common.hpp" +#include "../../Options.hpp" +#include +#include + +class Expression { +public: // method declarations + /** + * @brief Constructor + */ + Expression() = default; + + /** + * @brief Evaluation operator (for use after setting input and output data references) + */ + virtual void operator()() = 0; + + /** + * @brief Evaluation operator (supplying data references) + */ + virtual void operator()( + const std::vector& inputs, + const std::vector& results) = 0; + + /** + * @brief The maximum number of elements returned by the k'th output + * + * This is used to allocate memory for the output of the function and usually (but + * not always) corresponds to the number of non-zero elements (NNZ). + */ + virtual expr_int out_shape(int k) = 0; + + /** + * @brief Return the number of non-zero elements for the function output + */ + virtual expr_int nnz() = 0; + + /** + * @brief Return the number of non-zero elements for the function output + */ + virtual expr_int nnz_out() = 0; + + /** + * @brief Returns row indices in COO format (where the output data represents sparse matrix elements) + */ + virtual std::vector get_row() = 0; + + /** + * @brief Returns column indices in COO format (where the output data represents sparse matrix elements) + */ + virtual std::vector get_col() = 0; + +public: // data members + /** + * @brief Vector of pointers to the input data + */ + std::vector m_arg; // cppcheck-suppress unusedStructMember + + /** + * @brief Vector of pointers to the output data + */ + std::vector m_res; // cppcheck-suppress unusedStructMember +}; + +#endif // PYBAMM_EXPRESSION_HPP diff --git a/pybamm/solvers/c_solvers/idaklu/Expressions/Base/ExpressionSet.hpp b/pybamm/solvers/c_solvers/idaklu/Expressions/Base/ExpressionSet.hpp new file mode 100644 index 0000000000..a32f906a38 --- /dev/null +++ b/pybamm/solvers/c_solvers/idaklu/Expressions/Base/ExpressionSet.hpp @@ -0,0 +1,86 @@ +#ifndef PYBAMM_IDAKLU_EXPRESSION_SET_HPP +#define PYBAMM_IDAKLU_EXPRESSION_SET_HPP + +#include "ExpressionTypes.hpp" +#include "Expression.hpp" +#include "../../common.hpp" +#include "../../Options.hpp" +#include + +template +class ExpressionSet +{ +public: + + /** + * @brief Constructor + */ + ExpressionSet( + Expression* rhs_alg, + Expression* jac_times_cjmass, + const int jac_times_cjmass_nnz, + const int jac_bandwidth_lower, + const int jac_bandwidth_upper, + const np_array_int &jac_times_cjmass_rowvals_arg, // cppcheck-suppress unusedStructMember + const np_array_int &jac_times_cjmass_colptrs_arg, // cppcheck-suppress unusedStructMember + const int inputs_length, + Expression* jac_action, + Expression* mass_action, + Expression* sens, + Expression* events, + const int n_s, + const int n_e, + const int n_p, + const Options& options) + : number_of_states(n_s), + number_of_events(n_e), + number_of_parameters(n_p), + number_of_nnz(jac_times_cjmass_nnz), + jac_bandwidth_lower(jac_bandwidth_lower), + jac_bandwidth_upper(jac_bandwidth_upper), + rhs_alg(rhs_alg), + jac_times_cjmass(jac_times_cjmass), + jac_action(jac_action), + mass_action(mass_action), + sens(sens), + events(events), + tmp_state_vector(number_of_states), + tmp_sparse_jacobian_data(jac_times_cjmass_nnz), + options(options) + {}; + + int number_of_states; + int number_of_parameters; + int number_of_events; + int number_of_nnz; + int jac_bandwidth_lower; + int jac_bandwidth_upper; + + Expression *rhs_alg = nullptr; + Expression *jac_times_cjmass = nullptr; + Expression *jac_action = nullptr; + Expression *mass_action = nullptr; + Expression *sens = nullptr; + Expression *events = nullptr; + + // `cppcheck-suppress unusedStructMember` is used because codacy reports + // these members as unused, but they are inherited through variadics + std::vector var_fcns; // cppcheck-suppress unusedStructMember + std::vector dvar_dy_fcns; // cppcheck-suppress unusedStructMember + std::vector dvar_dp_fcns; // cppcheck-suppress unusedStructMember + + std::vector jac_times_cjmass_rowvals; // cppcheck-suppress unusedStructMember + std::vector jac_times_cjmass_colptrs; // cppcheck-suppress unusedStructMember + std::vector inputs; // cppcheck-suppress unusedStructMember + + Options options; + + virtual realtype *get_tmp_state_vector() = 0; + virtual realtype *get_tmp_sparse_jacobian_data() = 0; + +protected: + std::vector tmp_state_vector; + std::vector tmp_sparse_jacobian_data; +}; + +#endif // PYBAMM_IDAKLU_EXPRESSION_SET_HPP diff --git a/pybamm/solvers/c_solvers/idaklu/Expressions/Base/ExpressionTypes.hpp b/pybamm/solvers/c_solvers/idaklu/Expressions/Base/ExpressionTypes.hpp new file mode 100644 index 0000000000..c8d690c125 --- /dev/null +++ b/pybamm/solvers/c_solvers/idaklu/Expressions/Base/ExpressionTypes.hpp @@ -0,0 +1,6 @@ +#ifndef PYBAMM_EXPRESSION_TYPES_HPP +#define PYBAMM_EXPRESSION_TYPES_HPP + +using expr_int = long long int; + +#endif // PYBAMM_EXPRESSION_TYPES_HPP diff --git a/pybamm/solvers/c_solvers/idaklu/Expressions/Casadi/CasadiFunctions.cpp b/pybamm/solvers/c_solvers/idaklu/Expressions/Casadi/CasadiFunctions.cpp new file mode 100644 index 0000000000..b0c8ab1142 --- /dev/null +++ b/pybamm/solvers/c_solvers/idaklu/Expressions/Casadi/CasadiFunctions.cpp @@ -0,0 +1,80 @@ +#include "CasadiFunctions.hpp" +#include + +CasadiFunction::CasadiFunction(const BaseFunctionType &f) : Expression(), m_func(f) +{ + DEBUG("CasadiFunction constructor: " << m_func.name()); + + size_t sz_arg; + size_t sz_res; + size_t sz_iw; + size_t sz_w; + m_func.sz_work(sz_arg, sz_res, sz_iw, sz_w); + + int nnz = (sz_res>0) ? m_func.nnz_out() : 0; // cppcheck-suppress unreadVariable + DEBUG("name = "<< m_func.name() << " arg = " << sz_arg << " res = " + << sz_res << " iw = " << sz_iw << " w = " << sz_w << " nnz = " << nnz); + + m_arg.resize(sz_arg, nullptr); + m_res.resize(sz_res, nullptr); + m_iw.resize(sz_iw, 0); + m_w.resize(sz_w, 0); +} + +// only call this once m_arg and m_res have been set appropriately +void CasadiFunction::operator()() +{ + DEBUG("CasadiFunction operator(): " << m_func.name()); + int mem = m_func.checkout(); + m_func(m_arg.data(), m_res.data(), m_iw.data(), m_w.data(), mem); + m_func.release(mem); +} + +expr_int CasadiFunction::out_shape(int k) { + DEBUG("CasadiFunctions out_shape(): " << m_func.name() << " " << m_func.nnz_out()); + return static_cast(m_func.nnz_out()); +} + +expr_int CasadiFunction::nnz() { + DEBUG("CasadiFunction nnz(): " << m_func.name() << " " << static_cast(m_func.nnz_out())); + return static_cast(m_func.nnz_out()); +} + +expr_int CasadiFunction::nnz_out() { + DEBUG("CasadiFunction nnz_out(): " << m_func.name() << " " << static_cast(m_func.nnz_out())); + return static_cast(m_func.nnz_out()); +} + +std::vector CasadiFunction::get_row() { + return get_row(0); +} + +std::vector CasadiFunction::get_row(expr_int ind) { + DEBUG("CasadiFunction get_row(): " << m_func.name()); + casadi::Sparsity casadi_sparsity = m_func.sparsity_out(ind); + return casadi_sparsity.get_row(); +} + +std::vector CasadiFunction::get_col() { + return get_col(0); +} + +std::vector CasadiFunction::get_col(expr_int ind) { + DEBUG("CasadiFunction get_col(): " << m_func.name()); + casadi::Sparsity casadi_sparsity = m_func.sparsity_out(ind); + return casadi_sparsity.get_col(); +} + +void CasadiFunction::operator()(const std::vector& inputs, + const std::vector& results) +{ + DEBUG("CasadiFunction operator() with inputs and results: " << m_func.name()); + + // Set-up input arguments, provide result vector, then execute function + // Example call: fcn({in1, in2, in3}, {out1}) + for(size_t k=0; k +#include +#include +#include + +/** + * @brief Class for handling individual casadi functions + */ +class CasadiFunction : public Expression +{ +public: + + typedef casadi::Function BaseFunctionType; + + /** + * @brief Constructor + */ + explicit CasadiFunction(const BaseFunctionType &f); + + // Method overrides + void operator()() override; + void operator()(const std::vector& inputs, + const std::vector& results) override; + expr_int out_shape(int k) override; + expr_int nnz() override; + expr_int nnz_out() override; + std::vector get_row() override; + std::vector get_row(expr_int ind); + std::vector get_col() override; + std::vector get_col(expr_int ind); + +public: + /* + * @brief Casadi function + */ + BaseFunctionType m_func; + +private: + std::vector m_iw; // cppcheck-suppress unusedStructMember + std::vector m_w; // cppcheck-suppress unusedStructMember +}; + +/** + * @brief Class for handling casadi functions + */ +class CasadiFunctions : public ExpressionSet +{ +public: + + typedef CasadiFunction::BaseFunctionType BaseFunctionType; // expose typedef in class + + /** + * @brief Create a new CasadiFunctions object + */ + CasadiFunctions( + const BaseFunctionType &rhs_alg, + const BaseFunctionType &jac_times_cjmass, + const int jac_times_cjmass_nnz, + const int jac_bandwidth_lower, + const int jac_bandwidth_upper, + const np_array_int &jac_times_cjmass_rowvals_arg, + const np_array_int &jac_times_cjmass_colptrs_arg, + const int inputs_length, + const BaseFunctionType &jac_action, + const BaseFunctionType &mass_action, + const BaseFunctionType &sens, + const BaseFunctionType &events, + const int n_s, + const int n_e, + const int n_p, + const std::vector& var_fcns, + const std::vector& dvar_dy_fcns, + const std::vector& dvar_dp_fcns, + const Options& options + ) : + rhs_alg_casadi(rhs_alg), + jac_times_cjmass_casadi(jac_times_cjmass), + jac_action_casadi(jac_action), + mass_action_casadi(mass_action), + sens_casadi(sens), + events_casadi(events), + ExpressionSet( + static_cast(&rhs_alg_casadi), + static_cast(&jac_times_cjmass_casadi), + jac_times_cjmass_nnz, + jac_bandwidth_lower, + jac_bandwidth_upper, + jac_times_cjmass_rowvals_arg, + jac_times_cjmass_colptrs_arg, + inputs_length, + static_cast(&jac_action_casadi), + static_cast(&mass_action_casadi), + static_cast(&sens_casadi), + static_cast(&events_casadi), + n_s, n_e, n_p, + options) + { + // convert BaseFunctionType list to CasadiFunction list + // NOTE: You must allocate ALL std::vector elements before taking references + for (auto& var : var_fcns) + var_fcns_casadi.push_back(CasadiFunction(*var)); + for (int k = 0; k < var_fcns_casadi.size(); k++) + ExpressionSet::var_fcns.push_back(&this->var_fcns_casadi[k]); + + for (auto& var : dvar_dy_fcns) + dvar_dy_fcns_casadi.push_back(CasadiFunction(*var)); + for (int k = 0; k < dvar_dy_fcns_casadi.size(); k++) + this->dvar_dy_fcns.push_back(&this->dvar_dy_fcns_casadi[k]); + + for (auto& var : dvar_dp_fcns) + dvar_dp_fcns_casadi.push_back(CasadiFunction(*var)); + for (int k = 0; k < dvar_dp_fcns_casadi.size(); k++) + this->dvar_dp_fcns.push_back(&this->dvar_dp_fcns_casadi[k]); + + // copy across numpy array values + const int n_row_vals = jac_times_cjmass_rowvals_arg.request().size; + auto p_jac_times_cjmass_rowvals = jac_times_cjmass_rowvals_arg.unchecked<1>(); + jac_times_cjmass_rowvals.resize(n_row_vals); + for (int i = 0; i < n_row_vals; i++) { + jac_times_cjmass_rowvals[i] = p_jac_times_cjmass_rowvals[i]; + } + + const int n_col_ptrs = jac_times_cjmass_colptrs_arg.request().size; + auto p_jac_times_cjmass_colptrs = jac_times_cjmass_colptrs_arg.unchecked<1>(); + jac_times_cjmass_colptrs.resize(n_col_ptrs); + for (int i = 0; i < n_col_ptrs; i++) { + jac_times_cjmass_colptrs[i] = p_jac_times_cjmass_colptrs[i]; + } + + inputs.resize(inputs_length); + } + + CasadiFunction rhs_alg_casadi; + CasadiFunction jac_times_cjmass_casadi; + CasadiFunction jac_action_casadi; + CasadiFunction mass_action_casadi; + CasadiFunction sens_casadi; + CasadiFunction events_casadi; + + std::vector var_fcns_casadi; + std::vector dvar_dy_fcns_casadi; + std::vector dvar_dp_fcns_casadi; + + realtype* get_tmp_state_vector() override { + return tmp_state_vector.data(); + } + realtype* get_tmp_sparse_jacobian_data() override { + return tmp_sparse_jacobian_data.data(); + } +}; + +#endif // PYBAMM_IDAKLU_CASADI_FUNCTIONS_HPP diff --git a/pybamm/solvers/c_solvers/idaklu/Expressions/Expressions.hpp b/pybamm/solvers/c_solvers/idaklu/Expressions/Expressions.hpp new file mode 100644 index 0000000000..70380eaba7 --- /dev/null +++ b/pybamm/solvers/c_solvers/idaklu/Expressions/Expressions.hpp @@ -0,0 +1,6 @@ +#ifndef PYBAMM_IDAKLU_EXPRESSIONS_HPP +#define PYBAMM_IDAKLU_EXPRESSIONS_HPP + +#include "Base/ExpressionSet.hpp" + +#endif // PYBAMM_IDAKLU_EXPRESSIONS_HPP diff --git a/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEBaseFunction.hpp b/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEBaseFunction.hpp new file mode 100644 index 0000000000..d2ba7e4de0 --- /dev/null +++ b/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEBaseFunction.hpp @@ -0,0 +1,27 @@ +#ifndef PYBAMM_IDAKLU_IREE_BASE_FUNCTION_HPP +#define PYBAMM_IDAKLU_IREE_BASE_FUNCTION_HPP + +#include +#include + +/* + * @brief Function definition passed from PyBaMM + */ +class IREEBaseFunctionType +{ +public: // methods + const std::string& get_mlir() const { return mlir; } + +public: // data members + std::string mlir; // cppcheck-suppress unusedStructMember + std::vector kept_var_idx; // cppcheck-suppress unusedStructMember + expr_int nnz; // cppcheck-suppress unusedStructMember + expr_int numel; // cppcheck-suppress unusedStructMember + std::vector col; // cppcheck-suppress unusedStructMember + std::vector row; // cppcheck-suppress unusedStructMember + std::vector pytree_shape; // cppcheck-suppress unusedStructMember + std::vector pytree_sizes; // cppcheck-suppress unusedStructMember + expr_int n_args; // cppcheck-suppress unusedStructMember +}; + +#endif // PYBAMM_IDAKLU_IREE_BASE_FUNCTION_HPP diff --git a/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunction.hpp b/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunction.hpp new file mode 100644 index 0000000000..26f81c8f98 --- /dev/null +++ b/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunction.hpp @@ -0,0 +1,59 @@ +#ifndef PYBAMM_IDAKLU_IREE_FUNCTION_HPP +#define PYBAMM_IDAKLU_IREE_FUNCTION_HPP + +#include "../../Options.hpp" +#include "../Expressions.hpp" +#include +#include "iree_jit.hpp" +#include "IREEBaseFunction.hpp" + +/** + * @brief Class for handling individual iree functions + */ +class IREEFunction : public Expression +{ +public: + typedef IREEBaseFunctionType BaseFunctionType; + + /* + * @brief Constructor + */ + explicit IREEFunction(const BaseFunctionType &f); + + // Method overrides + void operator()() override; + void operator()(const std::vector& inputs, + const std::vector& results) override; + expr_int out_shape(int k) override; + expr_int nnz() override; + expr_int nnz_out() override; + std::vector get_col() override; + std::vector get_row() override; + + /* + * @brief Evaluate the MLIR function + */ + void evaluate(); + + /* + * @brief Evaluate the MLIR function + * @param n_outputs The number of outputs to return + */ + void evaluate(int n_outputs); + +public: + std::unique_ptr session; + std::vector> result; // cppcheck-suppress unusedStructMember + std::vector> input_shape; // cppcheck-suppress unusedStructMember + std::vector> output_shape; // cppcheck-suppress unusedStructMember + std::vector> input_data; // cppcheck-suppress unusedStructMember + + BaseFunctionType m_func; // cppcheck-suppress unusedStructMember + std::string module_name; // cppcheck-suppress unusedStructMember + std::string function_name; // cppcheck-suppress unusedStructMember + std::vector m_arg_argno; // cppcheck-suppress unusedStructMember + std::vector m_arg_argix; // cppcheck-suppress unusedStructMember + std::vector numel; // cppcheck-suppress unusedStructMember +}; + +#endif // PYBAMM_IDAKLU_IREE_FUNCTION_HPP diff --git a/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunctions.cpp b/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunctions.cpp new file mode 100644 index 0000000000..6837d21198 --- /dev/null +++ b/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunctions.cpp @@ -0,0 +1,230 @@ +#include +#include +#include +#include +#include + +#include "IREEFunctions.hpp" +#include "iree_jit.hpp" +#include "ModuleParser.hpp" + +IREEFunction::IREEFunction(const BaseFunctionType &f) : Expression(), m_func(f) +{ + DEBUG("IreeFunction constructor"); + const std::string& mlir = f.get_mlir(); + + // Parse IREE (MLIR) function string + if (mlir.size() == 0) { + DEBUG("Empty function --- skipping..."); + return; + } + + // Parse MLIR for module name, input and output shapes + ModuleParser parser(mlir); + module_name = parser.getModuleName(); + function_name = parser.getFunctionName(); + input_shape = parser.getInputShape(); + output_shape = parser.getOutputShape(); + + DEBUG("Compiling module: '" << module_name << "'"); + const char* device_uri = "local-sync"; + session = std::make_unique(device_uri, mlir); + DEBUG("compile complete."); + // Create index vectors into m_arg + // This is required since Jax expands input arguments through PyTrees, which need to + // be remapped to the corresponding expression call. For example: + // fcn(t, y, inputs, cj) with inputs = [[in1], [in2], [in3]] + // will produce a function with six inputs; we therefore need to be able to map + // arguments to their 1) corresponding input argument, and 2) the correct position + // within that argument. + m_arg_argno.clear(); + m_arg_argix.clear(); + int current_element = 0; + for (int i=0; i 2) || + ((input_shape[j].size() == 2) && (input_shape[j][1] > 1)) + ) { + std::cerr << "Unsupported input shape: " << input_shape[j].size() << " ["; + for (int k=0; k {res0} signature (i.e. x and z are reduced out) + // with kept_var_idx = [1] + // + // *********************************************************************************** + + DEBUG("Copying inputs, shape " << input_shape.size() << " - " << m_func.kept_var_idx.size()); + for (int j=0; j 1) { + // Index into argument using appropriate shape + for(int k=0; k(m_arg[m_arg_from][m_arg_argix[mlir_arg]+k]); + } + } else { + // Copy the entire vector + for(int k=0; k(m_arg[m_arg_from][k]); + } + } + } + + // Call the 'main' function of the module + const std::string mlir = m_func.get_mlir(); + DEBUG("Calling function '" << function_name << "'"); + auto status = session->iree_runtime_exec(function_name, input_shape, input_data, result); + if (!iree_status_is_ok(status)) { + iree_status_fprint(stderr, status); + std::cerr << "MLIR: " << mlir.substr(0,1000) << std::endl; + throw std::runtime_error("Execution failed"); + } + + // Copy results to output array + for(size_t k=0; k(result[k][j]); + } + } + + DEBUG("IreeFunction operator() complete"); +} + +expr_int IREEFunction::out_shape(int k) { + DEBUG("IreeFunction nnz(" << k << "): " << m_func.nnz); + auto elements = 1; + for (auto i : output_shape[k]) { + elements *= i; + } + return elements; +} + +expr_int IREEFunction::nnz() { + DEBUG("IreeFunction nnz: " << m_func.nnz); + return nnz_out(); +} + +expr_int IREEFunction::nnz_out() { + DEBUG("IreeFunction nnz_out" << m_func.nnz); + return m_func.nnz; +} + +std::vector IREEFunction::get_row() { + DEBUG("IreeFunction get_row" << m_func.row.size()); + return m_func.row; +} + +std::vector IREEFunction::get_col() { + DEBUG("IreeFunction get_col" << m_func.col.size()); + return m_func.col; +} + +void IREEFunction::operator()(const std::vector& inputs, + const std::vector& results) +{ + DEBUG("IreeFunction operator() with inputs and results"); + // Set-up input arguments, provide result vector, then execute function + // Example call: fcn({in1, in2, in3}, {out1}) + ASSERT(inputs.size() == m_func.n_args); + for(size_t k=0; k +#include "iree_jit.hpp" +#include "IREEFunction.hpp" + +/** + * @brief Class for handling iree functions + */ +class IREEFunctions : public ExpressionSet +{ +public: + std::unique_ptr iree_compiler; + + typedef IREEFunction::BaseFunctionType BaseFunctionType; // expose typedef in class + + int iree_init_status; + + int iree_init(const std::string& device_uri, const std::string& target_backends) { + // Initialise IREE + DEBUG("IREEFunctions: Initialising IREECompiler"); + iree_compiler = std::make_unique(device_uri.c_str()); + + int iree_argc = 2; + std::string target_backends_str = "--iree-hal-target-backends=" + target_backends; + const char* iree_argv[2] = {"iree", target_backends_str.c_str()}; + iree_compiler->init(iree_argc, iree_argv); + DEBUG("IREEFunctions: Initialised IREECompiler"); + return 0; + } + + int iree_init() { + return iree_init("local-sync", "llvm-cpu"); + } + + + /** + * @brief Create a new IREEFunctions object + */ + IREEFunctions( + const BaseFunctionType &rhs_alg, + const BaseFunctionType &jac_times_cjmass, + const int jac_times_cjmass_nnz, + const int jac_bandwidth_lower, + const int jac_bandwidth_upper, + const np_array_int &jac_times_cjmass_rowvals_arg, + const np_array_int &jac_times_cjmass_colptrs_arg, + const int inputs_length, + const BaseFunctionType &jac_action, + const BaseFunctionType &mass_action, + const BaseFunctionType &sens, + const BaseFunctionType &events, + const int n_s, + const int n_e, + const int n_p, + const std::vector& var_fcns, + const std::vector& dvar_dy_fcns, + const std::vector& dvar_dp_fcns, + const Options& options + ) : + iree_init_status(iree_init()), + rhs_alg_iree(rhs_alg), + jac_times_cjmass_iree(jac_times_cjmass), + jac_action_iree(jac_action), + mass_action_iree(mass_action), + sens_iree(sens), + events_iree(events), + ExpressionSet( + static_cast(&rhs_alg_iree), + static_cast(&jac_times_cjmass_iree), + jac_times_cjmass_nnz, + jac_bandwidth_lower, + jac_bandwidth_upper, + jac_times_cjmass_rowvals_arg, + jac_times_cjmass_colptrs_arg, + inputs_length, + static_cast(&jac_action_iree), + static_cast(&mass_action_iree), + static_cast(&sens_iree), + static_cast(&events_iree), + n_s, n_e, n_p, + options) + { + // convert BaseFunctionType list to IREEFunction list + // NOTE: You must allocate ALL std::vector elements before taking references + for (auto& var : var_fcns) + var_fcns_iree.push_back(IREEFunction(*var)); + for (int k = 0; k < var_fcns_iree.size(); k++) + ExpressionSet::var_fcns.push_back(&this->var_fcns_iree[k]); + + for (auto& var : dvar_dy_fcns) + dvar_dy_fcns_iree.push_back(IREEFunction(*var)); + for (int k = 0; k < dvar_dy_fcns_iree.size(); k++) + this->dvar_dy_fcns.push_back(&this->dvar_dy_fcns_iree[k]); + + for (auto& var : dvar_dp_fcns) + dvar_dp_fcns_iree.push_back(IREEFunction(*var)); + for (int k = 0; k < dvar_dp_fcns_iree.size(); k++) + this->dvar_dp_fcns.push_back(&this->dvar_dp_fcns_iree[k]); + + // copy across numpy array values + const int n_row_vals = jac_times_cjmass_rowvals_arg.request().size; + auto p_jac_times_cjmass_rowvals = jac_times_cjmass_rowvals_arg.unchecked<1>(); + jac_times_cjmass_rowvals.resize(n_row_vals); + for (int i = 0; i < n_row_vals; i++) { + jac_times_cjmass_rowvals[i] = p_jac_times_cjmass_rowvals[i]; + } + + const int n_col_ptrs = jac_times_cjmass_colptrs_arg.request().size; + auto p_jac_times_cjmass_colptrs = jac_times_cjmass_colptrs_arg.unchecked<1>(); + jac_times_cjmass_colptrs.resize(n_col_ptrs); + for (int i = 0; i < n_col_ptrs; i++) { + jac_times_cjmass_colptrs[i] = p_jac_times_cjmass_colptrs[i]; + } + + inputs.resize(inputs_length); + } + + IREEFunction rhs_alg_iree; + IREEFunction jac_times_cjmass_iree; + IREEFunction jac_action_iree; + IREEFunction mass_action_iree; + IREEFunction sens_iree; + IREEFunction events_iree; + + std::vector var_fcns_iree; + std::vector dvar_dy_fcns_iree; + std::vector dvar_dp_fcns_iree; + + realtype* get_tmp_state_vector() override { + return tmp_state_vector.data(); + } + realtype* get_tmp_sparse_jacobian_data() override { + return tmp_sparse_jacobian_data.data(); + } + + ~IREEFunctions() { + // cleanup IREE + iree_compiler->cleanup(); + } +}; + +#endif // PYBAMM_IDAKLU_IREE_FUNCTIONS_HPP diff --git a/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/ModuleParser.cpp b/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/ModuleParser.cpp new file mode 100644 index 0000000000..d1c5575ee2 --- /dev/null +++ b/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/ModuleParser.cpp @@ -0,0 +1,91 @@ +#include "ModuleParser.hpp" + +ModuleParser::ModuleParser(const std::string& mlir) : mlir(mlir) +{ + parse(); +} + +void ModuleParser::parse() +{ + // Parse module name + std::regex module_name_regex("module @([^\\s]+)"); // Match until first whitespace + std::smatch module_name_match; + std::regex_search(this->mlir, module_name_match, module_name_regex); + if (module_name_match.size() == 0) { + std::cerr << "Could not find module name in module" << std::endl; + std::cerr << "Module snippet: " << this->mlir.substr(0, 1000) << std::endl; + throw std::runtime_error("Could not find module name in module"); + } + module_name = module_name_match[1].str(); + DEBUG("Module name: " << module_name); + + // Assign function name + function_name = module_name + ".main"; + + // Isolate 'main' function call signature + std::regex main_func("public @main\\((.*?)\\) -> \\((.*?)\\)"); + std::smatch match; + std::regex_search(this->mlir, match, main_func); + if (match.size() == 0) { + std::cerr << "Could not find 'main' function in module" << std::endl; + std::cerr << "Module snippet: " << this->mlir.substr(0, 1000) << std::endl; + throw std::runtime_error("Could not find 'main' function in module"); + } + std::string main_sig_inputs = match[1].str(); + std::string main_sig_outputs = match[2].str(); + DEBUG( + "Main function signature: " << main_sig_inputs << " -> " << main_sig_outputs << '\n' + ); + + // Parse input sizes + input_shape.clear(); + std::regex input_size("tensor<(.*?)>"); + for(std::sregex_iterator i = std::sregex_iterator(main_sig_inputs.begin(), main_sig_inputs.end(), input_size); + i != std::sregex_iterator(); + ++i) + { + std::smatch matchi = *i; + std::string match_str = matchi.str(); + std::string shape_str = match_str.substr(7, match_str.size() - 8); // Remove 'tensor<>' from string + std::vector shape; + std::string dim_str; + for (char c : shape_str) { + if (c == 'x') { + shape.push_back(std::stoi(dim_str)); + dim_str = ""; + } else { + dim_str += c; + } + } + input_shape.push_back(shape); + } + + // Parse output sizes + output_shape.clear(); + std::regex output_size("tensor<(.*?)>"); + for( + std::sregex_iterator i = std::sregex_iterator(main_sig_outputs.begin(), main_sig_outputs.end(), output_size); + i != std::sregex_iterator(); + ++i + ) { + std::smatch matchi = *i; + std::string match_str = matchi.str(); + std::string shape_str = match_str.substr(7, match_str.size() - 8); // Remove 'tensor<>' from string + std::vector shape; + std::string dim_str; + for (char c : shape_str) { + if (c == 'x') { + shape.push_back(std::stoi(dim_str)); + dim_str = ""; + } else { + dim_str += c; + } + } + // If shape is empty, assume scalar (i.e. "tensor" or some singleton variant) + if (shape.size() == 0) { + shape.push_back(1); + } + // Add output to list + output_shape.push_back(shape); + } +} diff --git a/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/ModuleParser.hpp b/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/ModuleParser.hpp new file mode 100644 index 0000000000..2fbfdc086c --- /dev/null +++ b/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/ModuleParser.hpp @@ -0,0 +1,55 @@ +#ifndef PYBAMM_IDAKLU_IREE_MODULE_PARSER_HPP +#define PYBAMM_IDAKLU_IREE_MODULE_PARSER_HPP + +#include +#include +#include +#include +#include + +#include "../../common.hpp" + +class ModuleParser { +private: + std::string mlir; // cppcheck-suppress unusedStructMember + // codacy fix: member is referenced as this->mlir in parse() + std::string module_name; + std::string function_name; + std::vector> input_shape; + std::vector> output_shape; +public: + /** + * @brief Constructor + * @param mlir: string representation of MLIR code for the module + */ + explicit ModuleParser(const std::string& mlir); + + /** + * @brief Get the module name + * @return module name + */ + const std::string& getModuleName() const { return module_name; } + + /** + * @brief Get the function name + * @return function name + */ + const std::string& getFunctionName() const { return function_name; } + + /** + * @brief Get the input shape + * @return input shape + */ + const std::vector>& getInputShape() const { return input_shape; } + + /** + * @brief Get the output shape + * @return output shape + */ + const std::vector>& getOutputShape() const { return output_shape; } + +private: + void parse(); +}; + +#endif // PYBAMM_IDAKLU_IREE_MODULE_PARSER_HPP diff --git a/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/iree_jit.cpp b/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/iree_jit.cpp new file mode 100644 index 0000000000..c84c3928bd --- /dev/null +++ b/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/iree_jit.cpp @@ -0,0 +1,408 @@ +#include "iree_jit.hpp" +#include "iree/hal/buffer_view.h" +#include "iree/hal/buffer_view_util.h" +#include "../../common.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +// Used to suppress stderr output (see initIREE below) +#ifdef _WIN32 +#include +#define close _close +#define dup _dup +#define fileno _fileno +#define open _open +#define dup2 _dup2 +#define NULL_DEVICE "NUL" +#else +#define NULL_DEVICE "/dev/null" +#endif + +void IREESession::handle_compiler_error(iree_compiler_error_t *error) { + const char *msg = ireeCompilerErrorGetMessage(error); + fprintf(stderr, "Error from compiler API:\n%s\n", msg); + ireeCompilerErrorDestroy(error); +} + +void IREESession::cleanup_compiler_state(compiler_state_t s) { + if (s.inv) + ireeCompilerInvocationDestroy(s.inv); + if (s.output) + ireeCompilerOutputDestroy(s.output); + if (s.source) + ireeCompilerSourceDestroy(s.source); + if (s.session) + ireeCompilerSessionDestroy(s.session); +} + +IREECompiler::IREECompiler() { + this->device_uri = "local-sync"; +}; + +IREECompiler::~IREECompiler() { + ireeCompilerGlobalShutdown(); +}; + +int IREECompiler::init(int argc, const char **argv) { + return initIREE(argc, argv); // Initialisation and version checking +}; + +int IREECompiler::cleanup() { + return 0; +}; + +IREESession::IREESession() { + s.session = NULL; + s.source = NULL; + s.output = NULL; + s.inv = NULL; +}; + +IREESession::IREESession(const char *device_uri, const std::string& mlir_code) : IREESession() { + this->device_uri=device_uri; + this->mlir_code=mlir_code; + init(); +} + +int IREESession::init() { + if (initCompiler() != 0) // Prepare compiler inputs and outputs + return 1; + if (initCompileToByteCode() != 0) // Compile to bytecode + return 1; + if (initRuntime() != 0) // Initialise runtime environment + return 1; + return 0; +}; + +int IREECompiler::initIREE(int argc, const char **argv) { + + if (device_uri == NULL) { + DEBUG("No device URI provided, using local-sync\n"); + this->device_uri = "local-sync"; + } + + int cl_argc = argc; + const char *iree_compiler_lib = std::getenv("IREE_COMPILER_LIB"); + + // Load the compiler library and initialize it + // NOTE: On second and subsequent calls, the function will return false and display + // a message on stderr, but it is safe to ignore this message. For an improved user + // experience we actively suppress stderr during the call to this function but since + // this also suppresses any other error message, we actively check for the presence + // of the library file prior to the call. + + // Check if the library file exists + if (iree_compiler_lib == NULL) { + fprintf(stderr, "Error: IREE_COMPILER_LIB environment variable not set\n"); + return 1; + } + if (access(iree_compiler_lib, F_OK) == -1) { + fprintf(stderr, "Error: IREE_COMPILER_LIB file not found\n"); + return 1; + } + // Suppress stderr + int saved_stderr = dup(fileno(stderr)); + if (!freopen(NULL_DEVICE, "w", stderr)) + DEBUG("Error: failed redirecting stderr"); + // Load library + bool result = ireeCompilerLoadLibrary(iree_compiler_lib); + // Restore stderr + fflush(stderr); + dup2(saved_stderr, fileno(stderr)); + close(saved_stderr); + // Process result + if (!result) { + // Library may have already been loaded (can be safely ignored), + // or may not be found (critical error), we cannot tell which from the return value. + return 1; + } + // Must be balanced with a call to ireeCompilerGlobalShutdown() + ireeCompilerGlobalInitialize(); + + // To set global options (see `iree-compile --help` for possibilities), use + // |ireeCompilerGetProcessCLArgs| and |ireeCompilerSetupGlobalCL| + ireeCompilerGetProcessCLArgs(&cl_argc, &argv); + ireeCompilerSetupGlobalCL(cl_argc, argv, "iree-jit", false); + + // Check the API version before proceeding any further + uint32_t api_version = (uint32_t)ireeCompilerGetAPIVersion(); + uint16_t api_version_major = (uint16_t)((api_version >> 16) & 0xFFFFUL); + uint16_t api_version_minor = (uint16_t)(api_version & 0xFFFFUL); + DEBUG("Compiler API version: " << api_version_major << "." << api_version_minor); + if (api_version_major > IREE_COMPILER_EXPECTED_API_MAJOR || + api_version_minor < IREE_COMPILER_EXPECTED_API_MINOR) { + fprintf(stderr, + "Error: incompatible API version; built for version %" PRIu16 + ".%" PRIu16 " but loaded version %" PRIu16 ".%" PRIu16 "\n", + IREE_COMPILER_EXPECTED_API_MAJOR, IREE_COMPILER_EXPECTED_API_MINOR, + api_version_major, api_version_minor); + ireeCompilerGlobalShutdown(); + return 1; + } + + // Check for a build tag with release version information + const char *revision = ireeCompilerGetRevision(); // cppcheck-suppress unreadVariable + DEBUG("Compiler revision: '" << revision << "'"); + return 0; +}; + +int IREESession::initCompiler() { + + // A session provides a scope where one or more invocations can be executed + s.session = ireeCompilerSessionCreate(); + + // Read the MLIR from memory + error = ireeCompilerSourceWrapBuffer( + s.session, + "expr_buffer", // name of the buffer (does not need to match MLIR) + mlir_code.c_str(), + mlir_code.length() + 1, + true, + &s.source + ); + if (error) { + fprintf(stderr, "Error wrapping source buffer\n"); + handle_compiler_error(error); + cleanup_compiler_state(s); + return 1; + } + DEBUG("Wrapped buffer as a compiler source"); + + return 0; +}; + +int IREESession::initCompileToByteCode() { + // Use an invocation to compile from the input source to the output stream + iree_compiler_invocation_t *inv = ireeCompilerInvocationCreate(s.session); + ireeCompilerInvocationEnableConsoleDiagnostics(inv); + + if (!ireeCompilerInvocationParseSource(inv, s.source)) { + fprintf(stderr, "Error parsing input source into invocation\n"); + cleanup_compiler_state(s); + return 1; + } + + // Compile, specifying the target dialect phase + ireeCompilerInvocationSetCompileToPhase(inv, "end"); + + // Run the compiler invocation pipeline + if (!ireeCompilerInvocationPipeline(inv, IREE_COMPILER_PIPELINE_STD)) { + fprintf(stderr, "Error running compiler invocation\n"); + cleanup_compiler_state(s); + return 1; + } + DEBUG("Compilation successful"); + + // Create compiler 'output' to a memory buffer + error = ireeCompilerOutputOpenMembuffer(&s.output); + if (error) { + fprintf(stderr, "Error opening output membuffer\n"); + handle_compiler_error(error); + cleanup_compiler_state(s); + return 1; + } + + // Create bytecode in memory + error = ireeCompilerInvocationOutputVMBytecode(inv, s.output); + if (error) { + fprintf(stderr, "Error creating VM bytecode\n"); + handle_compiler_error(error); + cleanup_compiler_state(s); + return 1; + } + + // Once the bytecode has been written, retrieve the memory map + ireeCompilerOutputMapMemory(s.output, &contents, &size); + + return 0; +}; + +int IREESession::initRuntime() { + // Setup the shared runtime instance + iree_runtime_instance_options_t instance_options; + iree_runtime_instance_options_initialize(&instance_options); + iree_runtime_instance_options_use_all_available_drivers(&instance_options); + status = iree_runtime_instance_create( + &instance_options, iree_allocator_system(), &instance); + + // Create the HAL device used to run the workloads + if (iree_status_is_ok(status)) { + status = iree_hal_create_device( + iree_runtime_instance_driver_registry(instance), + iree_make_cstring_view(device_uri), + iree_runtime_instance_host_allocator(instance), &device); + } + + // Set up the session to run the module + if (iree_status_is_ok(status)) { + iree_runtime_session_options_t session_options; + iree_runtime_session_options_initialize(&session_options); + status = iree_runtime_session_create_with_device( + instance, &session_options, device, + iree_runtime_instance_host_allocator(instance), &session); + } + + // Load the compiled user module from a file + if (iree_status_is_ok(status)) { + /*status = iree_runtime_session_append_bytecode_module_from_file(session, module_path);*/ + status = iree_runtime_session_append_bytecode_module_from_memory( + session, + iree_make_const_byte_span(contents, size), + iree_allocator_null()); + } + + if (!iree_status_is_ok(status)) + return 1; + + return 0; +}; + +// Release the session and free all cached resources. +int IREESession::cleanup() { + iree_runtime_session_release(session); + iree_hal_device_release(device); + iree_runtime_instance_release(instance); + + int ret = (int)iree_status_code(status); + if (!iree_status_is_ok(status)) { + iree_status_fprint(stderr, status); + iree_status_ignore(status); + } + cleanup_compiler_state(s); + return ret; +} + +iree_status_t IREESession::iree_runtime_exec( + const std::string& function_name, + const std::vector>& inputs, + const std::vector>& data, + std::vector>& result +) { + + // Initialize the call to the function. + status = iree_runtime_call_initialize_by_name( + session, iree_make_cstring_view(function_name.c_str()), &call); + if (!iree_status_is_ok(status)) { + std::cerr << "Error: iree_runtime_call_initialize_by_name failed" << std::endl; + iree_status_fprint(stderr, status); + return status; + } + + // Append the function inputs with the HAL device allocator in use by the + // session. The buffers will be usable within the session and _may_ be usable + // in other sessions depending on whether they share a compatible device. + iree_hal_allocator_t* device_allocator = + iree_runtime_session_device_allocator(session); + host_allocator = iree_runtime_session_host_allocator(session); + status = iree_ok_status(); + if (iree_status_is_ok(status)) { + + for(int k=0; k arg_shape(input_shape.size()); + for (int i = 0; i < input_shape.size(); i++) { + arg_shape[i] = input_shape[i]; + } + int numel = 1; + for(int i = 0; i < input_shape.size(); i++) { + numel *= input_shape[i]; + } + std::vector arg_data(numel); + for(int i = 0; i < numel; i++) { + arg_data[i] = input_data[i]; + } + + status = iree_hal_buffer_view_allocate_buffer_copy( + device, device_allocator, + // Shape rank and dimensions: + arg_shape.size(), arg_shape.data(), + // Element type: + IREE_HAL_ELEMENT_TYPE_FLOAT_32, + // Encoding type: + IREE_HAL_ENCODING_TYPE_DENSE_ROW_MAJOR, + (iree_hal_buffer_params_t){ + // Intended usage of the buffer (transfers, dispatches, etc): + .usage = IREE_HAL_BUFFER_USAGE_DEFAULT, + // Access to allow to this memory: + .access = IREE_HAL_MEMORY_ACCESS_ALL, + // Where to allocate (host or device): + .type = IREE_HAL_MEMORY_TYPE_DEVICE_LOCAL, + }, + // The actual heap buffer to wrap or clone and its allocator: + iree_make_const_byte_span(&arg_data[0], sizeof(float) * arg_data.size()), + // Buffer view + storage are returned and owned by the caller: + &arg); + } + if (iree_status_is_ok(status)) { + // Add to the call inputs list (which retains the buffer view). + status = iree_runtime_call_inputs_push_back_buffer_view(&call, arg); + if (!iree_status_is_ok(status)) { + std::cerr << "Error: iree_runtime_call_inputs_push_back_buffer_view failed" << std::endl; + iree_status_fprint(stderr, status); + } + } + // Since the call retains the buffer view we can release it here. + iree_hal_buffer_view_release(arg); + } + } + + // Synchronously perform the call. + if (iree_status_is_ok(status)) { + status = iree_runtime_call_invoke(&call, /*flags=*/0); + } + if (!iree_status_is_ok(status)) { + std::cerr << "Error: iree_runtime_call_invoke failed" << std::endl; + iree_status_fprint(stderr, status); + } + + for(int k=0; k +#include +#include +#include + +#include +#include +#include + +#define IREE_COMPILER_EXPECTED_API_MAJOR 1 // At most this major version +#define IREE_COMPILER_EXPECTED_API_MINOR 2 // At least this minor version + +// Forward declaration +class IREESession; + +/* + * @brief IREECompiler class + * @details This class is used to compile MLIR code to IREE bytecode and + * create IREE sessions. + */ +class IREECompiler { +private: + /* + * @brief Device Uniform Resource Identifier (URI) + * @details The device URI is used to specify the device to be used by the + * IREE runtime. E.g. "local-sync" for CPU, "vulkan" for GPU, etc. + */ + const char *device_uri = NULL; + +private: + /* + * @brief Initialize the IREE runtime + */ + int initIREE(int argc, const char **argv); + +public: + /* + * @brief Default constructor + */ + IREECompiler(); + + /* + * @brief Destructor + */ + ~IREECompiler(); + + /* + * @brief Constructor with device URI + * @param device_uri Device URI + */ + explicit IREECompiler(const char *device_uri) + : IREECompiler() { this->device_uri=device_uri; } + + /* + * @brief Initialize the compiler + */ + int init(int argc, const char **argv); + + /* + * @brief Cleanup the compiler + * @details This method cleans up the compiler and all the IREE sessions + * created by the compiler. Returns 0 on success. + */ + int cleanup(); +}; + +/* + * @brief Compiler state + */ +typedef struct compiler_state_t { + iree_compiler_session_t *session; // cppcheck-suppress unusedStructMember + iree_compiler_source_t *source; // cppcheck-suppress unusedStructMember + iree_compiler_output_t *output; // cppcheck-suppress unusedStructMember + iree_compiler_invocation_t *inv; // cppcheck-suppress unusedStructMember +} compiler_state_t; + +/* + * @brief IREE session class + */ +class IREESession { +private: // data members + const char *device_uri = NULL; + compiler_state_t s; + iree_compiler_error_t *error = NULL; + void *contents = NULL; + uint64_t size = 0; + iree_runtime_session_t* session = NULL; + iree_status_t status; + iree_hal_device_t* device = NULL; + iree_runtime_instance_t* instance = NULL; + std::string mlir_code; // cppcheck-suppress unusedStructMember + iree_runtime_call_t call; + iree_allocator_t host_allocator; + +private: // private methods + void handle_compiler_error(iree_compiler_error_t *error); + void cleanup_compiler_state(compiler_state_t s); + int init(); + int initCompiler(); + int initCompileToByteCode(); + int initRuntime(); + +public: // public methods + + /* + * @brief Default constructor + */ + IREESession(); + + /* + * @brief Constructor with device URI and MLIR code + * @param device_uri Device URI + * @param mlir_code MLIR code + */ + explicit IREESession(const char *device_uri, const std::string& mlir_code); + + /* + * @brief Cleanup the IREE session + */ + int cleanup(); + + /* + * @brief Execute the pre-compiled byte-code with the given inputs + * @param function_name Function name to execute + * @param inputs List of input shapes + * @param data List of input data + * @param result List of output data + */ + iree_status_t iree_runtime_exec( + const std::string& function_name, + const std::vector>& inputs, + const std::vector>& data, + std::vector>& result + ); +}; + +#endif // IREE_JIT_HPP diff --git a/pybamm/solvers/c_solvers/idaklu/IDAKLUSolver.cpp b/pybamm/solvers/c_solvers/idaklu/IDAKLUSolver.cpp new file mode 100644 index 0000000000..b769d4d1d4 --- /dev/null +++ b/pybamm/solvers/c_solvers/idaklu/IDAKLUSolver.cpp @@ -0,0 +1 @@ +#include "IDAKLUSolver.hpp" diff --git a/pybamm/solvers/c_solvers/idaklu/CasadiSolver.hpp b/pybamm/solvers/c_solvers/idaklu/IDAKLUSolver.hpp similarity index 75% rename from pybamm/solvers/c_solvers/idaklu/CasadiSolver.hpp rename to pybamm/solvers/c_solvers/idaklu/IDAKLUSolver.hpp index dac94579f3..26e587e424 100644 --- a/pybamm/solvers/c_solvers/idaklu/CasadiSolver.hpp +++ b/pybamm/solvers/c_solvers/idaklu/IDAKLUSolver.hpp @@ -1,33 +1,27 @@ #ifndef PYBAMM_IDAKLU_CASADI_SOLVER_HPP #define PYBAMM_IDAKLU_CASADI_SOLVER_HPP -#include -using Function = casadi::Function; - -#include "casadi_functions.hpp" #include "common.hpp" -#include "options.hpp" -#include "solution.hpp" -#include "sundials_legacy_wrapper.hpp" +#include "Solution.hpp" /** * Abstract base class for solutions that can use different solvers and vector * implementations. * @brief An abstract base class for the Idaklu solver */ -class CasadiSolver +class IDAKLUSolver { public: /** * @brief Default constructor */ - CasadiSolver() = default; + IDAKLUSolver() = default; /** * @brief Default destructor */ - ~CasadiSolver() = default; + ~IDAKLUSolver() = default; /** * @brief Abstract solver method that returns a Solution class diff --git a/pybamm/solvers/c_solvers/idaklu/CasadiSolverOpenMP.hpp b/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.hpp similarity index 82% rename from pybamm/solvers/c_solvers/idaklu/CasadiSolverOpenMP.hpp rename to pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.hpp index 2312f9cf8f..8c49069b30 100644 --- a/pybamm/solvers/c_solvers/idaklu/CasadiSolverOpenMP.hpp +++ b/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.hpp @@ -1,14 +1,10 @@ -#ifndef PYBAMM_IDAKLU_CASADISOLVEROPENMP_HPP -#define PYBAMM_IDAKLU_CASADISOLVEROPENMP_HPP +#ifndef PYBAMM_IDAKLU_SOLVEROPENMP_HPP +#define PYBAMM_IDAKLU_SOLVEROPENMP_HPP -#include "CasadiSolver.hpp" -#include -using Function = casadi::Function; - -#include "casadi_functions.hpp" +#include "IDAKLUSolver.hpp" #include "common.hpp" -#include "options.hpp" -#include "solution.hpp" +#include "Options.hpp" +#include "Solution.hpp" #include "sundials_legacy_wrapper.hpp" /** @@ -40,7 +36,8 @@ using Function = casadi::Function; * 19. Destroy objects * 20. (N/A) Finalize MPI */ -class CasadiSolverOpenMP : public CasadiSolver +template +class IDAKLUSolverOpenMP : public IDAKLUSolver { // NB: cppcheck-suppress unusedStructMember is used because codacy reports // these members as unused even though they are important in child @@ -63,10 +60,10 @@ class CasadiSolverOpenMP : public CasadiSolver int jac_bandwidth_upper; // cppcheck-suppress unusedStructMember SUNMatrix J; SUNLinearSolver LS = nullptr; - std::unique_ptr functions; - realtype *res = nullptr; - realtype *res_dvar_dy = nullptr; - realtype *res_dvar_dp = nullptr; + std::unique_ptr functions; + std::vector res; + std::vector res_dvar_dy; + std::vector res_dvar_dp; Options options; #if SUNDIALS_VERSION_MAJOR >= 6 @@ -77,7 +74,7 @@ class CasadiSolverOpenMP : public CasadiSolver /** * @brief Constructor */ - CasadiSolverOpenMP( + IDAKLUSolverOpenMP( np_array atol_np, double rel_tol, np_array rhs_alg_id, @@ -86,18 +83,18 @@ class CasadiSolverOpenMP : public CasadiSolver int jac_times_cjmass_nnz, int jac_bandwidth_lower, int jac_bandwidth_upper, - std::unique_ptr functions, + std::unique_ptr functions, const Options& options); /** * @brief Destructor */ - ~CasadiSolverOpenMP(); + ~IDAKLUSolverOpenMP(); /** - * Evaluate casadi functions (including sensitivies) for each requested + * Evaluate functions (including sensitivies) for each requested * variable and store - * @brief Evaluate casadi functions + * @brief Evaluate functions */ void CalcVars( realtype *y_return, @@ -110,7 +107,7 @@ class CasadiSolverOpenMP : public CasadiSolver size_t *ySk); /** - * @brief Evaluate casadi functions for sensitivities + * @brief Evaluate functions for sensitivities */ void CalcVarsSensitivities( realtype *tret, @@ -144,4 +141,6 @@ class CasadiSolverOpenMP : public CasadiSolver void SetMatrix(); }; -#endif // PYBAMM_IDAKLU_CASADISOLVEROPENMP_HPP +#include "IDAKLUSolverOpenMP.inl" + +#endif // PYBAMM_IDAKLU_SOLVEROPENMP_HPP diff --git a/pybamm/solvers/c_solvers/idaklu/CasadiSolverOpenMP.cpp b/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl similarity index 82% rename from pybamm/solvers/c_solvers/idaklu/CasadiSolverOpenMP.cpp rename to pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl index ad51eda4e1..383037e2ca 100644 --- a/pybamm/solvers/c_solvers/idaklu/CasadiSolverOpenMP.cpp +++ b/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl @@ -1,10 +1,8 @@ -#include "CasadiSolverOpenMP.hpp" -#include "casadi_sundials_functions.hpp" -#include -#include -#include +#include "Expressions/Expressions.hpp" +#include "sundials_functions.hpp" -CasadiSolverOpenMP::CasadiSolverOpenMP( +template +IDAKLUSolverOpenMP::IDAKLUSolverOpenMP( np_array atol_np, double rel_tol, np_array rhs_alg_id, @@ -13,7 +11,7 @@ CasadiSolverOpenMP::CasadiSolverOpenMP( int jac_times_cjmass_nnz, int jac_bandwidth_lower, int jac_bandwidth_upper, - std::unique_ptr functions_arg, + std::unique_ptr functions_arg, const Options &options ) : atol_np(atol_np), @@ -28,8 +26,8 @@ CasadiSolverOpenMP::CasadiSolverOpenMP( options(options) { // Construction code moved to Initialize() which is called from the - // (child) CasadiSolver_XXX class constructors. - DEBUG("CasadiSolverOpenMP::CasadiSolverOpenMP"); + // (child) IDAKLUSolver_* class constructors. + DEBUG("IDAKLUSolverOpenMP:IDAKLUSolverOpenMP"); auto atol = atol_np.unchecked<1>(); // create SUNDIALS context object @@ -59,14 +57,14 @@ CasadiSolverOpenMP::CasadiSolverOpenMP( SetMatrix(); // initialise solver - IDAInit(ida_mem, residual_casadi, 0, yy, yp); + IDAInit(ida_mem, residual_eval, 0, yy, yp); // set tolerances rtol = RCONST(rel_tol); IDASVtolerances(ida_mem, rtol, avtol); // set events - IDARootInit(ida_mem, number_of_events, events_casadi); + IDARootInit(ida_mem, number_of_events, events_eval); void *user_data = functions.get(); IDASetUserData(ida_mem, user_data); @@ -77,7 +75,8 @@ CasadiSolverOpenMP::CasadiSolverOpenMP( } } -void CasadiSolverOpenMP::AllocateVectors() { +template +void IDAKLUSolverOpenMP::AllocateVectors() { // Create vectors yy = N_VNew_OpenMP(number_of_states, options.num_threads, sunctx); yp = N_VNew_OpenMP(number_of_states, options.num_threads, sunctx); @@ -85,7 +84,8 @@ void CasadiSolverOpenMP::AllocateVectors() { id = N_VNew_OpenMP(number_of_states, options.num_threads, sunctx); } -void CasadiSolverOpenMP::SetMatrix() { +template +void IDAKLUSolverOpenMP::SetMatrix() { // Create Matrix object if (options.jacobian == "sparse") { @@ -94,7 +94,7 @@ void CasadiSolverOpenMP::SetMatrix() { number_of_states, number_of_states, jac_times_cjmass_nnz, - CSC_MAT, // CSC is used by casadi; CSR requires a conversion step + CSC_MAT, sunctx ); } @@ -124,7 +124,8 @@ void CasadiSolverOpenMP::SetMatrix() { throw std::invalid_argument("Unsupported matrix requested"); } -void CasadiSolverOpenMP::Initialize() { +template +void IDAKLUSolverOpenMP::Initialize() { // Call after setting the solver // attach the linear solver @@ -139,18 +140,18 @@ void CasadiSolverOpenMP::Initialize() { IDABBDPrecInit( ida_mem, number_of_states, options.precon_half_bandwidth, options.precon_half_bandwidth, options.precon_half_bandwidth_keep, - options.precon_half_bandwidth_keep, 0.0, residual_casadi_approx, NULL); + options.precon_half_bandwidth_keep, 0.0, residual_eval_approx, NULL); } if (options.jacobian == "matrix-free") - IDASetJacTimes(ida_mem, NULL, jtimes_casadi); + IDASetJacTimes(ida_mem, NULL, jtimes_eval); else if (options.jacobian != "none") - IDASetJacFn(ida_mem, jacobian_casadi); + IDASetJacFn(ida_mem, jacobian_eval); if (number_of_parameters > 0) { IDASensInit(ida_mem, number_of_parameters, IDA_SIMULTANEOUS, - sensitivities_casadi, yyS, ypS); + sensitivities_eval, yyS, ypS); IDASensEEtolerances(ida_mem); } @@ -167,7 +168,8 @@ void CasadiSolverOpenMP::Initialize() { IDASetId(ida_mem, id); } -CasadiSolverOpenMP::~CasadiSolverOpenMP() +template +IDAKLUSolverOpenMP::~IDAKLUSolverOpenMP() { // Free memory if (number_of_parameters > 0) @@ -190,7 +192,8 @@ CasadiSolverOpenMP::~CasadiSolverOpenMP() SUNContext_Free(&sunctx); } -void CasadiSolverOpenMP::CalcVars( +template +void IDAKLUSolverOpenMP::CalcVars( realtype *y_return, size_t length_of_return_vector, size_t t_i, @@ -200,61 +203,61 @@ void CasadiSolverOpenMP::CalcVars( realtype *yS_return, size_t *ySk ) { - // Evaluate casadi functions for each requested variable and store + DEBUG("IDAKLUSolver::CalcVars"); + // Evaluate functions for each requested variable and store size_t j = 0; - for (auto& var_fcn : functions->var_casadi_fcns) { - var_fcn({tret, yval, functions->inputs.data()}, {res}); + for (auto& var_fcn : functions->var_fcns) { + (*var_fcn)({tret, yval, functions->inputs.data()}, {&res[0]}); // store in return vector - for (size_t jj=0; jjnnz_out(); jj++) y_return[t_i*length_of_return_vector + j++] = res[jj]; } // calculate sensitivities CalcVarsSensitivities(tret, yval, ySval, yS_return, ySk); } -void CasadiSolverOpenMP::CalcVarsSensitivities( +template +void IDAKLUSolverOpenMP::CalcVarsSensitivities( realtype *tret, realtype *yval, const std::vector& ySval, realtype *yS_return, size_t *ySk ) { + DEBUG("IDAKLUSolver::CalcVarsSensitivities"); // Calculate sensitivities - - // Loop over variables - realtype* dens_dvar_dp = new realtype[number_of_parameters]; + std::vector dens_dvar_dp = std::vector(number_of_parameters, 0); for (size_t dvar_k=0; dvar_kdvar_dy_fcns.size(); dvar_k++) { // Isolate functions - CasadiFunction dvar_dy = functions->dvar_dy_fcns[dvar_k]; - CasadiFunction dvar_dp = functions->dvar_dp_fcns[dvar_k]; + Expression* dvar_dy = functions->dvar_dy_fcns[dvar_k]; + Expression* dvar_dp = functions->dvar_dp_fcns[dvar_k]; // Calculate dvar/dy - dvar_dy({tret, yval, functions->inputs.data()}, {res_dvar_dy}); - casadi::Sparsity spdy = dvar_dy.sparsity_out(0); + (*dvar_dy)({tret, yval, functions->inputs.data()}, {&res_dvar_dy[0]}); // Calculate dvar/dp and convert to dense array for indexing - dvar_dp({tret, yval, functions->inputs.data()}, {res_dvar_dp}); - casadi::Sparsity spdp = dvar_dp.sparsity_out(0); + (*dvar_dp)({tret, yval, functions->inputs.data()}, {&res_dvar_dp[0]}); for(int k=0; knnz_out(); k++) + dens_dvar_dp[dvar_dp->get_row()[k]] = res_dvar_dp[k]; // Calculate sensitivities for(int paramk=0; paramknnz_out(); spk++) + yS_return[*ySk] += res_dvar_dy[spk] * ySval[paramk][dvar_dy->get_col()[spk]]; (*ySk)++; } } } -Solution CasadiSolverOpenMP::solve( +template +Solution IDAKLUSolverOpenMP::solve( np_array t_np, np_array y0_np, np_array yp0_np, np_array_dense inputs ) { - DEBUG("CasadiSolver::solve"); + DEBUG("IDAKLUSolver::solve"); int number_of_timesteps = t_np.request().size; auto t = t_np.unchecked<1>(); @@ -315,15 +318,15 @@ Solution CasadiSolverOpenMP::solve( int length_of_return_vector = 0; size_t max_res_size = 0; // maximum result size (for common result buffer) size_t max_res_dvar_dy = 0, max_res_dvar_dp = 0; - if (functions->var_casadi_fcns.size() > 0) { + if (functions->var_fcns.size() > 0) { // return only the requested variables list after computation - for (auto& var_fcn : functions->var_casadi_fcns) { - max_res_size = std::max(max_res_size, size_t(var_fcn.nnz_out())); - length_of_return_vector += var_fcn.nnz_out(); + for (auto& var_fcn : functions->var_fcns) { + max_res_size = std::max(max_res_size, size_t(var_fcn->out_shape(0))); + length_of_return_vector += var_fcn->nnz_out(); for (auto& dvar_fcn : functions->dvar_dy_fcns) - max_res_dvar_dy = std::max(max_res_dvar_dy, size_t(dvar_fcn.nnz_out())); + max_res_dvar_dy = std::max(max_res_dvar_dy, size_t(dvar_fcn->out_shape(0))); for (auto& dvar_fcn : functions->dvar_dp_fcns) - max_res_dvar_dp = std::max(max_res_dvar_dp, size_t(dvar_fcn.nnz_out())); + max_res_dvar_dp = std::max(max_res_dvar_dp, size_t(dvar_fcn->out_shape(0))); } } else { // Return full y state-vector @@ -336,9 +339,9 @@ Solution CasadiSolverOpenMP::solve( number_of_timesteps * length_of_return_vector]; - res = new realtype[max_res_size]; - res_dvar_dy = new realtype[max_res_dvar_dy]; - res_dvar_dp = new realtype[max_res_dvar_dp]; + res.resize(max_res_size); + res_dvar_dy.resize(max_res_dvar_dy); + res_dvar_dp.resize(max_res_dvar_dp); py::capsule free_t_when_done( t_return, @@ -366,8 +369,8 @@ Solution CasadiSolverOpenMP::solve( int t_i = 0; size_t ySk = 0; t_return[t_i] = t(t_i); - if (functions->var_casadi_fcns.size() > 0) { - // Evaluate casadi functions for each requested variable and store + if (functions->var_fcns.size() > 0) { + // Evaluate functions for each requested variable and store CalcVars(y_return, length_of_return_vector, t_i, &tret, yval, ySval, yS_return, &ySk); } else { @@ -401,8 +404,8 @@ Solution CasadiSolverOpenMP::solve( // Evaluate and store results for the time step t_return[t_i] = tret; - if (functions->var_casadi_fcns.size() > 0) { - // Evaluate casadi functions for each requested variable and store + if (functions->var_fcns.size() > 0) { + // Evaluate functions for each requested variable and store // NOTE: Indexing of yS_return is (time:var:param) CalcVars(y_return, length_of_return_vector, t_i, &tret, yval, ySval, yS_return, &ySk); @@ -446,7 +449,7 @@ Solution CasadiSolverOpenMP::solve( // Note: Ordering of vector is differnet if computing variables vs returning // the complete state vector np_array yS_ret; - if (functions->var_casadi_fcns.size() > 0) { + if (functions->var_fcns.size() > 0) { yS_ret = np_array( std::vector { number_of_timesteps, diff --git a/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP_solvers.cpp b/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP_solvers.cpp new file mode 100644 index 0000000000..45ceed0ada --- /dev/null +++ b/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP_solvers.cpp @@ -0,0 +1 @@ +#include "IDAKLUSolverOpenMP_solvers.hpp" diff --git a/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP_solvers.hpp b/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP_solvers.hpp new file mode 100644 index 0000000000..ebeb543232 --- /dev/null +++ b/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP_solvers.hpp @@ -0,0 +1,131 @@ +#ifndef PYBAMM_IDAKLU_CASADI_SOLVER_OPENMP_HPP +#define PYBAMM_IDAKLU_CASADI_SOLVER_OPENMP_HPP + +#include "IDAKLUSolverOpenMP.hpp" + +/** + * @brief IDAKLUSolver Dense implementation with OpenMP class + */ +template +class IDAKLUSolverOpenMP_Dense : public IDAKLUSolverOpenMP { +public: + using Base = IDAKLUSolverOpenMP; + template + IDAKLUSolverOpenMP_Dense(Args&& ... args) : Base(std::forward(args) ...) + { + Base::LS = SUNLinSol_Dense(Base::yy, Base::J, Base::sunctx); + Base::Initialize(); + } +}; + +/** + * @brief IDAKLUSolver KLU implementation with OpenMP class + */ +template +class IDAKLUSolverOpenMP_KLU : public IDAKLUSolverOpenMP { +public: + using Base = IDAKLUSolverOpenMP; + template + IDAKLUSolverOpenMP_KLU(Args&& ... args) : Base(std::forward(args) ...) + { + Base::LS = SUNLinSol_KLU(Base::yy, Base::J, Base::sunctx); + Base::Initialize(); + } +}; + +/** + * @brief IDAKLUSolver Banded implementation with OpenMP class + */ +template +class IDAKLUSolverOpenMP_Band : public IDAKLUSolverOpenMP { +public: + using Base = IDAKLUSolverOpenMP; + template + IDAKLUSolverOpenMP_Band(Args&& ... args) : Base(std::forward(args) ...) + { + Base::LS = SUNLinSol_Band(Base::yy, Base::J, Base::sunctx); + Base::Initialize(); + } +}; + +/** + * @brief IDAKLUSolver SPBCGS implementation with OpenMP class + */ +template +class IDAKLUSolverOpenMP_SPBCGS : public IDAKLUSolverOpenMP { +public: + using Base = IDAKLUSolverOpenMP; + template + IDAKLUSolverOpenMP_SPBCGS(Args&& ... args) : Base(std::forward(args) ...) + { + Base::LS = SUNLinSol_SPBCGS( + Base::yy, + Base::precon_type, + Base::options.linsol_max_iterations, + Base::sunctx + ); + Base::Initialize(); + } +}; + +/** + * @brief IDAKLUSolver SPFGMR implementation with OpenMP class + */ +template +class IDAKLUSolverOpenMP_SPFGMR : public IDAKLUSolverOpenMP { +public: + using Base = IDAKLUSolverOpenMP; + template + IDAKLUSolverOpenMP_SPFGMR(Args&& ... args) : Base(std::forward(args) ...) + { + Base::LS = SUNLinSol_SPFGMR( + Base::yy, + Base::precon_type, + Base::options.linsol_max_iterations, + Base::sunctx + ); + Base::Initialize(); + } +}; + +/** + * @brief IDAKLUSolver SPGMR implementation with OpenMP class + */ +template +class IDAKLUSolverOpenMP_SPGMR : public IDAKLUSolverOpenMP { +public: + using Base = IDAKLUSolverOpenMP; + template + IDAKLUSolverOpenMP_SPGMR(Args&& ... args) : Base(std::forward(args) ...) + { + Base::LS = SUNLinSol_SPGMR( + Base::yy, + Base::precon_type, + Base::options.linsol_max_iterations, + Base::sunctx + ); + Base::Initialize(); + } +}; + +/** + * @brief IDAKLUSolver SPTFQMR implementation with OpenMP class + */ +template +class IDAKLUSolverOpenMP_SPTFQMR : public IDAKLUSolverOpenMP { +public: + using Base = IDAKLUSolverOpenMP; + template + IDAKLUSolverOpenMP_SPTFQMR(Args&& ... args) : Base(std::forward(args) ...) + { + Base::LS = SUNLinSol_SPTFQMR( + Base::yy, + Base::precon_type, + Base::options.linsol_max_iterations, + Base::sunctx + ); + Base::Initialize(); + } +}; + +#endif // PYBAMM_IDAKLU_CASADI_SOLVER_OPENMP_HPP diff --git a/pybamm/solvers/c_solvers/idaklu/idaklu_jax.cpp b/pybamm/solvers/c_solvers/idaklu/IdakluJax.cpp similarity index 99% rename from pybamm/solvers/c_solvers/idaklu/idaklu_jax.cpp rename to pybamm/solvers/c_solvers/idaklu/IdakluJax.cpp index b338560259..15c2b2d811 100644 --- a/pybamm/solvers/c_solvers/idaklu/idaklu_jax.cpp +++ b/pybamm/solvers/c_solvers/idaklu/IdakluJax.cpp @@ -1,4 +1,4 @@ -#include "idaklu_jax.hpp" +#include "IdakluJax.hpp" #include #include diff --git a/pybamm/solvers/c_solvers/idaklu/idaklu_jax.hpp b/pybamm/solvers/c_solvers/idaklu/IdakluJax.hpp similarity index 100% rename from pybamm/solvers/c_solvers/idaklu/idaklu_jax.hpp rename to pybamm/solvers/c_solvers/idaklu/IdakluJax.hpp diff --git a/pybamm/solvers/c_solvers/idaklu/options.cpp b/pybamm/solvers/c_solvers/idaklu/Options.cpp similarity index 99% rename from pybamm/solvers/c_solvers/idaklu/options.cpp rename to pybamm/solvers/c_solvers/idaklu/Options.cpp index efad4d5de0..684ab47f33 100644 --- a/pybamm/solvers/c_solvers/idaklu/options.cpp +++ b/pybamm/solvers/c_solvers/idaklu/Options.cpp @@ -1,4 +1,4 @@ -#include "options.hpp" +#include "Options.hpp" #include #include diff --git a/pybamm/solvers/c_solvers/idaklu/options.hpp b/pybamm/solvers/c_solvers/idaklu/Options.hpp similarity index 100% rename from pybamm/solvers/c_solvers/idaklu/options.hpp rename to pybamm/solvers/c_solvers/idaklu/Options.hpp diff --git a/pybamm/solvers/c_solvers/idaklu/Solution.cpp b/pybamm/solvers/c_solvers/idaklu/Solution.cpp new file mode 100644 index 0000000000..7b50364379 --- /dev/null +++ b/pybamm/solvers/c_solvers/idaklu/Solution.cpp @@ -0,0 +1 @@ +#include "Solution.hpp" diff --git a/pybamm/solvers/c_solvers/idaklu/solution.hpp b/pybamm/solvers/c_solvers/idaklu/Solution.hpp similarity index 100% rename from pybamm/solvers/c_solvers/idaklu/solution.hpp rename to pybamm/solvers/c_solvers/idaklu/Solution.hpp diff --git a/pybamm/solvers/c_solvers/idaklu/casadi_functions.cpp b/pybamm/solvers/c_solvers/idaklu/casadi_functions.cpp deleted file mode 100644 index ddad4612c9..0000000000 --- a/pybamm/solvers/c_solvers/idaklu/casadi_functions.cpp +++ /dev/null @@ -1,105 +0,0 @@ -#include "casadi_functions.hpp" - -CasadiFunction::CasadiFunction(const Function &f) : m_func(f) -{ - size_t sz_arg; - size_t sz_res; - size_t sz_iw; - size_t sz_w; - m_func.sz_work(sz_arg, sz_res, sz_iw, sz_w); - //int nnz = (sz_res>0) ? m_func.nnz_out() : 0; - //std::cout << "name = "<< m_func.name() << " arg = " << sz_arg << " res = " - // << sz_res << " iw = " << sz_iw << " w = " << sz_w << " nnz = " << nnz << - // std::endl; - m_arg.resize(sz_arg, nullptr); - m_res.resize(sz_res, nullptr); - m_iw.resize(sz_iw, 0); - m_w.resize(sz_w, 0); -} - -// only call this once m_arg and m_res have been set appropriately -void CasadiFunction::operator()() -{ - int mem = m_func.checkout(); - m_func(m_arg.data(), m_res.data(), m_iw.data(), m_w.data(), mem); - m_func.release(mem); -} - -casadi_int CasadiFunction::nnz_out() { - return m_func.nnz_out(); -} - -casadi::Sparsity CasadiFunction::sparsity_out(casadi_int ind) { - return m_func.sparsity_out(ind); -} - -void CasadiFunction::operator()(const std::vector& inputs, - const std::vector& results) -{ - // Set-up input arguments, provide result vector, then execute function - // Example call: fcn({in1, in2, in3}, {out1}) - for(size_t k=0; k& var_casadi_fcns, - const std::vector& dvar_dy_fcns, - const std::vector& dvar_dp_fcns, - const Options& options) - : number_of_states(n_s), number_of_events(n_e), number_of_parameters(n_p), - number_of_nnz(jac_times_cjmass_nnz), - jac_bandwidth_lower(jac_bandwidth_lower), jac_bandwidth_upper(jac_bandwidth_upper), - rhs_alg(rhs_alg), - jac_times_cjmass(jac_times_cjmass), jac_action(jac_action), - mass_action(mass_action), sens(sens), events(events), - tmp_state_vector(number_of_states), - tmp_sparse_jacobian_data(jac_times_cjmass_nnz), - options(options) -{ - // convert casadi::Function list to CasadiFunction list - for (auto& var : var_casadi_fcns) { - this->var_casadi_fcns.push_back(CasadiFunction(*var)); - } - for (auto& var : dvar_dy_fcns) { - this->dvar_dy_fcns.push_back(CasadiFunction(*var)); - } - for (auto& var : dvar_dp_fcns) { - this->dvar_dp_fcns.push_back(CasadiFunction(*var)); - } - - // copy across numpy array values - const int n_row_vals = jac_times_cjmass_rowvals_arg.request().size; - auto p_jac_times_cjmass_rowvals = jac_times_cjmass_rowvals_arg.unchecked<1>(); - jac_times_cjmass_rowvals.resize(n_row_vals); - for (int i = 0; i < n_row_vals; i++) { - jac_times_cjmass_rowvals[i] = p_jac_times_cjmass_rowvals[i]; - } - - const int n_col_ptrs = jac_times_cjmass_colptrs_arg.request().size; - auto p_jac_times_cjmass_colptrs = jac_times_cjmass_colptrs_arg.unchecked<1>(); - jac_times_cjmass_colptrs.resize(n_col_ptrs); - for (int i = 0; i < n_col_ptrs; i++) { - jac_times_cjmass_colptrs[i] = p_jac_times_cjmass_colptrs[i]; - } - - inputs.resize(inputs_length); -} - -realtype *CasadiFunctions::get_tmp_state_vector() { - return tmp_state_vector.data(); -} -realtype *CasadiFunctions::get_tmp_sparse_jacobian_data() { - return tmp_sparse_jacobian_data.data(); -} diff --git a/pybamm/solvers/c_solvers/idaklu/casadi_functions.hpp b/pybamm/solvers/c_solvers/idaklu/casadi_functions.hpp deleted file mode 100644 index 1a33b957f8..0000000000 --- a/pybamm/solvers/c_solvers/idaklu/casadi_functions.hpp +++ /dev/null @@ -1,160 +0,0 @@ -#ifndef PYBAMM_IDAKLU_CASADI_FUNCTIONS_HPP -#define PYBAMM_IDAKLU_CASADI_FUNCTIONS_HPP - -#include "common.hpp" -#include "options.hpp" -#include -#include -#include - -/** - * Utility function to convert compressed-sparse-column (CSC) to/from - * compressed-sparse-row (CSR) matrix representation. Conversion is symmetric / - * invertible using this function. - * @brief Utility function to convert to/from CSC/CSR matrix representations. - * @param f Data vector containing the sparse matrix elements - * @param c Index pointer to column starts - * @param r Array of row indices - * @param nf New data vector that will contain the transformed sparse matrix - * @param nc New array of column indices - * @param nr New index pointer to row starts - */ -template -void csc_csr(const realtype f[], const T1 c[], const T1 r[], realtype nf[], T2 nc[], T2 nr[], int N, int cols) { - std::vector nn(cols+1); - std::vector rr(N); - for (int i=0; i& inputs, - const std::vector& results); - - /** - * @brief Return the number of non-zero elements for the function output - */ - casadi_int nnz_out(); - - /** - * @brief Return the number of non-zero elements for the function output - */ - casadi::Sparsity sparsity_out(casadi_int ind); - -public: - std::vector m_arg; - std::vector m_res; - -private: - const Function &m_func; - std::vector m_iw; - std::vector m_w; -}; - -/** - * @brief Class for handling casadi functions - */ -class CasadiFunctions -{ -public: - /** - * @brief Create a new CasadiFunctions object - */ - CasadiFunctions( - const Function &rhs_alg, - const Function &jac_times_cjmass, - const int jac_times_cjmass_nnz, - const int jac_bandwidth_lower, - const int jac_bandwidth_upper, - const np_array_int &jac_times_cjmass_rowvals, - const np_array_int &jac_times_cjmass_colptrs, - const int inputs_length, - const Function &jac_action, - const Function &mass_action, - const Function &sens, - const Function &events, - const int n_s, - const int n_e, - const int n_p, - const std::vector& var_casadi_fcns, - const std::vector& dvar_dy_fcns, - const std::vector& dvar_dp_fcns, - const Options& options - ); - -public: - int number_of_states; - int number_of_parameters; - int number_of_events; - int number_of_nnz; - int jac_bandwidth_lower; - int jac_bandwidth_upper; - - CasadiFunction rhs_alg; - CasadiFunction sens; - CasadiFunction jac_times_cjmass; - CasadiFunction jac_action; - CasadiFunction mass_action; - CasadiFunction events; - - // NB: cppcheck-suppress unusedStructMember is used because codacy reports - // these members as unused even though they are important - std::vector var_casadi_fcns; // cppcheck-suppress unusedStructMember - std::vector dvar_dy_fcns; // cppcheck-suppress unusedStructMember - std::vector dvar_dp_fcns; // cppcheck-suppress unusedStructMember - - std::vector jac_times_cjmass_rowvals; - std::vector jac_times_cjmass_colptrs; - std::vector inputs; - - Options options; - - realtype *get_tmp_state_vector(); - realtype *get_tmp_sparse_jacobian_data(); - -private: - std::vector tmp_state_vector; - std::vector tmp_sparse_jacobian_data; -}; - -#endif // PYBAMM_IDAKLU_CASADI_FUNCTIONS_HPP diff --git a/pybamm/solvers/c_solvers/idaklu/casadi_solver.hpp b/pybamm/solvers/c_solvers/idaklu/casadi_solver.hpp deleted file mode 100644 index 335907a93a..0000000000 --- a/pybamm/solvers/c_solvers/idaklu/casadi_solver.hpp +++ /dev/null @@ -1,36 +0,0 @@ -#ifndef PYBAMM_IDAKLU_CREATE_CASADI_SOLVER_HPP -#define PYBAMM_IDAKLU_CREATE_CASADI_SOLVER_HPP - -#include "CasadiSolver.hpp" - -/** - * Creates a concrete casadi solver given a linear solver, as specified in - * options_cpp.linear_solver. - * @brief Create a concrete casadi solver given a linear solver - */ -CasadiSolver *create_casadi_solver( - int number_of_states, - int number_of_parameters, - const Function &rhs_alg, - const Function &jac_times_cjmass, - const np_array_int &jac_times_cjmass_colptrs, - const np_array_int &jac_times_cjmass_rowvals, - const int jac_times_cjmass_nnz, - const int jac_bandwidth_lower, - const int jac_bandwidth_upper, - const Function &jac_action, - const Function &mass_action, - const Function &sens, - const Function &event, - const int number_of_events, - np_array rhs_alg_id, - np_array atol_np, - double rel_tol, - int inputs_length, - const std::vector& var_casadi_fcns, - const std::vector& dvar_dy_fcns, - const std::vector& dvar_dp_fcns, - py::dict options -); - -#endif // PYBAMM_IDAKLU_CREATE_CASADI_SOLVER_HPP diff --git a/pybamm/solvers/c_solvers/idaklu/casadi_sundials_functions.hpp b/pybamm/solvers/c_solvers/idaklu/casadi_sundials_functions.hpp deleted file mode 100644 index a2192030b4..0000000000 --- a/pybamm/solvers/c_solvers/idaklu/casadi_sundials_functions.hpp +++ /dev/null @@ -1,27 +0,0 @@ -#ifndef PYBAMM_IDAKLU_CASADI_SUNDIALS_FUNCTIONS_HPP -#define PYBAMM_IDAKLU_CASADI_SUNDIALS_FUNCTIONS_HPP - -#include "common.hpp" - -int residual_casadi(realtype tres, N_Vector yy, N_Vector yp, N_Vector rr, - void *user_data); - -int jtimes_casadi(realtype tt, N_Vector yy, N_Vector yp, N_Vector rr, - N_Vector v, N_Vector Jv, realtype cj, void *user_data, - N_Vector tmp1, N_Vector tmp2); - -int events_casadi(realtype t, N_Vector yy, N_Vector yp, realtype *events_ptr, - void *user_data); - -int sensitivities_casadi(int Ns, realtype t, N_Vector yy, N_Vector yp, - N_Vector resval, N_Vector *yS, N_Vector *ypS, - N_Vector *resvalS, void *user_data, N_Vector tmp1, - N_Vector tmp2, N_Vector tmp3); - -int jacobian_casadi(realtype tt, realtype cj, N_Vector yy, N_Vector yp, - N_Vector resvec, SUNMatrix JJ, void *user_data, - N_Vector tempv1, N_Vector tempv2, N_Vector tempv3); - -int residual_casadi_approx(sunindextype Nlocal, realtype tt, N_Vector yy, - N_Vector yp, N_Vector gval, void *user_data); -#endif // PYBAMM_IDAKLU_CASADI_SUNDIALS_FUNCTIONS_HPP diff --git a/pybamm/solvers/c_solvers/idaklu/common.hpp b/pybamm/solvers/c_solvers/idaklu/common.hpp index e0abbb5a1d..0ef7ee60a0 100644 --- a/pybamm/solvers/c_solvers/idaklu/common.hpp +++ b/pybamm/solvers/c_solvers/idaklu/common.hpp @@ -1,6 +1,8 @@ #ifndef PYBAMM_IDAKLU_COMMON_HPP #define PYBAMM_IDAKLU_COMMON_HPP +#include + #include /* prototypes for IDAS fcts., consts. */ #include /* access to IDABBDPRE preconditioner */ @@ -33,16 +35,58 @@ using np_array = py::array_t; using np_array_dense = py::array_t; using np_array_int = py::array_t; +/** + * Utility function to convert compressed-sparse-column (CSC) to/from + * compressed-sparse-row (CSR) matrix representation. Conversion is symmetric / + * invertible using this function. + * @brief Utility function to convert to/from CSC/CSR matrix representations. + * @param f Data vector containing the sparse matrix elements + * @param c Index pointer to column starts + * @param r Array of row indices + * @param nf New data vector that will contain the transformed sparse matrix + * @param nc New array of column indices + * @param nr New index pointer to row starts + */ +template +void csc_csr(const realtype f[], const T1 c[], const T1 r[], realtype nf[], T2 nc[], T2 nr[], int N, int cols) { + std::vector nn(cols+1); + std::vector rr(N); + for (int i=0; i; } \ std::cout << "]" << std::endl; } -#define DEBUG_v(v, N) {\ +#define DEBUG_v(v, M) {\ + int N = 2; \ std::cout << #v << "[n=" << N << "] = ["; \ for (int i = 0; i < N; i++) { \ std::cout << v[i]; \ @@ -82,6 +127,13 @@ using np_array_int = py::array_t; std::cerr << __FILE__ << ":" << __LINE__ << "," << #x << " = " << x << std::endl; \ } +#define ASSERT(x) { \ + if (!(x)) { \ + std::cerr << __FILE__ << ":" << __LINE__ << " Assertion failed: " << #x << std::endl; \ + throw std::runtime_error("Assertion failed: " #x); \ + } \ + } + #endif #endif // PYBAMM_IDAKLU_COMMON_HPP diff --git a/pybamm/solvers/c_solvers/idaklu/casadi_solver.cpp b/pybamm/solvers/c_solvers/idaklu/idaklu_solver.hpp similarity index 69% rename from pybamm/solvers/c_solvers/idaklu/casadi_solver.cpp rename to pybamm/solvers/c_solvers/idaklu/idaklu_solver.hpp index 9fcfa06510..a53b167ac4 100644 --- a/pybamm/solvers/c_solvers/idaklu/casadi_solver.cpp +++ b/pybamm/solvers/c_solvers/idaklu/idaklu_solver.hpp @@ -1,37 +1,42 @@ -#include "casadi_solver.hpp" -#include "CasadiSolver.hpp" -#include "CasadiSolverOpenMP_solvers.hpp" -#include "casadi_sundials_functions.hpp" -#include "common.hpp" +#ifndef PYBAMM_CREATE_IDAKLU_SOLVER_HPP +#define PYBAMM_CREATE_IDAKLU_SOLVER_HPP + +#include "IDAKLUSolverOpenMP_solvers.hpp" #include #include -CasadiSolver *create_casadi_solver( +/** + * Creates a concrete solver given a linear solver, as specified in + * options_cpp.linear_solver. + * @brief Create a concrete solver given a linear solver + */ +template +IDAKLUSolver *create_idaklu_solver( int number_of_states, int number_of_parameters, - const Function &rhs_alg, - const Function &jac_times_cjmass, + const typename ExprSet::BaseFunctionType &rhs_alg, + const typename ExprSet::BaseFunctionType &jac_times_cjmass, const np_array_int &jac_times_cjmass_colptrs, const np_array_int &jac_times_cjmass_rowvals, const int jac_times_cjmass_nnz, const int jac_bandwidth_lower, const int jac_bandwidth_upper, - const Function &jac_action, - const Function &mass_action, - const Function &sens, - const Function &events, + const typename ExprSet::BaseFunctionType &jac_action, + const typename ExprSet::BaseFunctionType &mass_action, + const typename ExprSet::BaseFunctionType &sens, + const typename ExprSet::BaseFunctionType &events, const int number_of_events, np_array rhs_alg_id, np_array atol_np, double rel_tol, int inputs_length, - const std::vector& var_casadi_fcns, - const std::vector& dvar_dy_fcns, - const std::vector& dvar_dp_fcns, + const std::vector& var_fcns, + const std::vector& dvar_dy_fcns, + const std::vector& dvar_dp_fcns, py::dict options ) { auto options_cpp = Options(options); - auto functions = std::make_unique( + auto functions = std::make_unique( rhs_alg, jac_times_cjmass, jac_times_cjmass_nnz, @@ -47,19 +52,19 @@ CasadiSolver *create_casadi_solver( number_of_states, number_of_events, number_of_parameters, - var_casadi_fcns, + var_fcns, dvar_dy_fcns, dvar_dp_fcns, options_cpp ); - CasadiSolver *casadiSolver = nullptr; + IDAKLUSolver *idakluSolver = nullptr; // Instantiate solver class if (options_cpp.linear_solver == "SUNLinSol_Dense") { DEBUG("\tsetting SUNLinSol_Dense linear solver"); - casadiSolver = new CasadiSolverOpenMP_Dense( + idakluSolver = new IDAKLUSolverOpenMP_Dense( atol_np, rel_tol, rhs_alg_id, @@ -75,7 +80,7 @@ CasadiSolver *create_casadi_solver( else if (options_cpp.linear_solver == "SUNLinSol_KLU") { DEBUG("\tsetting SUNLinSol_KLU linear solver"); - casadiSolver = new CasadiSolverOpenMP_KLU( + idakluSolver = new IDAKLUSolverOpenMP_KLU( atol_np, rel_tol, rhs_alg_id, @@ -91,7 +96,7 @@ CasadiSolver *create_casadi_solver( else if (options_cpp.linear_solver == "SUNLinSol_Band") { DEBUG("\tsetting SUNLinSol_Band linear solver"); - casadiSolver = new CasadiSolverOpenMP_Band( + idakluSolver = new IDAKLUSolverOpenMP_Band( atol_np, rel_tol, rhs_alg_id, @@ -107,7 +112,7 @@ CasadiSolver *create_casadi_solver( else if (options_cpp.linear_solver == "SUNLinSol_SPBCGS") { DEBUG("\tsetting SUNLinSol_SPBCGS_linear solver"); - casadiSolver = new CasadiSolverOpenMP_SPBCGS( + idakluSolver = new IDAKLUSolverOpenMP_SPBCGS( atol_np, rel_tol, rhs_alg_id, @@ -123,7 +128,7 @@ CasadiSolver *create_casadi_solver( else if (options_cpp.linear_solver == "SUNLinSol_SPFGMR") { DEBUG("\tsetting SUNLinSol_SPFGMR_linear solver"); - casadiSolver = new CasadiSolverOpenMP_SPFGMR( + idakluSolver = new IDAKLUSolverOpenMP_SPFGMR( atol_np, rel_tol, rhs_alg_id, @@ -139,7 +144,7 @@ CasadiSolver *create_casadi_solver( else if (options_cpp.linear_solver == "SUNLinSol_SPGMR") { DEBUG("\tsetting SUNLinSol_SPGMR solver"); - casadiSolver = new CasadiSolverOpenMP_SPGMR( + idakluSolver = new IDAKLUSolverOpenMP_SPGMR( atol_np, rel_tol, rhs_alg_id, @@ -155,7 +160,7 @@ CasadiSolver *create_casadi_solver( else if (options_cpp.linear_solver == "SUNLinSol_SPTFQMR") { DEBUG("\tsetting SUNLinSol_SPGMR solver"); - casadiSolver = new CasadiSolverOpenMP_SPTFQMR( + idakluSolver = new IDAKLUSolverOpenMP_SPTFQMR( atol_np, rel_tol, rhs_alg_id, @@ -169,9 +174,11 @@ CasadiSolver *create_casadi_solver( ); } - if (casadiSolver == nullptr) { + if (idakluSolver == nullptr) { throw std::invalid_argument("Unsupported solver requested"); } - return casadiSolver; + return idakluSolver; } + +#endif // PYBAMM_CREATE_IDAKLU_SOLVER_HPP diff --git a/pybamm/solvers/c_solvers/idaklu/python.hpp b/pybamm/solvers/c_solvers/idaklu/python.hpp index 0478d0946f..6231d13eb6 100644 --- a/pybamm/solvers/c_solvers/idaklu/python.hpp +++ b/pybamm/solvers/c_solvers/idaklu/python.hpp @@ -2,7 +2,7 @@ #define PYBAMM_IDAKLU_HPP #include "common.hpp" -#include "solution.hpp" +#include "Solution.hpp" #include using residual_type = std::function< diff --git a/pybamm/solvers/c_solvers/idaklu/solution.cpp b/pybamm/solvers/c_solvers/idaklu/solution.cpp deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/pybamm/solvers/c_solvers/idaklu/sundials_functions.hpp b/pybamm/solvers/c_solvers/idaklu/sundials_functions.hpp new file mode 100644 index 0000000000..c4024bc20a --- /dev/null +++ b/pybamm/solvers/c_solvers/idaklu/sundials_functions.hpp @@ -0,0 +1,36 @@ +#ifndef PYBAMM_SUNDIALS_FUNCTIONS_HPP +#define PYBAMM_SUNDIALS_FUNCTIONS_HPP + +#include "common.hpp" + +template +void axpy(int n, T alpha, const T* x, T* y) { + if (!x || !y) return; + for (int i=0; i #define NV_DATA NV_DATA_OMP // Serial: NV_DATA_S -int residual_casadi(realtype tres, N_Vector yy, N_Vector yp, N_Vector rr, - void *user_data) +template +int residual_eval(realtype tres, N_Vector yy, N_Vector yp, N_Vector rr, void *user_data) { - DEBUG("residual_casadi"); - CasadiFunctions *p_python_functions = - static_cast(user_data); + DEBUG("residual_eval"); + ExpressionSet *p_python_functions = + static_cast *>(user_data); - p_python_functions->rhs_alg.m_arg[0] = &tres; - p_python_functions->rhs_alg.m_arg[1] = NV_DATA(yy); - p_python_functions->rhs_alg.m_arg[2] = p_python_functions->inputs.data(); - p_python_functions->rhs_alg.m_res[0] = NV_DATA(rr); - p_python_functions->rhs_alg(); + DEBUG_VECTORn(yy, 100); + DEBUG_VECTORn(yp, 100); + + p_python_functions->rhs_alg->m_arg[0] = &tres; + p_python_functions->rhs_alg->m_arg[1] = NV_DATA(yy); + p_python_functions->rhs_alg->m_arg[2] = p_python_functions->inputs.data(); + p_python_functions->rhs_alg->m_res[0] = NV_DATA(rr); + (*p_python_functions->rhs_alg)(); + + DEBUG_VECTORn(rr, 100); realtype *tmp = p_python_functions->get_tmp_state_vector(); - p_python_functions->mass_action.m_arg[0] = NV_DATA(yp); - p_python_functions->mass_action.m_res[0] = tmp; - p_python_functions->mass_action(); + p_python_functions->mass_action->m_arg[0] = NV_DATA(yp); + p_python_functions->mass_action->m_res[0] = tmp; + (*p_python_functions->mass_action)(); // AXPY: y <- a*x + y const int ns = p_python_functions->number_of_states; - casadi::casadi_axpy(ns, -1., tmp, NV_DATA(rr)); + axpy(ns, -1., tmp, NV_DATA(rr)); - //DEBUG_VECTOR(yy); - //DEBUG_VECTOR(yp); - //DEBUG_VECTOR(rr); + DEBUG("mass - rhs"); + DEBUG_VECTORn(rr, 100); // now rr has rhs_alg(t, y) - mass_matrix * yp return 0; @@ -64,13 +68,14 @@ int residual_casadi(realtype tres, N_Vector yy, N_Vector yp, N_Vector rr, // within user_data. // // The case where G is mathematically identical to F is allowed. -int residual_casadi_approx(sunindextype Nlocal, realtype tt, N_Vector yy, +template +int residual_eval_approx(sunindextype Nlocal, realtype tt, N_Vector yy, N_Vector yp, N_Vector gval, void *user_data) { - DEBUG("residual_casadi_approx"); + DEBUG("residual_eval_approx"); // Just use true residual for now - int result = residual_casadi(tt, yy, yp, gval, user_data); + int result = residual_eval(tt, yy, yp, gval, user_data); return result; } @@ -94,32 +99,35 @@ int residual_casadi_approx(sunindextype Nlocal, realtype tt, N_Vector yy, // tmp2 are pointers to memory allocated for variables of type N Vector // which can // be used by IDALsJacTimesVecFn as temporary storage or work space. -int jtimes_casadi(realtype tt, N_Vector yy, N_Vector yp, N_Vector rr, +template +int jtimes_eval(realtype tt, N_Vector yy, N_Vector yp, N_Vector rr, N_Vector v, N_Vector Jv, realtype cj, void *user_data, N_Vector tmp1, N_Vector tmp2) { - DEBUG("jtimes_casadi"); - CasadiFunctions *p_python_functions = - static_cast(user_data); + DEBUG("jtimes_eval"); + T *p_python_functions = + static_cast(user_data); // Jv has ∂F/∂y v - p_python_functions->jac_action.m_arg[0] = &tt; - p_python_functions->jac_action.m_arg[1] = NV_DATA(yy); - p_python_functions->jac_action.m_arg[2] = p_python_functions->inputs.data(); - p_python_functions->jac_action.m_arg[3] = NV_DATA(v); - p_python_functions->jac_action.m_res[0] = NV_DATA(Jv); - p_python_functions->jac_action(); + p_python_functions->jac_action->m_arg[0] = &tt; + p_python_functions->jac_action->m_arg[1] = NV_DATA(yy); + p_python_functions->jac_action->m_arg[2] = p_python_functions->inputs.data(); + p_python_functions->jac_action->m_arg[3] = NV_DATA(v); + p_python_functions->jac_action->m_res[0] = NV_DATA(Jv); + (*p_python_functions->jac_action)(); // tmp has -∂F/∂y˙ v realtype *tmp = p_python_functions->get_tmp_state_vector(); - p_python_functions->mass_action.m_arg[0] = NV_DATA(v); - p_python_functions->mass_action.m_res[0] = tmp; - p_python_functions->mass_action(); + p_python_functions->mass_action->m_arg[0] = NV_DATA(v); + p_python_functions->mass_action->m_res[0] = tmp; + (*p_python_functions->mass_action)(); // AXPY: y <- a*x + y // Jv has ∂F/∂y v + cj ∂F/∂y˙ v const int ns = p_python_functions->number_of_states; - casadi::casadi_axpy(ns, -cj, tmp, NV_DATA(Jv)); + axpy(ns, -cj, tmp, NV_DATA(Jv)); + + DEBUG_VECTORn(Jv, 10); return 0; } @@ -141,14 +149,15 @@ int jtimes_casadi(realtype tt, N_Vector yy, N_Vector yp, N_Vector rr, // tmp3 are pointers to memory allocated for variables of type N Vector which // can // be used by IDALsJacFn function as temporary storage or work space. -int jacobian_casadi(realtype tt, realtype cj, N_Vector yy, N_Vector yp, +template +int jacobian_eval(realtype tt, realtype cj, N_Vector yy, N_Vector yp, N_Vector resvec, SUNMatrix JJ, void *user_data, N_Vector tempv1, N_Vector tempv2, N_Vector tempv3) { - DEBUG("jacobian_casadi"); + DEBUG("jacobian_eval"); - CasadiFunctions *p_python_functions = - static_cast(user_data); + T *p_python_functions = + static_cast(user_data); // create pointer to jac data, column pointers, and row values realtype *jac_data; @@ -164,16 +173,23 @@ int jacobian_casadi(realtype tt, realtype cj, N_Vector yy, N_Vector yp, jac_data = SUNDenseMatrix_Data(JJ); } + DEBUG_VECTORn(yy, 100); + // args are t, y, cj, put result in jacobian data matrix - p_python_functions->jac_times_cjmass.m_arg[0] = &tt; - p_python_functions->jac_times_cjmass.m_arg[1] = NV_DATA(yy); - p_python_functions->jac_times_cjmass.m_arg[2] = + p_python_functions->jac_times_cjmass->m_arg[0] = &tt; + p_python_functions->jac_times_cjmass->m_arg[1] = NV_DATA(yy); + p_python_functions->jac_times_cjmass->m_arg[2] = p_python_functions->inputs.data(); - p_python_functions->jac_times_cjmass.m_arg[3] = &cj; - p_python_functions->jac_times_cjmass.m_res[0] = jac_data; - - p_python_functions->jac_times_cjmass(); + p_python_functions->jac_times_cjmass->m_arg[3] = &cj; + p_python_functions->jac_times_cjmass->m_res[0] = jac_data; + (*p_python_functions->jac_times_cjmass)(); + DEBUG("jac_times_cjmass [" << sizeof(jac_data) << "]"); + DEBUG("t = " << tt); + DEBUG_VECTORn(yy, 100); + DEBUG("inputs = " << p_python_functions->inputs); + DEBUG("cj = " << cj); + DEBUG_v(jac_data, 100); if (p_python_functions->options.using_banded_matrix) { @@ -219,20 +235,12 @@ int jacobian_casadi(realtype tt, realtype cj, N_Vector yy, N_Vector yp, jac_colptrs[i] = p_jac_times_cjmass_colptrs[i]; } } else if (SUNSparseMatrix_SparseType(JJ) == CSR_MAT) { - std::vector newjac(SUNSparseMatrix_NNZ(JJ)); + // make a copy so that we can overwrite jac_data as CSR + std::vector newjac(&jac_data[0], &jac_data[SUNSparseMatrix_NNZ(JJ)]); sunindextype *jac_ptrs = SUNSparseMatrix_IndexPointers(JJ); sunindextype *jac_vals = SUNSparseMatrix_IndexValues(JJ); - // args are t, y, cj, put result in jacobian data matrix - p_python_functions->jac_times_cjmass.m_arg[0] = &tt; - p_python_functions->jac_times_cjmass.m_arg[1] = NV_DATA(yy); - p_python_functions->jac_times_cjmass.m_arg[2] = - p_python_functions->inputs.data(); - p_python_functions->jac_times_cjmass.m_arg[3] = &cj; - p_python_functions->jac_times_cjmass.m_res[0] = newjac.data(); - p_python_functions->jac_times_cjmass(); - - // convert (casadi's) CSC format to CSR + // convert CSC format to CSR csc_csr< std::remove_pointer_tjac_times_cjmass_rowvals.data())>, std::remove_pointer_t @@ -253,18 +261,20 @@ int jacobian_casadi(realtype tt, realtype cj, N_Vector yy, N_Vector yp, return (0); } -int events_casadi(realtype t, N_Vector yy, N_Vector yp, realtype *events_ptr, +template +int events_eval(realtype t, N_Vector yy, N_Vector yp, realtype *events_ptr, void *user_data) { - CasadiFunctions *p_python_functions = - static_cast(user_data); + DEBUG("events_eval"); + T *p_python_functions = + static_cast(user_data); // args are t, y, put result in events_ptr - p_python_functions->events.m_arg[0] = &t; - p_python_functions->events.m_arg[1] = NV_DATA(yy); - p_python_functions->events.m_arg[2] = p_python_functions->inputs.data(); - p_python_functions->events.m_res[0] = events_ptr; - p_python_functions->events(); + p_python_functions->events->m_arg[0] = &t; + p_python_functions->events->m_arg[1] = NV_DATA(yy); + p_python_functions->events->m_arg[2] = p_python_functions->inputs.data(); + p_python_functions->events->m_res[0] = events_ptr; + (*p_python_functions->events)(); return (0); } @@ -290,52 +300,52 @@ int events_casadi(realtype t, N_Vector yy, N_Vector yp, realtype *events_ptr, // occurred (in which case idas will attempt to correct), // or a negative value if it failed unrecoverably (in which case the integration // is halted and IDA SRES FAIL is returned) -// -int sensitivities_casadi(int Ns, realtype t, N_Vector yy, N_Vector yp, +template +int sensitivities_eval(int Ns, realtype t, N_Vector yy, N_Vector yp, N_Vector resval, N_Vector *yS, N_Vector *ypS, N_Vector *resvalS, void *user_data, N_Vector tmp1, N_Vector tmp2, N_Vector tmp3) { - DEBUG("sensitivities_casadi"); - CasadiFunctions *p_python_functions = - static_cast(user_data); + DEBUG("sensitivities_eval"); + T *p_python_functions = + static_cast(user_data); const int np = p_python_functions->number_of_parameters; // args are t, y put result in rr - p_python_functions->sens.m_arg[0] = &t; - p_python_functions->sens.m_arg[1] = NV_DATA(yy); - p_python_functions->sens.m_arg[2] = p_python_functions->inputs.data(); + p_python_functions->sens->m_arg[0] = &t; + p_python_functions->sens->m_arg[1] = NV_DATA(yy); + p_python_functions->sens->m_arg[2] = p_python_functions->inputs.data(); for (int i = 0; i < np; i++) { - p_python_functions->sens.m_res[i] = NV_DATA(resvalS[i]); + p_python_functions->sens->m_res[i] = NV_DATA(resvalS[i]); } // resvalsS now has (∂F/∂p i ) - p_python_functions->sens(); + (*p_python_functions->sens)(); for (int i = 0; i < np; i++) { // put (∂F/∂y)s i (t) in tmp realtype *tmp = p_python_functions->get_tmp_state_vector(); - p_python_functions->jac_action.m_arg[0] = &t; - p_python_functions->jac_action.m_arg[1] = NV_DATA(yy); - p_python_functions->jac_action.m_arg[2] = p_python_functions->inputs.data(); - p_python_functions->jac_action.m_arg[3] = NV_DATA(yS[i]); - p_python_functions->jac_action.m_res[0] = tmp; - p_python_functions->jac_action(); + p_python_functions->jac_action->m_arg[0] = &t; + p_python_functions->jac_action->m_arg[1] = NV_DATA(yy); + p_python_functions->jac_action->m_arg[2] = p_python_functions->inputs.data(); + p_python_functions->jac_action->m_arg[3] = NV_DATA(yS[i]); + p_python_functions->jac_action->m_res[0] = tmp; + (*p_python_functions->jac_action)(); const int ns = p_python_functions->number_of_states; - casadi::casadi_axpy(ns, 1., tmp, NV_DATA(resvalS[i])); + axpy(ns, 1., tmp, NV_DATA(resvalS[i])); // put -(∂F/∂ ẏ) ṡ i (t) in tmp2 - p_python_functions->mass_action.m_arg[0] = NV_DATA(ypS[i]); - p_python_functions->mass_action.m_res[0] = tmp; - p_python_functions->mass_action(); + p_python_functions->mass_action->m_arg[0] = NV_DATA(ypS[i]); + p_python_functions->mass_action->m_res[0] = tmp; + (*p_python_functions->mass_action)(); // (∂F/∂y)s i (t)+(∂F/∂ ẏ) ṡ i (t)+(∂F/∂p i ) // AXPY: y <- a*x + y - casadi::casadi_axpy(ns, -1., tmp, NV_DATA(resvalS[i])); + axpy(ns, -1., tmp, NV_DATA(resvalS[i])); } return 0; diff --git a/pybamm/solvers/idaklu_solver.py b/pybamm/solvers/idaklu_solver.py index fef4cbce3c..f1f32b1e63 100644 --- a/pybamm/solvers/idaklu_solver.py +++ b/pybamm/solvers/idaklu_solver.py @@ -2,13 +2,25 @@ # Solver class using sundials with the KLU sparse linear solver # # mypy: ignore-errors +import os import casadi import pybamm import numpy as np import numbers import scipy.sparse as sparse +from scipy.linalg import bandwidth import importlib +import warnings + +if pybamm.have_jax(): + import jax + from jax import numpy as jnp + + try: + import iree.compiler + except ImportError: # pragma: no cover + pass idaklu_spec = importlib.util.find_spec("pybamm.solvers.idaklu") if idaklu_spec is not None: @@ -24,6 +36,15 @@ def have_idaklu(): return idaklu_spec is not None +def have_iree(): + try: + import iree.compiler # noqa: F401 + + return True + except ImportError: # pragma: no cover + return False + + class IDAKLUSolver(pybamm.BaseSolver): """ Solve a discretised model, using sundials with the KLU sparse linear solver. @@ -75,6 +96,8 @@ class IDAKLUSolver(pybamm.BaseSolver): "precon_half_bandwidth_keep": 5, # Number of threads available for OpenMP "num_threads": 1, + # Evaluation engine to use for jax, can be 'jax'(native) or 'iree' + "jax_evaluator": "jax", } Note: These options only have an effect if model.convert_to_format == 'casadi' @@ -103,6 +126,7 @@ def __init__( "precon_half_bandwidth": 5, "precon_half_bandwidth_keep": 5, "num_threads": 1, + "jax_evaluator": "jax", } if options is None: options = default_options @@ -110,6 +134,10 @@ def __init__( for key, value in default_options.items(): if key not in options: options[key] = value + if options["jax_evaluator"] not in ["jax", "iree"]: + raise pybamm.SolverError( + "Evaluation engine must be 'jax' or 'iree' for IDAKLU solver" + ) self._options = options self.output_variables = [] if output_variables is None else output_variables @@ -183,10 +211,14 @@ def inputs_to_dict(inputs): # only casadi solver needs sensitivity ics if model.convert_to_format != "casadi": y0S = None - if self.output_variables: + if self.output_variables and not ( + model.convert_to_format == "jax" + and self._options["jax_evaluator"] == "iree" + ): raise pybamm.SolverError( "output_variables can only be specified " - 'with convert_to_format="casadi"' + 'with convert_to_format="casadi", or convert_to_format="jax" ' + 'with jax_evaluator="iree"' ) # pragma: no cover if y0S is not None: if isinstance(y0S, casadi.DM): @@ -293,7 +325,7 @@ def resfn(t, y, inputs, ydot): ) ) - else: + elif self._options["jax_evaluator"] == "jax": t0 = 0 if t_eval is None else t_eval[0] jac_y0_t0 = model.jac_rhs_algebraic_eval(t0, y0, inputs_dict) if sparse.issparse(jac_y0_t0): @@ -355,7 +387,7 @@ def get_jac_col_ptrs(self): ) ], ) - else: + elif self._options["jax_evaluator"] == "jax": def rootfn(t, y, inputs): new_inputs = inputs_to_dict(inputs) @@ -437,40 +469,220 @@ def sensfn(resvalS, t, y, inputs, yp, yS, ypS): rtol = self.rtol atol = self._check_atol_type(atol, y0.size) - if model.convert_to_format == "casadi": - rhs_algebraic = idaklu.generate_function(rhs_algebraic.serialize()) - jac_times_cjmass = idaklu.generate_function(jac_times_cjmass.serialize()) - jac_rhs_algebraic_action = idaklu.generate_function( - jac_rhs_algebraic_action.serialize() - ) - rootfn = idaklu.generate_function(rootfn.serialize()) - mass_action = idaklu.generate_function(mass_action.serialize()) - sensfn = idaklu.generate_function(sensfn.serialize()) + if model.convert_to_format == "casadi" or ( + model.convert_to_format == "jax" + and self._options["jax_evaluator"] == "iree" + ): + if model.convert_to_format == "casadi": + # Serialize casadi functions + idaklu_solver_fcn = idaklu.create_casadi_solver + rhs_algebraic = idaklu.generate_function(rhs_algebraic.serialize()) + jac_times_cjmass = idaklu.generate_function( + jac_times_cjmass.serialize() + ) + jac_rhs_algebraic_action = idaklu.generate_function( + jac_rhs_algebraic_action.serialize() + ) + rootfn = idaklu.generate_function(rootfn.serialize()) + mass_action = idaklu.generate_function(mass_action.serialize()) + sensfn = idaklu.generate_function(sensfn.serialize()) + elif ( + model.convert_to_format == "jax" + and self._options["jax_evaluator"] == "iree" + ): + # Convert Jax functions to MLIR (also, demote to single precision) + idaklu_solver_fcn = idaklu.create_iree_solver + pybamm.demote_expressions_to_32bit = True + if pybamm.demote_expressions_to_32bit: + warnings.warn( + "Demoting expressions to 32-bit for MLIR conversion", + stacklevel=2, + ) + jnpfloat = jnp.float32 + else: # pragma: no cover + jnpfloat = jnp.float64 + raise pybamm.SolverError( + "Demoting expressions to 32-bit is required for MLIR conversion" + " at this time" + ) + + # input arguments (used for lowering) + t_eval = self._demote_64_to_32(jnp.array([0.0], dtype=jnpfloat)) + y0 = self._demote_64_to_32(model.y0) + inputs0 = self._demote_64_to_32(inputs_to_dict(inputs)) + cj = self._demote_64_to_32(jnp.array([1.0], dtype=jnpfloat)) # array + v0 = jnp.zeros(model.len_rhs_and_alg, jnpfloat) + mass_matrix = model.mass_matrix.entries.toarray() + mass_matrix_demoted = self._demote_64_to_32(mass_matrix) + + # rhs_algebraic + rhs_algebraic_demoted = model.rhs_algebraic_eval + rhs_algebraic_demoted._demote_constants() + + def fcn_rhs_algebraic(t, y, inputs): + # function wraps an expression tree (and names MLIR module) + return rhs_algebraic_demoted(t, y, inputs) + + rhs_algebraic = self._make_iree_function( + fcn_rhs_algebraic, t_eval, y0, inputs0 + ) + + # jac_times_cjmass + jac_rhs_algebraic_demoted = rhs_algebraic_demoted.get_jacobian() + + def fcn_jac_times_cjmass(t, y, p, cj): + return jac_rhs_algebraic_demoted(t, y, p) - cj * mass_matrix_demoted + + sparse_eval = sparse.csc_matrix( + fcn_jac_times_cjmass(t_eval, y0, inputs0, cj) + ) + jac_times_cjmass_nnz = sparse_eval.nnz + jac_times_cjmass_colptrs = sparse_eval.indptr + jac_times_cjmass_rowvals = sparse_eval.indices + jac_bw_lower, jac_bw_upper = bandwidth( + sparse_eval.todense() + ) # potentially slow + if jac_bw_upper <= 1: + jac_bw_upper = jac_bw_lower - 1 + if jac_bw_lower <= 1: + jac_bw_lower = jac_bw_upper + 1 + coo = sparse_eval.tocoo() # convert to COOrdinate format for indexing + + def fcn_jac_times_cjmass_sparse(t, y, p, cj): + return fcn_jac_times_cjmass(t, y, p, cj)[coo.row, coo.col] + + jac_times_cjmass = self._make_iree_function( + fcn_jac_times_cjmass_sparse, t_eval, y0, inputs0, cj + ) + + # Mass action + def fcn_mass_action(v): + return mass_matrix_demoted @ v + + mass_action_demoted = self._demote_64_to_32(fcn_mass_action) + mass_action = self._make_iree_function(mass_action_demoted, v0) + + # rootfn + for ix, _ in enumerate(model.terminate_events_eval): + model.terminate_events_eval[ix]._demote_constants() + + def fcn_rootfn(t, y, inputs): + return jnp.array( + [event(t, y, inputs) for event in model.terminate_events_eval], + dtype=jnpfloat, + ).reshape(-1) + + def fcn_rootfn_demoted(t, y, inputs): + return self._demote_64_to_32(fcn_rootfn)(t, y, inputs) + + rootfn = self._make_iree_function( + fcn_rootfn_demoted, t_eval, y0, inputs0 + ) + + # jac_rhs_algebraic_action + jac_rhs_algebraic_action_demoted = ( + rhs_algebraic_demoted.get_jacobian_action() + ) + + def fcn_jac_rhs_algebraic_action( + t, y, p, v + ): # sundials calls (t, y, inputs, v) + return jac_rhs_algebraic_action_demoted( + t, y, v, p + ) # jvp calls (t, y, v, inputs) + + jac_rhs_algebraic_action = self._make_iree_function( + fcn_jac_rhs_algebraic_action, t_eval, y0, inputs0, v0 + ) + + # sensfn + if model.jacp_rhs_algebraic_eval is None: + sensfn = idaklu.IREEBaseFunctionType() # empty equation + else: + sensfn_demoted = rhs_algebraic_demoted.get_sensitivities() + + def fcn_sensfn(t, y, p): + return sensfn_demoted(t, y, p) + + sensfn = self._make_iree_function( + fcn_sensfn, t_eval, jnp.zeros_like(y0), inputs0 + ) + + # output_variables + self.var_idaklu_fcns = [] + self.dvar_dy_idaklu_fcns = [] + self.dvar_dp_idaklu_fcns = [] + for key in self.output_variables: + fcn = self.computed_var_fcns[key] + fcn._demote_constants() + self.var_idaklu_fcns.append( + self._make_iree_function( + lambda t, y, p: fcn(t, y, p), # noqa: B023 + t_eval, + y0, + inputs0, + ) + ) + # Convert derivative functions for sensitivities + if (len(inputs) > 0) and (model.calculate_sensitivities): + dvar_dy = fcn.get_jacobian() + self.dvar_dy_idaklu_fcns.append( + self._make_iree_function( + lambda t, y, p: dvar_dy(t, y, p), # noqa: B023 + t_eval, + y0, + inputs0, + sparse_index=True, + ) + ) + dvar_dp = fcn.get_sensitivities() + self.dvar_dp_idaklu_fcns.append( + self._make_iree_function( + lambda t, y, p: dvar_dp(t, y, p), # noqa: B023 + t_eval, + y0, + inputs0, + ) + ) + + # Identify IREE library + iree_lib_path = os.path.join(iree.compiler.__path__[0], "_mlir_libs") + os.environ["IREE_COMPILER_LIB"] = os.path.join( + iree_lib_path, + next(f for f in os.listdir(iree_lib_path) if "IREECompiler" in f), + ) + + pybamm.demote_expressions_to_32bit = False + else: # pragma: no cover + raise pybamm.SolverError( + "Unsupported evaluation engine for convert_to_format='jax'" + ) self._setup = { - "jac_bandwidth_upper": jac_bw_upper, - "jac_bandwidth_lower": jac_bw_lower, - "rhs_algebraic": rhs_algebraic, - "jac_times_cjmass": jac_times_cjmass, - "jac_times_cjmass_colptrs": jac_times_cjmass_colptrs, - "jac_times_cjmass_rowvals": jac_times_cjmass_rowvals, - "jac_times_cjmass_nnz": jac_times_cjmass_nnz, - "jac_rhs_algebraic_action": jac_rhs_algebraic_action, - "mass_action": mass_action, - "sensfn": sensfn, - "rootfn": rootfn, - "num_of_events": num_of_events, - "ids": ids, + "solver_function": idaklu_solver_fcn, # callable + "jac_bandwidth_upper": jac_bw_upper, # int + "jac_bandwidth_lower": jac_bw_lower, # int + "rhs_algebraic": rhs_algebraic, # function + "jac_times_cjmass": jac_times_cjmass, # function + "jac_times_cjmass_colptrs": jac_times_cjmass_colptrs, # array + "jac_times_cjmass_rowvals": jac_times_cjmass_rowvals, # array + "jac_times_cjmass_nnz": jac_times_cjmass_nnz, # int + "jac_rhs_algebraic_action": jac_rhs_algebraic_action, # function + "mass_action": mass_action, # function + "sensfn": sensfn, # function + "rootfn": rootfn, # function + "num_of_events": num_of_events, # int + "ids": ids, # array "sensitivity_names": sensitivity_names, "number_of_sensitivity_parameters": number_of_sensitivity_parameters, "output_variables": self.output_variables, - "var_casadi_fcns": self.computed_var_fcns, + "var_fcns": self.computed_var_fcns, "var_idaklu_fcns": self.var_idaklu_fcns, "dvar_dy_idaklu_fcns": self.dvar_dy_idaklu_fcns, "dvar_dp_idaklu_fcns": self.dvar_dp_idaklu_fcns, } - solver = idaklu.create_casadi_solver( + solver = self._setup["solver_function"]( number_of_states=len(y0), number_of_parameters=self._setup["number_of_sensitivity_parameters"], rhs_alg=self._setup["rhs_algebraic"], @@ -489,7 +701,7 @@ def sensfn(resvalS, t, y, inputs, yp, yS, ypS): atol=atol, rtol=rtol, inputs=len(inputs), - var_casadi_fcns=self._setup["var_idaklu_fcns"], + var_fcns=self._setup["var_idaklu_fcns"], dvar_dy_fcns=self._setup["dvar_dy_idaklu_fcns"], dvar_dp_fcns=self._setup["dvar_dp_idaklu_fcns"], options=self._options, @@ -511,6 +723,56 @@ def sensfn(resvalS, t, y, inputs, yp, yS, ypS): return base_set_up_return + def _make_iree_function(self, fcn, *args, sparse_index=False): + # Initialise IREE function object + iree_fcn = idaklu.IREEBaseFunctionType() + # Get sparsity pattern index outputs as needed + try: + fcn_eval = fcn(*args) + if not isinstance(fcn_eval, np.ndarray): + fcn_eval = jax.flatten_util.ravel_pytree(fcn_eval)[0] + coo = sparse.coo_matrix(fcn_eval) + iree_fcn.nnz = coo.nnz + iree_fcn.numel = np.prod(coo.shape) + iree_fcn.col = coo.col + iree_fcn.row = coo.row + if sparse_index: + # Isolate NNZ elements while recording original sparsity structure + fcn_inner = fcn + + def fcn(*args): + return fcn_inner(*args)[coo.row, coo.col] + elif coo.nnz != iree_fcn.numel: + iree_fcn.nnz = iree_fcn.numel + iree_fcn.col = list(range(iree_fcn.numel)) + iree_fcn.row = [0] * iree_fcn.numel + except (TypeError, AttributeError) as error: # pragma: no cover + raise pybamm.SolverError( + "Could not get sparsity pattern for function {fcn.__name__}" + ) from error + # Lower to MLIR + lowered = jax.jit(fcn).lower(*args) + iree_fcn.mlir = lowered.as_text() + self._check_mlir_conversion(fcn.__name__, iree_fcn.mlir) + iree_fcn.kept_var_idx = list(lowered._lowering.compile_args["kept_var_idx"]) + # Record number of variables in each argument (these will flatten in the mlir) + iree_fcn.pytree_shape = [ + len(jax.tree_util.tree_flatten(arg)[0]) for arg in args + ] + # Record array length of each mlir variable + iree_fcn.pytree_sizes = [ + len(arg) for arg in jax.tree_util.tree_flatten(args)[0] + ] + iree_fcn.n_args = len(args) + return iree_fcn + + def _check_mlir_conversion(self, name, mlir: str): + if mlir.count("f64") > 0: # pragma: no cover + warnings.warn(f"f64 found in {name} (x{mlir.count('f64')})", stacklevel=2) + + def _demote_64_to_32(self, x: pybamm.EvaluatorJax): + return pybamm.EvaluatorJax._demote_64_to_32(x) + def _integrate(self, model, t_eval, inputs_dict=None): """ Solve a DAE model defined by residuals with initial conditions y0. @@ -527,10 +789,12 @@ def _integrate(self, model, t_eval, inputs_dict=None): inputs_dict = inputs_dict or {} # stack inputs if inputs_dict: + inputs_dict_keys = list(inputs_dict.keys()) # save order arrays_to_stack = [np.array(x).reshape(-1, 1) for x in inputs_dict.values()] inputs = np.vstack(arrays_to_stack) else: inputs = np.array([[]]) + inputs_dict_keys = [] # do this here cause y0 is set after set_up (calc consistent conditions) y0 = model.y0 @@ -539,25 +803,45 @@ def _integrate(self, model, t_eval, inputs_dict=None): y0 = y0.flatten() y0S = model.y0S - # only casadi solver needs sensitivity ics - if model.convert_to_format != "casadi": - y0S = None - if y0S is not None: - if isinstance(y0S, casadi.DM): - y0S = (y0S,) - - y0S = (x.full() for x in y0S) - y0S = [x.flatten() for x in y0S] - - # solver works with ydot0 set to zero - ydot0 = np.zeros_like(y0) - if y0S is not None: - ydot0S = [np.zeros_like(y0S_i) for y0S_i in y0S] - y0full = np.concatenate([y0, *y0S]) - ydot0full = np.concatenate([ydot0, *ydot0S]) + if ( + model.convert_to_format == "jax" + and self._options["jax_evaluator"] == "iree" + ): + if y0S is not None: + pybamm.demote_expressions_to_32bit = True + # preserve order of inputs + y0S = self._demote_64_to_32( + np.concatenate([y0S[k] for k in inputs_dict_keys]).flatten() + ) + y0full = self._demote_64_to_32(np.concatenate([y0, y0S]).flatten()) + ydot0S = self._demote_64_to_32(np.zeros_like(y0S)) + ydot0full = self._demote_64_to_32( + np.concatenate([np.zeros_like(y0), ydot0S]).flatten() + ) + pybamm.demote_expressions_to_32bit = False + else: + y0full = y0 + ydot0full = np.zeros_like(y0) else: - y0full = y0 - ydot0full = ydot0 + # only casadi solver needs sensitivity ics + if model.convert_to_format != "casadi": + y0S = None + if y0S is not None: + if isinstance(y0S, casadi.DM): + y0S = (y0S,) + + y0S = (x.full() for x in y0S) + y0S = [x.flatten() for x in y0S] + + # solver works with ydot0 set to zero + ydot0 = np.zeros_like(y0) + if y0S is not None: + ydot0S = [np.zeros_like(y0S_i) for y0S_i in y0S] + y0full = np.concatenate([y0, *y0S]) + ydot0full = np.concatenate([ydot0, *ydot0S]) + else: + y0full = y0 + ydot0full = ydot0 try: atol = model.atol @@ -568,7 +852,10 @@ def _integrate(self, model, t_eval, inputs_dict=None): atol = self._check_atol_type(atol, y0.size) timer = pybamm.Timer() - if model.convert_to_format == "casadi": + if model.convert_to_format == "casadi" or ( + model.convert_to_format == "jax" + and self._options["jax_evaluator"] == "iree" + ): sol = self._setup["solver"].solve( t_eval, y0full, @@ -656,12 +943,27 @@ def _integrate(self, model, t_eval, inputs_dict=None): model.variables_and_events[var], pybamm.ExplicitTimeIntegral ): continue - len_of_var = ( - self._setup["var_casadi_fcns"][var](0, 0, 0).sparsity().nnz() - ) + if model.convert_to_format == "casadi": + len_of_var = ( + self._setup["var_fcns"][var](0.0, 0.0, 0.0).sparsity().nnz() + ) + base_variables = [self._setup["var_fcns"][var]] + elif ( + model.convert_to_format == "jax" + and self._options["jax_evaluator"] == "iree" + ): + idx = self.output_variables.index(var) + len_of_var = self._setup["var_idaklu_fcns"][idx].nnz + base_variables = [self._setup["var_idaklu_fcns"][idx]] + else: # pragma: no cover + raise pybamm.SolverError( + "Unsupported evaluation engine for convert_to_format=" + + f"{model.convert_to_format} " + + f"(jax_evaluator={self._options['jax_evaluator']})" + ) newsol._variables[var] = pybamm.ProcessedVariableComputed( [model.variables_and_events[var]], - [self._setup["var_casadi_fcns"][var]], + base_variables, [sol.y[:, startk : (startk + len_of_var)]], newsol, ) diff --git a/pybamm/solvers/processed_variable_computed.py b/pybamm/solvers/processed_variable_computed.py index a069342254..a717c8b0cb 100644 --- a/pybamm/solvers/processed_variable_computed.py +++ b/pybamm/solvers/processed_variable_computed.py @@ -120,16 +120,25 @@ def _unroll_nnz(self, realdata=None): # unroll in nnz != numel, otherwise copy if realdata is None: realdata = self.base_variables_data - sp = self.base_variables_casadi[0](0, 0, 0).sparsity() - if sp.nnz() != sp.numel(): + if isinstance(self.base_variables_casadi[0], casadi.Function): # casadi fcn + sp = self.base_variables_casadi[0](0, 0, 0).sparsity() + nnz = sp.nnz() + numel = sp.numel() + row = sp.row() + elif "nnz" in dir(self.base_variables_casadi[0]): # IREE fcn + sp = self.base_variables_casadi[0] + nnz = sp.nnz + numel = sp.numel + row = sp.row + if nnz != numel: data = [None] * len(realdata) for datak in range(len(realdata)): data[datak] = np.zeros(self.base_eval_shape[0] * len(self.t_pts)) var_data = realdata[0].flatten() k = 0 for t_i in range(len(self.t_pts)): - base = t_i * sp.numel() - for r in sp.row(): + base = t_i * numel + for r in row: data[datak][base + r] = var_data[k] k = k + 1 else: diff --git a/pyproject.toml b/pyproject.toml index 890f884769..14c8d69b07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -116,12 +116,19 @@ dev = [ # To access the metadata for python packages "importlib-metadata; python_version < '3.10'", ] -# For the Jax solver. Note: these must be kept in sync with the versions defined in pybamm/util.py. +# For the Jax solver. +# Note: These must be kept in sync with the versions defined in pybamm/util.py, and +# must remain compatible with IREE (see noxfile.py for IREE compatibility). jax = [ "jax==0.4.27", "jaxlib==0.4.27", ] -# Contains all optional dependencies, except for jax and dev dependencies +# For MLIR expression evaluation (IDAKLU Solver) +iree = [ + # must be pip installed with --find-links=https://iree.dev/pip-release-links.html + "iree-compiler==20240507.886", # see IREE compatibility notes in noxfile.py +] +# Contains all optional dependencies, except for jax, iree, and dev dependencies all = [ "scikit-fem>=8.1.0", "pybamm[examples,plot,cite,bpx,tqdm]", diff --git a/setup.py b/setup.py index 6b97f73058..21dabcebb2 100644 --- a/setup.py +++ b/setup.py @@ -92,10 +92,14 @@ def run(self): use_python_casadi = True build_type = os.getenv("PYBAMM_CPP_BUILD_TYPE", "RELEASE") + idaklu_expr_casadi = os.getenv("PYBAMM_IDAKLU_EXPR_CASADI", "ON") + idaklu_expr_iree = os.getenv("PYBAMM_IDAKLU_EXPR_IREE", "OFF") cmake_args = [ f"-DCMAKE_BUILD_TYPE={build_type}", f"-DPYTHON_EXECUTABLE={sys.executable}", "-DUSE_PYTHON_CASADI={}".format("TRUE" if use_python_casadi else "FALSE"), + f"-DPYBAMM_IDAKLU_EXPR_CASADI={idaklu_expr_casadi}", + f"-DPYBAMM_IDAKLU_EXPR_IREE={idaklu_expr_iree}", ] if self.suitesparse_root: cmake_args.append( @@ -291,27 +295,39 @@ def compile_KLU(): name="pybamm.solvers.idaklu", # The sources list should mirror the list in CMakeLists.txt sources=[ - "pybamm/solvers/c_solvers/idaklu/casadi_functions.cpp", - "pybamm/solvers/c_solvers/idaklu/casadi_functions.hpp", - "pybamm/solvers/c_solvers/idaklu/casadi_solver.cpp", - "pybamm/solvers/c_solvers/idaklu/casadi_solver.hpp", - "pybamm/solvers/c_solvers/idaklu/CasadiSolver.cpp", - "pybamm/solvers/c_solvers/idaklu/CasadiSolver.hpp", - "pybamm/solvers/c_solvers/idaklu/CasadiSolverOpenMP.cpp", - "pybamm/solvers/c_solvers/idaklu/CasadiSolverOpenMP.hpp", - "pybamm/solvers/c_solvers/idaklu/CasadiSolverOpenMP_solvers.cpp", - "pybamm/solvers/c_solvers/idaklu/CasadiSolverOpenMP_solvers.hpp", - "pybamm/solvers/c_solvers/idaklu/casadi_sundials_functions.cpp", - "pybamm/solvers/c_solvers/idaklu/casadi_sundials_functions.hpp", - "pybamm/solvers/c_solvers/idaklu/idaklu_jax.cpp", - "pybamm/solvers/c_solvers/idaklu/idaklu_jax.hpp", + "pybamm/solvers/c_solvers/idaklu/Expressions/Expressions.hpp", + "pybamm/solvers/c_solvers/idaklu/Expressions/Base/Expression.hpp", + "pybamm/solvers/c_solvers/idaklu/Expressions/Base/ExpressionSet.hpp", + "pybamm/solvers/c_solvers/idaklu/Expressions/Base/ExpressionTypes.hpp", + "pybamm/solvers/c_solvers/idaklu/Expressions/Base/ExpressionSparsity.hpp", + "pybamm/solvers/c_solvers/idaklu/Expressions/Casadi/CasadiFunctions.cpp", + "pybamm/solvers/c_solvers/idaklu/Expressions/Casadi/CasadiFunctions.hpp", + "pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEBaseFunction.hpp", + "pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunction.hpp", + "pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunctions.cpp", + "pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunctions.hpp", + "pybamm/solvers/c_solvers/idaklu/Expressions/IREE/iree_jit.cpp", + "pybamm/solvers/c_solvers/idaklu/Expressions/IREE/iree_jit.hpp", + "pybamm/solvers/c_solvers/idaklu/Expressions/IREE/ModuleParser.cpp", + "pybamm/solvers/c_solvers/idaklu/Expressions/IREE/ModuleParser.hpp", + "pybamm/solvers/c_solvers/idaklu/idaklu_solver.hpp", + "pybamm/solvers/c_solvers/idaklu/IDAKLUSolver.cpp", + "pybamm/solvers/c_solvers/idaklu/IDAKLUSolver.hpp", + "pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl", + "pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.hpp", + "pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP_solvers.cpp", + "pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP_solvers.hpp", + "pybamm/solvers/c_solvers/idaklu/sundials_functions.inl", + "pybamm/solvers/c_solvers/idaklu/sundials_functions.hpp", + "pybamm/solvers/c_solvers/idaklu/IdakluJax.cpp", + "pybamm/solvers/c_solvers/idaklu/IdakluJax.hpp", "pybamm/solvers/c_solvers/idaklu/common.hpp", "pybamm/solvers/c_solvers/idaklu/python.hpp", "pybamm/solvers/c_solvers/idaklu/python.cpp", - "pybamm/solvers/c_solvers/idaklu/solution.cpp", - "pybamm/solvers/c_solvers/idaklu/solution.hpp", - "pybamm/solvers/c_solvers/idaklu/options.hpp", - "pybamm/solvers/c_solvers/idaklu/options.cpp", + "pybamm/solvers/c_solvers/idaklu/Solution.cpp", + "pybamm/solvers/c_solvers/idaklu/Solution.hpp", + "pybamm/solvers/c_solvers/idaklu/Options.hpp", + "pybamm/solvers/c_solvers/idaklu/Options.cpp", "pybamm/solvers/c_solvers/idaklu.cpp", ], ) diff --git a/tests/unit/test_expression_tree/test_operations/test_evaluate_python.py b/tests/unit/test_expression_tree/test_operations/test_evaluate_python.py index b02c75f386..6e1b155eca 100644 --- a/tests/unit/test_expression_tree/test_operations/test_evaluate_python.py +++ b/tests/unit/test_expression_tree/test_operations/test_evaluate_python.py @@ -80,7 +80,7 @@ def test_find_symbols(self): # test values of variable_symbols self.assertEqual(next(iter(variable_symbols.values())), "y[0:1]") self.assertEqual(list(variable_symbols.values())[1], "y[1:2]") - self.assertEqual(list(variable_symbols.values())[2], f"-{var_b}") + self.assertEqual(list(variable_symbols.values())[2], f"-({var_b})") var_child = pybamm.id_to_python_variable(expr.children[1].id) self.assertEqual( list(variable_symbols.values())[3], f"np.maximum({var_a},{var_child})" @@ -674,6 +674,76 @@ def test_evaluator_jax_inputs(self): result = evaluator(inputs={"a": 2}) self.assertEqual(result, 4) + @unittest.skipIf(not pybamm.have_jax(), "jax or jaxlib is not installed") + def test_evaluator_jax_demotion(self): + for demote in [True, False]: + pybamm.demote_expressions_to_32bit = demote # global flag + target_dtype = "32" if demote else "64" + if demote: + # Test only works after conversion to jax.numpy + for c in [ + 1.0, + 1, + ]: + self.assertEqual( + str(pybamm.EvaluatorJax._demote_64_to_32(c).dtype)[-2:], + target_dtype, + ) + for c in [ + np.float64(1.0), + np.int64(1), + np.array([1.0], dtype=np.float64), + np.array([1], dtype=np.int64), + jax.numpy.array([1.0], dtype=np.float64), + jax.numpy.array([1], dtype=np.int64), + ]: + self.assertEqual( + str(pybamm.EvaluatorJax._demote_64_to_32(c).dtype)[-2:], + target_dtype, + ) + for c in [ + {key: np.float64(1.0) for key in ["a", "b"]}, + ]: + expr_demoted = pybamm.EvaluatorJax._demote_64_to_32(c) + self.assertTrue( + all( + str(c_v.dtype)[-2:] == target_dtype + for c_k, c_v in expr_demoted.items() + ) + ) + for c in [ + (np.float64(1.0), np.float64(2.0)), + [np.float64(1.0), np.float64(2.0)], + ]: + expr_demoted = pybamm.EvaluatorJax._demote_64_to_32(c) + self.assertTrue( + all(str(c_i.dtype)[-2:] == target_dtype for c_i in expr_demoted) + ) + for dtype in [ + np.float64, + jax.numpy.float64, + ]: + c = pybamm.JaxCooMatrix([0, 1], [0, 1], dtype([1.0, 2.0]), (2, 2)) + c_demoted = pybamm.EvaluatorJax._demote_64_to_32(c) + self.assertTrue( + all(str(c_i.dtype)[-2:] == target_dtype for c_i in c_demoted.data) + ) + for dtype in [ + np.int64, + jax.numpy.int64, + ]: + c = pybamm.JaxCooMatrix( + dtype([0, 1]), dtype([0, 1]), [1.0, 2.0], (2, 2) + ) + c_demoted = pybamm.EvaluatorJax._demote_64_to_32(c) + self.assertTrue( + all(str(c_i.dtype)[-2:] == target_dtype for c_i in c_demoted.row) + ) + self.assertTrue( + all(str(c_i.dtype)[-2:] == target_dtype for c_i in c_demoted.col) + ) + pybamm.demote_expressions_to_32bit = False + @unittest.skipIf(not pybamm.have_jax(), "jax or jaxlib is not installed") def test_jax_coo_matrix(self): A = pybamm.JaxCooMatrix([0, 1], [0, 1], [1.0, 2.0], (2, 2)) diff --git a/tests/unit/test_solvers/test_idaklu_solver.py b/tests/unit/test_solvers/test_idaklu_solver.py index 1697623486..a80ab74b9e 100644 --- a/tests/unit/test_solvers/test_idaklu_solver.py +++ b/tests/unit/test_solvers/test_idaklu_solver.py @@ -18,15 +18,17 @@ def test_ida_roberts_klu(self): # this test implements a python version of the ida Roberts # example provided in sundials # see sundials ida examples pdf - for form in ["python", "casadi", "jax"]: - if form == "jax" and not pybamm.have_jax(): + for form in ["python", "casadi", "jax", "iree"]: + if (form == "jax" or form == "iree") and not pybamm.have_jax(): + continue + if (form == "iree") and not pybamm.have_iree(): continue if form == "casadi": root_method = "casadi" else: root_method = "lm" model = pybamm.BaseModel() - model.convert_to_format = form + model.convert_to_format = "jax" if form == "iree" else form u = pybamm.Variable("u") v = pybamm.Variable("v") model.rhs = {u: 0.1 * v} @@ -37,7 +39,10 @@ def test_ida_roberts_klu(self): disc = pybamm.Discretisation() disc.process_model(model) - solver = pybamm.IDAKLUSolver(root_method=root_method) + solver = pybamm.IDAKLUSolver( + root_method=root_method, + options={"jax_evaluator": "iree"} if form == "iree" else {}, + ) t_eval = np.linspace(0, 3, 100) solution = solver.solve(model, t_eval) @@ -59,8 +64,10 @@ def test_ida_roberts_klu(self): np.testing.assert_array_almost_equal(solution.y[0, :], true_solution) def test_model_events(self): - for form in ["python", "casadi", "jax"]: - if form == "jax" and not pybamm.have_jax(): + for form in ["python", "casadi", "jax", "iree"]: + if (form == "jax" or form == "iree") and not pybamm.have_jax(): + continue + if (form == "iree") and not pybamm.have_iree(): continue if form == "casadi": root_method = "casadi" @@ -68,7 +75,7 @@ def test_model_events(self): root_method = "lm" # Create model model = pybamm.BaseModel() - model.convert_to_format = form + model.convert_to_format = "jax" if form == "iree" else form var = pybamm.Variable("var") model.rhs = {var: 0.1 * var} model.initial_conditions = {var: 1} @@ -77,7 +84,12 @@ def test_model_events(self): disc = pybamm.Discretisation() model_disc = disc.process_model(model, inplace=False) # Solve - solver = pybamm.IDAKLUSolver(rtol=1e-8, atol=1e-8, root_method=root_method) + solver = pybamm.IDAKLUSolver( + rtol=1e-8, + atol=1e-8, + root_method=root_method, + options={"jax_evaluator": "iree"} if form == "iree" else {}, + ) t_eval = np.linspace(0, 1, 100) solution = solver.solve(model_disc, t_eval) np.testing.assert_array_equal(solution.t, t_eval) @@ -92,7 +104,12 @@ def test_model_events(self): # enforce events that won't be triggered model.events = [pybamm.Event("an event", var + 1)] model_disc = disc.process_model(model, inplace=False) - solver = pybamm.IDAKLUSolver(rtol=1e-8, atol=1e-8, root_method=root_method) + solver = pybamm.IDAKLUSolver( + rtol=1e-8, + atol=1e-8, + root_method=root_method, + options={"jax_evaluator": "iree"} if form == "iree" else {}, + ) solution = solver.solve(model_disc, t_eval) np.testing.assert_array_equal(solution.t, t_eval) np.testing.assert_array_almost_equal( @@ -102,7 +119,12 @@ def test_model_events(self): # enforce events that will be triggered model.events = [pybamm.Event("an event", 1.01 - var)] model_disc = disc.process_model(model, inplace=False) - solver = pybamm.IDAKLUSolver(rtol=1e-8, atol=1e-8, root_method=root_method) + solver = pybamm.IDAKLUSolver( + rtol=1e-8, + atol=1e-8, + root_method=root_method, + options={"jax_evaluator": "iree"} if form == "iree" else {}, + ) solution = solver.solve(model_disc, t_eval) self.assertLess(len(solution.t), len(t_eval)) np.testing.assert_array_almost_equal( @@ -124,7 +146,12 @@ def test_model_events(self): disc = get_discretisation_for_testing() disc.process_model(model) - solver = pybamm.IDAKLUSolver(rtol=1e-8, atol=1e-8, root_method=root_method) + solver = pybamm.IDAKLUSolver( + rtol=1e-8, + atol=1e-8, + root_method=root_method, + options={"jax_evaluator": "iree"} if form == "iree" else {}, + ) t_eval = np.linspace(0, 5, 100) solution = solver.solve(model, t_eval) np.testing.assert_array_less(solution.y[0, :-1], 1.5) @@ -140,15 +167,17 @@ def test_model_events(self): def test_input_params(self): # test a mix of scalar and vector input params - for form in ["python", "casadi", "jax"]: - if form == "jax" and not pybamm.have_jax(): + for form in ["python", "casadi", "jax", "iree"]: + if (form == "jax" or form == "iree") and not pybamm.have_jax(): + continue + if (form == "iree") and not pybamm.have_iree(): continue if form == "casadi": root_method = "casadi" else: root_method = "lm" model = pybamm.BaseModel() - model.convert_to_format = form + model.convert_to_format = "jax" if form == "iree" else form u1 = pybamm.Variable("u1") u2 = pybamm.Variable("u2") u3 = pybamm.Variable("u3") @@ -162,7 +191,10 @@ def test_input_params(self): disc = pybamm.Discretisation() disc.process_model(model) - solver = pybamm.IDAKLUSolver(root_method=root_method) + solver = pybamm.IDAKLUSolver( + root_method=root_method, + options={"jax_evaluator": "iree"} if form == "iree" else {}, + ) t_eval = np.linspace(0, 3, 100) a_value = 0.1 @@ -185,48 +217,63 @@ def test_input_params(self): true_solution = b_value * sol.t np.testing.assert_array_almost_equal(sol.y[1:3], true_solution) - def test_sensitivites_initial_condition(self): - for output_variables in [[], ["2v"]]: - model = pybamm.BaseModel() - model.convert_to_format = "casadi" - u = pybamm.Variable("u") - v = pybamm.Variable("v") - a = pybamm.InputParameter("a") - model.rhs = {u: -u} - model.algebraic = {v: a * u - v} - model.initial_conditions = {u: 1, v: 1} - model.variables = {"2v": 2 * v} - - disc = pybamm.Discretisation() - disc.process_model(model) - solver = pybamm.IDAKLUSolver(output_variables=output_variables) - - t_eval = np.linspace(0, 3, 100) - a_value = 0.1 - - sol = solver.solve( - model, t_eval, inputs={"a": a_value}, calculate_sensitivities=True - ) - - np.testing.assert_array_almost_equal( - sol["2v"].sensitivities["a"].full().flatten(), - np.exp(-sol.t) * 2, - decimal=4, - ) + def test_sensitivities_initial_condition(self): + for form in ["casadi", "iree"]: + for output_variables in [[], ["2v"]]: + if (form == "jax" or form == "iree") and not pybamm.have_jax(): + continue + if (form == "iree") and not pybamm.have_iree(): + continue + if form == "casadi": + root_method = "casadi" + else: + root_method = "lm" + model = pybamm.BaseModel() + model.convert_to_format = "jax" if form == "iree" else form + u = pybamm.Variable("u") + v = pybamm.Variable("v") + a = pybamm.InputParameter("a") + model.rhs = {u: -u} + model.algebraic = {v: a * u - v} + model.initial_conditions = {u: 1, v: 1} + model.variables = {"2v": 2 * v} + + disc = pybamm.Discretisation() + disc.process_model(model) + solver = pybamm.IDAKLUSolver( + root_method=root_method, + output_variables=output_variables, + options={"jax_evaluator": "iree"} if form == "iree" else {}, + ) + + t_eval = np.linspace(0, 3, 100) + a_value = 0.1 + + sol = solver.solve( + model, t_eval, inputs={"a": a_value}, calculate_sensitivities=True + ) + + np.testing.assert_array_almost_equal( + sol["2v"].sensitivities["a"].full().flatten(), + np.exp(-sol.t) * 2, + decimal=4, + ) def test_ida_roberts_klu_sensitivities(self): # this test implements a python version of the ida Roberts # example provided in sundials # see sundials ida examples pdf - for form in ["python", "casadi", "jax"]: - if form == "jax" and not pybamm.have_jax(): + for form in ["python", "casadi", "jax", "iree"]: + if (form == "jax" or form == "iree") and not pybamm.have_jax(): + continue + if (form == "iree") and not pybamm.have_iree(): continue if form == "casadi": root_method = "casadi" else: root_method = "lm" model = pybamm.BaseModel() - model.convert_to_format = form + model.convert_to_format = "jax" if form == "iree" else form u = pybamm.Variable("u") v = pybamm.Variable("v") a = pybamm.InputParameter("a") @@ -238,7 +285,10 @@ def test_ida_roberts_klu_sensitivities(self): disc = pybamm.Discretisation() disc.process_model(model) - solver = pybamm.IDAKLUSolver(root_method=root_method) + solver = pybamm.IDAKLUSolver( + root_method=root_method, + options={"jax_evaluator": "iree"} if form == "iree" else {}, + ) t_eval = np.linspace(0, 3, 100) a_value = 0.1 @@ -283,25 +333,32 @@ def test_ida_roberts_klu_sensitivities(self): dyda_fd = (sol_plus.y - sol_neg.y) / h dyda_fd = dyda_fd.transpose().reshape(-1, 1) - np.testing.assert_array_almost_equal(dyda_ida, dyda_fd) + decimal = ( + 2 if form == "iree" else 6 + ) # iree currently operates with single precision + np.testing.assert_array_almost_equal(dyda_ida, dyda_fd, decimal=decimal) # get the sensitivities for the variable d2uda = sol["2u"].sensitivities["a"] - np.testing.assert_array_almost_equal(2 * dyda_ida[0:200:2], d2uda) + np.testing.assert_array_almost_equal( + 2 * dyda_ida[0:200:2], d2uda, decimal=decimal + ) def test_sensitivities_with_events(self): # this test implements a python version of the ida Roberts # example provided in sundials # see sundials ida examples pdf - for form in ["casadi", "python", "jax"]: - if form == "jax" and not pybamm.have_jax(): + for form in ["casadi", "python", "jax", "iree"]: + if (form == "jax" or form == "iree") and not pybamm.have_jax(): + continue + if (form == "iree") and not pybamm.have_iree(): continue if form == "casadi": root_method = "casadi" else: root_method = "lm" model = pybamm.BaseModel() - model.convert_to_format = form + model.convert_to_format = "jax" if form == "iree" else form u = pybamm.Variable("u") v = pybamm.Variable("v") a = pybamm.InputParameter("a") @@ -314,7 +371,10 @@ def test_sensitivities_with_events(self): disc = pybamm.Discretisation() disc.process_model(model) - solver = pybamm.IDAKLUSolver(root_method=root_method) + solver = pybamm.IDAKLUSolver( + root_method=root_method, + options={"jax_evaluator": "iree"} if form == "iree" else {}, + ) t_eval = np.linspace(0, 3, 100) a_value = 0.1 @@ -351,8 +411,11 @@ def test_sensitivities_with_events(self): dyda_fd = (sol_plus.y[:, :max_index] - sol_neg.y[:, :max_index]) / h dyda_fd = dyda_fd.transpose().reshape(-1, 1) + decimal = ( + 2 if form == "iree" else 6 + ) # iree currently operates with single precision np.testing.assert_array_almost_equal( - dyda_ida[: (2 * max_index), :], dyda_fd + dyda_ida[: (2 * max_index), :], dyda_fd, decimal=decimal ) sol_plus = solver.solve( @@ -366,7 +429,7 @@ def test_sensitivities_with_events(self): dydb_fd = dydb_fd.transpose().reshape(-1, 1) np.testing.assert_array_almost_equal( - dydb_ida[: (2 * max_index), :], dydb_fd + dydb_ida[: (2 * max_index), :], dydb_fd, decimal=decimal ) def test_failures(self): @@ -421,15 +484,17 @@ def test_failures(self): solver.solve(model, t_eval) def test_dae_solver_algebraic_model(self): - for form in ["python", "casadi", "jax"]: - if form == "jax" and not pybamm.have_jax(): + for form in ["python", "casadi", "jax", "iree"]: + if (form == "jax" or form == "iree") and not pybamm.have_jax(): + continue + if (form == "iree") and not pybamm.have_iree(): continue if form == "casadi": root_method = "casadi" else: root_method = "lm" model = pybamm.BaseModel() - model.convert_to_format = form + model.convert_to_format = "jax" if form == "iree" else form var = pybamm.Variable("var") model.algebraic = {var: var + 1} model.initial_conditions = {var: 0} @@ -437,7 +502,10 @@ def test_dae_solver_algebraic_model(self): disc = pybamm.Discretisation() disc.process_model(model) - solver = pybamm.IDAKLUSolver(root_method=root_method) + solver = pybamm.IDAKLUSolver( + root_method=root_method, + options={"jax_evaluator": "iree"} if form == "iree" else {}, + ) t_eval = np.linspace(0, 1) solution = solver.solve(model, t_eval) np.testing.assert_array_equal(solution.y, -1) @@ -547,7 +615,7 @@ def test_options(self): soln = solver.solve(model, t_eval) def test_with_output_variables(self): - # Construct a model and solve for all vairables, then test + # Construct a model and solve for all variables, then test # the 'output_variables' option for each variable in turn, confirming # equivalence input_parameters = {} # Sensitivities dictionary @@ -649,76 +717,110 @@ def construct_model(): sol["x_s [m]"].initialise_1D() def test_with_output_variables_and_sensitivities(self): - # Construct a model and solve for all vairables, then test + # Construct a model and solve for all variables, then test # the 'output_variables' option for each variable in turn, confirming # equivalence - # construct model - model = pybamm.lithium_ion.DFN() - geometry = model.default_geometry - param = model.default_parameter_values - input_parameters = { # Sensitivities dictionary - "Current function [A]": 0.680616, - "Separator porosity": 1.0, - } - param.update({key: "[input]" for key in input_parameters}) - param.process_model(model) - param.process_geometry(geometry) - var_pts = {"x_n": 50, "x_s": 50, "x_p": 50, "r_n": 5, "r_p": 5} - mesh = pybamm.Mesh(geometry, model.default_submesh_types, var_pts) - disc = pybamm.Discretisation(mesh, model.default_spatial_methods) - disc.process_model(model) - t_eval = np.linspace(0, 3600, 100) + for form in ["casadi", "iree"]: + if (form == "jax" or form == "iree") and not pybamm.have_jax(): + continue + if (form == "iree") and not pybamm.have_iree(): + continue + if form == "casadi": + root_method = "casadi" + else: + root_method = "lm" + input_parameters = { # Sensitivities dictionary + "Current function [A]": 0.222, + "Separator porosity": 0.3, + } - options = { - "linear_solver": "SUNLinSol_KLU", - "jacobian": "sparse", - "num_threads": 4, - } + # construct model + model = pybamm.lithium_ion.DFN() + model.convert_to_format = "jax" if form == "iree" else form + geometry = model.default_geometry + param = model.default_parameter_values + param.update({key: "[input]" for key in input_parameters}) + param.process_model(model) + param.process_geometry(geometry) + var_pts = {"x_n": 50, "x_s": 50, "x_p": 50, "r_n": 5, "r_p": 5} + mesh = pybamm.Mesh(geometry, model.default_submesh_types, var_pts) + disc = pybamm.Discretisation(mesh, model.default_spatial_methods) + disc.process_model(model) - # Use a selection of variables of different types - output_variables = [ - "Voltage [V]", - "Time [min]", - "x [m]", - "Negative particle flux [mol.m-2.s-1]", - "Throughput capacity [A.h]", # ExplicitTimeIntegral - ] + t_eval = np.linspace(0, 3600, 100) + + options = { + "linear_solver": "SUNLinSol_KLU", + "jacobian": "sparse", + "num_threads": 4, + } + if form == "iree": + options["jax_evaluator"] = "iree" + + # Use a selection of variables of different types + output_variables = [ + "Voltage [V]", + "Time [min]", + "x [m]", + "Negative particle flux [mol.m-2.s-1]", + "Throughput capacity [A.h]", # ExplicitTimeIntegral + ] - # Use the full model as comparison (tested separately) - solver_all = pybamm.IDAKLUSolver( - atol=1e-8, - rtol=1e-8, - options=options, - ) - sol_all = solver_all.solve( - model, - t_eval, - inputs=input_parameters, - calculate_sensitivities=True, - ) + # Use the full model as comparison (tested separately) + solver_all = pybamm.IDAKLUSolver( + root_method=root_method, + atol=1e-8 if form != "iree" else 1e-0, # iree has reduced precision + rtol=1e-8 if form != "iree" else 1e-0, # iree has reduced precision + options=options, + ) + sol_all = solver_all.solve( + model, + t_eval, + inputs=input_parameters, + calculate_sensitivities=True, + ) - # Solve for a subset of variables and compare results - solver = pybamm.IDAKLUSolver( - atol=1e-8, - rtol=1e-8, - options=options, - output_variables=output_variables, - ) - sol = solver.solve( - model, - t_eval, - inputs=input_parameters, - calculate_sensitivities=True, - ) + # Solve for a subset of variables and compare results + solver = pybamm.IDAKLUSolver( + root_method=root_method, + atol=1e-8 if form != "iree" else 1e-0, # iree has reduced precision + rtol=1e-8 if form != "iree" else 1e-0, # iree has reduced precision + options=options, + output_variables=output_variables, + ) + sol = solver.solve( + model, + t_eval, + inputs=input_parameters, + calculate_sensitivities=True, + ) - # Compare output to sol_all - for varname in output_variables: - self.assertTrue(np.allclose(sol[varname].data, sol_all[varname].data)) + # Compare output to sol_all + tol = 1e-5 if form != "iree" else 1e-2 # iree has reduced precision + for varname in output_variables: + np.testing.assert_array_almost_equal( + sol[varname].data, sol_all[varname].data, tol + ) - # Mock a 1D current collector and initialise (none in the model) - sol["x_s [m]"].domain = ["current collector"] - sol["x_s [m]"].initialise_1D() + # Mock a 1D current collector and initialise (none in the model) + sol["x_s [m]"].domain = ["current collector"] + sol["x_s [m]"].initialise_1D() + + def test_bad_jax_evaluator(self): + model = pybamm.lithium_ion.DFN() + model.convert_to_format = "jax" + with self.assertRaises(pybamm.SolverError): + pybamm.IDAKLUSolver(options={"jax_evaluator": "bad_evaluator"}) + + def test_bad_jax_evaluator_output_variables(self): + model = pybamm.lithium_ion.DFN() + model.convert_to_format = "jax" + with self.assertRaises(pybamm.SolverError): + pybamm.IDAKLUSolver( + options={"jax_evaluator": "bad_evaluator"}, + output_variables=["Terminal voltage [V]"], + ) if __name__ == "__main__": From ae77c7423f111410066ca9d30c4629a54f1db753 Mon Sep 17 00:00:00 2001 From: Saransh Chopra Date: Mon, 15 Jul 2024 22:08:19 +0530 Subject: [PATCH 19/82] Fix directory (#4263) Co-authored-by: Eric G. Kratz --- .github/workflows/publish_pypi.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish_pypi.yml b/.github/workflows/publish_pypi.yml index 402bf9e859..8d328118a1 100644 --- a/.github/workflows/publish_pypi.yml +++ b/.github/workflows/publish_pypi.yml @@ -317,7 +317,7 @@ jobs: if: github.event.inputs.target == 'testpypi' uses: pypa/gh-action-pypi-publish@release/v1 with: - packages-dir: files/ + packages-dir: artifacts/ repository-url: https://test.pypi.org/legacy/ open_failure_issue: From 640c9f8bc64c6de1ec0f27b826bf88c546de8332 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jul 2024 16:43:47 -0400 Subject: [PATCH 20/82] Bump github/codeql-action from 3.25.11 to 3.25.12 in the actions group (#4268) Bumps the actions group with 1 update: [github/codeql-action](https://github.com/github/codeql-action). Updates `github/codeql-action` from 3.25.11 to 3.25.12 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/b611370bb5703a7efb587f9d136a52ea24c5c38c...4fa2a7953630fd2f3fb380f21be14ede0169dd4f) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/scorecard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 0c81f71bde..9a41c49b69 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -68,6 +68,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@b611370bb5703a7efb587f9d136a52ea24c5c38c # v3.25.11 + uses: github/codeql-action/upload-sarif@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.25.12 with: sarif_file: results.sarif From 6c1a5598d2dbfa9e899f74174ab830d39faf76c0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 15 Jul 2024 17:35:49 -0400 Subject: [PATCH 21/82] chore: update pre-commit hooks (#4269) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.5.1 → v0.5.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.1...v0.5.2) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8effca2b07..ecd3cd9199 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.1" + rev: "v0.5.2" hooks: - id: ruff args: [--fix, --show-fixes] From 4796ba956249e269b5c65dd370f7163729bf1ce3 Mon Sep 17 00:00:00 2001 From: Pradyot Ranjan <99216956+prady0t@users.noreply.github.com> Date: Tue, 16 Jul 2024 15:08:55 +0530 Subject: [PATCH 22/82] Moving a bunch of unit tests to pytest. (#4264) * Moving a bunch of unit tests to pytest Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * style: pre-commit fixes * style fix Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * Update tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_mpm_half_cell.py Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> * Update tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spm.py Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> * Update tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spme.py Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> * Update tests/unit/test_parameters/test_parameter_sets_class.py Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> * Update tests/unit/test_parameters/test_parameter_sets_class.py Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> * Update tests/unit/test_plotting/test_plot.py Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> * adding skips Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * Update tests/unit/test_plotting/test_plot.py Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> * adding reason for skips Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> --------- Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> --- tests/unit/test_doc_utils.py | 25 ++--- tests/unit/test_expression_tree/test_array.py | 31 ++----- tests/unit/test_expression_tree/test_d_dt.py | 47 ++++------ .../test_independent_variable.py | 72 ++++++--------- .../test_input_parameter.py | 46 ++++------ .../unit/test_expression_tree/test_matrix.py | 32 ++----- .../test_operations/test_unpack_symbols.py | 26 ++---- .../test_printing/test_print_name.py | 35 ++----- .../test_printing/test_sympy_overrides.py | 22 +---- .../unit/test_expression_tree/test_scalar.py | 45 ++++----- .../unit/test_expression_tree/test_vector.py | 27 ++---- tests/unit/test_logger.py | 31 ++----- .../test_zero_dimensional_submesh.py | 17 +--- tests/unit/test_models/test_event.py | 32 ++----- .../test_base_lead_acid_model.py | 33 ++----- .../test_lead_acid/test_basic_models.py | 14 +-- .../test_lead_acid/test_full.py | 22 +---- .../test_lithium_ion/test_Yang2017.py | 14 +-- .../test_base_lithium_ion_model.py | 41 +++------ .../test_lithium_ion/test_basic_models.py | 14 +-- .../test_lithium_ion/test_dfn.py | 18 +--- .../test_lithium_ion/test_dfn_half_cell.py | 17 +--- .../test_lithium_ion/test_mpm_half_cell.py | 22 +---- .../test_lithium_ion/test_msmr.py | 14 +-- .../test_lithium_ion/test_newman_tobias.py | 23 ++--- .../test_lithium_ion/test_spm.py | 34 +++---- .../test_lithium_ion/test_spm_half_cell.py | 16 +--- .../test_lithium_ion/test_spme.py | 18 +--- .../test_lithium_ion/test_spme_half_cell.py | 16 +--- tests/unit/test_models/test_model_info.py | 14 +-- .../test_submodels/test_base_submodel.py | 40 +++----- .../test_effective_current_collector.py | 37 +++----- .../test_particle_polynomial_profile.py | 17 +--- .../test_parameters/test_base_parameters.py | 35 +++---- .../test_electrical_parameters.py | 20 +--- .../test_geometric_parameters.py | 18 +--- .../test_parameter_sets/test_Ai2020.py | 19 +--- .../test_Ecker2015_graphite_halfcell.py | 19 +--- .../test_LCO_Ramadass2004.py | 19 +--- .../test_LGM50_ORegan2022.py | 19 +--- .../test_OKane2022_negative_halfcell.py | 19 +--- .../test_parameter_sets_class.py | 34 +++---- .../test_size_distribution_parameters.py | 17 +--- tests/unit/test_plotting/test_plot.py | 29 ++---- .../test_plot_summary_variables.py | 26 ++---- .../test_plot_thermal_components.py | 21 +---- .../test_plot_voltage_components.py | 30 ++---- tests/unit/test_settings.py | 42 ++++----- tests/unit/test_solvers/test_dummy_solver.py | 14 +-- tests/unit/test_solvers/test_lrudict.py | 30 +++--- .../test_zero_dimensional_method.py | 30 ++---- tests/unit/test_timer.py | 92 ++++++++----------- 52 files changed, 423 insertions(+), 1022 deletions(-) diff --git a/tests/unit/test_doc_utils.py b/tests/unit/test_doc_utils.py index a7a4a1e5d5..8e8a626535 100644 --- a/tests/unit/test_doc_utils.py +++ b/tests/unit/test_doc_utils.py @@ -3,14 +3,11 @@ # is generated, but rather that the docstrings are correctly modified # -import pybamm -import unittest -from tests import TestCase from inspect import getmro from pybamm.doc_utils import copy_parameter_doc_from_parent, doc_extend_parent -class TestDocUtils(TestCase): +class TestDocUtils: def test_copy_parameter_doc_from_parent(self): """Test if parameters from the parent class are copied to child class docstring""" @@ -38,7 +35,7 @@ def __init__(self, foo, bar): base_parameters = "".join(Base.__doc__.partition("Parameters")[1:]) derived_parameters = "".join(Derived.__doc__.partition("Parameters")[1:]) # check that the parameters section is in the docstring - self.assertMultiLineEqual(base_parameters, derived_parameters) + assert base_parameters == derived_parameters def test_doc_extend_parent(self): """Test if the child class has the Extends directive in its docstring""" @@ -57,21 +54,11 @@ def __init__(self, param): super().__init__(param) # check that the Extends directive is in the docstring - self.assertIn("**Extends:**", Derived.__doc__) + assert "**Extends:**" in Derived.__doc__ # check that the Extends directive maps to the correct base class base_cls_name = f"{getmro(Derived)[1].__module__}.{getmro(Derived)[1].__name__}" - self.assertEqual( - Derived.__doc__.partition("**Extends:**")[2].strip(), - f":class:`{base_cls_name}`", + assert ( + Derived.__doc__.partition("**Extends:**")[2].strip() + == f":class:`{base_cls_name}`" ) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_expression_tree/test_array.py b/tests/unit/test_expression_tree/test_array.py index b75c313f47..ffd29baa7e 100644 --- a/tests/unit/test_expression_tree/test_array.py +++ b/tests/unit/test_expression_tree/test_array.py @@ -1,20 +1,17 @@ # # Tests for the Array class # -from tests import TestCase -import unittest -import unittest.mock as mock + import numpy as np import sympy - import pybamm -class TestArray(TestCase): +class TestArray: def test_name(self): arr = pybamm.Array(np.array([1, 2, 3])) - self.assertEqual(arr.name, "Array of shape (3, 1)") + assert arr.name == "Array of shape (3, 1)" def test_list_entries(self): vect = pybamm.Array([1, 2, 3]) @@ -38,16 +35,14 @@ def test_meshgrid(self): np.testing.assert_array_equal(B, D.entries) def test_to_equation(self): - self.assertEqual( - pybamm.Array([1, 2]).to_equation(), sympy.Array([[1.0], [2.0]]) - ) + assert pybamm.Array([1, 2]).to_equation() == sympy.Array([[1.0], [2.0]]) - def test_to_from_json(self): + def test_to_from_json(self, mocker): arr = pybamm.Array(np.array([1, 2, 3])) json_dict = { "name": "Array of shape (3, 1)", - "id": mock.ANY, + "id": mocker.ANY, "domains": { "primary": [], "secondary": [], @@ -59,17 +54,7 @@ def test_to_from_json(self): # array to json conversion created_json = arr.to_json() - self.assertEqual(created_json, json_dict) + assert created_json == json_dict # json to array conversion - self.assertEqual(pybamm.Array._from_json(created_json), arr) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() + assert pybamm.Array._from_json(created_json) == arr diff --git a/tests/unit/test_expression_tree/test_d_dt.py b/tests/unit/test_expression_tree/test_d_dt.py index b5632f9f64..38d7e20e13 100644 --- a/tests/unit/test_expression_tree/test_d_dt.py +++ b/tests/unit/test_expression_tree/test_d_dt.py @@ -1,43 +1,42 @@ # # Tests for the Scalar class # -from tests import TestCase +import pytest import pybamm -import unittest import numpy as np -class TestDDT(TestCase): +class TestDDT: def test_time_derivative(self): a = pybamm.Scalar(5).diff(pybamm.t) - self.assertIsInstance(a, pybamm.Scalar) - self.assertEqual(a.value, 0) + assert isinstance(a, pybamm.Scalar) + assert a.value == 0 a = pybamm.t.diff(pybamm.t) - self.assertIsInstance(a, pybamm.Scalar) - self.assertEqual(a.value, 1) + assert isinstance(a, pybamm.Scalar) + assert a.value == 1 a = (pybamm.t**2).diff(pybamm.t) - self.assertEqual(a, (2 * pybamm.t**1 * 1)) - self.assertEqual(a.evaluate(t=1), 2) + assert a == (2 * pybamm.t**1 * 1) + assert a.evaluate(t=1) == 2 a = (2 + pybamm.t**2).diff(pybamm.t) - self.assertEqual(a.evaluate(t=1), 2) + assert a.evaluate(t=1) == 2 def test_time_derivative_of_variable(self): a = (pybamm.Variable("a")).diff(pybamm.t) - self.assertIsInstance(a, pybamm.VariableDot) - self.assertEqual(a.name, "a'") + assert isinstance(a, pybamm.VariableDot) + assert a.name == "a'" p = pybamm.Parameter("p") a = 1 + p * pybamm.Variable("a") diff_a = a.diff(pybamm.t) - self.assertIsInstance(diff_a, pybamm.Multiplication) - self.assertEqual(diff_a.children[0].name, "p") - self.assertEqual(diff_a.children[1].name, "a'") + assert isinstance(diff_a, pybamm.Multiplication) + assert diff_a.children[0].name == "p" + assert diff_a.children[1].name == "a'" - with self.assertRaises(pybamm.ModelError): + with pytest.raises(pybamm.ModelError): a = (pybamm.Variable("a")).diff(pybamm.t).diff(pybamm.t) def test_time_derivative_of_state_vector(self): @@ -45,21 +44,11 @@ def test_time_derivative_of_state_vector(self): y_dot = np.linspace(0, 2, 19) a = sv.diff(pybamm.t) - self.assertIsInstance(a, pybamm.StateVectorDot) - self.assertEqual(a.name[-1], "'") + assert isinstance(a, pybamm.StateVectorDot) + assert a.name[-1] == "'" np.testing.assert_array_equal( a.evaluate(y_dot=y_dot), np.linspace(0, 1, 10)[:, np.newaxis] ) - with self.assertRaises(pybamm.ModelError): + with pytest.raises(pybamm.ModelError): a = (sv).diff(pybamm.t).diff(pybamm.t) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_expression_tree/test_independent_variable.py b/tests/unit/test_expression_tree/test_independent_variable.py index f747b60d40..79c5ab9ea2 100644 --- a/tests/unit/test_expression_tree/test_independent_variable.py +++ b/tests/unit/test_expression_tree/test_independent_variable.py @@ -1,87 +1,73 @@ # # Tests for the Parameter class # -from tests import TestCase -import unittest - +import pytest import pybamm import sympy -class TestIndependentVariable(TestCase): +class TestIndependentVariable: def test_variable_init(self): a = pybamm.IndependentVariable("a") - self.assertEqual(a.name, "a") - self.assertEqual(a.domain, []) + assert a.name == "a" + assert a.domain == [] a = pybamm.IndependentVariable("a", domain=["test"]) - self.assertEqual(a.domain[0], "test") + assert a.domain[0] == "test" a = pybamm.IndependentVariable("a", domain="test") - self.assertEqual(a.domain[0], "test") - with self.assertRaises(TypeError): + assert a.domain[0] == "test" + with pytest.raises(TypeError): pybamm.IndependentVariable("a", domain=1) def test_time(self): t = pybamm.Time() - self.assertEqual(t.name, "time") - self.assertEqual(t.evaluate(4), 4) - with self.assertRaises(ValueError): + assert t.name == "time" + assert t.evaluate(4) == 4 + with pytest.raises(ValueError): t.evaluate(None) t = pybamm.t - self.assertEqual(t.name, "time") - self.assertEqual(t.evaluate(4), 4) - with self.assertRaises(ValueError): + assert t.name == "time" + assert t.evaluate(4) == 4 + with pytest.raises(ValueError): t.evaluate(None) - self.assertEqual(t.evaluate_for_shape(), 0) + assert t.evaluate_for_shape() == 0 def test_spatial_variable(self): x = pybamm.SpatialVariable("x", "negative electrode") - self.assertEqual(x.name, "x") - self.assertFalse(x.evaluates_on_edges("primary")) + assert x.name == "x" + assert not x.evaluates_on_edges("primary") y = pybamm.SpatialVariable("y", "separator") - self.assertEqual(y.name, "y") + assert y.name == "y" z = pybamm.SpatialVariable("z", "positive electrode") - self.assertEqual(z.name, "z") + assert z.name == "z" r = pybamm.SpatialVariable("r", "negative particle") - self.assertEqual(r.name, "r") - with self.assertRaises(NotImplementedError): + assert r.name == "r" + with pytest.raises(NotImplementedError): x.evaluate() - with self.assertRaisesRegex(ValueError, "domain must be"): + with pytest.raises(ValueError, match="domain must be"): pybamm.SpatialVariable("x", []) - with self.assertRaises(pybamm.DomainError): + with pytest.raises(pybamm.DomainError): pybamm.SpatialVariable("r_n", ["positive particle"]) - with self.assertRaises(pybamm.DomainError): + with pytest.raises(pybamm.DomainError): pybamm.SpatialVariable("r_p", ["negative particle"]) - with self.assertRaises(pybamm.DomainError): + with pytest.raises(pybamm.DomainError): pybamm.SpatialVariable("x", ["negative particle"]) def test_spatial_variable_edge(self): x = pybamm.SpatialVariableEdge("x", "negative electrode") - self.assertEqual(x.name, "x") - self.assertTrue(x.evaluates_on_edges("primary")) + assert x.name == "x" + assert x.evaluates_on_edges("primary") def test_to_equation(self): # Test print_name func = pybamm.IndependentVariable("a") func.print_name = "test" - self.assertEqual(func.to_equation(), sympy.Symbol("test")) + assert func.to_equation() == sympy.Symbol("test") - self.assertEqual( - pybamm.IndependentVariable("a").to_equation(), sympy.Symbol("a") - ) + assert pybamm.IndependentVariable("a").to_equation() == sympy.Symbol("a") # Test time - self.assertEqual(pybamm.t.to_equation(), sympy.Symbol("t")) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() + assert pybamm.t.to_equation() == sympy.Symbol("t") diff --git a/tests/unit/test_expression_tree/test_input_parameter.py b/tests/unit/test_expression_tree/test_input_parameter.py index a5fc79f2e2..87cbe79a31 100644 --- a/tests/unit/test_expression_tree/test_input_parameter.py +++ b/tests/unit/test_expression_tree/test_input_parameter.py @@ -1,54 +1,52 @@ # # Tests for the InputParameter class # -from tests import TestCase import numpy as np import pybamm -import unittest - +import pytest import unittest.mock as mock -class TestInputParameter(TestCase): +class TestInputParameter: def test_input_parameter_init(self): a = pybamm.InputParameter("a") - self.assertEqual(a.name, "a") - self.assertEqual(a.evaluate(inputs={"a": 1}), 1) - self.assertEqual(a.evaluate(inputs={"a": 5}), 5) + assert a.name == "a" + assert a.evaluate(inputs={"a": 1}) == 1 + assert a.evaluate(inputs={"a": 5}) == 5 a = pybamm.InputParameter("a", expected_size=10) - self.assertEqual(a._expected_size, 10) + assert a._expected_size == 10 np.testing.assert_array_equal( a.evaluate(inputs="shape test"), np.nan * np.ones((10, 1)) ) y = np.linspace(0, 1, 10) np.testing.assert_array_equal(a.evaluate(inputs={"a": y}), y[:, np.newaxis]) - with self.assertRaisesRegex( + with pytest.raises( ValueError, - "Input parameter 'a' was given an object of size '1' but was expecting an " + match="Input parameter 'a' was given an object of size '1' but was expecting an " "object of size '10'", ): a.evaluate(inputs={"a": 5}) def test_evaluate_for_shape(self): a = pybamm.InputParameter("a") - self.assertTrue(np.isnan(a.evaluate_for_shape())) - self.assertEqual(a.shape, ()) + assert np.isnan(a.evaluate_for_shape()) + assert a.shape == () a = pybamm.InputParameter("a", expected_size=10) - self.assertEqual(a.shape, (10, 1)) + assert a.shape == (10, 1) np.testing.assert_equal(a.evaluate_for_shape(), np.nan * np.ones((10, 1))) - self.assertEqual(a.evaluate_for_shape().shape, (10, 1)) + assert a.evaluate_for_shape().shape == (10, 1) def test_errors(self): a = pybamm.InputParameter("a") - with self.assertRaises(TypeError): + with pytest.raises(TypeError): a.evaluate(inputs="not a dictionary") - with self.assertRaises(KeyError): + with pytest.raises(KeyError): a.evaluate(inputs={"bad param": 5}) # if u is not provided it gets turned into a dictionary and then raises KeyError - with self.assertRaises(KeyError): + with pytest.raises(KeyError): a.evaluate() def test_to_from_json(self): @@ -62,17 +60,7 @@ def test_to_from_json(self): } # to_json - self.assertEqual(a.to_json(), json_dict) + assert a.to_json() == json_dict # from_json - self.assertEqual(pybamm.InputParameter._from_json(json_dict), a) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() + assert pybamm.InputParameter._from_json(json_dict) == a diff --git a/tests/unit/test_expression_tree/test_matrix.py b/tests/unit/test_expression_tree/test_matrix.py index 055902b15e..d34af0d83f 100644 --- a/tests/unit/test_expression_tree/test_matrix.py +++ b/tests/unit/test_expression_tree/test_matrix.py @@ -1,26 +1,22 @@ # # Tests for the Matrix class # -from tests import TestCase import pybamm import numpy as np from scipy.sparse import csr_matrix -import unittest -import unittest.mock as mock - -class TestMatrix(TestCase): - def setUp(self): +class TestMatrix: + def setup_method(self): self.A = np.array([[1, 2, 0], [0, 1, 0], [0, 0, 1]]) self.x = np.array([1, 2, 3]) self.mat = pybamm.Matrix(self.A) self.vect = pybamm.Vector(self.x) def test_array_wrapper(self): - self.assertEqual(self.mat.ndim, 2) - self.assertEqual(self.mat.shape, (3, 3)) - self.assertEqual(self.mat.size, 9) + assert self.mat.ndim == 2 + assert self.mat.shape == (3, 3) + assert self.mat.size == 9 def test_list_entry(self): mat = pybamm.Matrix([[1, 2, 0], [0, 1, 0], [0, 0, 1]]) @@ -40,11 +36,11 @@ def test_matrix_operations(self): (self.mat @ self.vect).evaluate(), np.array([[5], [2], [3]]) ) - def test_to_from_json(self): + def test_to_from_json(self, mocker): arr = pybamm.Matrix(csr_matrix([[0, 1, 0, 0], [0, 0, 0, 1]])) json_dict = { "name": "Sparse Matrix (2, 4)", - "id": mock.ANY, + "id": mocker.ANY, "domains": { "primary": [], "secondary": [], @@ -59,16 +55,6 @@ def test_to_from_json(self): }, } - self.assertEqual(arr.to_json(), json_dict) - - self.assertEqual(pybamm.Matrix._from_json(json_dict), arr) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys + assert arr.to_json() == json_dict - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() + assert pybamm.Matrix._from_json(json_dict) == arr diff --git a/tests/unit/test_expression_tree/test_operations/test_unpack_symbols.py b/tests/unit/test_expression_tree/test_operations/test_unpack_symbols.py index ce669212ae..6736094288 100644 --- a/tests/unit/test_expression_tree/test_operations/test_unpack_symbols.py +++ b/tests/unit/test_expression_tree/test_operations/test_unpack_symbols.py @@ -1,27 +1,25 @@ # # Tests for the symbol unpacker # -from tests import TestCase import pybamm -import unittest -class TestSymbolUnpacker(TestCase): +class TestSymbolUnpacker: def test_basic_symbols(self): a = pybamm.Scalar(1) unpacker = pybamm.SymbolUnpacker(pybamm.Scalar) unpacked = unpacker.unpack_symbol(a) - self.assertEqual(unpacked, set([a])) + assert unpacked == set([a]) b = pybamm.Parameter("b") unpacker_param = pybamm.SymbolUnpacker(pybamm.Parameter) unpacked = unpacker_param.unpack_symbol(a) - self.assertEqual(unpacked, set()) + assert unpacked == set() unpacked = unpacker_param.unpack_symbol(b) - self.assertEqual(unpacked, set([b])) + assert unpacked == set([b]) def test_binary(self): a = pybamm.Scalar(1) @@ -29,11 +27,11 @@ def test_binary(self): unpacker = pybamm.SymbolUnpacker(pybamm.Scalar) unpacked = unpacker.unpack_symbol(a + b) - self.assertEqual(unpacked, set([a])) + assert unpacked == set([a]) unpacker_param = pybamm.SymbolUnpacker(pybamm.Parameter) unpacked = unpacker_param.unpack_symbol(a + b) - self.assertEqual(unpacked, set([b])) + assert unpacked == set([b]) def test_unpack_list_of_symbols(self): a = pybamm.Scalar(1) @@ -42,14 +40,4 @@ def test_unpack_list_of_symbols(self): unpacker = pybamm.SymbolUnpacker(pybamm.Parameter) unpacked = unpacker.unpack_list_of_symbols([a + b, a - c, b + c]) - self.assertEqual(unpacked, set([b, c])) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() + assert unpacked == set([b, c]) diff --git a/tests/unit/test_expression_tree/test_printing/test_print_name.py b/tests/unit/test_expression_tree/test_printing/test_print_name.py index 9d74d6f1ab..c15ce18616 100644 --- a/tests/unit/test_expression_tree/test_printing/test_print_name.py +++ b/tests/unit/test_expression_tree/test_printing/test_print_name.py @@ -2,57 +2,42 @@ Tests for the print_name.py """ -from tests import TestCase -import unittest - import pybamm -class TestPrintName(TestCase): +class TestPrintName: def test_prettify_print_name(self): param = pybamm.LithiumIonParameters() param2 = pybamm.LeadAcidParameters() # Test PRINT_NAME_OVERRIDES - self.assertEqual(param.current_with_time.print_name, "I") + assert param.current_with_time.print_name == "I" # Test superscripts - self.assertEqual( - param.n.prim.c_init.print_name, r"c_{\mathrm{n}}^{\mathrm{init}}" - ) + assert param.n.prim.c_init.print_name == r"c_{\mathrm{n}}^{\mathrm{init}}" # Test subscripts - self.assertEqual(param.n.C_dl(0).print_name, r"C_{\mathrm{dl,n}}") + assert param.n.C_dl(0).print_name == r"C_{\mathrm{dl,n}}" # Test bar c_e_av = pybamm.Variable("c_e_av") c_e_av.print_name = "c_e_av" - self.assertEqual(c_e_av.print_name, r"\overline{c}_{\mathrm{e}}") + assert c_e_av.print_name == r"\overline{c}_{\mathrm{e}}" # Test greek letters - self.assertEqual(param2.delta.print_name, r"\delta") + assert param2.delta.print_name == r"\delta" # Test create_copy() a_n = param2.n.prim.a - self.assertEqual(a_n.create_copy().print_name, r"a_{\mathrm{n}}") + assert a_n.create_copy().print_name == r"a_{\mathrm{n}}" # Test eps eps_n = pybamm.Variable("eps_n") - self.assertEqual(eps_n.print_name, r"\epsilon_{\mathrm{n}}") + assert eps_n.print_name == r"\epsilon_{\mathrm{n}}" eps_n = pybamm.Variable("eps_c_e_n") - self.assertEqual(eps_n.print_name, r"(\epsilon c)_{\mathrm{e,n}}") + assert eps_n.print_name == r"(\epsilon c)_{\mathrm{e,n}}" # tplus t_plus = pybamm.Variable("t_plus") - self.assertEqual(t_plus.print_name, r"t_{\mathrm{+}}") - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() + assert t_plus.print_name == r"t_{\mathrm{+}}" diff --git a/tests/unit/test_expression_tree/test_printing/test_sympy_overrides.py b/tests/unit/test_expression_tree/test_printing/test_sympy_overrides.py index 4b19c7d822..4ce073af4b 100644 --- a/tests/unit/test_expression_tree/test_printing/test_sympy_overrides.py +++ b/tests/unit/test_expression_tree/test_printing/test_sympy_overrides.py @@ -2,31 +2,17 @@ Tests for the sympy_overrides.py """ -from tests import TestCase -import unittest - -import pybamm from pybamm.expression_tree.printing.sympy_overrides import custom_print_func import sympy -class TestCustomPrint(TestCase): - def test_print_Derivative(self): +class TestCustomPrint: + def test_print_derivative(self): # Test force_partial der1 = sympy.Derivative("y", "x") der1.force_partial = True - self.assertEqual(custom_print_func(der1), "\\frac{\\partial}{\\partial x} y") + assert custom_print_func(der1) == "\\frac{\\partial}{\\partial x} y" # Test derivative der2 = sympy.Derivative("x") - self.assertEqual(custom_print_func(der2), "\\frac{d}{d x} x") - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() + assert custom_print_func(der2) == "\\frac{d}{d x} x" diff --git a/tests/unit/test_expression_tree/test_scalar.py b/tests/unit/test_expression_tree/test_scalar.py index 34ea1aa514..986d3d3ccb 100644 --- a/tests/unit/test_expression_tree/test_scalar.py +++ b/tests/unit/test_expression_tree/test_scalar.py @@ -1,64 +1,51 @@ # # Tests for the Scalar class # -from tests import TestCase -import unittest -import unittest.mock as mock import pybamm -class TestScalar(TestCase): +class TestScalar: def test_scalar_eval(self): a = pybamm.Scalar(5) - self.assertEqual(a.value, 5) - self.assertEqual(a.evaluate(), 5) + assert a.value == 5 + assert a.evaluate() == 5 def test_scalar_operations(self): a = pybamm.Scalar(5) b = pybamm.Scalar(6) - self.assertEqual((a + b).evaluate(), 11) - self.assertEqual((a - b).evaluate(), -1) - self.assertEqual((a * b).evaluate(), 30) - self.assertEqual((a / b).evaluate(), 5 / 6) + assert (a + b).evaluate() == 11 + assert (a - b).evaluate() == -1 + assert (a * b).evaluate() == 30 + assert (a / b).evaluate() == 5 / 6 def test_scalar_eq(self): a1 = pybamm.Scalar(4) a2 = pybamm.Scalar(4) - self.assertEqual(a1, a2) + assert a1 == a2 a3 = pybamm.Scalar(5) - self.assertNotEqual(a1, a3) + assert a1 != a3 def test_to_equation(self): a = pybamm.Scalar(3) b = pybamm.Scalar(4) # Test value - self.assertEqual(str(a.to_equation()), "3.0") + assert str(a.to_equation()) == "3.0" # Test print_name b.print_name = "test" - self.assertEqual(str(b.to_equation()), "test") + assert str(b.to_equation()) == "test" def test_copy(self): a = pybamm.Scalar(5) b = a.create_copy() - self.assertEqual(a, b) + assert a == b - def test_to_from_json(self): + def test_to_from_json(self, mocker): a = pybamm.Scalar(5) - json_dict = {"name": "5.0", "id": mock.ANY, "value": 5.0} + json_dict = {"name": "5.0", "id": mocker.ANY, "value": 5.0} - self.assertEqual(a.to_json(), json_dict) + assert a.to_json() == json_dict - self.assertEqual(pybamm.Scalar._from_json(json_dict), a) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() + assert pybamm.Scalar._from_json(json_dict) == a diff --git a/tests/unit/test_expression_tree/test_vector.py b/tests/unit/test_expression_tree/test_vector.py index 34f817cf9c..e7b902fc73 100644 --- a/tests/unit/test_expression_tree/test_vector.py +++ b/tests/unit/test_expression_tree/test_vector.py @@ -1,22 +1,21 @@ # # Tests for the Vector class # -from tests import TestCase import pybamm import numpy as np -import unittest +import pytest -class TestVector(TestCase): - def setUp(self): +class TestVector: + def setup_method(self): self.x = np.array([[1], [2], [3]]) self.vect = pybamm.Vector(self.x) def test_array_wrapper(self): - self.assertEqual(self.vect.ndim, 2) - self.assertEqual(self.vect.shape, (3, 1)) - self.assertEqual(self.vect.size, 3) + assert self.vect.ndim == 2 + assert self.vect.shape == (3, 1) + assert self.vect.size == 3 def test_column_reshape(self): vect1d = pybamm.Vector(np.array([1, 2, 3])) @@ -39,17 +38,7 @@ def test_vector_operations(self): ) def test_wrong_size_entries(self): - with self.assertRaisesRegex( - ValueError, "Entries must have 1 dimension or be column vector" + with pytest.raises( + ValueError, match="Entries must have 1 dimension or be column vector" ): pybamm.Vector(np.ones((4, 5))) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_logger.py b/tests/unit/test_logger.py index 0897bc5835..06e2444c16 100644 --- a/tests/unit/test_logger.py +++ b/tests/unit/test_logger.py @@ -1,28 +1,27 @@ # # Tests the logger class. # -from tests import TestCase +import pytest import pybamm -import unittest -class TestLogger(TestCase): +class TestLogger: def test_logger(self): logger = pybamm.logger - self.assertEqual(logger.level, 30) + assert logger.level == 30 pybamm.set_logging_level("INFO") - self.assertEqual(logger.level, 20) + assert logger.level == 20 pybamm.set_logging_level("ERROR") - self.assertEqual(logger.level, 40) + assert logger.level == 40 pybamm.set_logging_level("VERBOSE") - self.assertEqual(logger.level, 15) + assert logger.level == 15 pybamm.set_logging_level("NOTICE") - self.assertEqual(logger.level, 25) + assert logger.level == 25 pybamm.set_logging_level("SUCCESS") - self.assertEqual(logger.level, 35) + assert logger.level == 35 pybamm.set_logging_level("SPAM") - self.assertEqual(logger.level, 5) + assert logger.level == 5 pybamm.logger.spam("Test spam level") pybamm.logger.verbose("Test verbose level") pybamm.logger.notice("Test notice level") @@ -32,15 +31,5 @@ def test_logger(self): pybamm.set_logging_level("WARNING") def test_exceptions(self): - with self.assertRaises(ValueError): + with pytest.raises(ValueError): pybamm.get_new_logger("test", None) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_meshes/test_zero_dimensional_submesh.py b/tests/unit/test_meshes/test_zero_dimensional_submesh.py index 8bc1bc2e75..d9e3ebb5dd 100644 --- a/tests/unit/test_meshes/test_zero_dimensional_submesh.py +++ b/tests/unit/test_meshes/test_zero_dimensional_submesh.py @@ -1,12 +1,11 @@ import pybamm -import unittest -from tests import TestCase +import pytest -class TestSubMesh0D(TestCase): +class TestSubMesh0D: def test_exceptions(self): position = {"x": {"position": 0}, "y": {"position": 0}} - with self.assertRaises(pybamm.GeometryError): + with pytest.raises(pybamm.GeometryError): pybamm.SubMesh0D(position) def test_init(self): @@ -14,13 +13,3 @@ def test_init(self): generator = pybamm.SubMesh0D mesh = generator(position, None) mesh.add_ghost_meshes() - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_models/test_event.py b/tests/unit/test_models/test_event.py index 84b0dcde84..0636a0f5bd 100644 --- a/tests/unit/test_models/test_event.py +++ b/tests/unit/test_models/test_event.py @@ -1,27 +1,25 @@ # # Tests Event class # -from tests import TestCase import pybamm import numpy as np -import unittest -class TestEvent(TestCase): +class TestEvent: def test_event(self): expression = pybamm.Scalar(1) event = pybamm.Event("my event", expression) - self.assertEqual(event.name, "my event") - self.assertEqual(event.__str__(), "my event") - self.assertEqual(event.expression, expression) - self.assertEqual(event.event_type, pybamm.EventType.TERMINATION) + assert event.name == "my event" + assert event.__str__() == "my event" + assert event.expression == expression + assert event.event_type == pybamm.EventType.TERMINATION def test_expression_evaluate(self): # Test t expression = pybamm.t event = pybamm.Event("my event", expression) - self.assertEqual(event.evaluate(t=1), 1) + assert event.evaluate(t=1) == 1 # Test y sv = pybamm.StateVector(slice(0, 10)) @@ -46,7 +44,7 @@ def test_event_types(self): for event_type in event_types: event = pybamm.Event("my event", pybamm.Scalar(1), event_type) - self.assertEqual(event.event_type, event_type) + assert event.event_type == event_type def test_to_from_json(self): expression = pybamm.Scalar(1) @@ -58,24 +56,14 @@ def test_to_from_json(self): } event_ser_json = event.to_json() - self.assertEqual(event_ser_json, event_json) + assert event_ser_json == event_json event_json["expression"] = expression new_event = pybamm.Event._from_json(event_json) # check for equal expressions - self.assertEqual(new_event.expression, event.expression) + assert new_event.expression == event.expression # check for equal event types - self.assertEqual(new_event.event_type, event.event_type) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() + assert new_event.event_type == event.event_type diff --git a/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_base_lead_acid_model.py b/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_base_lead_acid_model.py index ec280cdd1f..5d9ea27e2f 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_base_lead_acid_model.py +++ b/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_base_lead_acid_model.py @@ -1,46 +1,33 @@ # # Tests for the base lead acid model class # -from tests import TestCase import pybamm -import unittest +import pytest -class TestBaseLeadAcidModel(TestCase): +class TestBaseLeadAcidModel: def test_default_geometry(self): model = pybamm.lead_acid.BaseModel({"dimensionality": 0}) - self.assertEqual( - model.default_geometry["current collector"]["z"]["position"], 1 - ) + assert model.default_geometry["current collector"]["z"]["position"] == 1 model = pybamm.lead_acid.BaseModel({"dimensionality": 1}) - self.assertEqual(model.default_geometry["current collector"]["z"]["min"], 0) + assert model.default_geometry["current collector"]["z"]["min"] == 0 model = pybamm.lead_acid.BaseModel({"dimensionality": 2}) - self.assertEqual(model.default_geometry["current collector"]["y"]["min"], 0) + assert model.default_geometry["current collector"]["y"]["min"] == 0 def test_incompatible_options(self): - with self.assertRaisesRegex( + with pytest.raises( pybamm.OptionError, - "Lead-acid models can only have thermal effects if dimensionality is 0.", + match="Lead-acid models can only have thermal effects if dimensionality is 0.", ): pybamm.lead_acid.BaseModel({"dimensionality": 1, "thermal": "lumped"}) - with self.assertRaisesRegex(pybamm.OptionError, "SEI"): + with pytest.raises(pybamm.OptionError, match="SEI"): pybamm.lead_acid.BaseModel({"SEI": "constant"}) - with self.assertRaisesRegex(pybamm.OptionError, "lithium plating"): + with pytest.raises(pybamm.OptionError, match="lithium plating"): pybamm.lead_acid.BaseModel({"lithium plating": "reversible"}) - with self.assertRaisesRegex(pybamm.OptionError, "MSMR"): + with pytest.raises(pybamm.OptionError, match="MSMR"): pybamm.lead_acid.BaseModel( { "open-circuit potential": "MSMR", "particle": "MSMR", } ) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_basic_models.py b/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_basic_models.py index 65b9f6bc9f..a7a708b394 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_basic_models.py +++ b/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_basic_models.py @@ -1,22 +1,10 @@ # # Tests for the basic lead acid models # -from tests import TestCase import pybamm -import unittest -class TestBasicModels(TestCase): +class TestBasicModels: def test_basic_full_lead_acid_well_posed(self): model = pybamm.lead_acid.BasicFull() model.check_well_posedness() - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_full.py b/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_full.py index c07c5c84c6..569851ec2a 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_full.py +++ b/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_full.py @@ -1,12 +1,10 @@ # # Tests for the lead-acid Full model # -from tests import TestCase import pybamm -import unittest -class TestLeadAcidFull(TestCase): +class TestLeadAcidFull: def test_well_posed(self): model = pybamm.lead_acid.Full() model.check_well_posedness() @@ -21,7 +19,7 @@ def test_well_posed_with_convection(self): model.check_well_posedness() -class TestLeadAcidFullSurfaceForm(TestCase): +class TestLeadAcidFullSurfaceForm: def test_well_posed_differential(self): options = {"surface form": "differential"} model = pybamm.lead_acid.Full(options) @@ -38,7 +36,7 @@ def test_well_posed_algebraic(self): model.check_well_posedness() -class TestLeadAcidFullSideReactions(TestCase): +class TestLeadAcidFullSideReactions: def test_well_posed(self): options = {"hydrolysis": "true"} model = pybamm.lead_acid.Full(options) @@ -48,20 +46,10 @@ def test_well_posed_surface_form_differential(self): options = {"hydrolysis": "true", "surface form": "differential"} model = pybamm.lead_acid.Full(options) model.check_well_posedness() - self.assertIsInstance(model.default_solver, pybamm.CasadiSolver) + assert isinstance(model.default_solver, pybamm.CasadiSolver) def test_well_posed_surface_form_algebraic(self): options = {"hydrolysis": "true", "surface form": "algebraic"} model = pybamm.lead_acid.Full(options) model.check_well_posedness() - self.assertIsInstance(model.default_solver, pybamm.CasadiSolver) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() + assert isinstance(model.default_solver, pybamm.CasadiSolver) diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_Yang2017.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_Yang2017.py index 9631cf9f82..2fd18c17c6 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_Yang2017.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_Yang2017.py @@ -1,22 +1,10 @@ # # Tests for the lithium-ion DFN model # -from tests import TestCase import pybamm -import unittest -class TestYang2017(TestCase): +class TestYang2017: def test_well_posed(self): model = pybamm.lithium_ion.Yang2017() model.check_well_posedness() - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_base_lithium_ion_model.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_base_lithium_ion_model.py index fbc916d4a5..bfeb489661 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_base_lithium_ion_model.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_base_lithium_ion_model.py @@ -1,59 +1,44 @@ # # Tests for the base lead acid model class # -from tests import TestCase import pybamm -import unittest import os +import pytest -class TestBaseLithiumIonModel(TestCase): +class TestBaseLithiumIonModel: def test_incompatible_options(self): - with self.assertRaisesRegex(pybamm.OptionError, "convection not implemented"): + with pytest.raises(pybamm.OptionError, match="convection not implemented"): pybamm.lithium_ion.BaseModel({"convection": "uniform transverse"}) def test_default_parameters(self): # check parameters are read in ok model = pybamm.lithium_ion.BaseModel() - self.assertEqual( - model.default_parameter_values["Reference temperature [K]"], 298.15 - ) + assert model.default_parameter_values["Reference temperature [K]"] == 298.15 # change path and try again cwd = os.getcwd() os.chdir("..") model = pybamm.lithium_ion.BaseModel() - self.assertEqual( - model.default_parameter_values["Reference temperature [K]"], 298.15 - ) + assert model.default_parameter_values["Reference temperature [K]"] == 298.15 os.chdir(cwd) def test_insert_reference_electrode(self): model = pybamm.lithium_ion.SPM() model.insert_reference_electrode() - self.assertIn("Negative electrode 3E potential [V]", model.variables) - self.assertIn("Positive electrode 3E potential [V]", model.variables) - self.assertIn("Reference electrode potential [V]", model.variables) + assert "Negative electrode 3E potential [V]" in model.variables + assert "Positive electrode 3E potential [V]" in model.variables + assert "Reference electrode potential [V]" in model.variables model = pybamm.lithium_ion.SPM({"working electrode": "positive"}) model.insert_reference_electrode() - self.assertNotIn("Negative electrode potential [V]", model.variables) - self.assertIn("Positive electrode 3E potential [V]", model.variables) - self.assertIn("Reference electrode potential [V]", model.variables) + assert "Negative electrode potential [V]" not in model.variables + assert "Positive electrode 3E potential [V]" in model.variables + assert "Reference electrode potential [V]" in model.variables model = pybamm.lithium_ion.SPM({"dimensionality": 2}) - with self.assertRaisesRegex( - NotImplementedError, "Reference electrode can only be" + with pytest.raises( + NotImplementedError, match="Reference electrode can only be" ): model.insert_reference_electrode() - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_basic_models.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_basic_models.py index 2f00bb260c..8462e7c803 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_basic_models.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_basic_models.py @@ -1,12 +1,10 @@ # # Tests for the basic lithium-ion models # -from tests import TestCase import pybamm -import unittest -class TestBasicModels(TestCase): +class TestBasicModels: def test_dfn_well_posed(self): model = pybamm.lithium_ion.BasicDFN() model.check_well_posedness() @@ -23,13 +21,3 @@ def test_dfn_half_cell_well_posed(self): def test_dfn_composite_well_posed(self): model = pybamm.lithium_ion.BasicDFNComposite() model.check_well_posedness() - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py index 20fc69e541..cddd59c352 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py @@ -1,19 +1,19 @@ # # Tests for the lithium-ion DFN model # -from tests import TestCase import pybamm -import unittest +import pytest from tests import BaseUnitTestLithiumIon -class TestDFN(BaseUnitTestLithiumIon, TestCase): +class TestDFN(BaseUnitTestLithiumIon): + @pytest.fixture(autouse=True) def setUp(self): self.model = pybamm.lithium_ion.DFN def test_electrolyte_options(self): options = {"electrolyte conductivity": "integrated"} - with self.assertRaisesRegex(pybamm.OptionError, "electrolyte conductivity"): + with pytest.raises(pybamm.OptionError, match="electrolyte conductivity"): pybamm.lithium_ion.DFN(options) def test_well_posed_size_distribution(self): @@ -66,13 +66,3 @@ def test_well_posed_msmr_with_psd(self): "intercalation kinetics": "MSMR", } self.check_well_posedness(options) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_dfn_half_cell.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_dfn_half_cell.py index 78d9ebda94..389fcf9429 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_dfn_half_cell.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_dfn_half_cell.py @@ -1,22 +1,13 @@ # # Tests for the lithium-ion half-cell DFN model # -from tests import TestCase + import pybamm -import unittest from tests import BaseUnitTestLithiumIonHalfCell +import pytest -class TestDFNHalfCell(BaseUnitTestLithiumIonHalfCell, TestCase): +class TestDFNHalfCell(BaseUnitTestLithiumIonHalfCell): + @pytest.fixture(autouse=True) def setUp(self): self.model = pybamm.lithium_ion.DFN - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_mpm_half_cell.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_mpm_half_cell.py index 77d51f6cf7..e5637968c3 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_mpm_half_cell.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_mpm_half_cell.py @@ -1,12 +1,10 @@ # # Tests for the lithium-ion MPM model # -from tests import TestCase import pybamm -import unittest -class TestMPM(TestCase): +class TestMPM: def test_well_posed(self): options = {"thermal": "isothermal", "working electrode": "positive"} model = pybamm.lithium_ion.MPM(options) @@ -20,9 +18,9 @@ def test_well_posed(self): def test_default_parameter_values(self): # check default parameters are added correctly model = pybamm.lithium_ion.MPM({"working electrode": "positive"}) - self.assertEqual( - model.default_parameter_values["Positive minimum particle radius [m]"], - 0.0, + assert ( + model.default_parameter_values["Positive minimum particle radius [m]"] + == 0.0 ) def test_lumped_thermal_model_1D(self): @@ -44,7 +42,7 @@ def test_differential_surface_form(self): model.check_well_posedness() -class TestMPMExternalCircuits(TestCase): +class TestMPMExternalCircuits: def test_well_posed_voltage(self): options = {"operating mode": "voltage", "working electrode": "positive"} model = pybamm.lithium_ion.MPM(options) @@ -67,13 +65,3 @@ def external_circuit_function(variables): } model = pybamm.lithium_ion.MPM(options) model.check_well_posedness() - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_msmr.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_msmr.py index 96369fbac2..4f1958d095 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_msmr.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_msmr.py @@ -1,22 +1,10 @@ # # Tests for the lithium-ion MSMR model # -from tests import TestCase import pybamm -import unittest -class TestMSMR(TestCase): +class TestMSMR: def test_well_posed(self): model = pybamm.lithium_ion.MSMR({"number of MSMR reactions": ("6", "4")}) model.check_well_posedness() - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_newman_tobias.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_newman_tobias.py index 5369d94b29..5ceb039747 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_newman_tobias.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_newman_tobias.py @@ -1,42 +1,37 @@ # # Tests for the lithium-ion Newman-Tobias model # -from tests import TestCase import pybamm -import unittest +import pytest from tests import BaseUnitTestLithiumIon -class TestNewmanTobias(BaseUnitTestLithiumIon, TestCase): +class TestNewmanTobias(BaseUnitTestLithiumIon): + @pytest.fixture(autouse=True) def setUp(self): self.model = pybamm.lithium_ion.NewmanTobias def test_electrolyte_options(self): options = {"electrolyte conductivity": "integrated"} - with self.assertRaisesRegex(pybamm.OptionError, "electrolyte conductivity"): + with pytest.raises(pybamm.OptionError, match="electrolyte conductivity"): pybamm.lithium_ion.NewmanTobias(options) + @pytest.mark.skip(reason="Test currently not implemented") def test_well_posed_particle_phases(self): pass # skip this test + @pytest.mark.skip(reason="Test currently not implemented") def test_well_posed_particle_phases_thermal(self): pass # Skip this test + @pytest.mark.skip(reason="Test currently not implemented") def test_well_posed_particle_phases_sei(self): pass # skip this test + @pytest.mark.skip(reason="Test currently not implemented") def test_well_posed_composite_kinetic_hysteresis(self): pass # skip this test + @pytest.mark.skip(reason="Test currently not implemented") def test_well_posed_composite_diffusion_hysteresis(self): pass # skip this test - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spm.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spm.py index 45cf00877b..99affc7ddd 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spm.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spm.py @@ -1,19 +1,19 @@ # # Tests for the lithium-ion SPM model # -from tests import TestCase import pybamm -import unittest from tests import BaseUnitTestLithiumIon +import pytest -class TestSPM(BaseUnitTestLithiumIon, TestCase): +class TestSPM(BaseUnitTestLithiumIon): + @pytest.fixture(autouse=True) def setUp(self): self.model = pybamm.lithium_ion.SPM def test_electrolyte_options(self): options = {"electrolyte conductivity": "full"} - with self.assertRaisesRegex(pybamm.OptionError, "electrolyte conductivity"): + with pytest.raises(pybamm.OptionError, match="electrolyte conductivity"): pybamm.lithium_ion.SPM(options) def test_kinetics_options(self): @@ -21,7 +21,7 @@ def test_kinetics_options(self): "surface form": "false", "intercalation kinetics": "Marcus-Hush-Chidsey", } - with self.assertRaisesRegex(pybamm.OptionError, "Inverse kinetics"): + with pytest.raises(pybamm.OptionError, match="Inverse kinetics"): pybamm.lithium_ion.SPM(options) def test_x_average_options(self): @@ -37,11 +37,11 @@ def test_x_average_options(self): # Check model with distributed side reactions throws an error options["x-average side reactions"] = "false" - with self.assertRaisesRegex(pybamm.OptionError, "cannot be 'false' for SPM"): + with pytest.raises(pybamm.OptionError, match="cannot be 'false' for SPM"): pybamm.lithium_ion.SPM(options) def test_distribution_options(self): - with self.assertRaisesRegex(pybamm.OptionError, "surface form"): + with pytest.raises(pybamm.OptionError, match="surface form"): pybamm.lithium_ion.SPM({"particle size": "distribution"}) def test_particle_size_distribution(self): @@ -53,10 +53,10 @@ def test_new_model(self): new_model = model.new_copy() model_T_eqn = model.rhs[model.variables["Cell temperature [K]"]] new_model_T_eqn = new_model.rhs[new_model.variables["Cell temperature [K]"]] - self.assertEqual(new_model_T_eqn, model_T_eqn) - self.assertEqual(new_model.name, model.name) - self.assertEqual(new_model.use_jacobian, model.use_jacobian) - self.assertEqual(new_model.convert_to_format, model.convert_to_format) + assert new_model_T_eqn == model_T_eqn + assert new_model.name == model.name + assert new_model.use_jacobian == model.use_jacobian + assert new_model.convert_to_format == model.convert_to_format # with custom submodels options = {"stress-induced diffusion": "false", "thermal": "x-full"} @@ -72,14 +72,4 @@ def test_new_model(self): new_model = model.new_copy() new_model_cs_eqn = list(new_model.rhs.values())[1] model_cs_eqn = list(model.rhs.values())[1] - self.assertEqual(new_model_cs_eqn, model_cs_eqn) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() + assert new_model_cs_eqn == model_cs_eqn diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spm_half_cell.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spm_half_cell.py index 0d6ba93ce0..c1b6b34745 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spm_half_cell.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spm_half_cell.py @@ -1,22 +1,12 @@ # # Tests for the lithium-ion half-cell SPM model # -from tests import TestCase import pybamm -import unittest from tests import BaseUnitTestLithiumIonHalfCell +import pytest -class TestSPMHalfCell(BaseUnitTestLithiumIonHalfCell, TestCase): +class TestSPMHalfCell(BaseUnitTestLithiumIonHalfCell): + @pytest.fixture(autouse=True) def setUp(self): self.model = pybamm.lithium_ion.SPM - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spme.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spme.py index 72222ee060..b0d38fa9c7 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spme.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spme.py @@ -1,13 +1,13 @@ # # Tests for the lithium-ion SPMe model # -from tests import TestCase import pybamm -import unittest from tests import BaseUnitTestLithiumIon +import pytest -class TestSPMe(BaseUnitTestLithiumIon, TestCase): +class TestSPMe(BaseUnitTestLithiumIon): + @pytest.fixture(autouse=True) def setUp(self): self.model = pybamm.lithium_ion.SPMe @@ -31,19 +31,9 @@ def setUp(self): def test_electrolyte_options(self): options = {"electrolyte conductivity": "full"} - with self.assertRaisesRegex(pybamm.OptionError, "electrolyte conductivity"): + with pytest.raises(pybamm.OptionError, match="electrolyte conductivity"): pybamm.lithium_ion.SPMe(options) def test_integrated_conductivity(self): options = {"electrolyte conductivity": "integrated"} self.check_well_posedness(options) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spme_half_cell.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spme_half_cell.py index f1930df026..2a814c113e 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spme_half_cell.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_spme_half_cell.py @@ -3,21 +3,11 @@ # This is achieved by using the {"working electrode": "positive"} option # import pybamm -import unittest -from tests import TestCase from tests import BaseUnitTestLithiumIonHalfCell +import pytest -class TestSPMeHalfCell(BaseUnitTestLithiumIonHalfCell, TestCase): +class TestSPMeHalfCell(BaseUnitTestLithiumIonHalfCell): + @pytest.fixture(autouse=True) def setUp(self): self.model = pybamm.lithium_ion.SPMe - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_models/test_model_info.py b/tests/unit/test_models/test_model_info.py index 144d763bf1..b754399872 100644 --- a/tests/unit/test_models/test_model_info.py +++ b/tests/unit/test_models/test_model_info.py @@ -1,12 +1,10 @@ # # Tests getting model info # -from tests import TestCase import pybamm -import unittest -class TestModelInfo(TestCase): +class TestModelInfo: def test_find_parameter_info(self): model = pybamm.lithium_ion.SPM() model.info("Negative particle diffusivity [m2.s-1]") @@ -16,13 +14,3 @@ def test_find_parameter_info(self): model.info("Negative particle diffusivity [m2.s-1]") model.info("Not a parameter") - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_models/test_submodels/test_base_submodel.py b/tests/unit/test_models/test_submodels/test_base_submodel.py index 1519a2fea2..9f2a9c3549 100644 --- a/tests/unit/test_models/test_submodels/test_base_submodel.py +++ b/tests/unit/test_models/test_submodels/test_base_submodel.py @@ -1,52 +1,50 @@ # # Test base submodel # -from tests import TestCase - +import pytest import pybamm -import unittest -class TestBaseSubModel(TestCase): +class TestBaseSubModel: def test_domain(self): # Accepted string submodel = pybamm.BaseSubModel(None, "negative", phase="primary") - self.assertEqual(submodel.domain, "negative") + assert submodel.domain == "negative" # None submodel = pybamm.BaseSubModel(None, None) - self.assertEqual(submodel.domain, None) + assert submodel.domain is None # bad string - with self.assertRaises(pybamm.DomainError): + with pytest.raises(pybamm.DomainError): pybamm.BaseSubModel(None, "bad string") def test_phase(self): # Without domain submodel = pybamm.BaseSubModel(None, None) - self.assertEqual(submodel.phase, None) + assert submodel.phase is None - with self.assertRaisesRegex(ValueError, "Phase must be None"): + with pytest.raises(ValueError, match="Phase must be None"): pybamm.BaseSubModel(None, None, phase="primary") # With domain submodel = pybamm.BaseSubModel(None, "negative", phase="primary") - self.assertEqual(submodel.phase, "primary") - self.assertEqual(submodel.phase_name, "") + assert submodel.phase == "primary" + assert submodel.phase_name == "" submodel = pybamm.BaseSubModel( None, "negative", options={"particle phases": "2"}, phase="secondary" ) - self.assertEqual(submodel.phase, "secondary") - self.assertEqual(submodel.phase_name, "secondary ") + assert submodel.phase == "secondary" + assert submodel.phase_name == "secondary " - with self.assertRaisesRegex(ValueError, "Phase must be 'primary'"): + with pytest.raises(ValueError, match="Phase must be 'primary'"): pybamm.BaseSubModel(None, "negative", phase="secondary") - with self.assertRaisesRegex(ValueError, "Phase must be either 'primary'"): + with pytest.raises(ValueError, match="Phase must be either 'primary'"): pybamm.BaseSubModel( None, "negative", options={"particle phases": "2"}, phase="tertiary" ) - with self.assertRaisesRegex(ValueError, "Phase must be 'primary'"): + with pytest.raises(ValueError, match="Phase must be 'primary'"): # 2 phases in the negative but only 1 in the positive pybamm.BaseSubModel( None, @@ -54,13 +52,3 @@ def test_phase(self): options={"particle phases": ("2", "1")}, phase="secondary", ) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_models/test_submodels/test_effective_current_collector.py b/tests/unit/test_models/test_submodels/test_effective_current_collector.py index cbab3134d4..b2437ec1d9 100644 --- a/tests/unit/test_models/test_submodels/test_effective_current_collector.py +++ b/tests/unit/test_models/test_submodels/test_effective_current_collector.py @@ -1,13 +1,12 @@ # # Tests for the Effective Current Collector Resistance models # -from tests import TestCase +import pytest import pybamm -import unittest import numpy as np -class TestEffectiveResistance(TestCase): +class TestEffectiveResistance: def test_well_posed(self): model = pybamm.current_collector.EffectiveResistance({"dimensionality": 1}) model.check_well_posedness() @@ -17,36 +16,34 @@ def test_well_posed(self): def test_default_parameters(self): model = pybamm.current_collector.EffectiveResistance({"dimensionality": 1}) - self.assertEqual( - model.default_parameter_values, pybamm.ParameterValues("Marquis2019") - ) + assert model.default_parameter_values == pybamm.ParameterValues("Marquis2019") def test_default_geometry(self): model = pybamm.current_collector.EffectiveResistance({"dimensionality": 1}) - self.assertTrue("current collector" in model.default_geometry) - self.assertNotIn("negative electrode", model.default_geometry) + assert "current collector" in model.default_geometry + assert "negative electrode" not in model.default_geometry model = pybamm.current_collector.EffectiveResistance({"dimensionality": 2}) - self.assertTrue("current collector" in model.default_geometry) - self.assertNotIn("negative electrode", model.default_geometry) + assert "current collector" in model.default_geometry + assert "negative electrode" not in model.default_geometry def test_default_var_pts(self): model = pybamm.current_collector.EffectiveResistance({"dimensionality": 1}) - self.assertEqual(model.default_var_pts, {"y": 32, "z": 32}) + assert model.default_var_pts == {"y": 32, "z": 32} def test_default_solver(self): model = pybamm.current_collector.EffectiveResistance({"dimensionality": 1}) - self.assertIsInstance(model.default_solver, pybamm.CasadiAlgebraicSolver) + assert isinstance(model.default_solver, pybamm.CasadiAlgebraicSolver) model = pybamm.current_collector.EffectiveResistance({"dimensionality": 2}) - self.assertIsInstance(model.default_solver, pybamm.CasadiAlgebraicSolver) + assert isinstance(model.default_solver, pybamm.CasadiAlgebraicSolver) def test_bad_option(self): - with self.assertRaisesRegex(pybamm.OptionError, "Dimension of"): + with pytest.raises(pybamm.OptionError, match="Dimension of"): pybamm.current_collector.EffectiveResistance({"dimensionality": 10}) -class TestEffectiveResistancePostProcess(TestCase): +class TestEffectiveResistancePostProcess: def test_get_processed_variables(self): # solve cheap SPM to test post-processing (think of an alternative test?) models = [ @@ -87,13 +84,3 @@ def test_get_processed_variables(self): processed_var(t=solution_1D.t[5], z=pts) else: processed_var(t=solution_1D.t[5], y=pts, z=pts) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_models/test_submodels/test_particle_polynomial_profile.py b/tests/unit/test_models/test_submodels/test_particle_polynomial_profile.py index 787230d9f3..57f1436f2d 100644 --- a/tests/unit/test_models/test_submodels/test_particle_polynomial_profile.py +++ b/tests/unit/test_models/test_submodels/test_particle_polynomial_profile.py @@ -1,22 +1,11 @@ # # Tests for the polynomial profile submodel # -from tests import TestCase import pybamm -import unittest +import pytest -class TestParticlePolynomialProfile(TestCase): +class TestParticlePolynomialProfile: def test_errors(self): - with self.assertRaisesRegex(ValueError, "Particle type must be"): + with pytest.raises(ValueError, match="Particle type must be"): pybamm.particle.PolynomialProfile(None, "negative", {}) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_parameters/test_base_parameters.py b/tests/unit/test_parameters/test_base_parameters.py index 6c87cdcd88..2c48074a71 100644 --- a/tests/unit/test_parameters/test_base_parameters.py +++ b/tests/unit/test_parameters/test_base_parameters.py @@ -2,48 +2,37 @@ Tests for the base_parameters.py """ -from tests import TestCase import pybamm -import unittest +import pytest -class TestBaseParameters(TestCase): +class TestBaseParameters: def test_getattr__(self): param = pybamm.LithiumIonParameters() # ending in _n / _s / _p - with self.assertRaisesRegex(AttributeError, "param.n.L"): + with pytest.raises(AttributeError, match="param.n.L"): param.L_n - with self.assertRaisesRegex(AttributeError, "param.s.L"): + with pytest.raises(AttributeError, match="param.s.L"): param.L_s - with self.assertRaisesRegex(AttributeError, "param.p.L"): + with pytest.raises(AttributeError, match="param.p.L"): param.L_p # _n_ in the name - with self.assertRaisesRegex(AttributeError, "param.n.prim.c_max"): + with pytest.raises(AttributeError, match="param.n.prim.c_max"): param.c_n_max # _n_ or _p_ not in name - with self.assertRaisesRegex( - AttributeError, "has no attribute 'c_n_not_a_parameter" + with pytest.raises( + AttributeError, match="has no attribute 'c_n_not_a_parameter" ): param.c_n_not_a_parameter - with self.assertRaisesRegex(AttributeError, "has no attribute 'c_s_test"): + with pytest.raises(AttributeError, match="has no attribute 'c_s_test"): pybamm.electrical_parameters.c_s_test - self.assertEqual(param.n.cap_init, param.n.Q_init) - self.assertEqual(param.p.prim.cap_init, param.p.prim.Q_init) + assert param.n.cap_init == param.n.Q_init + assert param.p.prim.cap_init == param.p.prim.Q_init def test__setattr__(self): # domain gets added as a subscript param = pybamm.GeometricParameters() - self.assertEqual(param.n.L.print_name, r"L_{\mathrm{n}}") - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() + assert param.n.L.print_name == r"L_{\mathrm{n}}" diff --git a/tests/unit/test_parameters/test_electrical_parameters.py b/tests/unit/test_parameters/test_electrical_parameters.py index 92bceaf632..7601c30721 100644 --- a/tests/unit/test_parameters/test_electrical_parameters.py +++ b/tests/unit/test_parameters/test_electrical_parameters.py @@ -1,13 +1,11 @@ # # Tests for the electrical parameters # -from tests import TestCase +import pytest import pybamm -import unittest - -class TestElectricalParameters(TestCase): +class TestElectricalParameters: def test_current_functions(self): # create current functions param = pybamm.electrical_parameters @@ -27,17 +25,7 @@ def test_current_functions(self): current_density_eval = parameter_values.process_symbol(current_density) # check current - self.assertEqual(current_eval.evaluate(t=3), 2) + assert current_eval.evaluate(t=3) == 2 # check current density - self.assertAlmostEqual(current_density_eval.evaluate(t=3), 2 / (8 * 0.1 * 0.1)) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() + assert current_density_eval.evaluate(t=3) == pytest.approx(2 / (8 * 0.1 * 0.1)) diff --git a/tests/unit/test_parameters/test_geometric_parameters.py b/tests/unit/test_parameters/test_geometric_parameters.py index 6e59259a12..7e000bf645 100644 --- a/tests/unit/test_parameters/test_geometric_parameters.py +++ b/tests/unit/test_parameters/test_geometric_parameters.py @@ -1,12 +1,10 @@ # # Tests for the standard parameters # -from tests import TestCase import pybamm -import unittest -class TestGeometricParameters(TestCase): +class TestGeometricParameters: def test_macroscale_parameters(self): geo = pybamm.geometric_parameters L_n = geo.n.L @@ -26,16 +24,4 @@ def test_macroscale_parameters(self): L_p_eval = parameter_values.process_symbol(L_p) L_x_eval = parameter_values.process_symbol(L_x) - self.assertEqual( - (L_n_eval + L_s_eval + L_p_eval).evaluate(), L_x_eval.evaluate() - ) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() + assert (L_n_eval + L_s_eval + L_p_eval).evaluate() == L_x_eval.evaluate() diff --git a/tests/unit/test_parameters/test_parameter_sets/test_Ai2020.py b/tests/unit/test_parameters/test_parameter_sets/test_Ai2020.py index 8816551ab6..f7302330bf 100644 --- a/tests/unit/test_parameters/test_parameter_sets/test_Ai2020.py +++ b/tests/unit/test_parameters/test_parameter_sets/test_Ai2020.py @@ -1,12 +1,11 @@ # # Tests for Ai (2020) Enertech parameter set loads # -from tests import TestCase +import pytest import pybamm -import unittest -class TestAi2020(TestCase): +class TestAi2020: def test_functions(self): param = pybamm.ParameterValues("Ai2020") sto = pybamm.Scalar(0.5) @@ -42,16 +41,6 @@ def test_functions(self): } for name, value in fun_test.items(): - self.assertAlmostEqual( - param.evaluate(param[name](*value[0])), value[1], places=4 + assert param.evaluate(param[name](*value[0])) == pytest.approx( + value[1], abs=0.0001 ) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_parameters/test_parameter_sets/test_Ecker2015_graphite_halfcell.py b/tests/unit/test_parameters/test_parameter_sets/test_Ecker2015_graphite_halfcell.py index f435ef6d36..6000b997b7 100644 --- a/tests/unit/test_parameters/test_parameter_sets/test_Ecker2015_graphite_halfcell.py +++ b/tests/unit/test_parameters/test_parameter_sets/test_Ecker2015_graphite_halfcell.py @@ -1,12 +1,11 @@ # # Tests for O'Kane (2022) parameter set # -from tests import TestCase +import pytest import pybamm -import unittest -class TestEcker2015_graphite_halfcell(TestCase): +class TestEcker2015_graphite_halfcell: def test_functions(self): param = pybamm.ParameterValues("Ecker2015_graphite_halfcell") sto = pybamm.Scalar(0.5) @@ -33,16 +32,6 @@ def test_functions(self): } for name, value in fun_test.items(): - self.assertAlmostEqual( - param.evaluate(param[name](*value[0])), value[1], places=4 + assert param.evaluate(param[name](*value[0])) == pytest.approx( + value[1], abs=0.0001 ) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_parameters/test_parameter_sets/test_LCO_Ramadass2004.py b/tests/unit/test_parameters/test_parameter_sets/test_LCO_Ramadass2004.py index 2de67b9e62..e6c4b04fdf 100644 --- a/tests/unit/test_parameters/test_parameter_sets/test_LCO_Ramadass2004.py +++ b/tests/unit/test_parameters/test_parameter_sets/test_LCO_Ramadass2004.py @@ -1,12 +1,11 @@ # # Tests for Ai (2020) Enertech parameter set loads # -from tests import TestCase +import pytest import pybamm -import unittest -class TestRamadass2004(TestCase): +class TestRamadass2004: def test_functions(self): param = pybamm.ParameterValues("Ramadass2004") sto = pybamm.Scalar(0.5) @@ -40,16 +39,6 @@ def test_functions(self): } for name, value in fun_test.items(): - self.assertAlmostEqual( - param.evaluate(param[name](*value[0])), value[1], places=4 + assert param.evaluate(param[name](*value[0])) == pytest.approx( + value[1], abs=0.0001 ) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_parameters/test_parameter_sets/test_LGM50_ORegan2022.py b/tests/unit/test_parameters/test_parameter_sets/test_LGM50_ORegan2022.py index f878b7d790..05a38b6245 100644 --- a/tests/unit/test_parameters/test_parameter_sets/test_LGM50_ORegan2022.py +++ b/tests/unit/test_parameters/test_parameter_sets/test_LGM50_ORegan2022.py @@ -1,12 +1,11 @@ # # Tests for LG M50 parameter set loads # -from tests import TestCase +import pytest import pybamm -import unittest -class TestORegan2022(TestCase): +class TestORegan2022: def test_functions(self): param = pybamm.ParameterValues("ORegan2022") T = pybamm.Scalar(298.15) @@ -68,16 +67,6 @@ def test_functions(self): } for name, value in fun_test.items(): - self.assertAlmostEqual( - param.evaluate(param[name](*value[0])), value[1], places=4 + assert param.evaluate(param[name](*value[0])) == pytest.approx( + value[1], abs=0.0001 ) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_parameters/test_parameter_sets/test_OKane2022_negative_halfcell.py b/tests/unit/test_parameters/test_parameter_sets/test_OKane2022_negative_halfcell.py index 04a19e1002..bf39457dc4 100644 --- a/tests/unit/test_parameters/test_parameter_sets/test_OKane2022_negative_halfcell.py +++ b/tests/unit/test_parameters/test_parameter_sets/test_OKane2022_negative_halfcell.py @@ -1,12 +1,11 @@ # # Tests for O'Kane (2022) parameter set # -from tests import TestCase +import pytest import pybamm -import unittest -class TestOKane2022_graphite_SiOx_halfcell(TestCase): +class TestOKane2022_graphite_SiOx_halfcell: def test_functions(self): param = pybamm.ParameterValues("OKane2022_graphite_SiOx_halfcell") sto = pybamm.Scalar(0.9) @@ -31,16 +30,6 @@ def test_functions(self): } for name, value in fun_test.items(): - self.assertAlmostEqual( - param.evaluate(param[name](*value[0])), value[1], places=4 + assert param.evaluate(param[name](*value[0])) == pytest.approx( + value[1], abs=0.0001 ) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_parameters/test_parameter_sets_class.py b/tests/unit/test_parameters/test_parameter_sets_class.py index b14000f987..342cf127aa 100644 --- a/tests/unit/test_parameters/test_parameter_sets_class.py +++ b/tests/unit/test_parameters/test_parameter_sets_class.py @@ -1,23 +1,22 @@ # # Tests for the ParameterSets class # -from tests import TestCase - +import pytest +import re import pybamm -import unittest -class TestParameterSets(TestCase): +class TestParameterSets: def test_name_interface(self): """Test that pybamm.parameters_sets. returns the name of the parameter set and a depreciation warning """ - with self.assertWarns(DeprecationWarning): + with pytest.warns(DeprecationWarning): out = pybamm.parameter_sets.Marquis2019 - self.assertEqual(out, "Marquis2019") + assert out == "Marquis2019" - # Expect error for parameter set's that aren't real - with self.assertRaises(AttributeError): + # Expect an error for parameter sets that aren't real + with pytest.raises(AttributeError): pybamm.parameter_sets.not_a_real_parameter_set def test_all_registered(self): @@ -26,26 +25,15 @@ def test_all_registered(self): known_entry_points = set( ep.name for ep in pybamm.parameter_sets.get_entries("pybamm_parameter_sets") ) - self.assertEqual(set(pybamm.parameter_sets.keys()), known_entry_points) - self.assertEqual(len(known_entry_points), len(pybamm.parameter_sets)) + assert set(pybamm.parameter_sets.keys()) == known_entry_points + assert len(known_entry_points) == len(pybamm.parameter_sets) def test_get_docstring(self): """Test that :meth:`pybamm.parameter_sets.get_doctstring` works""" docstring = pybamm.parameter_sets.get_docstring("Marquis2019") - self.assertRegex(docstring, "Parameters for a Kokam SLPB78205130H cell") + assert re.search("Parameters for a Kokam SLPB78205130H cell", docstring) def test_iter(self): """Test that iterating `pybamm.parameter_sets` iterates over keys""" for k in pybamm.parameter_sets: - self.assertIsInstance(k, str) - self.assertIn(k, pybamm.parameter_sets) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() + assert isinstance(k, str) diff --git a/tests/unit/test_parameters/test_size_distribution_parameters.py b/tests/unit/test_parameters/test_size_distribution_parameters.py index 5deeaa62be..414b422055 100644 --- a/tests/unit/test_parameters/test_size_distribution_parameters.py +++ b/tests/unit/test_parameters/test_size_distribution_parameters.py @@ -2,13 +2,12 @@ # Tests particle size distribution parameters are loaded into a parameter set # and give expected values # +import pytest import pybamm -import unittest import numpy as np -from tests import TestCase -class TestSizeDistributionParameters(TestCase): +class TestSizeDistributionParameters: def test_parameter_values(self): values = pybamm.lithium_ion.BaseModel().default_parameter_values param = pybamm.LithiumIonParameters() @@ -20,7 +19,7 @@ def test_parameter_values(self): ) # check negative parameters aren't there yet - with self.assertRaises(KeyError): + with pytest.raises(KeyError): values["Negative maximum particle radius [m]"] # now add distribution parameter values for negative electrode @@ -41,13 +40,3 @@ def test_parameter_values(self): R_test = pybamm.Scalar(1.0) values.evaluate(param.n.prim.f_a_dist(R_test)) values.evaluate(param.p.prim.f_a_dist(R_test)) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_plotting/test_plot.py b/tests/unit/test_plotting/test_plot.py index f36e20cd6f..1c049269c3 100644 --- a/tests/unit/test_plotting/test_plot.py +++ b/tests/unit/test_plotting/test_plot.py @@ -1,14 +1,13 @@ import pybamm -import unittest +import pytest import numpy as np -from tests import TestCase import matplotlib.pyplot as plt from matplotlib import use use("Agg") -class TestPlot(TestCase): +class TestPlot: def test_plot(self): x = pybamm.Array(np.array([0, 3, 10])) y = pybamm.Array(np.array([6, 16, 78])) @@ -16,13 +15,13 @@ def test_plot(self): _, ax = plt.subplots() ax_out = pybamm.plot(x, y, ax=ax, show_plot=False) - self.assertEqual(ax_out, ax) + assert ax_out == ax def test_plot_fail(self): x = pybamm.Array(np.array([0])) - with self.assertRaisesRegex(TypeError, "x must be 'pybamm.Array'"): + with pytest.raises(TypeError, match="x must be 'pybamm.Array'"): pybamm.plot("bad", x) - with self.assertRaisesRegex(TypeError, "y must be 'pybamm.Array'"): + with pytest.raises(TypeError, match="y must be 'pybamm.Array'"): pybamm.plot(x, "bad") def test_plot2D(self): @@ -38,23 +37,13 @@ def test_plot2D(self): _, ax = plt.subplots() ax_out = pybamm.plot2D(X, Y, Y, ax=ax, show_plot=False) - self.assertEqual(ax_out, ax) + assert ax_out == ax def test_plot2D_fail(self): x = pybamm.Array(np.array([0])) - with self.assertRaisesRegex(TypeError, "x must be 'pybamm.Array'"): + with pytest.raises(TypeError, match="x must be 'pybamm.Array'"): pybamm.plot2D("bad", x, x) - with self.assertRaisesRegex(TypeError, "y must be 'pybamm.Array'"): + with pytest.raises(TypeError, match="y must be 'pybamm.Array'"): pybamm.plot2D(x, "bad", x) - with self.assertRaisesRegex(TypeError, "z must be 'pybamm.Array'"): + with pytest.raises(TypeError, match="z must be 'pybamm.Array'"): pybamm.plot2D(x, x, "bad") - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_plotting/test_plot_summary_variables.py b/tests/unit/test_plotting/test_plot_summary_variables.py index e896b1f468..5f1a650ced 100644 --- a/tests/unit/test_plotting/test_plot_summary_variables.py +++ b/tests/unit/test_plotting/test_plot_summary_variables.py @@ -1,10 +1,8 @@ import pybamm -import unittest import numpy as np -from tests import TestCase -class TestPlotSummaryVariables(TestCase): +class TestPlotSummaryVariables: def test_plot(self): model = pybamm.lithium_ion.SPM({"SEI": "ec reaction limited"}) parameter_values = pybamm.ParameterValues("Mohtat2020") @@ -39,11 +37,11 @@ def test_plot(self): axes = pybamm.plot_summary_variables(sol, show_plot=False) axes = axes.flatten() - self.assertEqual(len(axes), 9) + assert len(axes) == 9 for output_var, ax in zip(output_variables, axes): - self.assertEqual(ax.get_xlabel(), "Cycle number") - self.assertEqual(ax.get_ylabel(), output_var) + assert ax.get_xlabel() == "Cycle number" + assert ax.get_ylabel() == output_var cycle_number, var = ax.get_lines()[0].get_data() np.testing.assert_array_equal( @@ -56,11 +54,11 @@ def test_plot(self): ) axes = axes.flatten() - self.assertEqual(len(axes), 9) + assert len(axes) == 9 for output_var, ax in zip(output_variables, axes): - self.assertEqual(ax.get_xlabel(), "Cycle number") - self.assertEqual(ax.get_ylabel(), output_var) + assert ax.get_xlabel() == "Cycle number" + assert ax.get_ylabel() == output_var cycle_number, var = ax.get_lines()[0].get_data() np.testing.assert_array_equal( @@ -73,13 +71,3 @@ def test_plot(self): cycle_number, sol.summary_variables["Cycle number"] ) np.testing.assert_array_equal(var, sol.summary_variables[output_var]) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_plotting/test_plot_thermal_components.py b/tests/unit/test_plotting/test_plot_thermal_components.py index 99b3d40cac..2b4cdf1e1e 100644 --- a/tests/unit/test_plotting/test_plot_thermal_components.py +++ b/tests/unit/test_plotting/test_plot_thermal_components.py @@ -1,14 +1,13 @@ +import pytest import pybamm -import unittest import numpy as np -from tests import TestCase import matplotlib.pyplot as plt from matplotlib import use use("Agg") -class TestPlotThermalComponents(TestCase): +class TestPlotThermalComponents: def test_plot_with_solution(self): model = pybamm.lithium_ion.SPM({"thermal": "lumped"}) sim = pybamm.Simulation( @@ -30,22 +29,12 @@ def test_plot_with_solution(self): _, ax = plt.subplots(1, 2) _, ax_out = pybamm.plot_thermal_components(sol, ax=ax, show_legend=True) - self.assertEqual(ax_out[0], ax[0]) - self.assertEqual(ax_out[1], ax[1]) + assert ax_out[0] == ax[0] + assert ax_out[1] == ax[1] def test_not_implemented(self): model = pybamm.lithium_ion.SPM({"thermal": "x-full"}) sim = pybamm.Simulation(model) sol = sim.solve([0, 3600]) - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): pybamm.plot_thermal_components(sol) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_plotting/test_plot_voltage_components.py b/tests/unit/test_plotting/test_plot_voltage_components.py index 1773d576d9..2b9da43fc1 100644 --- a/tests/unit/test_plotting/test_plot_voltage_components.py +++ b/tests/unit/test_plotting/test_plot_voltage_components.py @@ -1,14 +1,13 @@ +import pytest import pybamm -import unittest import numpy as np -from tests import TestCase import matplotlib.pyplot as plt from matplotlib import use use("Agg") -class TestPlotVoltageComponents(TestCase): +class TestPlotVoltageComponents: def test_plot_with_solution(self): model = pybamm.lithium_ion.SPM() sim = pybamm.Simulation(model) @@ -23,7 +22,7 @@ def test_plot_with_solution(self): _, ax = plt.subplots() _, ax_out = pybamm.plot_voltage_components(sol, ax=ax, show_legend=True) - self.assertEqual(ax_out, ax) + assert ax_out == ax def test_plot_with_simulation(self): model = pybamm.lithium_ion.SPM() @@ -40,7 +39,7 @@ def test_plot_with_simulation(self): _, ax = plt.subplots() _, ax_out = pybamm.plot_voltage_components(sim, ax=ax, show_legend=True) - self.assertEqual(ax_out, ax) + assert ax_out == ax def test_plot_from_solution(self): model = pybamm.lithium_ion.SPM() @@ -56,7 +55,7 @@ def test_plot_from_solution(self): _, ax = plt.subplots() _, ax_out = sol.plot_voltage_components(ax=ax, show_legend=True) - self.assertEqual(ax_out, ax) + assert ax_out == ax def test_plot_from_simulation(self): model = pybamm.lithium_ion.SPM() @@ -73,25 +72,12 @@ def test_plot_from_simulation(self): _, ax = plt.subplots() _, ax_out = sim.plot_voltage_components(ax=ax, show_legend=True) - self.assertEqual(ax_out, ax) + assert ax_out == ax def test_plot_without_solution(self): model = pybamm.lithium_ion.SPM() sim = pybamm.Simulation(model) - with self.assertRaises(ValueError) as error: + with pytest.raises(ValueError) as error: sim.plot_voltage_components() - - self.assertEqual( - str(error.exception), "The simulation has not been solved yet." - ) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() + assert str(error.exception) == "The simulation has not been solved yet." diff --git a/tests/unit/test_settings.py b/tests/unit/test_settings.py index a3b62f8ee4..d70d3d4c40 100644 --- a/tests/unit/test_settings.py +++ b/tests/unit/test_settings.py @@ -1,52 +1,42 @@ # # Tests the settings class. # -from tests import TestCase + import pybamm -import unittest +import pytest -class TestSettings(TestCase): +class TestSettings: def test_simplify(self): - self.assertTrue(pybamm.settings.simplify) + assert pybamm.settings.simplify pybamm.settings.simplify = False - self.assertFalse(pybamm.settings.simplify) + assert not pybamm.settings.simplify pybamm.settings.simplify = True def test_smoothing_parameters(self): - self.assertEqual(pybamm.settings.min_max_mode, "exact") - self.assertEqual(pybamm.settings.heaviside_smoothing, "exact") - self.assertEqual(pybamm.settings.abs_smoothing, "exact") + assert pybamm.settings.min_max_mode == "exact" + assert pybamm.settings.heaviside_smoothing == "exact" + assert pybamm.settings.abs_smoothing == "exact" pybamm.settings.set_smoothing_parameters(10) - self.assertEqual(pybamm.settings.min_max_smoothing, 10) - self.assertEqual(pybamm.settings.heaviside_smoothing, 10) - self.assertEqual(pybamm.settings.abs_smoothing, 10) + assert pybamm.settings.min_max_smoothing == 10 + assert pybamm.settings.heaviside_smoothing == 10 + assert pybamm.settings.abs_smoothing == 10 pybamm.settings.set_smoothing_parameters("exact") # Test errors - with self.assertRaisesRegex(ValueError, "greater than 1"): + with pytest.raises(ValueError, match="greater than 1"): pybamm.settings.min_max_mode = "smooth" pybamm.settings.min_max_smoothing = 0.9 - with self.assertRaisesRegex(ValueError, "positive number"): + with pytest.raises(ValueError, match="positive number"): pybamm.settings.min_max_mode = "soft" pybamm.settings.min_max_smoothing = -10 - with self.assertRaisesRegex(ValueError, "positive number"): + with pytest.raises(ValueError, match="positive number"): pybamm.settings.heaviside_smoothing = -10 - with self.assertRaisesRegex(ValueError, "positive number"): + with pytest.raises(ValueError, match="positive number"): pybamm.settings.abs_smoothing = -10 - with self.assertRaisesRegex(ValueError, "'soft', or 'smooth'"): + with pytest.raises(ValueError, match="'soft', or 'smooth'"): pybamm.settings.min_max_mode = "unknown" pybamm.settings.set_smoothing_parameters("exact") - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_solvers/test_dummy_solver.py b/tests/unit/test_solvers/test_dummy_solver.py index 7c7b9a35f7..acce1d3543 100644 --- a/tests/unit/test_solvers/test_dummy_solver.py +++ b/tests/unit/test_solvers/test_dummy_solver.py @@ -1,14 +1,11 @@ # # Tests for the Dummy Solver class # -from tests import TestCase import pybamm import numpy as np -import unittest -import sys -class TestDummySolver(TestCase): +class TestDummySolver: def test_dummy_solver(self): model = pybamm.BaseModel() v = pybamm.Scalar(1) @@ -44,12 +41,3 @@ def test_dummy_solver_step(self): np.testing.assert_array_equal(len(sol.t), t_eval.size * 2 - 2) np.testing.assert_array_equal(sol.y, np.zeros((1, sol.t.size))) np.testing.assert_array_equal(sol["v"].data, np.ones(sol.t.size)) - - -if __name__ == "__main__": - print("Add -v for more debug output") - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_solvers/test_lrudict.py b/tests/unit/test_solvers/test_lrudict.py index a5378da786..ab38bddbc5 100644 --- a/tests/unit/test_solvers/test_lrudict.py +++ b/tests/unit/test_solvers/test_lrudict.py @@ -1,12 +1,12 @@ # # Tests for the LRUDict class # -import unittest +import pytest from pybamm.solvers.lrudict import LRUDict from collections import OrderedDict -class TestLRUDict(unittest.TestCase): +class TestLRUDict: def test_lrudict_defaultbehaviour(self): """Default behaviour [no LRU] mimics Dict""" d = LRUDict() @@ -20,27 +20,27 @@ def test_lrudict_defaultbehaviour(self): dd.get(count - 2) # assertCountEqual checks that the same elements are present in # both lists, not just that the lists are of equal count - self.assertCountEqual(set(d.keys()), set(dd.keys())) - self.assertCountEqual(set(d.values()), set(dd.values())) + assert set(d.keys()) == set(dd.keys()) + assert set(d.values()) == set(dd.values()) def test_lrudict_noitems(self): """Edge case: no items in LRU, raises KeyError on assignment""" d = LRUDict(maxsize=-1) - with self.assertRaises(KeyError): + with pytest.raises(KeyError): d["a"] = 1 def test_lrudict_singleitem(self): """Only the last added element should ever be present""" d = LRUDict(maxsize=1) item_list = range(1, 100) - self.assertEqual(len(d), 0) + assert len(d) == 0 for item in item_list: d[item] = item - self.assertEqual(len(d), 1) - self.assertIsNotNone(d[item]) + assert len(d) == 1 + assert d[item] is not None # Finally, pop the only item and check that the dictionary is empty d.popitem() - self.assertEqual(len(d), 0) + assert len(d) == 0 def test_lrudict_multiitem(self): """Check that the correctly ordered items are always present""" @@ -59,17 +59,17 @@ def test_lrudict_multiitem(self): expected = OrderedDict( (k, expected[k]) for k in list(expected.keys())[-maxsize:] ) - self.assertListEqual(list(d.keys()), list(expected.keys())) - self.assertListEqual(list(d.values()), list(expected.values())) + assert list(d.keys()) == list(expected.keys()) + assert list(d.values()) == list(expected.values()) def test_lrudict_invalidkey(self): d = LRUDict() value = 1 d["a"] = value # Access with valid key - self.assertEqual(d["a"], value) # checks getitem() - self.assertEqual(d.get("a"), value) # checks get() + assert d["a"] == value # checks getitem() + assert d.get("a") == value # checks get() # Access with invalid key - with self.assertRaises(KeyError): + with pytest.raises(KeyError): _ = d["b"] # checks getitem() - self.assertIsNone(d.get("b")) # checks get() + assert d.get("b") is None # checks get() diff --git a/tests/unit/test_spatial_methods/test_zero_dimensional_method.py b/tests/unit/test_spatial_methods/test_zero_dimensional_method.py index b3ec859412..1c620c7872 100644 --- a/tests/unit/test_spatial_methods/test_zero_dimensional_method.py +++ b/tests/unit/test_spatial_methods/test_zero_dimensional_method.py @@ -1,14 +1,12 @@ # # Test for the base Spatial Method class # -from tests import TestCase import numpy as np import pybamm -import unittest from tests import get_mesh_for_testing, get_discretisation_for_testing -class TestZeroDimensionalSpatialMethod(TestCase): +class TestZeroDimensionalSpatialMethod: def test_identity_ops(self): test_mesh = np.array([1, 2, 3]) spatial_method = pybamm.ZeroDimensionalSpatialMethod() @@ -16,14 +14,14 @@ def test_identity_ops(self): np.testing.assert_array_equal(spatial_method._mesh, test_mesh) a = pybamm.Symbol("a") - self.assertEqual(a, spatial_method.integral(None, a, "primary")) - self.assertEqual(a, spatial_method.indefinite_integral(None, a, "forward")) - self.assertEqual(a, spatial_method.boundary_value_or_flux(None, a)) - self.assertEqual((-a), spatial_method.indefinite_integral(None, a, "backward")) + assert a == spatial_method.integral(None, a, "primary") + assert a == spatial_method.indefinite_integral(None, a, "forward") + assert a == spatial_method.boundary_value_or_flux(None, a) + assert (-a) == spatial_method.indefinite_integral(None, a, "backward") mass_matrix = spatial_method.mass_matrix(None, None) - self.assertIsInstance(mass_matrix, pybamm.Matrix) - self.assertEqual(mass_matrix.shape, (1, 1)) + assert isinstance(mass_matrix, pybamm.Matrix) + assert mass_matrix.shape == (1, 1) np.testing.assert_array_equal(mass_matrix.entries, 1) def test_discretise_spatial_variable(self): @@ -38,7 +36,7 @@ def test_discretise_spatial_variable(self): r = pybamm.SpatialVariable("r", ["negative particle"]) for var in [x1, x2, r]: var_disc = spatial_method.spatial_variable(var) - self.assertIsInstance(var_disc, pybamm.Vector) + assert isinstance(var_disc, pybamm.Vector) np.testing.assert_array_equal( var_disc.evaluate()[:, 0], mesh[var.domain].nodes ) @@ -49,7 +47,7 @@ def test_discretise_spatial_variable(self): r_edge = pybamm.SpatialVariableEdge("r", ["negative particle"]) for var in [x1_edge, x2_edge, r_edge]: var_disc = spatial_method.spatial_variable(var) - self.assertIsInstance(var_disc, pybamm.Vector) + assert isinstance(var_disc, pybamm.Vector) np.testing.assert_array_equal( var_disc.evaluate()[:, 0], mesh[var.domain].edges ) @@ -70,13 +68,3 @@ def test_averages(self): np.testing.assert_array_equal( var_disc.evaluate(y=y), expr_disc.evaluate(y=y) ) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_timer.py b/tests/unit/test_timer.py index 228cdd5dce..6ef62f791e 100644 --- a/tests/unit/test_timer.py +++ b/tests/unit/test_timer.py @@ -5,11 +5,9 @@ # (see https://github.com/pints-team/pints) # import pybamm -import unittest -from tests import TestCase -class TestTimer(TestCase): +class TestTimer: """ Tests the basic methods of the Timer class. """ @@ -20,64 +18,54 @@ def __init__(self, name): def test_timing(self): t = pybamm.Timer() a = t.time().value - self.assertGreaterEqual(a, 0) + assert a >= 0 for _ in range(100): - self.assertGreater(t.time().value, a) + assert t.time().value > a a = t.time().value t.reset() b = t.time().value - self.assertGreaterEqual(b, 0) - self.assertLess(b, a) + assert b >= 0 + assert b < a def test_timer_format(self): - self.assertEqual(str(pybamm.TimerTime(1e-9)), "1.000 ns") - self.assertEqual(str(pybamm.TimerTime(0.000000123456789)), "123.457 ns") - self.assertEqual(str(pybamm.TimerTime(1e-6)), "1.000 us") - self.assertEqual(str(pybamm.TimerTime(0.000123456789)), "123.457 us") - self.assertEqual(str(pybamm.TimerTime(0.999e-3)), "999.000 us") - self.assertEqual(str(pybamm.TimerTime(1e-3)), "1.000 ms") - self.assertEqual(str(pybamm.TimerTime(0.123456789)), "123.457 ms") - self.assertEqual(str(pybamm.TimerTime(2)), "2.000 s") - self.assertEqual(str(pybamm.TimerTime(2.5)), "2.500 s") - self.assertEqual(str(pybamm.TimerTime(12.5)), "12.500 s") - self.assertEqual(str(pybamm.TimerTime(59.41)), "59.410 s") - self.assertEqual(str(pybamm.TimerTime(59.4126347547)), "59.413 s") - self.assertEqual(str(pybamm.TimerTime(60.2)), "1 minute, 0 seconds") - self.assertEqual(str(pybamm.TimerTime(61)), "1 minute, 1 second") - self.assertEqual(str(pybamm.TimerTime(121)), "2 minutes, 1 second") - self.assertEqual( - str(pybamm.TimerTime(604800)), - "1 week, 0 days, 0 hours, 0 minutes, 0 seconds", + assert str(pybamm.TimerTime(1e-9)) == "1.000 ns" + assert str(pybamm.TimerTime(0.000000123456789)) == "123.457 ns" + assert str(pybamm.TimerTime(1e-6)) == "1.000 us" + assert str(pybamm.TimerTime(0.000123456789)) == "123.457 us" + assert str(pybamm.TimerTime(0.999e-3)) == "999.000 us" + assert str(pybamm.TimerTime(1e-3)) == "1.000 ms" + assert str(pybamm.TimerTime(0.123456789)) == "123.457 ms" + assert str(pybamm.TimerTime(2)) == "2.000 s" + assert str(pybamm.TimerTime(2.5)) == "2.500 s" + assert str(pybamm.TimerTime(12.5)) == "12.500 s" + assert str(pybamm.TimerTime(59.41)) == "59.410 s" + assert str(pybamm.TimerTime(59.4126347547)) == "59.413 s" + assert str(pybamm.TimerTime(60.2)) == "1 minute, 0 seconds" + assert str(pybamm.TimerTime(61)) == "1 minute, 1 second" + assert str(pybamm.TimerTime(121)) == "2 minutes, 1 second" + assert ( + str(pybamm.TimerTime(604800)) + == "1 week, 0 days, 0 hours, 0 minutes, 0 seconds" ) - self.assertEqual( - str(pybamm.TimerTime(2 * 604800 + 3 * 3600 + 60 + 4)), - "2 weeks, 0 days, 3 hours, 1 minute, 4 seconds", + assert ( + str(pybamm.TimerTime(2 * 604800 + 3 * 3600 + 60 + 4)) + == "2 weeks, 0 days, 3 hours, 1 minute, 4 seconds" ) - self.assertEqual(repr(pybamm.TimerTime(1.5)), "pybamm.TimerTime(1.5)") + assert repr(pybamm.TimerTime(1.5)) == "pybamm.TimerTime(1.5)" def test_timer_operations(self): - self.assertEqual((pybamm.TimerTime(1) + 2).value, 3) - self.assertEqual((1 + pybamm.TimerTime(1)).value, 2) - self.assertEqual((pybamm.TimerTime(1) - 2).value, -1) - self.assertEqual((pybamm.TimerTime(1) - pybamm.TimerTime(2)).value, -1) - self.assertEqual((1 - pybamm.TimerTime(1)).value, 0) - self.assertEqual((pybamm.TimerTime(4) * 2).value, 8) - self.assertEqual((pybamm.TimerTime(4) * pybamm.TimerTime(2)).value, 8) - self.assertEqual((2 * pybamm.TimerTime(5)).value, 10) - self.assertEqual((pybamm.TimerTime(4) / 2).value, 2) - self.assertEqual((pybamm.TimerTime(4) / pybamm.TimerTime(2)).value, 2) - self.assertEqual((2 / pybamm.TimerTime(5)).value, 2 / 5) + assert (pybamm.TimerTime(1) + 2).value == 3 + assert (1 + pybamm.TimerTime(1)).value == 2 + assert (pybamm.TimerTime(1) - 2).value == -1 + assert (pybamm.TimerTime(1) - pybamm.TimerTime(2)).value == -1 + assert (1 - pybamm.TimerTime(1)).value == 0 + assert (pybamm.TimerTime(4) * 2).value == 8 + assert (pybamm.TimerTime(4) * pybamm.TimerTime(2)).value == 8 + assert (2 * pybamm.TimerTime(5)).value == 10 + assert (pybamm.TimerTime(4) / 2).value == 2 + assert (pybamm.TimerTime(4) / pybamm.TimerTime(2)).value == 2 + assert (2 / pybamm.TimerTime(5)).value == 2 / 5 - self.assertTrue(pybamm.TimerTime(1) == pybamm.TimerTime(1)) - self.assertTrue(pybamm.TimerTime(1) != pybamm.TimerTime(2)) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() + assert pybamm.TimerTime(1) == pybamm.TimerTime(1) + assert pybamm.TimerTime(1) != pybamm.TimerTime(2) From 9afe45e38f370cce69a1e8f70f20964cc4789388 Mon Sep 17 00:00:00 2001 From: Pradyot Ranjan <99216956+prady0t@users.noreply.github.com> Date: Tue, 16 Jul 2024 18:57:55 +0530 Subject: [PATCH 23/82] Using `tempfile` for `standard_model_test` (#4270) * Using tempfile for standard_model_test Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * style: pre-commit fixes --------- Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- tests/integration/test_models/standard_model_tests.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/tests/integration/test_models/standard_model_tests.py b/tests/integration/test_models/standard_model_tests.py index 7f0e9e6137..3f9cb56354 100644 --- a/tests/integration/test_models/standard_model_tests.py +++ b/tests/integration/test_models/standard_model_tests.py @@ -3,10 +3,9 @@ # import pybamm import tests -import uuid +import tempfile import numpy as np -import os class StandardModelTest: @@ -141,9 +140,8 @@ def test_sensitivities(self, param_name, param_value, output_name="Voltage [V]") ) def test_serialisation(self, solver=None, t_eval=None): - # Generating unique file names to avoid race conditions when run in parallel. - unique_id = uuid.uuid4() - file_name = f"test_model_{unique_id}" + temp = tempfile.NamedTemporaryFile(prefix="test_model") + file_name = temp.name self.model.save_model( file_name, variables=self.model.variables, mesh=self.disc.mesh ) @@ -178,8 +176,7 @@ def test_serialisation(self, solver=None, t_eval=None): np.testing.assert_array_almost_equal( new_solution.all_ys[x], self.solution.all_ys[x], decimal=accuracy ) - - os.remove(file_name + ".json") + temp.close() def test_all( self, param=None, disc=None, solver=None, t_eval=None, skip_output_tests=False From 27c29df2f82ccd3867eefe131f467ef356bcdf8c Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Thu, 18 Jul 2024 14:31:11 -0700 Subject: [PATCH 24/82] fix degradation options when one of the phases has no degradation (#4163) * fix degradation options when one of the phases has no degradation * skip Newman Tobias test --------- Co-authored-by: Eric G. Kratz Co-authored-by: Arjun Verma --- .../lithium_ion/base_lithium_ion_model.py | 6 +++-- .../active_material/loss_active_material.py | 4 ++- .../base_lithium_ion_tests.py | 25 +++++++++++++++++++ .../test_lithium_ion/test_newman_tobias.py | 4 +++ 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py b/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py index 479e8203ed..6db56b74c4 100644 --- a/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py +++ b/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py @@ -265,9 +265,9 @@ def set_sei_submodel(self): reaction_loc = "x-average" else: reaction_loc = "full electrode" - sei_option = getattr(self.options, domain)["SEI"] phases = self.options.phases[domain] for phase in phases: + sei_option = getattr(getattr(self.options, domain), phase)["SEI"] if sei_option == "none": submodel = pybamm.sei.NoSEI(self.param, domain, self.options, phase) elif sei_option == "constant": @@ -333,9 +333,11 @@ def set_lithium_plating_submodel(self): for domain in self.options.whole_cell_domains: if domain != "separator": domain = domain.split()[0].lower() - lithium_plating_opt = getattr(self.options, domain)["lithium plating"] phases = self.options.phases[domain] for phase in phases: + lithium_plating_opt = getattr(getattr(self.options, domain), phase)[ + "lithium plating" + ] if lithium_plating_opt == "none": submodel = pybamm.lithium_plating.NoPlating( self.param, domain, self.options, phase diff --git a/pybamm/models/submodels/active_material/loss_active_material.py b/pybamm/models/submodels/active_material/loss_active_material.py index 7816122e07..6f027d89e6 100644 --- a/pybamm/models/submodels/active_material/loss_active_material.py +++ b/pybamm/models/submodels/active_material/loss_active_material.py @@ -60,7 +60,9 @@ def get_coupled_variables(self, variables): domain, Domain = self.domain_Domain deps_solid_dt = 0 - lam_option = getattr(self.options, self.domain)["loss of active material"] + lam_option = getattr(getattr(self.options, domain), self.phase)[ + "loss of active material" + ] if "stress" in lam_option: # obtain the rate of loss of active materials (LAM) by stress # This is loss of active material model by mechanical effects diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_tests.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_tests.py index 7e1f2d5cac..c8a3f6b509 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_tests.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_tests.py @@ -559,3 +559,28 @@ def test_well_posed_composite_diffusion_hysteresis(self): "open-circuit potential": (("current sigmoid", "single"), "single"), } self.check_well_posedness(options) + + def test_well_posed_composite_different_degradation(self): + # phases have same degradation + options = { + "particle phases": ("2", "1"), + "SEI": ("ec reaction limited", "none"), + "lithium plating": ("reversible", "none"), + "open-circuit potential": (("current sigmoid", "single"), "single"), + } + self.check_well_posedness(options) + # phases have different degradation + options = { + "particle phases": ("2", "1"), + "SEI": (("ec reaction limited", "solvent-diffusion limited"), "none"), + "lithium plating": (("reversible", "irreversible"), "none"), + "open-circuit potential": (("current sigmoid", "single"), "single"), + } + self.check_well_posedness(options) + # one of the phases has no degradation + options = { + "particle phases": ("2", "1"), + "SEI": (("none", "solvent-diffusion limited"), "none"), + "lithium plating": (("none", "irreversible"), "none"), + } + self.check_well_posedness(options) diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_newman_tobias.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_newman_tobias.py index 5ceb039747..c979474e13 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_newman_tobias.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_newman_tobias.py @@ -35,3 +35,7 @@ def test_well_posed_composite_kinetic_hysteresis(self): @pytest.mark.skip(reason="Test currently not implemented") def test_well_posed_composite_diffusion_hysteresis(self): pass # skip this test + + @pytest.mark.skip(reason="Test currently not implemented") + def test_well_posed_composite_different_degradation(self): + pass # skip this test From ed412f8638965a499fa6793dc13b56ad92344763 Mon Sep 17 00:00:00 2001 From: Pradyot Ranjan <99216956+prady0t@users.noreply.github.com> Date: Sat, 20 Jul 2024 01:08:20 +0530 Subject: [PATCH 25/82] Removing false flagging of assert statements from tests. (#4236) * Removing flase flagging of assert statements from tests Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * style: pre-commit fixes * Using ruff to check for asserts Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * Using TypeError instead of assert Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * Adding back bandit file Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * style: pre-commit fixes * Adding tests Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> --------- Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> Co-authored-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Eric G. Kratz --- bandit.yml | 2 ++ pybamm/expression_tree/symbol.py | 3 ++- pybamm/settings.py | 10 ++++++---- pyproject.toml | 3 ++- tests/unit/test_expression_tree/test_symbol.py | 2 ++ tests/unit/test_settings.py | 7 +++++++ 6 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 bandit.yml diff --git a/bandit.yml b/bandit.yml new file mode 100644 index 0000000000..87da61e530 --- /dev/null +++ b/bandit.yml @@ -0,0 +1,2 @@ +# To ignore false flagging of assert statements in tests by Codacy. +skips: ['B101'] diff --git a/pybamm/expression_tree/symbol.py b/pybamm/expression_tree/symbol.py index df549747c9..aa9ebe66db 100644 --- a/pybamm/expression_tree/symbol.py +++ b/pybamm/expression_tree/symbol.py @@ -282,7 +282,8 @@ def name(self): @name.setter def name(self, value: str): - assert isinstance(value, str) + if not isinstance(value, str): + raise TypeError(f"{value} must be of type str") self._name = value @property diff --git a/pybamm/settings.py b/pybamm/settings.py index 2ccd9bcd13..d190eaf47e 100644 --- a/pybamm/settings.py +++ b/pybamm/settings.py @@ -29,8 +29,9 @@ def debug_mode(self): return self._debug_mode @debug_mode.setter - def debug_mode(self, value): - assert isinstance(value, bool) + def debug_mode(self, value: bool): + if not isinstance(value, bool): + raise TypeError(f"{value} must be of type bool") self._debug_mode = value @property @@ -38,8 +39,9 @@ def simplify(self): return self._simplify @simplify.setter - def simplify(self, value): - assert isinstance(value, bool) + def simplify(self, value: bool): + if not isinstance(value, bool): + raise TypeError(f"{value} must be of type bool") self._simplify = value def set_smoothing_parameters(self, k): diff --git a/pyproject.toml b/pyproject.toml index 005999d937..2bc6f4f3d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -200,6 +200,7 @@ extend-select = [ "UP", # pyupgrade "YTT", # flake8-2020 "TID252", # relative-imports + "S101", # to identify use of assert statement ] ignore = [ "E741", # Ambiguous variable name @@ -220,7 +221,7 @@ ignore = [ ] [tool.ruff.lint.per-file-ignores] -"tests/*" = ["T20"] +"tests/*" = ["T20", "S101"] "docs/*" = ["T20"] "examples/*" = ["T20"] "**.ipynb" = ["E402", "E703"] diff --git a/tests/unit/test_expression_tree/test_symbol.py b/tests/unit/test_expression_tree/test_symbol.py index 668c076907..e42f8dc8ef 100644 --- a/tests/unit/test_expression_tree/test_symbol.py +++ b/tests/unit/test_expression_tree/test_symbol.py @@ -18,6 +18,8 @@ class TestSymbol(TestCase): def test_symbol_init(self): sym = pybamm.Symbol("a symbol") + with self.assertRaises(TypeError): + sym.name = 1 self.assertEqual(sym.name, "a symbol") self.assertEqual(str(sym), "a symbol") diff --git a/tests/unit/test_settings.py b/tests/unit/test_settings.py index d70d3d4c40..6573929ad9 100644 --- a/tests/unit/test_settings.py +++ b/tests/unit/test_settings.py @@ -8,6 +8,9 @@ class TestSettings: def test_simplify(self): + with pytest.raises(TypeError): + pybamm.settings.simplify = "Not Bool" + assert pybamm.settings.simplify pybamm.settings.simplify = False @@ -15,6 +18,10 @@ def test_simplify(self): pybamm.settings.simplify = True + def test_debug_mode(self): + with pytest.raises(TypeError): + pybamm.settings.debug_mode = "Not bool" + def test_smoothing_parameters(self): assert pybamm.settings.min_max_mode == "exact" assert pybamm.settings.heaviside_smoothing == "exact" From cf268ae397e97b731d7492f61f8cb78d64f6678f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Jul 2024 01:04:45 +0530 Subject: [PATCH 26/82] Bump github/codeql-action from 3.25.12 to 3.25.13 in the actions group (#4283) Bumps the actions group with 1 update: [github/codeql-action](https://github.com/github/codeql-action). Updates `github/codeql-action` from 3.25.12 to 3.25.13 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/4fa2a7953630fd2f3fb380f21be14ede0169dd4f...2d790406f505036ef40ecba973cc774a50395aac) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/scorecard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 9a41c49b69..224725e0f7 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -68,6 +68,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.25.12 + uses: github/codeql-action/upload-sarif@2d790406f505036ef40ecba973cc774a50395aac # v3.25.13 with: sarif_file: results.sarif From 668e563b01749bcbb55a017e39b0cf94d8256619 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 22 Jul 2024 17:12:02 -0400 Subject: [PATCH 27/82] chore: update pre-commit hooks (#4284) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.5.2 → v0.5.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.2...v0.5.4) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ecd3cd9199..6b2f300f38 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.2" + rev: "v0.5.4" hooks: - id: ruff args: [--fix, --show-fixes] From ce407ae31b3627d33cad2e855f2df3786c562fd6 Mon Sep 17 00:00:00 2001 From: Brady Planden <55357039+BradyPlanden@users.noreply.github.com> Date: Tue, 23 Jul 2024 17:00:12 +0100 Subject: [PATCH 28/82] Corrects "electrode diffusivity" error catch (#4267) * fix: error catch for electrode diffusivity * refactor: update diffusivity error catch and test * tests: up coverage for diffusivity name catches * Update pybamm/util.py Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> --------- Co-authored-by: Eric G. Kratz Co-authored-by: Ferran Brosa Planella Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> --- pybamm/util.py | 22 +++++++++++++++------- tests/unit/test_util.py | 15 +++++++++++++++ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/pybamm/util.py b/pybamm/util.py index 0fe02c835b..130cb5ba48 100644 --- a/pybamm/util.py +++ b/pybamm/util.py @@ -57,14 +57,22 @@ def __getitem__(self, key): try: return super().__getitem__(key) except KeyError as error: - if "particle diffusivity" in key: - warn( - f"The parameter '{key.replace('particle', 'electrode')}' " - f"has been renamed to '{key}'", - DeprecationWarning, - stacklevel=2, + if "electrode diffusivity" in key or "particle diffusivity" in key: + old_term, new_term = ( + ("electrode", "particle") + if "electrode diffusivity" in key + else ("particle", "electrode") ) - return super().__getitem__(key.replace("particle", "electrode")) + alternative_key = key.replace(old_term, new_term) + + if old_term == "electrode": + warn( + f"The parameter '{alternative_key}' has been renamed to '{key}' and will be removed in a future release. Using '{key}'", + DeprecationWarning, + stacklevel=2, + ) + + return super().__getitem__(alternative_key) if key in ["Negative electrode SOC", "Positive electrode SOC"]: domain = key.split(" ")[0] raise KeyError( diff --git a/tests/unit/test_util.py b/tests/unit/test_util.py index 21673a44fe..abcdb4dcae 100644 --- a/tests/unit/test_util.py +++ b/tests/unit/test_util.py @@ -34,6 +34,11 @@ def test_fuzzy_dict(self): "SEI current": 3, "Lithium plating current": 4, "A dimensional variable [m]": 5, + "Positive particle diffusivity [m2.s-1]": 6, + } + ) + d2 = pybamm.FuzzyDict( + { "Positive electrode diffusivity [m2.s-1]": 6, } ) @@ -56,6 +61,16 @@ def test_fuzzy_dict(self): with pytest.raises(KeyError, match="Upper voltage"): d.__getitem__("Open-circuit voltage at 100% SOC [V]") + assert ( + d2["Positive particle diffusivity [m2.s-1]"] + == d["Positive particle diffusivity [m2.s-1]"] + ) + + assert ( + d2["Positive electrode diffusivity [m2.s-1]"] + == d["Positive electrode diffusivity [m2.s-1]"] + ) + with pytest.warns(DeprecationWarning): assert ( d["Positive electrode diffusivity [m2.s-1]"] From ba39170aa22d248b59975afd39aa64dc6e1ab431 Mon Sep 17 00:00:00 2001 From: Marc Berliner <34451391+MarcBerliner@users.noreply.github.com> Date: Wed, 24 Jul 2024 09:55:50 -0400 Subject: [PATCH 29/82] Add more `idaklu` solver options (number 2) (#4282) * IDAKLU options updates * format * update defaults and docstrings * Update CHANGELOG.md * Update test_idaklu_solver.py * Move changelog to `Unreleased` * Update CHANGELOG.md --------- Co-authored-by: kratman Co-authored-by: Eric G. Kratz --- CHANGELOG.md | 4 + .../idaklu/Expressions/Base/ExpressionSet.hpp | 6 +- .../Expressions/Casadi/CasadiFunctions.hpp | 4 +- .../idaklu/Expressions/IREE/IREEFunctions.hpp | 4 +- .../c_solvers/idaklu/IDAKLUSolverOpenMP.hpp | 17 +- .../c_solvers/idaklu/IDAKLUSolverOpenMP.inl | 359 +++++++++++------- .../idaklu/IDAKLUSolverOpenMP_solvers.hpp | 8 +- pybamm/solvers/c_solvers/idaklu/Options.cpp | 44 ++- pybamm/solvers/c_solvers/idaklu/Options.hpp | 42 +- .../c_solvers/idaklu/idaklu_solver.hpp | 42 +- .../c_solvers/idaklu/sundials_functions.inl | 8 +- pybamm/solvers/idaklu_solver.py | 234 ++++++++---- .../base_lithium_ion_tests.py | 6 +- tests/unit/test_solvers/test_idaklu_solver.py | 75 +++- 14 files changed, 587 insertions(+), 266 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index decbacf529..97276a275b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # [Unreleased](https://github.com/pybamm-team/PyBaMM/) +## Features + +- Added additional user-configurable options to the (`IDAKLUSolver`) and adjusted the default values to improve performance. ([#4282](https://github.com/pybamm-team/PyBaMM/pull/4282)) + # [v24.5rc2](https://github.com/pybamm-team/PyBaMM/tree/v24.5rc2) - 2024-07-12 ## Features diff --git a/pybamm/solvers/c_solvers/idaklu/Expressions/Base/ExpressionSet.hpp b/pybamm/solvers/c_solvers/idaklu/Expressions/Base/ExpressionSet.hpp index a32f906a38..13c746a37d 100644 --- a/pybamm/solvers/c_solvers/idaklu/Expressions/Base/ExpressionSet.hpp +++ b/pybamm/solvers/c_solvers/idaklu/Expressions/Base/ExpressionSet.hpp @@ -31,7 +31,7 @@ class ExpressionSet const int n_s, const int n_e, const int n_p, - const Options& options) + const SetupOptions& options) : number_of_states(n_s), number_of_events(n_e), number_of_parameters(n_p), @@ -46,7 +46,7 @@ class ExpressionSet events(events), tmp_state_vector(number_of_states), tmp_sparse_jacobian_data(jac_times_cjmass_nnz), - options(options) + setup_opts(options) {}; int number_of_states; @@ -73,7 +73,7 @@ class ExpressionSet std::vector jac_times_cjmass_colptrs; // cppcheck-suppress unusedStructMember std::vector inputs; // cppcheck-suppress unusedStructMember - Options options; + SetupOptions setup_opts; virtual realtype *get_tmp_state_vector() = 0; virtual realtype *get_tmp_sparse_jacobian_data() = 0; diff --git a/pybamm/solvers/c_solvers/idaklu/Expressions/Casadi/CasadiFunctions.hpp b/pybamm/solvers/c_solvers/idaklu/Expressions/Casadi/CasadiFunctions.hpp index cc4b7cbb63..64db2e6106 100644 --- a/pybamm/solvers/c_solvers/idaklu/Expressions/Casadi/CasadiFunctions.hpp +++ b/pybamm/solvers/c_solvers/idaklu/Expressions/Casadi/CasadiFunctions.hpp @@ -76,7 +76,7 @@ class CasadiFunctions : public ExpressionSet const std::vector& var_fcns, const std::vector& dvar_dy_fcns, const std::vector& dvar_dp_fcns, - const Options& options + const SetupOptions& setup_opts ) : rhs_alg_casadi(rhs_alg), jac_times_cjmass_casadi(jac_times_cjmass), @@ -98,7 +98,7 @@ class CasadiFunctions : public ExpressionSet static_cast(&sens_casadi), static_cast(&events_casadi), n_s, n_e, n_p, - options) + setup_opts) { // convert BaseFunctionType list to CasadiFunction list // NOTE: You must allocate ALL std::vector elements before taking references diff --git a/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunctions.hpp b/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunctions.hpp index cc864f4046..9a3169a46e 100644 --- a/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunctions.hpp +++ b/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunctions.hpp @@ -59,7 +59,7 @@ class IREEFunctions : public ExpressionSet const std::vector& var_fcns, const std::vector& dvar_dy_fcns, const std::vector& dvar_dp_fcns, - const Options& options + const SetupOptions& setup_opts ) : iree_init_status(iree_init()), rhs_alg_iree(rhs_alg), @@ -82,7 +82,7 @@ class IREEFunctions : public ExpressionSet static_cast(&sens_iree), static_cast(&events_iree), n_s, n_e, n_p, - options) + setup_opts) { // convert BaseFunctionType list to IREEFunction list // NOTE: You must allocate ALL std::vector elements before taking references diff --git a/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.hpp b/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.hpp index 8c49069b30..98148a3c9f 100644 --- a/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.hpp +++ b/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.hpp @@ -64,7 +64,8 @@ class IDAKLUSolverOpenMP : public IDAKLUSolver std::vector res; std::vector res_dvar_dy; std::vector res_dvar_dp; - Options options; + SetupOptions setup_opts; + SolverOptions solver_opts; #if SUNDIALS_VERSION_MAJOR >= 6 SUNContext sunctx; @@ -84,7 +85,9 @@ class IDAKLUSolverOpenMP : public IDAKLUSolver int jac_bandwidth_lower, int jac_bandwidth_upper, std::unique_ptr functions, - const Options& options); + const SetupOptions &setup_opts, + const SolverOptions &solver_opts + ); /** * @brief Destructor @@ -139,6 +142,16 @@ class IDAKLUSolverOpenMP : public IDAKLUSolver * @brief Allocate memory for matrices (noting appropriate matrix format/types) */ void SetMatrix(); + + /** + * @brief Apply user-configurable IDA options + */ + void SetSolverOptions(); + + /** + * @brief Check the return flag for errors + */ + void CheckErrors(int const & flag); }; #include "IDAKLUSolverOpenMP.inl" diff --git a/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl b/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl index 383037e2ca..340baa2d30 100644 --- a/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl +++ b/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl @@ -3,27 +3,29 @@ template IDAKLUSolverOpenMP::IDAKLUSolverOpenMP( - np_array atol_np, + np_array atol_np_input, double rel_tol, - np_array rhs_alg_id, - int number_of_parameters, - int number_of_events, - int jac_times_cjmass_nnz, - int jac_bandwidth_lower, - int jac_bandwidth_upper, + np_array rhs_alg_id_input, + int number_of_parameters_input, + int number_of_events_input, + int jac_times_cjmass_nnz_input, + int jac_bandwidth_lower_input, + int jac_bandwidth_upper_input, std::unique_ptr functions_arg, - const Options &options + const SetupOptions &setup_input, + const SolverOptions &solver_input ) : - atol_np(atol_np), - rhs_alg_id(rhs_alg_id), - number_of_states(atol_np.request().size), - number_of_parameters(number_of_parameters), - number_of_events(number_of_events), - jac_times_cjmass_nnz(jac_times_cjmass_nnz), - jac_bandwidth_lower(jac_bandwidth_lower), - jac_bandwidth_upper(jac_bandwidth_upper), + atol_np(atol_np_input), + rhs_alg_id(rhs_alg_id_input), + number_of_states(atol_np_input.request().size), + number_of_parameters(number_of_parameters_input), + number_of_events(number_of_events_input), + jac_times_cjmass_nnz(jac_times_cjmass_nnz_input), + jac_bandwidth_lower(jac_bandwidth_lower_input), + jac_bandwidth_upper(jac_bandwidth_upper_input), functions(std::move(functions_arg)), - options(options) + setup_opts(setup_input), + solver_opts(solver_input) { // Construction code moved to Initialize() which is called from the // (child) IDAKLUSolver_* class constructors. @@ -38,17 +40,17 @@ IDAKLUSolverOpenMP::IDAKLUSolverOpenMP( // create the vector of initial values AllocateVectors(); - if (number_of_parameters > 0) - { + if (number_of_parameters > 0) { yyS = N_VCloneVectorArray(number_of_parameters, yy); ypS = N_VCloneVectorArray(number_of_parameters, yp); } // set initial values realtype *atval = N_VGetArrayPointer(avtol); - for (int i = 0; i < number_of_states; i++) + for (int i = 0; i < number_of_states; i++) { atval[i] = atol[i]; - for (int is = 0; is < number_of_parameters; is++) - { + } + + for (int is = 0; is < number_of_parameters; is++) { N_VConst(RCONST(0.0), yyS[is]); N_VConst(RCONST(0.0), ypS[is]); } @@ -63,14 +65,16 @@ IDAKLUSolverOpenMP::IDAKLUSolverOpenMP( rtol = RCONST(rel_tol); IDASVtolerances(ida_mem, rtol, avtol); - // set events + // Set events IDARootInit(ida_mem, number_of_events, events_eval); + + // Set user data void *user_data = functions.get(); IDASetUserData(ida_mem, user_data); - // specify preconditioner type + // Specify preconditioner type precon_type = SUN_PREC_NONE; - if (options.preconditioner != "none") { + if (this->setup_opts.preconditioner != "none") { precon_type = SUN_PREC_LEFT; } } @@ -78,17 +82,83 @@ IDAKLUSolverOpenMP::IDAKLUSolverOpenMP( template void IDAKLUSolverOpenMP::AllocateVectors() { // Create vectors - yy = N_VNew_OpenMP(number_of_states, options.num_threads, sunctx); - yp = N_VNew_OpenMP(number_of_states, options.num_threads, sunctx); - avtol = N_VNew_OpenMP(number_of_states, options.num_threads, sunctx); - id = N_VNew_OpenMP(number_of_states, options.num_threads, sunctx); + yy = N_VNew_OpenMP(number_of_states, setup_opts.num_threads, sunctx); + yp = N_VNew_OpenMP(number_of_states, setup_opts.num_threads, sunctx); + avtol = N_VNew_OpenMP(number_of_states, setup_opts.num_threads, sunctx); + id = N_VNew_OpenMP(number_of_states, setup_opts.num_threads, sunctx); } +template +void IDAKLUSolverOpenMP::SetSolverOptions() { + // Maximum order of the linear multistep method + CheckErrors(IDASetMaxOrd(ida_mem, solver_opts.max_order_bdf)); + + // Maximum number of steps to be taken by the solver in its attempt to reach + // the next output time + CheckErrors(IDASetMaxNumSteps(ida_mem, solver_opts.max_num_steps)); + + // Initial step size + CheckErrors(IDASetInitStep(ida_mem, solver_opts.dt_init)); + + // Maximum absolute step size + CheckErrors(IDASetMaxStep(ida_mem, solver_opts.dt_max)); + + // Maximum number of error test failures in attempting one step + CheckErrors(IDASetMaxErrTestFails(ida_mem, solver_opts.max_error_test_failures)); + + // Maximum number of nonlinear solver iterations at one step + CheckErrors(IDASetMaxNonlinIters(ida_mem, solver_opts.max_nonlinear_iterations)); + + // Maximum number of nonlinear solver convergence failures at one step + CheckErrors(IDASetMaxConvFails(ida_mem, solver_opts.max_convergence_failures)); + + // Safety factor in the nonlinear convergence test + CheckErrors(IDASetNonlinConvCoef(ida_mem, solver_opts.nonlinear_convergence_coefficient)); + + // Suppress algebraic variables from error test + CheckErrors(IDASetSuppressAlg(ida_mem, solver_opts.suppress_algebraic_error)); + + // Positive constant in the Newton iteration convergence test within the initial + // condition calculation + CheckErrors(IDASetNonlinConvCoefIC(ida_mem, solver_opts.nonlinear_convergence_coefficient_ic)); + + // Maximum number of steps allowed when icopt=IDA_YA_YDP_INIT in IDACalcIC + CheckErrors(IDASetMaxNumStepsIC(ida_mem, solver_opts.max_num_steps_ic)); + + // Maximum number of the approximate Jacobian or preconditioner evaluations + // allowed when the Newton iteration appears to be slowly converging + CheckErrors(IDASetMaxNumJacsIC(ida_mem, solver_opts.max_num_jacobians_ic)); + + // Maximum number of Newton iterations allowed in any one attempt to solve + // the initial conditions calculation problem + CheckErrors(IDASetMaxNumItersIC(ida_mem, solver_opts.max_num_iterations_ic)); + + // Maximum number of linesearch backtracks allowed in any Newton iteration, + // when solving the initial conditions calculation problem + CheckErrors(IDASetMaxBacksIC(ida_mem, solver_opts.max_linesearch_backtracks_ic)); + + // Turn off linesearch + CheckErrors(IDASetLineSearchOffIC(ida_mem, solver_opts.linesearch_off_ic)); + + // Ratio between linear and nonlinear tolerances + CheckErrors(IDASetEpsLin(ida_mem, solver_opts.epsilon_linear_tolerance)); + + // Increment factor used in DQ Jv approximation + CheckErrors(IDASetIncrementFactor(ida_mem, solver_opts.increment_factor)); + + int LS_type = SUNLinSolGetType(LS); + if (LS_type == SUNLINEARSOLVER_DIRECT || LS_type == SUNLINEARSOLVER_MATRIX_ITERATIVE) { + // Enable or disable linear solution scaling + CheckErrors(IDASetLinearSolutionScaling(ida_mem, solver_opts.linear_solution_scaling)); + } +} + + + template void IDAKLUSolverOpenMP::SetMatrix() { // Create Matrix object - if (options.jacobian == "sparse") - { + if (setup_opts.jacobian == "sparse") { DEBUG("\tsetting sparse matrix"); J = SUNSparseMatrix( number_of_states, @@ -97,8 +167,7 @@ void IDAKLUSolverOpenMP::SetMatrix() { CSC_MAT, sunctx ); - } - else if (options.jacobian == "banded") { + } else if (setup_opts.jacobian == "banded") { DEBUG("\tsetting banded matrix"); J = SUNBandMatrix( number_of_states, @@ -106,22 +175,19 @@ void IDAKLUSolverOpenMP::SetMatrix() { jac_bandwidth_lower, sunctx ); - } else if (options.jacobian == "dense" || options.jacobian == "none") - { + } else if (setup_opts.jacobian == "dense" || setup_opts.jacobian == "none") { DEBUG("\tsetting dense matrix"); J = SUNDenseMatrix( number_of_states, number_of_states, sunctx ); - } - else if (options.jacobian == "matrix-free") - { + } else if (setup_opts.jacobian == "matrix-free") { DEBUG("\tsetting matrix-free"); J = NULL; - } - else + } else { throw std::invalid_argument("Unsupported matrix requested"); + } } template @@ -129,61 +195,64 @@ void IDAKLUSolverOpenMP::Initialize() { // Call after setting the solver // attach the linear solver - if (LS == nullptr) + if (LS == nullptr) { throw std::invalid_argument("Linear solver not set"); - IDASetLinearSolver(ida_mem, LS, J); + } + CheckErrors(IDASetLinearSolver(ida_mem, LS, J)); - if (options.preconditioner != "none") - { + if (setup_opts.preconditioner != "none") { DEBUG("\tsetting IDADDB preconditioner"); // setup preconditioner - IDABBDPrecInit( - ida_mem, number_of_states, options.precon_half_bandwidth, - options.precon_half_bandwidth, options.precon_half_bandwidth_keep, - options.precon_half_bandwidth_keep, 0.0, residual_eval_approx, NULL); + CheckErrors(IDABBDPrecInit( + ida_mem, number_of_states, setup_opts.precon_half_bandwidth, + setup_opts.precon_half_bandwidth, setup_opts.precon_half_bandwidth_keep, + setup_opts.precon_half_bandwidth_keep, 0.0, residual_eval_approx, NULL)); } - if (options.jacobian == "matrix-free") - IDASetJacTimes(ida_mem, NULL, jtimes_eval); - else if (options.jacobian != "none") - IDASetJacFn(ida_mem, jacobian_eval); + if (setup_opts.jacobian == "matrix-free") { + CheckErrors(IDASetJacTimes(ida_mem, NULL, jtimes_eval)); + } else if (setup_opts.jacobian != "none") { + CheckErrors(IDASetJacFn(ida_mem, jacobian_eval)); + } - if (number_of_parameters > 0) - { - IDASensInit(ida_mem, number_of_parameters, IDA_SIMULTANEOUS, - sensitivities_eval, yyS, ypS); - IDASensEEtolerances(ida_mem); + if (number_of_parameters > 0) { + CheckErrors(IDASensInit(ida_mem, number_of_parameters, IDA_SIMULTANEOUS, + sensitivities_eval, yyS, ypS)); + CheckErrors(IDASensEEtolerances(ida_mem)); } - SUNLinSolInitialize(LS); + CheckErrors(SUNLinSolInitialize(LS)); auto id_np_val = rhs_alg_id.unchecked<1>(); realtype *id_val; id_val = N_VGetArrayPointer(id); int ii; - for (ii = 0; ii < number_of_states; ii++) + for (ii = 0; ii < number_of_states; ii++) { id_val[ii] = id_np_val[ii]; + } - IDASetId(ida_mem, id); + // Variable types: differential (1) and algebraic (0) + CheckErrors(IDASetId(ida_mem, id)); } template -IDAKLUSolverOpenMP::~IDAKLUSolverOpenMP() -{ +IDAKLUSolverOpenMP::~IDAKLUSolverOpenMP() { + bool sensitivity = number_of_parameters > 0; // Free memory - if (number_of_parameters > 0) - IDASensFree(ida_mem); + if (sensitivity) { + IDASensFree(ida_mem); + } + + CheckErrors(SUNLinSolFree(LS)); - SUNLinSolFree(LS); SUNMatDestroy(J); N_VDestroy(avtol); N_VDestroy(yy); N_VDestroy(yp); N_VDestroy(id); - if (number_of_parameters > 0) - { + if (sensitivity) { N_VDestroyVectorArray(yyS, number_of_parameters); N_VDestroyVectorArray(ypS, number_of_parameters); } @@ -209,8 +278,9 @@ void IDAKLUSolverOpenMP::CalcVars( for (auto& var_fcn : functions->var_fcns) { (*var_fcn)({tret, yval, functions->inputs.data()}, {&res[0]}); // store in return vector - for (size_t jj=0; jjnnz_out(); jj++) + for (size_t jj=0; jjnnz_out(); jj++) { y_return[t_i*length_of_return_vector + j++] = res[jj]; + } } // calculate sensitivities CalcVarsSensitivities(tret, yval, ySval, yS_return, ySk); @@ -235,15 +305,18 @@ void IDAKLUSolverOpenMP::CalcVarsSensitivities( (*dvar_dy)({tret, yval, functions->inputs.data()}, {&res_dvar_dy[0]}); // Calculate dvar/dp and convert to dense array for indexing (*dvar_dp)({tret, yval, functions->inputs.data()}, {&res_dvar_dp[0]}); - for(int k=0; knnz_out(); k++) + } + for (int k=0; knnz_out(); k++) { dens_dvar_dp[dvar_dp->get_row()[k]] = res_dvar_dp[k]; + } // Calculate sensitivities - for(int paramk=0; paramknnz_out(); spk++) + for (int spk=0; spknnz_out(); spk++) { yS_return[*ySk] += res_dvar_dy[spk] * ySval[paramk][dvar_dy->get_col()[spk]]; + } (*ySk)++; } } @@ -265,21 +338,25 @@ Solution IDAKLUSolverOpenMP::solve( auto y0 = y0_np.unchecked<1>(); auto yp0 = yp0_np.unchecked<1>(); auto n_coeffs = number_of_states + number_of_parameters * number_of_states; + bool const sensitivity = number_of_parameters > 0; - if (y0.size() != n_coeffs) + if (y0.size() != n_coeffs) { throw std::domain_error( "y0 has wrong size. Expected " + std::to_string(n_coeffs) + " but got " + std::to_string(y0.size())); + } - if (yp0.size() != n_coeffs) + if (yp0.size() != n_coeffs) { throw std::domain_error( "yp0 has wrong size. Expected " + std::to_string(n_coeffs) + " but got " + std::to_string(yp0.size())); + } // set inputs auto p_inputs = inputs.unchecked<2>(); - for (int i = 0; i < functions->inputs.size(); i++) + for (int i = 0; i < functions->inputs.size(); i++) { functions->inputs[i] = p_inputs(i, 0); + } // set initial conditions realtype *yval = N_VGetArrayPointer(yy); @@ -295,21 +372,29 @@ Solution IDAKLUSolverOpenMP::solve( } } - for (int i = 0; i < number_of_states; i++) - { + for (int i = 0; i < number_of_states; i++) { yval[i] = y0[i]; ypval[i] = yp0[i]; } - IDAReInit(ida_mem, t0, yy, yp); - if (number_of_parameters > 0) - IDASensReInit(ida_mem, IDA_SIMULTANEOUS, yyS, ypS); + SetSolverOptions(); + + CheckErrors(IDAReInit(ida_mem, t0, yy, yp)); + if (sensitivity) { + CheckErrors(IDASensReInit(ida_mem, IDA_SIMULTANEOUS, yyS, ypS)); + } // correct initial values - DEBUG("IDACalcIC"); - IDACalcIC(ida_mem, IDA_YA_YDP_INIT, t(1)); - if (number_of_parameters > 0) - IDAGetSens(ida_mem, &t0, yyS); + int const init_type = solver_opts.init_all_y_ic ? IDA_Y_INIT : IDA_YA_YDP_INIT; + if (solver_opts.calc_ic) { + DEBUG("IDACalcIC"); + // IDACalcIC will throw a warning if it fails to find initial conditions + IDACalcIC(ida_mem, init_type, t(1)); + } + + if (sensitivity) { + CheckErrors(IDAGetSens(ida_mem, &t0, yyS)); + } realtype tret; realtype t_final = t(number_of_timesteps - 1); @@ -323,10 +408,12 @@ Solution IDAKLUSolverOpenMP::solve( for (auto& var_fcn : functions->var_fcns) { max_res_size = std::max(max_res_size, size_t(var_fcn->out_shape(0))); length_of_return_vector += var_fcn->nnz_out(); - for (auto& dvar_fcn : functions->dvar_dy_fcns) + for (auto& dvar_fcn : functions->dvar_dy_fcns) { max_res_dvar_dy = std::max(max_res_dvar_dy, size_t(dvar_fcn->out_shape(0))); - for (auto& dvar_fcn : functions->dvar_dp_fcns) + } + for (auto& dvar_fcn : functions->dvar_dp_fcns) { max_res_dvar_dp = std::max(max_res_dvar_dp, size_t(dvar_fcn->out_shape(0))); + } } } else { // Return full y state-vector @@ -375,63 +462,62 @@ Solution IDAKLUSolverOpenMP::solve( &tret, yval, ySval, yS_return, &ySk); } else { // Retain complete copy of the state vector - for (int j = 0; j < number_of_states; j++) + for (int j = 0; j < number_of_states; j++) { y_return[j] = yval[j]; - for (int j = 0; j < number_of_parameters; j++) - { + } + for (int j = 0; j < number_of_parameters; j++) { const int base_index = j * number_of_timesteps * number_of_states; - for (int k = 0; k < number_of_states; k++) + for (int k = 0; k < number_of_states; k++) { yS_return[base_index + k] = ySval[j][k]; + } } } // Subsequent states (t_i>0) int retval; t_i = 1; - while (true) - { + while (true) { realtype t_next = t(t_i); IDASetStopTime(ida_mem, t_next); DEBUG("IDASolve"); retval = IDASolve(ida_mem, t_final, &tret, yy, yp, IDA_NORMAL); - if (retval == IDA_TSTOP_RETURN || + if (!(retval == IDA_TSTOP_RETURN || retval == IDA_SUCCESS || - retval == IDA_ROOT_RETURN) - { - if (number_of_parameters > 0) - IDAGetSens(ida_mem, &tret, yyS); - - // Evaluate and store results for the time step - t_return[t_i] = tret; - if (functions->var_fcns.size() > 0) { - // Evaluate functions for each requested variable and store - // NOTE: Indexing of yS_return is (time:var:param) - CalcVars(y_return, length_of_return_vector, t_i, - &tret, yval, ySval, yS_return, &ySk); - } else { - // Retain complete copy of the state vector - for (int j = 0; j < number_of_states; j++) - y_return[t_i * number_of_states + j] = yval[j]; - for (int j = 0; j < number_of_parameters; j++) - { - const int base_index = - j * number_of_timesteps * number_of_states + - t_i * number_of_states; - for (int k = 0; k < number_of_states; k++) - // NOTE: Indexing of yS_return is (time:param:yvec) - yS_return[base_index + k] = ySval[j][k]; + retval == IDA_ROOT_RETURN)) { + // failed + break; + } + + if (sensitivity) { + CheckErrors(IDAGetSens(ida_mem, &tret, yyS)); + } + + // Evaluate and store results for the time step + t_return[t_i] = tret; + if (functions->var_fcns.size() > 0) { + // Evaluate functions for each requested variable and store + // NOTE: Indexing of yS_return is (time:var:param) + CalcVars(y_return, length_of_return_vector, t_i, + &tret, yval, ySval, yS_return, &ySk); + } else { + // Retain complete copy of the state vector + for (int j = 0; j < number_of_states; j++) { + y_return[t_i * number_of_states + j] = yval[j]; + } + for (int j = 0; j < number_of_parameters; j++) { + const int base_index = + j * number_of_timesteps * number_of_states + + t_i * number_of_states; + for (int k = 0; k < number_of_states; k++) { + // NOTE: Indexing of yS_return is (time:param:yvec) + yS_return[base_index + k] = ySval[j][k]; } } - t_i += 1; - - if (retval == IDA_SUCCESS || - retval == IDA_ROOT_RETURN) - break; } - else - { - // failed + t_i += 1; + + if (retval == IDA_SUCCESS || retval == IDA_ROOT_RETURN) { break; } } @@ -473,13 +559,12 @@ Solution IDAKLUSolverOpenMP::solve( Solution sol(retval, t_ret, y_ret, yS_ret); - if (options.print_stats) - { + if (solver_opts.print_stats) { long nsteps, nrevals, nlinsetups, netfails; int klast, kcur; realtype hinused, hlast, hcur, tcur; - IDAGetIntegratorStats( + CheckErrors(IDAGetIntegratorStats( ida_mem, &nsteps, &nrevals, @@ -491,14 +576,15 @@ Solution IDAKLUSolverOpenMP::solve( &hlast, &hcur, &tcur - ); + )); long nniters, nncfails; - IDAGetNonlinSolvStats(ida_mem, &nniters, &nncfails); + CheckErrors(IDAGetNonlinSolvStats(ida_mem, &nniters, &nncfails)); long int ngevalsBBDP = 0; - if (options.using_iterative_solver) - IDABBDPrecGetNumGfnEvals(ida_mem, &ngevalsBBDP); + if (setup_opts.using_iterative_solver) { + CheckErrors(IDABBDPrecGetNumGfnEvals(ida_mem, &ngevalsBBDP)); + } py::print("Solver Stats:"); py::print("\tNumber of steps =", nsteps); @@ -519,3 +605,12 @@ Solution IDAKLUSolverOpenMP::solve( return sol; } + +template +void IDAKLUSolverOpenMP::CheckErrors(int const & flag) { + if (flag < 0) { + auto message = (std::string("IDA failed with flag ") + std::to_string(flag)).c_str(); + py::set_error(PyExc_ValueError, message); + throw py::error_already_set(); + } +} diff --git a/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP_solvers.hpp b/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP_solvers.hpp index ebeb543232..5f6f29b47b 100644 --- a/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP_solvers.hpp +++ b/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP_solvers.hpp @@ -61,7 +61,7 @@ class IDAKLUSolverOpenMP_SPBCGS : public IDAKLUSolverOpenMP { Base::LS = SUNLinSol_SPBCGS( Base::yy, Base::precon_type, - Base::options.linsol_max_iterations, + Base::setup_opts.linsol_max_iterations, Base::sunctx ); Base::Initialize(); @@ -81,7 +81,7 @@ class IDAKLUSolverOpenMP_SPFGMR : public IDAKLUSolverOpenMP { Base::LS = SUNLinSol_SPFGMR( Base::yy, Base::precon_type, - Base::options.linsol_max_iterations, + Base::setup_opts.linsol_max_iterations, Base::sunctx ); Base::Initialize(); @@ -101,7 +101,7 @@ class IDAKLUSolverOpenMP_SPGMR : public IDAKLUSolverOpenMP { Base::LS = SUNLinSol_SPGMR( Base::yy, Base::precon_type, - Base::options.linsol_max_iterations, + Base::setup_opts.linsol_max_iterations, Base::sunctx ); Base::Initialize(); @@ -121,7 +121,7 @@ class IDAKLUSolverOpenMP_SPTFQMR : public IDAKLUSolverOpenMP { Base::LS = SUNLinSol_SPTFQMR( Base::yy, Base::precon_type, - Base::options.linsol_max_iterations, + Base::setup_opts.linsol_max_iterations, Base::sunctx ); Base::Initialize(); diff --git a/pybamm/solvers/c_solvers/idaklu/Options.cpp b/pybamm/solvers/c_solvers/idaklu/Options.cpp index 684ab47f33..b6a33e016e 100644 --- a/pybamm/solvers/c_solvers/idaklu/Options.cpp +++ b/pybamm/solvers/c_solvers/idaklu/Options.cpp @@ -5,15 +5,14 @@ using namespace std::string_literals; -Options::Options(py::dict options) - : print_stats(options["print_stats"].cast()), - jacobian(options["jacobian"].cast()), - preconditioner(options["preconditioner"].cast()), - linsol_max_iterations(options["linsol_max_iterations"].cast()), - linear_solver(options["linear_solver"].cast()), - precon_half_bandwidth(options["precon_half_bandwidth"].cast()), - precon_half_bandwidth_keep(options["precon_half_bandwidth_keep"].cast()), - num_threads(options["num_threads"].cast()) +SetupOptions::SetupOptions(py::dict &py_opts) + : jacobian(py_opts["jacobian"].cast()), + preconditioner(py_opts["preconditioner"].cast()), + precon_half_bandwidth(py_opts["precon_half_bandwidth"].cast()), + precon_half_bandwidth_keep(py_opts["precon_half_bandwidth_keep"].cast()), + num_threads(py_opts["num_threads"].cast()), + linear_solver(py_opts["linear_solver"].cast()), + linsol_max_iterations(py_opts["linsol_max_iterations"].cast()) { using_sparse_matrix = true; @@ -119,3 +118,30 @@ Options::Options(py::dict options) preconditioner = "none"; } } + +SolverOptions::SolverOptions(py::dict &py_opts) + : print_stats(py_opts["print_stats"].cast()), + // IDA main solver + max_order_bdf(py_opts["max_order_bdf"].cast()), + max_num_steps(py_opts["max_num_steps"].cast()), + dt_init(RCONST(py_opts["dt_init"].cast())), + dt_max(RCONST(py_opts["dt_max"].cast())), + max_error_test_failures(py_opts["max_error_test_failures"].cast()), + max_nonlinear_iterations(py_opts["max_nonlinear_iterations"].cast()), + max_convergence_failures(py_opts["max_convergence_failures"].cast()), + nonlinear_convergence_coefficient(RCONST(py_opts["nonlinear_convergence_coefficient"].cast())), + nonlinear_convergence_coefficient_ic(RCONST(py_opts["nonlinear_convergence_coefficient_ic"].cast())), + suppress_algebraic_error(py_opts["suppress_algebraic_error"].cast()), + // IDA initial conditions calculation + calc_ic(py_opts["calc_ic"].cast()), + init_all_y_ic(py_opts["init_all_y_ic"].cast()), + max_num_steps_ic(py_opts["max_num_steps_ic"].cast()), + max_num_jacobians_ic(py_opts["max_num_jacobians_ic"].cast()), + max_num_iterations_ic(py_opts["max_num_iterations_ic"].cast()), + max_linesearch_backtracks_ic(py_opts["max_linesearch_backtracks_ic"].cast()), + linesearch_off_ic(py_opts["linesearch_off_ic"].cast()), + // IDALS linear solver interface + linear_solution_scaling(py_opts["linear_solution_scaling"].cast()), + epsilon_linear_tolerance(RCONST(py_opts["epsilon_linear_tolerance"].cast())), + increment_factor(RCONST(py_opts["increment_factor"].cast())) +{} diff --git a/pybamm/solvers/c_solvers/idaklu/Options.hpp b/pybamm/solvers/c_solvers/idaklu/Options.hpp index b70d0f4a30..66a175cfff 100644 --- a/pybamm/solvers/c_solvers/idaklu/Options.hpp +++ b/pybamm/solvers/c_solvers/idaklu/Options.hpp @@ -4,22 +4,52 @@ #include "common.hpp" /** - * @brief Options passed to the idaklu solver by pybamm + * @brief SetupOptions passed to the idaklu setup by pybamm */ -struct Options { - bool print_stats; +struct SetupOptions { bool using_sparse_matrix; bool using_banded_matrix; bool using_iterative_solver; std::string jacobian; - std::string linear_solver; // klu, lapack, spbcg std::string preconditioner; // spbcg - int linsol_max_iterations; int precon_half_bandwidth; int precon_half_bandwidth_keep; int num_threads; - explicit Options(py::dict options); + // IDALS linear solver interface + std::string linear_solver; // klu, lapack, spbcg + int linsol_max_iterations; + explicit SetupOptions(py::dict &py_opts); +}; +/** + * @brief SolverOptions passed to the idaklu solver by pybamm + */ +struct SolverOptions { + bool print_stats; + // IDA main solver + int max_order_bdf; + int max_num_steps; + double dt_init; + double dt_max; + int max_error_test_failures; + int max_nonlinear_iterations; + int max_convergence_failures; + double nonlinear_convergence_coefficient; + double nonlinear_convergence_coefficient_ic; + sunbooleantype suppress_algebraic_error; + // IDA initial conditions calculation + bool calc_ic; + bool init_all_y_ic; + int max_num_steps_ic; + int max_num_jacobians_ic; + int max_num_iterations_ic; + int max_linesearch_backtracks_ic; + sunbooleantype linesearch_off_ic; + // IDALS linear solver interface + sunbooleantype linear_solution_scaling; + double epsilon_linear_tolerance; + double increment_factor; + explicit SolverOptions(py::dict &py_opts); }; #endif // PYBAMM_OPTIONS_HPP diff --git a/pybamm/solvers/c_solvers/idaklu/idaklu_solver.hpp b/pybamm/solvers/c_solvers/idaklu/idaklu_solver.hpp index a53b167ac4..ce1765aa82 100644 --- a/pybamm/solvers/c_solvers/idaklu/idaklu_solver.hpp +++ b/pybamm/solvers/c_solvers/idaklu/idaklu_solver.hpp @@ -33,9 +33,10 @@ IDAKLUSolver *create_idaklu_solver( const std::vector& var_fcns, const std::vector& dvar_dy_fcns, const std::vector& dvar_dp_fcns, - py::dict options + py::dict py_opts ) { - auto options_cpp = Options(options); + auto setup_opts = SetupOptions(py_opts); + auto solver_opts = SolverOptions(py_opts); auto functions = std::make_unique( rhs_alg, jac_times_cjmass, @@ -55,13 +56,13 @@ IDAKLUSolver *create_idaklu_solver( var_fcns, dvar_dy_fcns, dvar_dp_fcns, - options_cpp + setup_opts ); IDAKLUSolver *idakluSolver = nullptr; // Instantiate solver class - if (options_cpp.linear_solver == "SUNLinSol_Dense") + if (setup_opts.linear_solver == "SUNLinSol_Dense") { DEBUG("\tsetting SUNLinSol_Dense linear solver"); idakluSolver = new IDAKLUSolverOpenMP_Dense( @@ -74,10 +75,11 @@ IDAKLUSolver *create_idaklu_solver( jac_bandwidth_lower, jac_bandwidth_upper, std::move(functions), - options_cpp + setup_opts, + solver_opts ); } - else if (options_cpp.linear_solver == "SUNLinSol_KLU") + else if (setup_opts.linear_solver == "SUNLinSol_KLU") { DEBUG("\tsetting SUNLinSol_KLU linear solver"); idakluSolver = new IDAKLUSolverOpenMP_KLU( @@ -90,10 +92,11 @@ IDAKLUSolver *create_idaklu_solver( jac_bandwidth_lower, jac_bandwidth_upper, std::move(functions), - options_cpp + setup_opts, + solver_opts ); } - else if (options_cpp.linear_solver == "SUNLinSol_Band") + else if (setup_opts.linear_solver == "SUNLinSol_Band") { DEBUG("\tsetting SUNLinSol_Band linear solver"); idakluSolver = new IDAKLUSolverOpenMP_Band( @@ -106,10 +109,11 @@ IDAKLUSolver *create_idaklu_solver( jac_bandwidth_lower, jac_bandwidth_upper, std::move(functions), - options_cpp + setup_opts, + solver_opts ); } - else if (options_cpp.linear_solver == "SUNLinSol_SPBCGS") + else if (setup_opts.linear_solver == "SUNLinSol_SPBCGS") { DEBUG("\tsetting SUNLinSol_SPBCGS_linear solver"); idakluSolver = new IDAKLUSolverOpenMP_SPBCGS( @@ -122,10 +126,11 @@ IDAKLUSolver *create_idaklu_solver( jac_bandwidth_lower, jac_bandwidth_upper, std::move(functions), - options_cpp + setup_opts, + solver_opts ); } - else if (options_cpp.linear_solver == "SUNLinSol_SPFGMR") + else if (setup_opts.linear_solver == "SUNLinSol_SPFGMR") { DEBUG("\tsetting SUNLinSol_SPFGMR_linear solver"); idakluSolver = new IDAKLUSolverOpenMP_SPFGMR( @@ -138,10 +143,11 @@ IDAKLUSolver *create_idaklu_solver( jac_bandwidth_lower, jac_bandwidth_upper, std::move(functions), - options_cpp + setup_opts, + solver_opts ); } - else if (options_cpp.linear_solver == "SUNLinSol_SPGMR") + else if (setup_opts.linear_solver == "SUNLinSol_SPGMR") { DEBUG("\tsetting SUNLinSol_SPGMR solver"); idakluSolver = new IDAKLUSolverOpenMP_SPGMR( @@ -154,10 +160,11 @@ IDAKLUSolver *create_idaklu_solver( jac_bandwidth_lower, jac_bandwidth_upper, std::move(functions), - options_cpp + setup_opts, + solver_opts ); } - else if (options_cpp.linear_solver == "SUNLinSol_SPTFQMR") + else if (setup_opts.linear_solver == "SUNLinSol_SPTFQMR") { DEBUG("\tsetting SUNLinSol_SPGMR solver"); idakluSolver = new IDAKLUSolverOpenMP_SPTFQMR( @@ -170,7 +177,8 @@ IDAKLUSolver *create_idaklu_solver( jac_bandwidth_lower, jac_bandwidth_upper, std::move(functions), - options_cpp + setup_opts, + solver_opts ); } diff --git a/pybamm/solvers/c_solvers/idaklu/sundials_functions.inl b/pybamm/solvers/c_solvers/idaklu/sundials_functions.inl index 532995dfb4..98257275a8 100644 --- a/pybamm/solvers/c_solvers/idaklu/sundials_functions.inl +++ b/pybamm/solvers/c_solvers/idaklu/sundials_functions.inl @@ -161,11 +161,11 @@ int jacobian_eval(realtype tt, realtype cj, N_Vector yy, N_Vector yp, // create pointer to jac data, column pointers, and row values realtype *jac_data; - if (p_python_functions->options.using_sparse_matrix) + if (p_python_functions->setup_opts.using_sparse_matrix) { jac_data = SUNSparseMatrix_Data(JJ); } - else if (p_python_functions->options.using_banded_matrix) { + else if (p_python_functions->setup_opts.using_banded_matrix) { jac_data = p_python_functions->get_tmp_sparse_jacobian_data(); } else @@ -191,7 +191,7 @@ int jacobian_eval(realtype tt, realtype cj, N_Vector yy, N_Vector yp, DEBUG("cj = " << cj); DEBUG_v(jac_data, 100); - if (p_python_functions->options.using_banded_matrix) + if (p_python_functions->setup_opts.using_banded_matrix) { // copy data from temporary matrix to the banded matrix auto jac_colptrs = p_python_functions->jac_times_cjmass_colptrs.data(); @@ -207,7 +207,7 @@ int jacobian_eval(realtype tt, realtype cj, N_Vector yy, N_Vector yp, } } } - else if (p_python_functions->options.using_sparse_matrix) + else if (p_python_functions->setup_opts.using_sparse_matrix) { if (SUNSparseMatrix_SparseType(JJ) == CSC_MAT) { diff --git a/pybamm/solvers/idaklu_solver.py b/pybamm/solvers/idaklu_solver.py index f1f32b1e63..8741a595f8 100644 --- a/pybamm/solvers/idaklu_solver.py +++ b/pybamm/solvers/idaklu_solver.py @@ -75,29 +75,83 @@ class IDAKLUSolver(pybamm.BaseSolver): .. code-block:: python options = { - # print statistics of the solver after every solve + # Print statistics of the solver after every solve "print_stats": False, - # jacobian form, can be "none", "dense", - # "banded", "sparse", "matrix-free" - "jacobian": "sparse", + # Number of threads available for OpenMP + "num_threads": 1, + # Evaluation engine to use for jax, can be 'jax'(native) or 'iree' + "jax_evaluator": "jax", + ## Linear solver interface # name of sundials linear solver to use options are: "SUNLinSol_KLU", # "SUNLinSol_Dense", "SUNLinSol_Band", "SUNLinSol_SPBCGS", # "SUNLinSol_SPFGMR", "SUNLinSol_SPGMR", "SUNLinSol_SPTFQMR", "linear_solver": "SUNLinSol_KLU", - # preconditioner for iterative solvers, can be "none", "BBDP" + # Jacobian form, can be "none", "dense", + # "banded", "sparse", "matrix-free" + "jacobian": "sparse", + # Preconditioner for iterative solvers, can be "none", "BBDP" "preconditioner": "BBDP", - # for iterative linear solvers, max number of iterations - "linsol_max_iterations": 5, - # for iterative linear solver preconditioner, bandwidth of + # For iterative linear solver preconditioner, bandwidth of # approximate jacobian "precon_half_bandwidth": 5, - # for iterative linear solver preconditioner, bandwidth of + # For iterative linear solver preconditioner, bandwidth of # approximate jacobian that is kept "precon_half_bandwidth_keep": 5, - # Number of threads available for OpenMP - "num_threads": 1, - # Evaluation engine to use for jax, can be 'jax'(native) or 'iree' - "jax_evaluator": "jax", + # For iterative linear solvers, max number of iterations + "linsol_max_iterations": 5, + # Ratio between linear and nonlinear tolerances + "epsilon_linear_tolerance": 0.05, + # Increment factor used in DQ Jacobian-vector product approximation + "increment_factor": 1.0, + # Enable or disable linear solution scaling + "linear_solution_scaling": True, + ## Main solver + # Maximum order of the linear multistep method + "max_order_bdf": 5, + # Maximum number of steps to be taken by the solver in its attempt to + # reach the next output time. + # Note: this value differs from the IDA default of 500 + "max_num_steps": 100000, + # Initial step size. The solver default is used if this is left at 0.0 + "dt_init": 0.0, + # Maximum absolute step size. The solver default is used if this is + # left at 0.0 + "dt_max": 0.0, + # Maximum number of error test failures in attempting one step + "max_error_test_failures": 10, + # Maximum number of nonlinear solver iterations at one step + "max_nonlinear_iterations": 4, + # Maximum number of nonlinear solver convergence failures at one step + "max_convergence_failures": 10, + # Safety factor in the nonlinear convergence test + "nonlinear_convergence_coefficient": 0.33, + # Suppress algebraic variables from error test + "suppress_algebraic_error": False, + ## Initial conditions calculation + # Positive constant in the Newton iteration convergence test within the + # initial condition calculation + "nonlinear_convergence_coefficient_ic": 0.0033, + # Maximum number of steps allowed when `init_all_y_ic = False` + "max_num_steps_ic": 5, + # Maximum number of the approximate Jacobian or preconditioner evaluations + # allowed when the Newton iteration appears to be slowly converging + # Note: this value differs from the IDA default of 4 + "max_num_jacobians_ic": 40, + # Maximum number of Newton iterations allowed in any one attempt to solve + # the initial conditions calculation problem + # Note: this value differs from the IDA default of 10 + "max_num_iterations_ic": 100, + # Maximum number of linesearch backtracks allowed in any Newton iteration, + # when solving the initial conditions calculation problem + "max_linesearch_backtracks_ic": 100, + # Turn off linesearch + "linesearch_off_ic": False, + # How to calculate the initial conditions. + # "True": calculate all y0 given ydot0 + # "False": calculate y_alg0 and ydot_diff0 given y_diff0 + "init_all_y_ic": False, + # Calculate consistent initial conditions + "calc_ic": True, } Note: These options only have an effect if model.convert_to_format == 'casadi' @@ -107,7 +161,7 @@ class IDAKLUSolver(pybamm.BaseSolver): def __init__( self, - rtol=1e-6, + rtol=1e-4, atol=1e-6, root_method="casadi", root_tol=1e-6, @@ -120,13 +174,33 @@ def __init__( default_options = { "print_stats": False, "jacobian": "sparse", - "linear_solver": "SUNLinSol_KLU", "preconditioner": "BBDP", - "linsol_max_iterations": 5, "precon_half_bandwidth": 5, "precon_half_bandwidth_keep": 5, "num_threads": 1, "jax_evaluator": "jax", + "linear_solver": "SUNLinSol_KLU", + "linsol_max_iterations": 5, + "epsilon_linear_tolerance": 0.05, + "increment_factor": 1.0, + "linear_solution_scaling": True, + "max_order_bdf": 5, + "max_num_steps": 100000, + "dt_init": 0.0, + "dt_max": 0.0, + "max_error_test_failures": 10, + "max_nonlinear_iterations": 40, + "max_convergence_failures": 100, + "nonlinear_convergence_coefficient": 0.33, + "suppress_algebraic_error": False, + "nonlinear_convergence_coefficient_ic": 0.0033, + "max_num_steps_ic": 5, + "max_num_jacobians_ic": 4, + "max_num_iterations_ic": 10, + "max_linesearch_backtracks_ic": 100, + "linesearch_off_ic": False, + "init_all_y_ic": False, + "calc_ic": True, } if options is None: options = default_options @@ -912,73 +986,71 @@ def _integrate(self, model, t_eval, inputs_dict=None): else: yS_out = False - if sol.flag in [0, 2]: - # 0 = solved for all t_eval - if sol.flag == 0: - termination = "final time" - # 2 = found root(s) - elif sol.flag == 2: - termination = "event" - - newsol = pybamm.Solution( - sol.t, - np.transpose(y_out), - model, - inputs_dict, - np.array([t[-1]]), - np.transpose(y_out[-1])[:, np.newaxis], - termination, - sensitivities=yS_out, - ) - newsol.integration_time = integration_time - if self.output_variables: - # Populate variables and sensititivies dictionaries directly - number_of_samples = sol.y.shape[0] // number_of_timesteps - sol.y = sol.y.reshape((number_of_timesteps, number_of_samples)) - startk = 0 - for _, var in enumerate(self.output_variables): - # ExplicitTimeIntegral's are not computed as part of the solver and - # do not need to be converted - if isinstance( - model.variables_and_events[var], pybamm.ExplicitTimeIntegral - ): - continue - if model.convert_to_format == "casadi": - len_of_var = ( - self._setup["var_fcns"][var](0.0, 0.0, 0.0).sparsity().nnz() - ) - base_variables = [self._setup["var_fcns"][var]] - elif ( - model.convert_to_format == "jax" - and self._options["jax_evaluator"] == "iree" - ): - idx = self.output_variables.index(var) - len_of_var = self._setup["var_idaklu_fcns"][idx].nnz - base_variables = [self._setup["var_idaklu_fcns"][idx]] - else: # pragma: no cover - raise pybamm.SolverError( - "Unsupported evaluation engine for convert_to_format=" - + f"{model.convert_to_format} " - + f"(jax_evaluator={self._options['jax_evaluator']})" - ) - newsol._variables[var] = pybamm.ProcessedVariableComputed( - [model.variables_and_events[var]], - base_variables, - [sol.y[:, startk : (startk + len_of_var)]], - newsol, - ) - # Add sensitivities - newsol[var]._sensitivities = {} - if model.calculate_sensitivities: - for paramk, param in enumerate(inputs_dict.keys()): - newsol[var].add_sensitivity( - param, - [sol.yS[:, startk : (startk + len_of_var), paramk]], - ) - startk += len_of_var - return newsol + # 0 = solved for all t_eval + if sol.flag == 0: + termination = "final time" + # 2 = found root(s) + elif sol.flag == 2: + termination = "event" else: raise pybamm.SolverError("idaklu solver failed") + newsol = pybamm.Solution( + sol.t, + np.transpose(y_out), + model, + inputs_dict, + np.array([t[-1]]), + np.transpose(y_out[-1])[:, np.newaxis], + termination, + sensitivities=yS_out, + ) + newsol.integration_time = integration_time + if self.output_variables: + # Populate variables and sensititivies dictionaries directly + number_of_samples = sol.y.shape[0] // number_of_timesteps + sol.y = sol.y.reshape((number_of_timesteps, number_of_samples)) + startk = 0 + for var in self.output_variables: + # ExplicitTimeIntegral's are not computed as part of the solver and + # do not need to be converted + if isinstance( + model.variables_and_events[var], pybamm.ExplicitTimeIntegral + ): + continue + if model.convert_to_format == "casadi": + len_of_var = ( + self._setup["var_fcns"][var](0.0, 0.0, 0.0).sparsity().nnz() + ) + base_variables = [self._setup["var_fcns"][var]] + elif ( + model.convert_to_format == "jax" + and self._options["jax_evaluator"] == "iree" + ): + idx = self.output_variables.index(var) + len_of_var = self._setup["var_idaklu_fcns"][idx].nnz + base_variables = [self._setup["var_idaklu_fcns"][idx]] + else: # pragma: no cover + raise pybamm.SolverError( + "Unsupported evaluation engine for convert_to_format=" + + f"{model.convert_to_format} " + + f"(jax_evaluator={self._options['jax_evaluator']})" + ) + newsol._variables[var] = pybamm.ProcessedVariableComputed( + [model.variables_and_events[var]], + base_variables, + [sol.y[:, startk : (startk + len_of_var)]], + newsol, + ) + # Add sensitivities + newsol[var]._sensitivities = {} + if model.calculate_sensitivities: + for paramk, param in enumerate(inputs_dict.keys()): + newsol[var].add_sensitivity( + param, + [sol.yS[:, startk : (startk + len_of_var), paramk]], + ) + startk += len_of_var + return newsol def jaxify( self, diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_tests.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_tests.py index 4db5ddea61..1fff91b476 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_tests.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_tests.py @@ -20,10 +20,12 @@ def test_basic_processing(self): def test_sensitivities(self): model = self.model() param = pybamm.ParameterValues("Ecker2015") + rtol = 1e-6 + atol = 1e-6 if pybamm.have_idaklu(): - solver = pybamm.IDAKLUSolver() + solver = pybamm.IDAKLUSolver(rtol=rtol, atol=atol) else: - solver = pybamm.CasadiSolver() + solver = pybamm.CasadiSolver(rtol=rtol, atol=atol) modeltest = tests.StandardModelTest( model, parameter_values=param, solver=solver ) diff --git a/tests/unit/test_solvers/test_idaklu_solver.py b/tests/unit/test_solvers/test_idaklu_solver.py index a80ab74b9e..562dde27a5 100644 --- a/tests/unit/test_solvers/test_idaklu_solver.py +++ b/tests/unit/test_solvers/test_idaklu_solver.py @@ -241,6 +241,8 @@ def test_sensitivities_initial_condition(self): disc = pybamm.Discretisation() disc.process_model(model) solver = pybamm.IDAKLUSolver( + rtol=1e-6, + atol=1e-6, root_method=root_method, output_variables=output_variables, options={"jax_evaluator": "iree"} if form == "iree" else {}, @@ -539,7 +541,7 @@ def test_banded(self): np.testing.assert_array_almost_equal(soln.y, soln_banded.y, 5) - def test_options(self): + def test_setup_options(self): model = pybamm.BaseModel() u = pybamm.Variable("u") v = pybamm.Variable("v") @@ -584,8 +586,13 @@ def test_options(self): "jacobian": jacobian, "linear_solver": linear_solver, "preconditioner": precon, + "max_num_steps": 10000, } - solver = pybamm.IDAKLUSolver(options=options) + solver = pybamm.IDAKLUSolver( + atol=1e-8, + rtol=1e-8, + options=options, + ) if ( jacobian == "none" and (linear_solver == "SUNLinSol_Dense") @@ -614,6 +621,70 @@ def test_options(self): with self.assertRaises(ValueError): soln = solver.solve(model, t_eval) + def test_solver_options(self): + model = pybamm.BaseModel() + u = pybamm.Variable("u") + v = pybamm.Variable("v") + model.rhs = {u: -0.1 * u} + model.algebraic = {v: v - u} + model.initial_conditions = {u: 1, v: 1} + disc = pybamm.Discretisation() + disc.process_model(model) + + t_eval = np.linspace(0, 1) + solver = pybamm.IDAKLUSolver() + soln_base = solver.solve(model, t_eval) + + options_success = { + "max_order_bdf": 4, + "max_num_steps": 490, + "dt_init": 0.01, + "dt_max": 1000.9, + "max_error_test_failures": 11, + "max_nonlinear_iterations": 5, + "max_convergence_failures": 11, + "nonlinear_convergence_coefficient": 1.0, + "suppress_algebraic_error": True, + "nonlinear_convergence_coefficient_ic": 0.01, + "max_num_steps_ic": 6, + "max_num_jacobians_ic": 5, + "max_num_iterations_ic": 11, + "max_linesearch_backtracks_ic": 101, + "linesearch_off_ic": True, + "init_all_y_ic": False, + "linear_solver": "SUNLinSol_KLU", + "linsol_max_iterations": 6, + "epsilon_linear_tolerance": 0.06, + "increment_factor": 0.99, + "linear_solution_scaling": False, + } + + # test everything works + for option in options_success: + options = {option: options_success[option]} + solver = pybamm.IDAKLUSolver(rtol=1e-6, atol=1e-6, options=options) + soln = solver.solve(model, t_eval) + + np.testing.assert_array_almost_equal(soln.y, soln_base.y, 5) + + options_fail = { + "max_order_bdf": -1, + "max_num_steps_ic": -1, + "max_num_jacobians_ic": -1, + "max_num_iterations_ic": -1, + "max_linesearch_backtracks_ic": -1, + "epsilon_linear_tolerance": -1.0, + "increment_factor": -1.0, + } + + # test that the solver throws a warning + for option in options_fail: + options = {option: options_fail[option]} + solver = pybamm.IDAKLUSolver(options=options) + + with self.assertRaises(ValueError): + solver.solve(model, t_eval) + def test_with_output_variables(self): # Construct a model and solve for all variables, then test # the 'output_variables' option for each variable in turn, confirming From 1d409568a41f5caabdbe2bfd89114d2a569ead4c Mon Sep 17 00:00:00 2001 From: "Eric G. Kratz" Date: Thu, 25 Jul 2024 09:32:09 -0400 Subject: [PATCH 30/82] Remove mentions of 3.8 (#4291) --- docs/source/user_guide/installation/gnu-linux-mac.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/user_guide/installation/gnu-linux-mac.rst b/docs/source/user_guide/installation/gnu-linux-mac.rst index 121c6df437..7e69afa839 100644 --- a/docs/source/user_guide/installation/gnu-linux-mac.rst +++ b/docs/source/user_guide/installation/gnu-linux-mac.rst @@ -6,7 +6,7 @@ GNU/Linux & macOS Prerequisites ------------- -To use PyBaMM, you must have Python 3.8, 3.9, 3.10, 3.11, or 3.12 installed. +To use PyBaMM, you must have Python 3.9, 3.10, 3.11, or 3.12 installed. .. tab:: Debian-based distributions (Debian, Ubuntu) @@ -43,7 +43,7 @@ User install We recommend to install PyBaMM within a virtual environment, in order not to alter any distribution Python files. -First, make sure you are using Python 3.8, 3.9, 3.10, 3.11, or 3.12. +First, make sure you are using Python 3.9, 3.10, 3.11, or 3.12. To create a virtual environment ``env`` within your current directory type: .. code:: bash From 91c97448d1127534c4d06bc68e2a769c9be4e7ff Mon Sep 17 00:00:00 2001 From: Robert Timms <43040151+rtimms@users.noreply.github.com> Date: Thu, 25 Jul 2024 15:30:19 -0700 Subject: [PATCH 31/82] fix bug in composite surface form model (#4293) --- .../composite_surface_form_conductivity.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pybamm/models/submodels/electrolyte_conductivity/surface_potential_form/composite_surface_form_conductivity.py b/pybamm/models/submodels/electrolyte_conductivity/surface_potential_form/composite_surface_form_conductivity.py index 73aa301011..3b701ab2ac 100644 --- a/pybamm/models/submodels/electrolyte_conductivity/surface_potential_form/composite_surface_form_conductivity.py +++ b/pybamm/models/submodels/electrolyte_conductivity/surface_potential_form/composite_surface_form_conductivity.py @@ -99,6 +99,10 @@ def __init__(self, param, domain, options=None): def set_rhs(self, variables): domain = self.domain + a = variables[ + f"X-averaged {domain} electrode surface area to volume ratio [m-1]" + ] + sum_a_j = variables[ f"Sum of x-averaged {domain} electrode volumetric " "interfacial current densities [A.m-3]" @@ -116,7 +120,7 @@ def set_rhs(self, variables): C_dl = self.domain_param.C_dl(T) - self.rhs[delta_phi] = 1 / C_dl * (sum_a_j_av - sum_a_j) + self.rhs[delta_phi] = 1 / (a * C_dl) * (sum_a_j_av - sum_a_j) class CompositeAlgebraic(BaseModel): From e8da2857583cba0dfa93f5a5854cc69fccb256ea Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Sat, 27 Jul 2024 00:20:16 +0530 Subject: [PATCH 32/82] Set date to 2024-07-26 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a739834290..a09b2dfe67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ - Added additional user-configurable options to the (`IDAKLUSolver`) and adjusted the default values to improve performance. ([#4282](https://github.com/pybamm-team/PyBaMM/pull/4282)) -# [v24.5](https://github.com/pybamm-team/PyBaMM/tree/v24.5) - 2024-07-31 +# [v24.5](https://github.com/pybamm-team/PyBaMM/tree/v24.5) - 2024-07-26 ## Features From 48b4e48d776ad2c254e5fae8db94706c57a8cc38 Mon Sep 17 00:00:00 2001 From: Pradyot Ranjan <99216956+prady0t@users.noreply.github.com> Date: Sat, 27 Jul 2024 00:45:08 +0530 Subject: [PATCH 33/82] Removing `run-tests.py` file. (#4180) * Removing run-test.py file Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * Removing redundent files Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * Removing redundent files Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * style: pre-commit fixes * Update test_examples.py * Update noxfile.py Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> * Removing run-test.py file Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * Removing redundent files Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * Removing redundent files Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * Removing example marker Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * adding conftest.py file Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * adding back changes Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * style: pre-commit fixes * Update test_examples.py * style: pre-commit fixes * Removing run-test.py file Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * Removing redundent files Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * Removing redundent files Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * style: pre-commit fixes * Update test_examples.py * Update noxfile.py Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> * adding back changes Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * removing loop for file discovery Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * removing loop for file discovery Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * Adding more flags to pytest Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * style: pre-commit fixes * Adding markers for unit and integration tests Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * Adding markers for unit and integration tests Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * style: pre-commit fixes * Added back assertDomainEqual Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * style: pre-commit fixes * Added fixture for debug mode Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * style: pre-commit fixes * Removed style failures Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * Removed style failures Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * Update conftest.py Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> * style: pre-commit fixes * Update conftest.py Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> * Update noxfile.py Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> * Update pyproject.toml Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> * Update tests/testcase.py Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> * Update tests/test_scripts.py Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> * Update tests/test_scripts.py Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> * style: pre-commit fixes * using -m instead of args Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * removed redundent files Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * style: pre-commit fixes * fixing style failure Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * style: pre-commit fixes * adding documentation changes Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * Update CONTRIBUTING.md Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> * Update CONTRIBUTING.md Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> * adding suggestions Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * removing -s flag and getting file names in scripts log Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * Update pyproject.toml Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> * Update CONTRIBUTING.md Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> --------- Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> Co-authored-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Co-authored-by: Arjun Verma Co-authored-by: Eric G. Kratz --- CONTRIBUTING.md | 20 +-- conftest.py | 46 +++++++ noxfile.py | 22 ++-- pyproject.toml | 9 +- run-tests.py | 276 ------------------------------------------ tests/test_scripts.py | 22 ++++ tests/testcase.py | 46 +------ 7 files changed, 103 insertions(+), 338 deletions(-) create mode 100644 conftest.py delete mode 100755 run-tests.py create mode 100644 tests/test_scripts.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 450a061d39..f0f2274080 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -149,9 +149,13 @@ The `test_optional_dependencies` function extracts `pybamm` mandatory distributi ## Testing -All code requires testing. We use the [unittest](https://docs.python.org/3.3/library/unittest.html) package for our tests. (These tests typically just check that the code runs without error, and so, are more _debugging_ than _testing_ in a strict sense. Nevertheless, they are very useful to have!) +All code requires testing. We use the [pytest](https://docs.pytest.org/en/stable/) package for our tests. (These tests typically just check that the code runs without error, and so, are more _debugging_ than _testing_ in a strict sense. Nevertheless, they are very useful to have!) -We also use [pytest](https://docs.pytest.org/en/latest/) along with the [nbmake](https://github.com/treebeardtech/nbmake) and the [pytest-xdist](https://pypi.org/project/pytest-xdist/) plugins to test the example notebooks. +We use following plugins for various needs: + +[nbmake](https://github.com/treebeardtech/nbmake)) : plugins to test the example notebooks. + +[pytest-xdist](https://pypi.org/project/pytest-xdist/)) : plugins to run tests in parallel. If you have `nox` installed, to run unit tests, type @@ -162,14 +166,14 @@ nox -s unit else, type ```bash -python run-tests.py --unit +pytest -m unit ``` ### Writing tests Every new feature should have its own test. To create ones, have a look at the `test` directory and see if there's a test for a similar method. Copy-pasting this is a good way to start. -Next, add some simple (and speedy!) tests of your main features. If these run without exceptions that's a good start! Next, check the output of your methods using any of these [assert methods](https://docs.python.org/3.3/library/unittest.html#assert-methods). +Next, add some simple (and speedy!) tests of your main features. If these run without exceptions that's a good start! Next, check the output of your methods using [assert statements](https://docs.pytest.org/en/7.1.x/how-to/assert.html). ### Running more tests @@ -193,7 +197,7 @@ nox -s examples Alternatively, you may use `pytest` directly with the `--nbmake` flag: ```bash -pytest --nbmake +pytest --nbmake docs/source/examples/ ``` which runs all the notebooks in the `docs/source/examples/notebooks/` folder in parallel by default, using the `pytest-xdist` plugin. @@ -245,19 +249,19 @@ This also means that, if you can't fix the bug yourself, it will be much easier 1. Run individual test scripts instead of the whole test suite: ```bash - python tests/unit/path/to/test + pytest tests/unit/path/to/test ``` You can also run an individual test from a particular script, e.g. ```bash - python tests/unit/test_quick_plot.py TestQuickPlot.test_failure + pytest tests/unit/test_plotting/test_quick_plot.py::TestQuickPlot::test_simple_ode_model ``` If you want to run several, but not all, the tests from a script, you can restrict which tests are run from a particular script by using the skipping decorator: ```python - @unittest.skip("") + @pytest.mark.skip("") def test_bit_of_code(self): ... ``` diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000000..90439f910b --- /dev/null +++ b/conftest.py @@ -0,0 +1,46 @@ +import pytest +import numpy as np +import pybamm + + +def pytest_addoption(parser): + parser.addoption( + "--scripts", + action="store_true", + default=False, + help="execute the example scripts", + ) + parser.addoption( + "--unit", action="store_true", default=False, help="run unit tests" + ) + parser.addoption( + "--integration", + action="store_true", + default=False, + help="run integration tests", + ) + + +def pytest_configure(config): + config.addinivalue_line("markers", "scripts: mark test as an example script") + config.addinivalue_line("markers", "unit: mark test as a unit test") + config.addinivalue_line("markers", "integration: mark test as an integration test") + + +def pytest_collection_modifyitems(items): + for item in items: + if "unit" in item.nodeid: + item.add_marker(pytest.mark.unit) + elif "integration" in item.nodeid: + item.add_marker(pytest.mark.integration) + + +@pytest.fixture(autouse=True) +# Set the random seed to 42 for all tests +def set_random_seed(): + np.random.seed(42) + + +@pytest.fixture(autouse=True) +def set_debug_value(): + pybamm.settings.debug_mode = True diff --git a/noxfile.py b/noxfile.py index 373b77f71f..28e16dfa93 100644 --- a/noxfile.py +++ b/noxfile.py @@ -54,9 +54,9 @@ def set_iree_state(): homedir = os.getenv("HOME") PYBAMM_ENV = { - "SUNDIALS_INST": f"{homedir}/.local", "LD_LIBRARY_PATH": f"{homedir}/.local/lib", "PYTHONIOENCODING": "utf-8", + "MPLBACKEND": "Agg", # Expression evaluators (...EXPR_CASADI cannot be fully disabled at this time) "PYBAMM_IDAKLU_EXPR_CASADI": os.getenv("PYBAMM_IDAKLU_EXPR_CASADI", "ON"), "PYBAMM_IDAKLU_EXPR_IREE": set_iree_state(), @@ -156,7 +156,7 @@ def run_integration(session): set_environment_variables(PYBAMM_ENV, session=session) session.install("setuptools", silent=False) session.install("-e", ".[all,dev,jax]", silent=False) - session.run("python", "run-tests.py", "--integration") + session.run("python", "-m", "pytest", "-m", "integration") @nox.session(name="doctests") @@ -166,7 +166,13 @@ def run_doctests(session): # See: https://bitbucket.org/pybtex-devs/pybtex/issues/169/ session.install("setuptools", silent=False) session.install("-e", ".[all,dev,docs]", silent=False) - session.run("python", "run-tests.py", "--doctest") + session.run( + "python", + "-m", + "pytest", + "--doctest-plus", + "pybamm", + ) @nox.session(name="unit") @@ -184,7 +190,7 @@ def run_unit(session): PYBAMM_ENV.get("IREE_INDEX_URL"), silent=False, ) - session.run("python", "run-tests.py", "--unit") + session.run("python", "-m", "pytest", "-m", "unit") @nox.session(name="examples") @@ -194,7 +200,9 @@ def run_examples(session): session.install("setuptools", silent=False) session.install("-e", ".[all,dev]", silent=False) notebooks_to_test = session.posargs if session.posargs else [] - session.run("pytest", "--nbmake", *notebooks_to_test, external=True) + session.run( + "pytest", "--nbmake", *notebooks_to_test, "docs/source/examples/", external=True + ) @nox.session(name="scripts") @@ -206,7 +214,7 @@ def run_scripts(session): # is fixed session.install("setuptools", silent=False) session.install("-e", ".[all,dev]", silent=False) - session.run("python", "run-tests.py", "--scripts") + session.run("python", "-m", "pytest", "-m", "scripts") @nox.session(name="dev") @@ -249,7 +257,7 @@ def run_tests(session): set_environment_variables(PYBAMM_ENV, session=session) session.install("setuptools", silent=False) session.install("-e", ".[all,dev,jax]", silent=False) - session.run("python", "run-tests.py", "--all") + session.run("python", "-m", "pytest", "-m", "unit or integration") @nox.session(name="docs") diff --git a/pyproject.toml b/pyproject.toml index 2bc6f4f3d7..496942c0e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -228,20 +228,21 @@ ignore = [ "docs/source/examples/notebooks/models/lithium-plating.ipynb" = ["F821"] [tool.pytest.ini_options] -minversion = "6" +minversion = "8" +# Use pytest-xdist to run tests in parallel by default, exit with +# error if not installed required_plugins = [ "pytest-xdist", "pytest-mock", ] addopts = [ "-nauto", - "-v", - "-ra", + "-vra", "--strict-config", "--strict-markers", ] testpaths = [ - "docs/source/examples/", + "tests", ] console_output_style = "progress" xfail_strict = true diff --git a/run-tests.py b/run-tests.py deleted file mode 100755 index 0b29ed2fc7..0000000000 --- a/run-tests.py +++ /dev/null @@ -1,276 +0,0 @@ -#!/usr/bin/env python -# -# Runs all unit tests included in PyBaMM. -# -# The code in this file is adapted from Pints -# (see https://github.com/pints-team/pints) -# -import os -import pybamm -import sys -import argparse -import subprocess -import pytest - - -def run_code_tests(executable=False, folder: str = "unit", interpreter="python"): - """ - Runs tests, exits if they don't finish. - Parameters - ---------- - executable : bool (default False) - If True, tests are run in subprocesses using the executable 'python'. - Must be True for travis tests (otherwise tests always 'pass') - folder : str - Which folder to run the tests from (unit, integration or both ('all')) - """ - if folder == "all": - tests = "tests/" - else: - tests = "tests/" + folder - if folder == "unit": - pybamm.settings.debug_mode = True - if interpreter == "python": - # Make sure to refer to the interpreter for the - # currently activated virtual environment - interpreter = sys.executable - if executable is False: - ret = pytest.main(["-v", tests]) - else: - print(f"Running {folder} tests with executable {interpreter}") - cmd = [interpreter, "-m", "pytest", "-v", tests] - p = subprocess.Popen(cmd) - try: - ret = p.wait() - except KeyboardInterrupt: - try: - p.terminate() - except OSError: - pass - p.wait() - print("") - sys.exit(1) - - if ret != 0: - sys.exit(ret) - - -def run_doc_tests(): - """ - Checks if the documentation can be built, runs any doctests (currently not - used). - """ - print("Checking for doctests.") - try: - subprocess.run( - [ - f"{sys.executable}", - "-m", - "pytest", - "--doctest-plus", - "pybamm", - ], - check=True, - ) - except subprocess.CalledProcessError as e: - print(f"FAILED with exit code {e.returncode}") - sys.exit(e.returncode) - - -def run_scripts(executable="python"): - """ - Run example scripts tests. Exits if they fail. - """ - - # Scan and run - print("Testing scripts with executable `" + str(executable) + "`") - - # Test scripts in examples - # TODO: add scripts to docs/source/examples - if not scan_for_scripts("examples", True, executable): - print("\nErrors encountered in scripts") - sys.exit(1) - print("\nOK") - - -def scan_for_scripts(root, recursive=True, executable="python"): - """ - Scans for, and tests, all scripts in a directory. - """ - ok = True - debug = False - - # Scan path - for filename in os.listdir(root): - path = os.path.join(root, filename) - - # Recurse into subdirectories - if recursive and os.path.isdir(path): - # Ignore hidden directories - if filename[:1] == ".": - continue - ok &= scan_for_scripts(path, recursive, executable) - - # Test scripts - elif os.path.splitext(path)[1] == ".py": - if debug: - print(path) - else: - ok &= test_script(path, executable) - - # Return True if every script is ok - return ok - - -def test_script(path, executable="python"): - """ - Tests a single script, exits if it doesn't finish. - """ - import pybamm - - b = pybamm.Timer() - print("Test " + path + " ... ", end="") - sys.stdout.flush() - - # Tell matplotlib not to produce any figures - env = dict(os.environ) - env["MPLBACKEND"] = "Agg" - - # Run in subprocess - cmd = [executable, path] - try: - p = subprocess.Popen( - cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env - ) - stdout, stderr = p.communicate() - # TODO: Use p.communicate(timeout=3600) if Python3 only - if p.returncode != 0: - # Show failing code, output and errors before returning - print("ERROR") - print("-- stdout " + "-" * (79 - 10)) - print(str(stdout, "utf-8")) - print("-- stderr " + "-" * (79 - 10)) - print(str(stderr, "utf-8")) - print("-" * 79) - return False - except KeyboardInterrupt: - p.terminate() - print("ABORTED") - sys.exit(1) - - # Sucessfully run - print(f"ok ({b.time()})") - return True - - -if __name__ == "__main__": - # Set up argument parsing - parser = argparse.ArgumentParser( - description="Run unit tests for PyBaMM.", - epilog="To run individual unit tests, use e.g. '$ tests/unit/test_timer.py'", - ) - - # Unit tests - parser.add_argument( - "--integration", - action="store_true", - help="Run integration tests using the python interpreter.", - ) - parser.add_argument( - "--unit", - action="store_true", - help="Run unit tests using the `python` interpreter.", - ) - parser.add_argument( - "--all", - action="store_true", - help="Run all tests (unit and integration) using the `python` interpreter.", - ) - parser.add_argument( - "--nosub", - action="store_true", - help="Run unit tests without starting a subprocess.", - ) - # Example notebooks tests - parser.add_argument( - "--examples", - action="store_true", - help="Test all Jupyter notebooks in `docs/source/examples/` (deprecated, use nox or pytest instead).", - ) - parser.add_argument( - "--debook", - nargs=2, - metavar=("in", "out"), - help="Export a Jupyter notebook to a Python file for manual testing.", - ) - # Scripts tests - parser.add_argument( - "--scripts", - action="store_true", - help="Test all example scripts in `examples/`.", - ) - # Doctests - parser.add_argument( - "--doctest", - action="store_true", - help="Run any doctests, check if docs can be built", - ) - # Combined test sets - parser.add_argument( - "--quick", - action="store_true", - help="Run quick checks (code tests, docs)", - ) - # Non-standard Python interpreter name for subprocesses - parser.add_argument( - "--interpreter", - nargs="?", - default="python", - metavar="python", - help="Give the name of the Python interpreter if it is not 'python'", - ) - # Parse! - args = parser.parse_args() - - # Run tests - has_run = False - # Unit vs integration - interpreter = args.interpreter - # Unit tests - if args.integration: - has_run = True - run_code_tests(True, "integration", interpreter) - if args.unit: - has_run = True - run_code_tests(True, "unit", interpreter) - if args.all: - has_run = True - run_code_tests(True, "all", interpreter) - if args.nosub: - has_run = True - run_code_tests(folder="unit", interpreter=interpreter) - # Doctests - if args.doctest: - has_run = True - run_doc_tests() - # Notebook tests (deprecated) - elif args.examples: - raise ValueError( - "Notebook tests are deprecated, use nox -s examples or pytest instead" - ) - if args.debook: - raise ValueError( - "Notebook tests are deprecated, use nox -s examples or pytest instead" - ) - # Scripts tests - elif args.scripts: - has_run = True - run_scripts(interpreter) - # Combined test sets - if args.quick: - has_run = True - run_code_tests("all", interpreter=interpreter) - run_doc_tests() - # Help - if not has_run: - parser.print_help() diff --git a/tests/test_scripts.py b/tests/test_scripts.py new file mode 100644 index 0000000000..89255b17ff --- /dev/null +++ b/tests/test_scripts.py @@ -0,0 +1,22 @@ +import runpy + +import pytest +from pathlib import Path + + +ROOT_DIR = Path(__file__).parent.parent + + +class TestExamples: + """ + A class to test the example scripts. + """ + + def list_of_files(): + file_list = (ROOT_DIR / "examples" / "scripts").rglob("*.py") + return [pytest.param(file, id=file.name) for file in file_list] + + @pytest.mark.parametrize("files", list_of_files()) + @pytest.mark.scripts + def test_example_scripts(self, files): + runpy.run_path(files) diff --git a/tests/testcase.py b/tests/testcase.py index ae4019bcb3..0b9b1f5dee 100644 --- a/tests/testcase.py +++ b/tests/testcase.py @@ -2,52 +2,12 @@ # Custom TestCase class for pybamm # import unittest -import hashlib -from functools import wraps -from types import FunctionType -import numpy as np -def FixRandomSeed(method): +class TestCase(unittest.TestCase): """ - Wraps a method so that the random seed is set to a hash of the method name - - As the wrapper fixes the random seed before calling the method, tests can - explicitly reinstate the random seed within their method bodies as desired, - e.g. by calling np.random.seed(None) to restore normal behaviour. - - Generating a random seed from the method name allows particularly awkward - sequences to be altered by changing the method name, such as by adding a - trailing underscore, or other hash modifier, if required. - """ - - @wraps(method) - def wrapped(*args, **kwargs): - np.random.seed( - int(hashlib.sha256(method.__name__.encode()).hexdigest(), 16) % (2**32) - ) - return method(*args, **kwargs) - - return wrapped - - -class MakeAllTestsDeterministic(type): - """ - Metaclass that wraps all class methods with FixRandomSeed() - """ - - def __new__(meta, classname, bases, classDict): - newClassDict = {} - for attributeName, attribute in classDict.items(): - if isinstance(attribute, FunctionType): - attribute = FixRandomSeed(attribute) - newClassDict[attributeName] = attribute - return type.__new__(meta, classname, bases, newClassDict) - - -class TestCase(unittest.TestCase, metaclass=MakeAllTestsDeterministic): - """ - Custom TestCase class for pybamm + Custom TestCase class for PyBaMM + TO BE REMOVED """ def assertDomainEqual(self, a, b): From b7da8f1d82a2e70cac9cced341989fc3fc158ffd Mon Sep 17 00:00:00 2001 From: "Eric G. Kratz" Date: Fri, 26 Jul 2024 15:19:33 -0400 Subject: [PATCH 34/82] Update CHANGELOG.md --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a09b2dfe67..4b0df2786c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - Updates multiprocess `Pool` in `BaseSolver.solve()` to be constructed with context `fork`. Adds small example for multiprocess inputs. ([#3974](https://github.com/pybamm-team/PyBaMM/pull/3974)) - Lithium plating now works on composite electrodes ([#3919](https://github.com/pybamm-team/PyBaMM/pull/3919)) - Added lithium plating parameters to `Ecker2015` and `Ecker2015_graphite_halfcell` parameter sets ([#3919](https://github.com/pybamm-team/PyBaMM/pull/3919)) +- Added a JAX interface to the IDAKLU solver ([#3658](https://github.com/pybamm-team/PyBaMM/pull/3658)) - Added custom experiment steps ([#3835](https://github.com/pybamm-team/PyBaMM/pull/3835)) - MSMR open-circuit voltage model now depends on the temperature ([#3832](https://github.com/pybamm-team/PyBaMM/pull/3832)) - Added support for macOS arm64 (M-series) platforms. ([#3789](https://github.com/pybamm-team/PyBaMM/pull/3789)) @@ -29,7 +30,6 @@ - Added `WyciskOpenCircuitPotential` for differential capacity hysteresis state open-circuit potential submodel ([#3593](https://github.com/pybamm-team/PyBaMM/pull/3593)) - Transport efficiency submodel has new options from the literature relating to different tortuosity factor models and also a new option called "tortuosity factor" for specifying the value or function directly as parameters ([#3437](https://github.com/pybamm-team/PyBaMM/pull/3437)) - Heat of mixing source term can now be included into thermal models ([#2837](https://github.com/pybamm-team/PyBaMM/pull/2837)) -- Added a JAX interface to the IDAKLU solver ([#3658](https://github.com/pybamm-team/PyBaMM/pull/3658)) ## Bug Fixes @@ -69,8 +69,8 @@ - Renamed "have_optional_dependency" to "import_optional_dependency" ([#3866](https://github.com/pybamm-team/PyBaMM/pull/3866)) - Integrated the `[latexify]` extra into the core PyBaMM package, deprecating the `pybamm[latexify]` set of optional dependencies. SymPy is now a required dependency and will be installed upon installing PyBaMM ([#3848](https://github.com/pybamm-team/PyBaMM/pull/3848)) - Renamed "testing" argument for plots to "show_plot" and flipped its meaning (show_plot=True is now the default and shows the plot) ([#3842](https://github.com/pybamm-team/PyBaMM/pull/3842)) -- Dropped support for BPX version 0.3.0 and below ([#3414](https://github.com/pybamm-team/PyBaMM/pull/3414)) - The function `get_spatial_var` in `pybamm.QuickPlot.py` is made private. ([#3755](https://github.com/pybamm-team/PyBaMM/pull/3755)) +- Dropped support for BPX version 0.3.0 and below ([#3414](https://github.com/pybamm-team/PyBaMM/pull/3414)) # [v24.1](https://github.com/pybamm-team/PyBaMM/tree/v24.1) - 2024-01-31 From ce24332bac19d6dd3edfc6b888724106770acb03 Mon Sep 17 00:00:00 2001 From: "Eric G. Kratz" Date: Fri, 26 Jul 2024 15:20:50 -0400 Subject: [PATCH 35/82] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b0df2786c..044c85609b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,11 +18,11 @@ - Updates multiprocess `Pool` in `BaseSolver.solve()` to be constructed with context `fork`. Adds small example for multiprocess inputs. ([#3974](https://github.com/pybamm-team/PyBaMM/pull/3974)) - Lithium plating now works on composite electrodes ([#3919](https://github.com/pybamm-team/PyBaMM/pull/3919)) - Added lithium plating parameters to `Ecker2015` and `Ecker2015_graphite_halfcell` parameter sets ([#3919](https://github.com/pybamm-team/PyBaMM/pull/3919)) -- Added a JAX interface to the IDAKLU solver ([#3658](https://github.com/pybamm-team/PyBaMM/pull/3658)) - Added custom experiment steps ([#3835](https://github.com/pybamm-team/PyBaMM/pull/3835)) - MSMR open-circuit voltage model now depends on the temperature ([#3832](https://github.com/pybamm-team/PyBaMM/pull/3832)) - Added support for macOS arm64 (M-series) platforms. ([#3789](https://github.com/pybamm-team/PyBaMM/pull/3789)) - Added the ability to specify a custom solver tolerance in `get_initial_stoichiometries` and related functions ([#3714](https://github.com/pybamm-team/PyBaMM/pull/3714)) +- Added a JAX interface to the IDAKLU solver ([#3658](https://github.com/pybamm-team/PyBaMM/pull/3658)) - Modified `step` function to take an array of time `t_eval` as an argument and deprecated use of `npts`. ([#3627](https://github.com/pybamm-team/PyBaMM/pull/3627)) - Renamed "electrode diffusivity" to "particle diffusivity" as a non-breaking change with a deprecation warning ([#3624](https://github.com/pybamm-team/PyBaMM/pull/3624)) - Add support for BPX version 0.4.0 which allows for blended electrodes and user-defined parameters in BPX([#3414](https://github.com/pybamm-team/PyBaMM/pull/3414)) From e05b45557ea4a46f232beaf3cd7da428a240dc63 Mon Sep 17 00:00:00 2001 From: Pradyot Ranjan <99216956+prady0t@users.noreply.github.com> Date: Sun, 28 Jul 2024 19:36:59 +0530 Subject: [PATCH 36/82] Adding a plugin to annotate pytest failures on GitHub (#4294) * Adding a plugin to annotate pytest failures on GitHub Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * style: pre-commit fixes --------- Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> Co-authored-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Arjun Verma --- noxfile.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/noxfile.py b/noxfile.py index 28e16dfa93..c1510379d1 100644 --- a/noxfile.py +++ b/noxfile.py @@ -137,6 +137,9 @@ def run_coverage(session): set_environment_variables(PYBAMM_ENV, session=session) session.install("setuptools", silent=False) session.install("coverage", silent=False) + # Using plugin here since coverage runs unit tests on linux with latest python version. + if "CI" in os.environ: + session.install("pytest-github-actions-annotate-failures") session.install("-e", ".[all,dev,jax]", silent=False) if PYBAMM_ENV.get("PYBAMM_IDAKLU_EXPR_IREE") == "ON": # See comments in 'dev' session @@ -155,6 +158,12 @@ def run_integration(session): """Run the integration tests.""" set_environment_variables(PYBAMM_ENV, session=session) session.install("setuptools", silent=False) + if ( + "CI" in os.environ + and sys.version_info[:2] == (3, 12) + and sys.platform == "linux" + ): + session.install("pytest-github-actions-annotate-failures") session.install("-e", ".[all,dev,jax]", silent=False) session.run("python", "-m", "pytest", "-m", "integration") From 238c2ad9bc740b637ee1b12df77afeae223b6e00 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 29 Jul 2024 20:56:09 +0000 Subject: [PATCH 37/82] chore: update pre-commit hooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.5.4 → v0.5.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.4...v0.5.5) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6b2f300f38..dc4c7b1c34 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.4" + rev: "v0.5.5" hooks: - id: ruff args: [--fix, --show-fixes] From 5408ce3063a9d0f0e2a21c9deafc003703e531f6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jul 2024 17:41:57 -0400 Subject: [PATCH 38/82] Bump the actions group with 2 updates (#4302) Bumps the actions group with 2 updates: [ossf/scorecard-action](https://github.com/ossf/scorecard-action) and [github/codeql-action](https://github.com/github/codeql-action). Updates `ossf/scorecard-action` from 2.3.3 to 2.4.0 - [Release notes](https://github.com/ossf/scorecard-action/releases) - [Changelog](https://github.com/ossf/scorecard-action/blob/main/RELEASE.md) - [Commits](https://github.com/ossf/scorecard-action/compare/dc50aa9510b46c811795eb24b2f1ba02a914e534...62b2cac7ed8198b15735ed49ab1e5cf35480ba46) Updates `github/codeql-action` from 3.25.13 to 3.25.15 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/2d790406f505036ef40ecba973cc774a50395aac...afb54ba388a7dca6ecae48f608c4ff05ff4cc77a) --- updated-dependencies: - dependency-name: ossf/scorecard-action dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/scorecard.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 224725e0f7..b44c13f92e 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -37,7 +37,7 @@ jobs: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@dc50aa9510b46c811795eb24b2f1ba02a914e534 # v2.3.3 + uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0 with: results_file: results.sarif results_format: sarif @@ -68,6 +68,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@2d790406f505036ef40ecba973cc774a50395aac # v3.25.13 + uses: github/codeql-action/upload-sarif@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15 with: sarif_file: results.sarif From f255c389c436e3df91a5d5aa515d222f3e2d6d35 Mon Sep 17 00:00:00 2001 From: Pip Liggins Date: Tue, 30 Jul 2024 14:40:28 +0100 Subject: [PATCH 39/82] 3937 events with idaklu output variables - solver edit (#4300) * Add test that fails * Remove duplicate attribute assignment * IDAKLU solver returns additional y_term variable containing the final state vector slice * Add final state vector slice to python-idaklu, tests pass * Reduce memory load so y_term is only filled if output variables are specified. Otherwise empty array. * Edit changelog --------- Co-authored-by: Martin Robinson --- CHANGELOG.md | 4 +++ pybamm/solvers/c_solvers/idaklu.cpp | 1 + .../c_solvers/idaklu/IDAKLUSolverOpenMP.inl | 23 ++++++++++++-- pybamm/solvers/c_solvers/idaklu/Solution.hpp | 5 ++-- pybamm/solvers/c_solvers/idaklu/python.cpp | 3 +- pybamm/solvers/idaklu_solver.py | 5 +++- pybamm/solvers/solution.py | 4 --- tests/unit/test_solvers/test_idaklu_solver.py | 30 +++++++++++++++++++ 8 files changed, 65 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 044c85609b..1de6947420 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ - Added additional user-configurable options to the (`IDAKLUSolver`) and adjusted the default values to improve performance. ([#4282](https://github.com/pybamm-team/PyBaMM/pull/4282)) +## Bug Fixes + +- Fixed bug where IDAKLU solver failed when `output variables` were specified and an event triggered. ([#4300](https://github.com/pybamm-team/PyBaMM/pull/4300)) + # [v24.5](https://github.com/pybamm-team/PyBaMM/tree/v24.5) - 2024-07-26 ## Features diff --git a/pybamm/solvers/c_solvers/idaklu.cpp b/pybamm/solvers/c_solvers/idaklu.cpp index 3afed5faa8..3427c01853 100644 --- a/pybamm/solvers/c_solvers/idaklu.cpp +++ b/pybamm/solvers/c_solvers/idaklu.cpp @@ -187,5 +187,6 @@ PYBIND11_MODULE(idaklu, m) .def_readwrite("t", &Solution::t) .def_readwrite("y", &Solution::y) .def_readwrite("yS", &Solution::yS) + .def_readwrite("y_term", &Solution::y_term) .def_readwrite("flag", &Solution::flag); } diff --git a/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl b/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl index 340baa2d30..96ebb40d10 100644 --- a/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl +++ b/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl @@ -401,6 +401,7 @@ Solution IDAKLUSolverOpenMP::solve( // set return vectors int length_of_return_vector = 0; + int length_of_final_sv_slice = 0; size_t max_res_size = 0; // maximum result size (for common result buffer) size_t max_res_dvar_dy = 0, max_res_dvar_dp = 0; if (functions->var_fcns.size() > 0) { @@ -414,6 +415,7 @@ Solution IDAKLUSolverOpenMP::solve( for (auto& dvar_fcn : functions->dvar_dp_fcns) { max_res_dvar_dp = std::max(max_res_dvar_dp, size_t(dvar_fcn->out_shape(0))); } + length_of_final_sv_slice = number_of_states; } } else { // Return full y state-vector @@ -425,6 +427,7 @@ Solution IDAKLUSolverOpenMP::solve( realtype *yS_return = new realtype[number_of_parameters * number_of_timesteps * length_of_return_vector]; + realtype *yterm_return = new realtype[length_of_final_sv_slice]; res.resize(max_res_size); res_dvar_dy.resize(max_res_dvar_dy); @@ -451,6 +454,13 @@ Solution IDAKLUSolverOpenMP::solve( delete[] vect; } ); + py::capsule free_yterm_when_done( + yterm_return, + [](void *f) { + realtype *vect = reinterpret_cast(f); + delete[] vect; + } + ); // Initial state (t_i=0) int t_i = 0; @@ -518,6 +528,10 @@ Solution IDAKLUSolverOpenMP::solve( t_i += 1; if (retval == IDA_SUCCESS || retval == IDA_ROOT_RETURN) { + if (functions->var_fcns.size() > 0) { + // store final state slice if outout variables are specified + yterm_return = yval; + } break; } } @@ -532,7 +546,7 @@ Solution IDAKLUSolverOpenMP::solve( &y_return[0], free_y_when_done ); - // Note: Ordering of vector is differnet if computing variables vs returning + // Note: Ordering of vector is different if computing variables vs returning // the complete state vector np_array yS_ret; if (functions->var_fcns.size() > 0) { @@ -556,8 +570,13 @@ Solution IDAKLUSolverOpenMP::solve( free_yS_when_done ); } + np_array y_term = np_array( + length_of_final_sv_slice, + &yterm_return[0], + free_yterm_when_done + ); - Solution sol(retval, t_ret, y_ret, yS_ret); + Solution sol(retval, t_ret, y_ret, yS_ret, y_term); if (solver_opts.print_stats) { long nsteps, nrevals, nlinsetups, netfails; diff --git a/pybamm/solvers/c_solvers/idaklu/Solution.hpp b/pybamm/solvers/c_solvers/idaklu/Solution.hpp index 92e22d02b6..ad0ea06762 100644 --- a/pybamm/solvers/c_solvers/idaklu/Solution.hpp +++ b/pybamm/solvers/c_solvers/idaklu/Solution.hpp @@ -12,8 +12,8 @@ class Solution /** * @brief Constructor */ - Solution(int retval, np_array t_np, np_array y_np, np_array yS_np) - : flag(retval), t(t_np), y(y_np), yS(yS_np) + Solution(int retval, np_array t_np, np_array y_np, np_array yS_np, np_array y_term_np) + : flag(retval), t(t_np), y(y_np), yS(yS_np), y_term(y_term_np) { } @@ -21,6 +21,7 @@ class Solution np_array t; np_array y; np_array yS; + np_array y_term; }; #endif // PYBAMM_IDAKLU_COMMON_HPP diff --git a/pybamm/solvers/c_solvers/idaklu/python.cpp b/pybamm/solvers/c_solvers/idaklu/python.cpp index 03090c9850..015f504086 100644 --- a/pybamm/solvers/c_solvers/idaklu/python.cpp +++ b/pybamm/solvers/c_solvers/idaklu/python.cpp @@ -478,8 +478,9 @@ Solution solve_python(np_array t_np, np_array y0_np, np_array yp0_np, std::vector {number_of_parameters, number_of_timesteps, number_of_states}, &yS_return[0] ); + np_array yterm_ret = np_array(0); - Solution sol(retval, t_ret, y_ret, yS_ret); + Solution sol(retval, t_ret, y_ret, yS_ret, yterm_ret); return sol; } diff --git a/pybamm/solvers/idaklu_solver.py b/pybamm/solvers/idaklu_solver.py index 8741a595f8..fa92563e3b 100644 --- a/pybamm/solvers/idaklu_solver.py +++ b/pybamm/solvers/idaklu_solver.py @@ -816,6 +816,7 @@ def _make_iree_function(self, fcn, *args, sparse_index=False): def fcn(*args): return fcn_inner(*args)[coo.row, coo.col] + elif coo.nnz != iree_fcn.numel: iree_fcn.nnz = iree_fcn.numel iree_fcn.col = list(range(iree_fcn.numel)) @@ -969,8 +970,10 @@ def _integrate(self, model, t_eval, inputs_dict=None): if self.output_variables: # Substitute empty vectors for state vector 'y' y_out = np.zeros((number_of_timesteps * number_of_states, 0)) + y_event = sol.y_term else: y_out = sol.y.reshape((number_of_timesteps, number_of_states)) + y_event = y_out[-1] # return sensitivity solution, we need to flatten yS to # (#timesteps * #states (where t is changing the quickest),) @@ -1000,7 +1003,7 @@ def _integrate(self, model, t_eval, inputs_dict=None): model, inputs_dict, np.array([t[-1]]), - np.transpose(y_out[-1])[:, np.newaxis], + np.transpose(y_event)[:, np.newaxis], termination, sensitivities=yS_out, ) diff --git a/pybamm/solvers/solution.py b/pybamm/solvers/solution.py index 39281ef4b0..c3c8451634 100644 --- a/pybamm/solvers/solution.py +++ b/pybamm/solvers/solution.py @@ -100,10 +100,6 @@ def __init__( self.sensitivities = sensitivities - self._t_event = t_event - self._y_event = y_event - self._termination = termination - # Check no ys are too large if check_solution: self.check_ys_are_not_too_large() diff --git a/tests/unit/test_solvers/test_idaklu_solver.py b/tests/unit/test_solvers/test_idaklu_solver.py index 562dde27a5..0f67385017 100644 --- a/tests/unit/test_solvers/test_idaklu_solver.py +++ b/tests/unit/test_solvers/test_idaklu_solver.py @@ -893,6 +893,36 @@ def test_bad_jax_evaluator_output_variables(self): output_variables=["Terminal voltage [V]"], ) + def test_with_output_variables_and_event_termination(self): + model = pybamm.lithium_ion.DFN() + parameter_values = pybamm.ParameterValues("Chen2020") + + sim = pybamm.Simulation( + model, + parameter_values=parameter_values, + solver=pybamm.IDAKLUSolver(output_variables=["Terminal voltage [V]"]), + ) + sol = sim.solve(np.linspace(0, 3600, 1000)) + self.assertEqual(sol.termination, "event: Minimum voltage [V]") + + # create an event that doesn't require the state vector + eps_p = model.variables["Positive electrode porosity"] + model.events.append( + pybamm.Event( + "Zero positive electrode porosity cut-off", + pybamm.min(eps_p), + pybamm.EventType.TERMINATION, + ) + ) + + sim3 = pybamm.Simulation( + model, + parameter_values=parameter_values, + solver=pybamm.IDAKLUSolver(output_variables=["Terminal voltage [V]"]), + ) + sol3 = sim3.solve(np.linspace(0, 3600, 1000)) + self.assertEqual(sol3.termination, "event: Minimum voltage [V]") + if __name__ == "__main__": print("Add -v for more debug output") From 2de4de67bc807ccde841da9bb3be6637944ec2cf Mon Sep 17 00:00:00 2001 From: Marc Berliner <34451391+MarcBerliner@users.noreply.github.com> Date: Wed, 31 Jul 2024 04:11:59 -0400 Subject: [PATCH 40/82] Improve consistent initialization speed and robustness (#4301) * consistent initalization * update description * add test * Update CHANGELOG.md * (fix): failing tests * adjust test tolerance * adjust tolerances * codecov, fix tests * adjust IREE tol * Martin's comments attempt to fix the iree tols * revert tests, shorter tspan * fix codecov --- CHANGELOG.md | 3 + .../notebooks/solvers/dae-solver.ipynb | 21 +- pybamm/discretisations/discretisation.py | 39 +- pybamm/solvers/base_solver.py | 673 +++++++++--------- pybamm/solvers/idaklu_solver.py | 350 +++++---- .../base_lithium_ion_tests.py | 6 +- tests/unit/test_solvers/test_base_solver.py | 33 +- tests/unit/test_solvers/test_idaklu_solver.py | 42 +- 8 files changed, 631 insertions(+), 536 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1de6947420..1ab711cf21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ - Added additional user-configurable options to the (`IDAKLUSolver`) and adjusted the default values to improve performance. ([#4282](https://github.com/pybamm-team/PyBaMM/pull/4282)) +## Optimizations + +- Improved performance and reliability of DAE consistent initialization. ([#4301](https://github.com/pybamm-team/PyBaMM/pull/4301)) ## Bug Fixes - Fixed bug where IDAKLU solver failed when `output variables` were specified and an event triggered. ([#4300](https://github.com/pybamm-team/PyBaMM/pull/4300)) diff --git a/docs/source/examples/notebooks/solvers/dae-solver.ipynb b/docs/source/examples/notebooks/solvers/dae-solver.ipynb index 4149efab9d..8cf7a70161 100644 --- a/docs/source/examples/notebooks/solvers/dae-solver.ipynb +++ b/docs/source/examples/notebooks/solvers/dae-solver.ipynb @@ -18,9 +18,10 @@ "name": "stdout", "output_type": "stream", "text": [ - "\u001B[33mWARNING: You are using pip version 22.0.4; however, version 22.3.1 is available.\n", - "You should consider upgrading via the '/home/mrobins/git/PyBaMM/env/bin/python -m pip install --upgrade pip' command.\u001B[0m\u001B[33m\n", - "\u001B[0mNote: you may need to restart the kernel to use updated packages.\n" + "\n", + "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m24.1\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m24.2\u001b[0m\n", + "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpip install --upgrade pip\u001b[0m\n", + "Note: you may need to restart the kernel to use updated packages.\n" ] } ], @@ -82,7 +83,7 @@ "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAABQkAAAGGCAYAAADYVwfrAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjYuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8o6BhiAAAACXBIWXMAAA9hAAAPYQGoP6dpAACPC0lEQVR4nOzdd3hUZfrG8e/MpJNMCKSHSEI39CJNUdHQRBAb4KoUUVfXhugquEqxof4s6IqirAjYAFERBWNBERQEAVF6DTUNCMmQhBQy8/tjYCCSkAkkOSn357rmijnzzjnPGXbx9jnnvK/J4XA4EBERERERERERkVrLbHQBIiIiIiIiIiIiYiw1CUVERERERERERGo5NQlFRERERERERERqOTUJRUREREREREREajk1CUVERERERERERGo5NQlFRERERERERERqOTUJRUREREREREREajk1CUVERERERERERGo5D6MLcIfdbicpKYmAgABMJpPR5YiIiIhUaQ6Hg2PHjhEZGYnZXPOuCSsbioiIiLjP3WxYLZqESUlJREdHG12GiIiISLWyf/9+GjRoYHQZ5U7ZUERERKTsSsuG1aJJGBAQADhPxmq1GlyNiIiISNVms9mIjo52ZaiaRtlQRERExH3uZsNq0SQ89RiJ1WpVEBQRERFxU019FFfZUERERKTsSsuGNW+SGhERERERERERESkTNQlFRERERERERERqOTUJRUREREREREREarlqMSehiIiIVKzCwkIKCgqMLkPc5OnpicViMboMERERqaKU7WqX8sqGahKKiIjUYg6Hg5SUFDIyMowuRcqobt26hIeH19jFSURERKTslO1qr/LIhmoSioiI1GKnQmRoaCh+fn5qOFUDDoeDnJwc0tLSAIiIiDC4IhEREakqlO1qn/LMhmoSioiI1FKFhYWuEFm/fn2jy5Ey8PX1BSAtLY3Q0FA9eiwiIiLKdrVYeWXDMi9csmzZMgYMGEBkZCQmk4kFCxaU+pmlS5fSoUMHvL29adKkCTNnzjyPUiuBvRASl8OG+c6f9kKjKxIREakwp+ap8fPzM7gSOR+n/twqar6hyZMnc8kllxAQEEBoaCiDBg1i27ZtpX7u008/pUWLFvj4+NC6dWsWL15c5H2Hw8H48eOJiIjA19eX+Ph4duzYUSHncEGUC0VEpJpRtqvdyiMblrlJmJ2dTdu2bZk6dapb4xMTE+nfvz89e/Zk/fr1jB49mjvvvJNvv/22zMVWqM0LYUormHUtfDbK+XNKK+d2ERGRGkyPoVRPFf3n9vPPP3Pffffx22+/8f3331NQUEDv3r3Jzs4u8TMrVqzglltuYdSoUfzxxx8MGjSIQYMGsXHjRteYl156iTfeeINp06axatUq6tSpQ58+fcjNza3Q8ykT5UIREanGlO1qp/L4czc5HA7HhRTwxRdfMGjQoBLHPP744yxatKhIOBw6dCgZGRkkJCS4dRybzUZgYCCZmZlYrdbzLbdkmxfCvGHA37+Kk1/w4NkQN7D8jysiImKg3NxcEhMTiY2NxcfHx+hypIzO9edXEdnp0KFDhIaG8vPPP3P55ZcXO2bIkCFkZ2fz9ddfu7Z17dqVdu3aMW3aNBwOB5GRkTzyyCM8+uijAGRmZhIWFsbMmTMZOnSoW7VUaDZULhQRkWpK2a52K49sWOY7Cctq5cqVxMfHF9nWp08fVq5cWdGHdo+9EBIe5+wgyOltCWP1iImIiEgtdPvtt/P8889X+nGHDh3KK6+8UunHPZfMzEwA6tWrV+KY0nJfYmIiKSkpRcYEBgbSpUuXqpENlQtFRESkBPn5+TRp0oQVK1ZU+nFjYmJYs2ZNhR+rwpuEKSkphIWFFdkWFhaGzWbj+PHjxX4mLy8Pm81W5FVh9q4AW9I5BjjAdtA5TkRERGqNP//8k8WLF/Pggw+WOCY9PZ0HHniA5s2b4+vry0UXXcSDDz7oaqidaebMmWfNy7x06VJMJhMZGRlFtj/55JM899xzxe7HCHa7ndGjR3PppZfSqlWrEseVlPtSUlJc75/aVtKY4lRaNlQuFBERqXTuzoO8Z88eRowYUfkFnjRt2jRiY2Pp3r17iWP+/PNPbrnlFqKjo/H19eXiiy/m9ddfL3bsiBEj2LNnT5FtEydOpF27dkW2eXl58eijj/L4449f6CmUqsKbhOdj8uTJBAYGul7R0dEVd7Cs1PIdJyIiIjXCf//7X26++Wb8/f1LHJOUlERSUhIvv/wyGzduZObMmSQkJDBq1CjXmNdee41jx465fj927BivvfbaOY/dqlUrGjduzIcffnjhJ1IO7rvvPjZu3MicOXMMOX6lZUPlQhERkUpX2jzIH330Ebt27XKNdzgcTJ06laNHj1ZajQ6HgzfffLNIxivO2rVrCQ0N5cMPP2TTpk385z//Ydy4cbz55puA8wLz1KlTOXPmv127dvHRRx+dc7+33norv/zyC5s2bbrwkzmHCm8ShoeHk5paNEilpqZitVpdSzT/3bhx48jMzHS99u/fX3EF+oeVPqYs40RERKTC2e12Jk+eTGxsLL6+vrRt25b58+fjcDiIj4+nT58+rvCVnp5OgwYNGD9+PHD67r1FixbRpk0bfHx86Nq1a5H5kwsLC5k/fz4DBgw4Zx2tWrXis88+Y8CAATRu3JirrrqK5557jq+++ooTJ04AEBQURK9evfjll1/45Zdf6NWrF0FBQezZs4eePXu6xphMpiJXxwcMGGBYU+5M999/P19//TU//fQTDRo0OOfYknJfeHi46/1T20oaU5xKy4bKhSIiIpUuISGBESNG0LJlS9q2bcvMmTPZt28fa9euBSA2Npbhw4czbdo0Dhw4QN++fTl48CDe3t4AZGRkcOeddxISEoLVauWqq67izz//BJxzKoeHhxeZPmbFihV4eXmxZMkS4PTde++88w7R0dH4+fkxePDgIk90rF27ll27dtG/f/9znssdd9zB66+/zhVXXEGjRo247bbbGDlyJJ9//jkAPj4+HDx4kL59+3LgwAGmTZvGiBEjiI2NZebMmUyaNIk///wTk8mEyWRyPYUSFBTEpZdeWuHZ0KNC9w5069aNxYsXF9n2/fff061btxI/4+3t7frDrnANu4M1EmzJFD//jMn5fsOSbycVERGpCRwOB8cLjJlrzdfTUqYV2SZPnsyHH37ItGnTaNq0KcuWLeO2224jJCSEWbNm0bp1a9544w0eeugh7rnnHqKiolxNwlP+/e9/8/rrrxMeHs4TTzzBgAED2L59O56envz1119kZmbSqVOnMp/LqQmhPTycMWvEiBFcddVVdO7cGYDVq1dz0UUXUVhYyGeffcaNN97Itm3bzrqA2rlzZ5577jny8vIqLxedweFw8MADD/DFF1+wdOlSYmNjS/1Mt27dWLJkCaNHj3ZtOzP3xcbGEh4ezpIlS1yP0thsNlatWsW9995b4n4rLRuWkgsdmDApF4qISDVRnbLdmf4+D3L37t356aefiI+P59dff+Wrr76iX79+rvE333wzvr6+fPPNNwQGBvLOO+9w9dVXs337dkJCQpgxYwaDBg2id+/eNG/enNtvv53777+fq6++2rWPnTt3Mm/ePL766itsNhujRo3iX//6l+sOv+XLl9OsWTMCAgLO63xOnYufnx/PP/88ixcvZuDAgZw4cYIff/wRT09P2rdvz8aNG0lISOCHH34AnHM3n9K5c2eWL19e5uOXRZmbhFlZWezcudP1e2JiIuvXr6devXpcdNFFjBs3joMHDzJ79mwA7rnnHt58800ee+wx7rjjDn788UfmzZvHokWLyu8sLoTZAn1fPLmKnYkzA6EDk3Mdu74vOMeJiIjUYMcLCokb/60hx978dB/8vNyLJXl5eTz//PP88MMPruZTo0aN+OWXX3jnnXf4+OOPeeeddxg2bBgpKSksXryYP/74w9W0O2XChAn06tULgFmzZtGgQQO++OILBg8ezN69e7FYLISGhpbpPA4fPswzzzzD3Xff7dr24Ycf8uabb7quPA8ePJj777+f2267zRUYQ0NDqVu3bpF9RUZGkp+fT0pKCg0bNixTHeXhvvvu4+OPP+bLL78kICDANWdgYGCgq5k5bNgwoqKimDx5MgAPPfQQV1xxBa+88gr9+/dnzpw5rFmzhnfffRcAk8nE6NGjefbZZ2natCmxsbE89dRTREZGMmjQoEo/x7OcIxfaHWAyoVwoIiLVRnXJdmcqbh7kVatW8e9//5vu3bvj6enJlClTWLlyJU888QRr1qxh9erVpKWluS4ovvzyyyxYsID58+dz9913c80113DXXXdx66230qlTJ+rUqePKLqfk5uYye/ZsoqKiAOe0M/379+eVV14hPDycvXv3EhkZWebzWbFiBXPnznX1wHJzc3n++edZtWoVV155JZ06dSI+Pp7/+7//o3Pnzvj7++Ph4VHsExaRkZHs3bu3zDWURZkfN16zZg3t27enffv2AIwZM4b27du7rs4nJyezb98+1/jY2FgWLVrE999/T9u2bXnllVf43//+R58+fcrpFMpB3EAYPBusEUU2p5nqY795lvN9ERERqRJ27txJTk4OvXr1wt/f3/WaPXu2a76am2++meuvv54XXniBl19+maZNm561nzOfaqhXrx7Nmzdny5YtABw/fhxvb+8iV8Cff/75Isc7M++A8464/v37ExcXx8SJE13b09LS+P777+nRowc9evTg+++/Jy0trdTzPNWIy8nJcf/LKUdvv/02mZmZXHnllURERLhec+fOdY3Zt28f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pk2bhp+fHzNmzCh2/IoVK7j00kv5xz/+QUxMDL179+aWW24p9e7D2qBpWAA3d4wGYPLirTgcDoMrEhERERGjXN40mEub1Ce/0M4r320zuhwRERGpZcrUJMzPz2ft2rXEx8ef3oHZTHx8PCtXriz2M927d2ft2rWupuDu3btZvHgx11xzTYnHycvLw2azFXnVVA/3aoa3h5k1e4/y7aZUo8sREREREYOYTCbG9nXOW71gfRIbDmQaXJGIiIjUJmVqEh4+fJjCwkLCwsKKbA8LCyMlJaXYz/zjH//g6aef5rLLLsPT05PGjRtz5ZVXnvNx48mTJxMYGOh6RUdHl6XMaiU80Ic7e8QCMPmbLeSdKDS4IhERERExSusGgVzXLhKAZ77erCdNREREpNJU+OrGS5cu5fnnn+ett95i3bp1fP755yxatIhnnnmmxM+MGzeOzMxM12v//v0VXaah7r2yCSEB3uw9ksPsFXuNLkdEREREDPRY3xZ4e5hZvSedhI3FX4gXERERKW9lahIGBwdjsVhITS36WGxqairh4eHFfuapp57i9ttv584776R169Zcf/31PP/880yePBm73V7sZ7y9vbFarUVeNZm/tweP9m4GwBs/7iA9O9/gikRERETEKFF1fbn78kYATP5mq540ERERkUpRpiahl5cXHTt2ZMmSJa5tdrudJUuW0K1bt2I/k5OTg9lc9DAWiwVAj0+c4aaO0cRFWDmWe4IpP2w3uhwRERERMdA9VzQmNMCbfek5zFqxx+hyREREpBYo8+PGY8aMYfr06cyaNYstW7Zw7733kp2dzciRIwEYNmwY48aNc40fMGAAb7/9NnPmzCExMZHvv/+ep556igEDBriahQIWs4knr3VOVP3Rqn3sSD1mcEUiIiIiYpQ63h482qc5AP9dspMjWXkGVyQiIiI1nUdZPzBkyBAOHTrE+PHjSUlJoV27diQkJLgWM9m3b1+ROweffPJJTCYTTz75JAcPHiQkJIQBAwbw3HPPld9Z1BDdGwfTKy6M7zen8tziLcwc2dnokkRERETEIDd1aMCsFXvYlGTjtR+28+yg1kaXJCIiIjWYyVENnvm12WwEBgaSmZlZ4+cnTDycTe/Xfqag0MGsOzpzRbMQo0sSERGRaqamZ6eafn5n+m33EYa++xtmEySMvpxmYQFGlyQiIiLVjLvZqcJX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", + "image/png": "iVBORw0KGgoAAAANSUhEUgAABQkAAAGGCAYAAADYVwfrAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/TGe4hAAAACXBIWXMAAA9hAAAPYQGoP6dpAACPC0lEQVR4nOzdd3hUZfrG8e/MpJNMCKSHSEI39CJNUdHQRBAb4KoUUVfXhugquEqxof4s6IqirAjYAFERBWNBERQEAVF6DTUNCMmQhBQy8/tjYCCSkAkkOSn357rmijnzzjnPGXbx9jnnvK/J4XA4EBERERERERERkVrLbHQBIiIiIiIiIiIiYiw1CUVERERERERERGo5NQlFRERERERERERqOTUJRUREREREREREajk1CUVERERERERERGo5NQlFRERERERERERqOTUJRUREREREREREajk1CUVERERERERERGo5D6MLcIfdbicpKYmAgABMJpPR5YiIiIhUaQ6Hg2PHjhEZGYnZXPOuCSsbioiIiLjP3WxYLZqESUlJREdHG12GiIiISLWyf/9+GjRoYHQZ5U7ZUERERKTsSsuG1aJJGBAQADhPxmq1GlyNiIiISNVms9mIjo52ZaiaRtlQRERExH3uZsNq0SQ89RiJ1WpVEBQRERFxU019FFfZUERERKTsSsuGNW+SGhERERERERERESkTNQlFRERERERERERqOTUJRUREREREREREarlqMSehiIiIVKzCwkIKCgqMLkPc5OnpicViMboMERERqaKU7WqX8sqGahKKiIjUYg6Hg5SUFDIyMowuRcqobt26hIeH19jFSURERKTslO1qr/LIhmoSioiI1GKnQmRoaCh+fn5qOFUDDoeDnJwc0tLSAIiIiDC4IhEREakqlO1qn/LMhmoSioiI1FKFhYWuEFm/fn2jy5Ey8PX1BSAtLY3Q0FA9eiwiIiLKdrVYeWXDMi9csmzZMgYMGEBkZCQmk4kFCxaU+pmlS5fSoUMHvL29adKkCTNnzjyPUiuBvRASl8OG+c6f9kKjKxIREakwp+ap8fPzM7gSOR+n/twqar6hyZMnc8kllxAQEEBoaCiDBg1i27ZtpX7u008/pUWLFvj4+NC6dWsWL15c5H2Hw8H48eOJiIjA19eX+Ph4duzYUSHncEGUC0VEpJpRtqvdyiMblrlJmJ2dTdu2bZk6dapb4xMTE+nfvz89e/Zk/fr1jB49mjvvvJNvv/22zMVWqM0LYUormHUtfDbK+XNKK+d2ERGRGkyPoVRPFf3n9vPPP3Pffffx22+/8f3331NQUEDv3r3Jzs4u8TMrVqzglltuYdSoUfzxxx8MGjSIQYMGsXHjRteYl156iTfeeINp06axatUq6tSpQ58+fcjNza3Q8ykT5UIREanGlO1qp/L4czc5HA7HhRTwxRdfMGjQoBLHPP744yxatKhIOBw6dCgZGRkkJCS4dRybzUZgYCCZmZlYrdbzLbdkmxfCvGHA37+Kk1/w4NkQN7D8jysiImKg3NxcEhMTiY2NxcfHx+hypIzO9edXEdnp0KFDhIaG8vPPP3P55ZcXO2bIkCFkZ2fz9ddfu7Z17dqVdu3aMW3aNBwOB5GRkTzyyCM8+uijAGRmZhIWFsbMmTMZOnSoW7VUaDZULhQRkWpK2a52K49sWOY7Cctq5cqVxMfHF9nWp08fVq5cWdGHdo+9EBIe5+wgyOltCWP1iImIiEgtdPvtt/P8889X+nGHDh3KK6+8UunHPZfMzEwA6tWrV+KY0nJfYmIiKSkpRcYEBgbSpUuXqpENlQtFRESkBPn5+TRp0oQVK1ZU+nFjYmJYs2ZNhR+rwpuEKSkphIWFFdkWFhaGzWbj+PHjxX4mLy8Pm81W5FVh9q4AW9I5BjjAdtA5TkRERGqNP//8k8WLF/Pggw+WOCY9PZ0HHniA5s2b4+vry0UXXcSDDz7oaqidaebMmWfNy7x06VJMJhMZGRlFtj/55JM899xzxe7HCHa7ndGjR3PppZfSqlWrEseVlPtSUlJc75/aVtKY4lRaNlQuFBERqXTuzoO8Z88eRowYUfkFnjRt2jRiY2Pp3r17iWP+/PNPbrnlFqKjo/H19eXiiy/m9ddfL3bsiBEj2LNnT5FtEydOpF27dkW2eXl58eijj/L4449f6CmUqsKbhOdj8uTJBAYGul7R0dEVd7Cs1PIdJyIiIjXCf//7X26++Wb8/f1LHJOUlERSUhIvv/wyGzduZObMmSQkJDBq1CjXmNdee41jx465fj927BivvfbaOY/dqlUrGjduzIcffnjhJ1IO7rvvPjZu3MicOXMMOX6lZUPlQhERkUpX2jzIH330Ebt27XKNdzgcTJ06laNHj1ZajQ6HgzfffLNIxivO2rVrCQ0N5cMPP2TTpk385z//Ydy4cbz55puA8wLz1KlTOXPmv127dvHRRx+dc7+33norv/zyC5s2bbrwkzmHCm8ShoeHk5paNEilpqZitVpdSzT/3bhx48jMzHS99u/fX3EF+oeVPqYs40RERKTC2e12Jk+eTGxsLL6+vrRt25b58+fjcDiIj4+nT58+rvCVnp5OgwYNGD9+PHD67r1FixbRpk0bfHx86Nq1a5H5kwsLC5k/fz4DBgw4Zx2tWrXis88+Y8CAATRu3JirrrqK5557jq+++ooTJ04AEBQURK9evfjll1/45Zdf6NWrF0FBQezZs4eePXu6xphMpiJXxwcMGGBYU+5M999/P19//TU//fQTDRo0OOfYknJfeHi46/1T20oaU5xKy4bKhSIiIpUuISGBESNG0LJlS9q2bcvMmTPZt28fa9euBSA2Npbhw4czbdo0Dhw4QN++fTl48CDe3t4AZGRkcOeddxISEoLVauWqq67izz//BJxzKoeHhxeZPmbFihV4eXmxZMkS4PTde++88w7R0dH4+fkxePDgIk90rF27ll27dtG/f/9znssdd9zB66+/zhVXXEGjRo247bbbGDlyJJ9//jkAPj4+HDx4kL59+3LgwAGmTZvGiBEjiI2NZebMmUyaNIk///wTk8mEyWRyPYUSFBTEpZdeWuHZ0KNC9w5069aNxYsXF9n2/fff061btxI/4+3t7frDrnANu4M1EmzJFD//jMn5fsOSbycVERGpCRwOB8cLjJlrzdfTUqYV2SZPnsyHH37ItGnTaNq0KcuWLeO2224jJCSEWbNm0bp1a9544w0eeugh7rnnHqKiolxNwlP+/e9/8/rrrxMeHs4TTzzBgAED2L59O56envz1119kZmbSqVOnMp/LqQmhPTycMWvEiBFcddVVdO7cGYDVq1dz0UUXUVhYyGeffcaNN97Itm3bzrqA2rlzZ5577jny8vIqLxedweFw8MADD/DFF1+wdOlSYmNjS/1Mt27dWLJkCaNHj3ZtOzP3xcbGEh4ezpIlS1yP0thsNlatWsW9995b4n4rLRuWkgsdmDApF4qISDVRnbLdmf4+D3L37t356aefiI+P59dff+Wrr76iX79+rvE333wzvr6+fPPNNwQGBvLOO+9w9dVXs337dkJCQpgxYwaDBg2id+/eNG/enNtvv53777+fq6++2rWPnTt3Mm/ePL766itsNhujRo3iX//6l+sOv+XLl9OsWTMCAgLO63xOnYufnx/PP/88ixcvZuDAgZw4cYIff/wRT09P2rdvz8aNG0lISOCHH34AnHM3n9K5c2eWL19e5uOXRZmbhFlZWezcudP1e2JiIuvXr6devXpcdNFFjBs3joMHDzJ79mwA7rnnHt58800ee+wx7rjjDn788UfmzZvHokWLyu8sLoTZAn1fPLmKnYkzA6EDk3Mdu74vOMeJiIjUYMcLCokb/60hx978dB/8vNyLJXl5eTz//PP88MMPruZTo0aN+OWXX3jnnXf4+OOPeeeddxg2bBgpKSksXryYP/74w9W0O2XChAn06tULgFmzZtGgQQO++OILBg8ezN69e7FYLISGhpbpPA4fPswzzzzD3Xff7dr24Ycf8uabb7quPA8ePJj777+f2267zRUYQ0NDqVu3bpF9RUZGkp+fT0pKCg0bNixTHeXhvvvu4+OPP+bLL78kICDANWdgYGCgq5k5bNgwoqKimDx5MgAPPfQQV1xxBa+88gr9+/dnzpw5rFmzhnfffRcAk8nE6NGjefbZZ2natCmxsbE89dRTREZGMmjQoEo/x7OcIxfaHWAyoVwoIiLVRnXJdmcqbh7kVatW8e9//5vu3bvj6enJlClTWLlyJU888QRr1qxh9erVpKWluS4ovvzyyyxYsID58+dz9913c80113DXXXdx66230qlTJ+rUqePKLqfk5uYye/ZsoqKiAOe0M/379+eVV14hPDycvXv3EhkZWebzWbFiBXPnznX1wHJzc3n++edZtWoVV155JZ06dSI+Pp7/+7//o3Pnzvj7++Ph4VHsExaRkZHs3bu3zDWURZkfN16zZg3t27enffv2AIwZM4b27du7rs4nJyezb98+1/jY2FgWLVrE999/T9u2bXnllVf43//+R58+fcrpFMpB3EAYPBusEUU2p5nqY795lvN9ERERqRJ27txJTk4OvXr1wt/f3/WaPXu2a76am2++meuvv54XXniBl19+maZNm561nzOfaqhXrx7Nmzdny5YtABw/fhxvb+8iV8Cff/75Isc7M++A8464/v37ExcXx8SJE13b09LS+P777+nRowc9evTg+++/Jy0trdTzPNWIy8nJcf/LKUdvv/02mZmZXHnllURERLhec+fOdY3Zt28f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pk2bhp+fHzNmzCh2/IoVK7j00kv5xz/+QUxMDL179+aWW24p9e7D2qBpWAA3d4wGYPLirTgcDoMrEhERERGjXN40mEub1Ce/0M4r320zuhwRERGpZcrUJMzPz2ft2rXEx8ef3oHZTHx8PCtXriz2M927d2ft2rWupuDu3btZvHgx11xzTYnHycvLw2azFXnVVA/3aoa3h5k1e4/y7aZUo8sREREREYOYTCbG9nXOW71gfRIbDmQaXJGIiIjUJmVqEh4+fJjCwkLCwsKKbA8LCyMlJaXYz/zjH//g6aef5rLLLsPT05PGjRtz5ZVXnvNx48mTJxMYGOh6RUdHl6XMaiU80Ic7e8QCMPmbLeSdKDS4IhERERExSusGgVzXLhKAZ77erCdNREREpNJU+OrGS5cu5fnnn+ett95i3bp1fP755yxatIhnnnmmxM+MGzeOzMxM12v//v0VXaah7r2yCSEB3uw9ksPsFXuNLkdEREREDPRY3xZ4e5hZvSedhI3FX4gXERERKW9lahIGBwdjsVhITS36WGxqairh4eHFfuapp57i9ttv584776R169Zcf/31PP/880yePBm73V7sZ7y9vbFarUVeNZm/tweP9m4GwBs/7iA9O9/gikRERETEKFF1fbn78kYATP5mq540ERERkUpRpiahl5cXHTt2ZMmSJa5tdrudJUuW0K1bt2I/k5OTg9lc9DAWiwVAj0+c4aaO0cRFWDmWe4IpP2w3uhwRERERMdA9VzQmNMCbfek5zFqxx+hyREREpBYo8+PGY8aMYfr06cyaNYstW7Zw7733kp2dzciRIwEYNmwY48aNc40fMGAAb7/9NnPmzCExMZHvv/+ep556igEDBriahQIWs4knr3VOVP3Rqn3sSD1mcEUiIiIiYpQ63h482qc5AP9dspMjWXkGVyQiIiI1nUdZPzBkyBAOHTrE+PHjSUlJoV27diQkJLgWM9m3b1+ROweffPJJTCYTTz75JAcPHiQkJIQBAwbw3HPPld9Z1BDdGwfTKy6M7zen8tziLcwc2dnokkRERETEIDd1aMCsFXvYlGTjtR+28+yg1kaXJCIiIjWYyVENnvm12WwEBgaSmZlZ4+cnTDycTe/Xfqag0MGsOzpzRbMQo0sSERGRaqamZ6eafn5n+m33EYa++xtmEySMvpxmYQFGlyQiIiLVjLvZqcJX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", "text/plain": [ "
" ] @@ -169,7 +170,7 @@ "outputs": [ { "data": { - "image/png": "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Ga8sGzZsIDQ0lI8//pitW7fyn//8hwkTJjBt2jTAfoF5+vTpnDnz3969e/nkk0/Oetzhw4fz66+/snXr1gs/mbOo9iZheHg4qaklg1RqaioBAQGOJZr/bsKECWRmZjpehw4dqr4C/cLOPaYy40RERKTaWa1WpkyZQmxsLN7e3nTq1IlFixZhs9mIj4+nX79+jvCVnp5O48aNmThxInD67r2lS5fSsWNHvLy8uPTSS0vMn2yxWFi0aBGDBg06ax3t27fn888/Z9CgQTRr1ow+ffrw/PPP8/XXX1NUVARAgwYNuOaaa/j111/59ddfueaaa2jQoAH79++nd+/ejjEGg6HE1fFBgwY5rSl3pvvvv59vvvmGn3/+mcaNG591bHm5Lzw83PH9qW3ljSlLjWVD5UIREZEal5CQwKhRo2jXrh2dOnVi9uzZHDx4kA0bNgAQGxvLyJEjmTFjBocPH6Z///4cOXIET09PADIyMrjrrrsICQkhICCAPn368OeffwL2OZXDw8NLTB+zevVqPDw8WL58OXD67r13332X6OhofHx8GDp0aIknOjZs2MDevXsZOHDgWc/lzjvv5I033uDKK6+kadOm3H777YwePZovvvgCAC8vL44cOUL//v05fPgwM2bMYNSoUcTGxjJ79myefvpp/vzzTwwGAwaDwfEUSoMGDbjsssuqPRu6VevRgR49erBs2bIS23744Qd69OhR7j6enp6Ov+xq16QnBESCOZmy558x2L9vUv7tpCIiIq7AZrNxstA5c615u5sqtSLblClT+Pjjj5kxYwYtWrRg5cqV3H777YSEhDBnzhw6dOjAm2++yUMPPcQ999xDVFSUo0l4yv/93//xxhtvEB4ezhNPPMGgQYPYtWsX7u7u/PXXX2RmZtKtW7dKn8upCaHd3Owxa9SoUfTp04fu3bsDsG7dOi666CIsFguff/45N910Ezt37ix1AbV79+48//zz5Ofn11wuOoPNZuOBBx7gyy+/ZMWKFcTGxp5znx49erB8+XIefvhhx7Yzc19sbCzh4eEsX77c8SiN2Wxm7dq13HvvveUet8ay4TlyoQ0DBuVCERGpI+pStjvT3+dB7tmzJz///DPx8fH89ttvfP311wwYMMAx/uabb8bb25tvv/2WwMBA3n33Xa6++mp27dpFSEgIs2bNYsiQIfTt25dWrVpxxx13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", + "image/png": "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Ga8sGzZsIDQ0lI8//pitW7fyn//8hwkTJjBt2jTAfoF5+vTpnDnz3969e/nkk0/Oetzhw4fz66+/snXr1gs/mbOo9iZheHg4qaklg1RqaioBAQGOJZr/bsKECWRmZjpehw4dqr4C/cLOPaYy40RERKTaWa1WpkyZQmxsLN7e3nTq1IlFixZhs9mIj4+nX79+jvCVnp5O48aNmThxInD67r2lS5fSsWNHvLy8uPTSS0vMn2yxWFi0aBGDBg06ax3t27fn888/Z9CgQTRr1ow+ffrw/PPP8/XXX1NUVARAgwYNuOaaa/j111/59ddfueaaa2jQoAH79++nd+/ejjEGg6HE1fFBgwY5rSl3pvvvv59vvvmGn3/+mcaNG591bHm5Lzw83PH9qW3ljSlLjWVD5UIREZEal5CQwKhRo2jXrh2dOnVi9uzZHDx4kA0bNgAQGxvLyJEjmTFjBocPH6Z///4cOXIET09PADIyMrjrrrsICQkhICCAPn368OeffwL2OZXDw8NLTB+zevVqPDw8WL58OXD67r13332X6OhofHx8GDp0aIknOjZs2MDevXsZOHDgWc/lzjvv5I033uDKK6+kadOm3H777YwePZovvvgCAC8vL44cOUL//v05fPgwM2bMYNSoUcTGxjJ79myefvpp/vzzTwwGAwaDwfEUSoMGDbjsssuqPRu6VevRgR49erBs2bIS23744Qd69OhR7j6enp6Ov+xq16QnBESCOZmy558x2L9vUv7tpCIiIq7AZrNxstA5c615u5sqtSLblClT+Pjjj5kxYwYtWrRg5cqV3H777YSEhDBnzhw6dOjAm2++yUMPPcQ999xDVFSUo0l4yv/93//xxhtvEB4ezhNPPMGgQYPYtWsX7u7u/PXXX2RmZtKtW7dKn8upCaHd3Owxa9SoUfTp04fu3bsDsG7dOi666CIsFguff/45N910Ezt37ix1AbV79+48//zz5Ofn11wuOoPNZuOBBx7gyy+/ZMWKFcTGxp5znx49erB8+XIefvhhx7Yzc19sbCzh4eEsX77c8SiN2Wxm7dq13HvvveUet8ay4TlyoQ0DBuVCERGpI+pStjvT3+dB7tmzJz///DPx8fH89ttvfP311wwYMMAx/uabb8bb25tvv/2WwMBA3n33Xa6++mp27dpFSEgIs2bNYsiQIfTt25dWrVpxxx13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", "text/plain": [ "
" ] @@ -261,14 +262,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Finding consistent initial conditions\n", + "## Finding consistent initialization\n", "\n", - "The solver will fail if initial conditions that are inconsistent with the algebraic equations are provided. However, before solving the DAE solvers automatically use `_set_initial_conditions` to obtain consistent initial conditions, starting from a guess of bad initial conditions, using a simple root-finding algorithm. " + "The solver will fail if initial conditions that are inconsistent with the algebraic equations are provided. However, before solving the DAE solvers automatically use `_set_consistent_initialization` to obtain consistent initial conditions, starting from a guess of bad initial conditions, using a simple root-finding algorithm. " ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 8, "metadata": {}, "outputs": [ { @@ -298,7 +299,7 @@ "\n", "dae_solver = pybamm.CasadiSolver(mode=\"safe\")\n", "dae_solver.set_up(model)\n", - "dae_solver._set_initial_conditions(model, 0, {}, True)\n", + "dae_solver._set_consistent_initialization(model, 0, {})\n", "print(f\"y0_fixed={model.y0}\")" ] }, @@ -348,7 +349,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.15" + "version": "3.11.7" }, "vscode": { "interpreter": { diff --git a/pybamm/discretisations/discretisation.py b/pybamm/discretisations/discretisation.py index 24625cf8fc..af4bd2edd6 100644 --- a/pybamm/discretisations/discretisation.py +++ b/pybamm/discretisations/discretisation.py @@ -9,14 +9,7 @@ def has_bc_of_form(symbol, side, bcs, form): - if symbol in bcs: - if bcs[symbol][side][1] == form: - return True - else: - return False - - else: - return False + return (symbol in bcs) and (bcs[symbol][side][1] == form) class Discretisation: @@ -614,15 +607,14 @@ def create_mass_matrix(self, model): for var in sorted_model_variables: if var.domain == []: # If variable domain empty then mass matrix is just 1 - mass_list.append(1.0) - mass_inv_list.append(1.0) + mass = 1.0 + mass_inv = 1.0 else: mass = ( self.spatial_methods[var.domain[0]] .mass_matrix(var, self.bcs) .entries ) - mass_list.append(mass) if isinstance( self.spatial_methods[var.domain[0]], (pybamm.ZeroDimensionalSpatialMethod, pybamm.FiniteVolume), @@ -630,11 +622,13 @@ def create_mass_matrix(self, model): # for 0D methods the mass matrix is just a scalar 1 and for # finite volumes the mass matrix is identity, so no need to # compute the inverse - mass_inv_list.append(mass) + mass_inv = mass else: # inverse is more efficient in csc format mass_inv = inv(csc_matrix(mass)) - mass_inv_list.append(mass_inv) + + mass_list.append(mass) + mass_inv_list.append(mass_inv) # Create lumped mass matrix (of zeros) of the correct shape for the # discretised algebraic equations @@ -645,14 +639,21 @@ def create_mass_matrix(self, model): # Create block diagonal (sparse) mass matrix (if model is not empty) # and inverse (if model has odes) - if len(model.rhs) + len(model.algebraic) > 0: + N_rhs = len(model.rhs) + N_alg = len(model.algebraic) + + has_mass_matrix = N_rhs > 0 or N_alg > 0 + has_mass_matrix_inv = N_rhs > 0 + + if has_mass_matrix: mass_matrix = pybamm.Matrix(block_diag(mass_list, format="csr")) - if len(model.rhs) > 0: - mass_matrix_inv = pybamm.Matrix(block_diag(mass_inv_list, format="csr")) - else: - mass_matrix_inv = None else: - mass_matrix, mass_matrix_inv = None, None + mass_matrix = None + + if has_mass_matrix_inv: + mass_matrix_inv = pybamm.Matrix(block_diag(mass_inv_list, format="csr")) + else: + mass_matrix_inv = None return mass_matrix, mass_matrix_inv diff --git a/pybamm/solvers/base_solver.py b/pybamm/solvers/base_solver.py index 0eb573e87a..b4ff3a5774 100644 --- a/pybamm/solvers/base_solver.py +++ b/pybamm/solvers/base_solver.py @@ -123,9 +123,9 @@ def set_up(self, model, inputs=None, t_eval=None, ics_only=False): model.calculate_sensitivities = [] # see if we need to form the explicit sensitivity equations - calculate_sensitivities_explicit = False - if model.calculate_sensitivities and not isinstance(self, pybamm.IDAKLUSolver): - calculate_sensitivities_explicit = True + calculate_sensitivities_explicit = ( + model.calculate_sensitivities and not isinstance(self, pybamm.IDAKLUSolver) + ) self._set_up_model_sensitivities_inplace( model, inputs, calculate_sensitivities_explicit @@ -145,33 +145,8 @@ def set_up(self, model, inputs=None, t_eval=None, ics_only=False): model.initial_conditions_eval = initial_conditions model.jacp_initial_conditions_eval = jacp_ic - # evaluate initial condition - y0_total_size = ( - model.len_rhs + model.len_rhs_sens + model.len_alg + model.len_alg_sens - ) - y_zero = np.zeros((y0_total_size, 1)) - if model.convert_to_format == "casadi": - # stack inputs - inputs_casadi = casadi.vertcat(*[x for x in inputs.values()]) - model.y0 = initial_conditions(0.0, y_zero, inputs_casadi) - if jacp_ic is None: - model.y0S = None - else: - model.y0S = jacp_ic(0.0, y_zero, inputs_casadi) - else: - model.y0 = initial_conditions(0.0, y_zero, inputs) - if jacp_ic is None: - model.y0S = None - else: - # we are calculating the derivative wrt the inputs - # so need to make sure we convert int -> float - # This is to satisfy JAX jacfwd function which requires - # float inputs - inputs_float = { - key: float(value) if isinstance(value, int) else value - for key, value in inputs.items() - } - model.y0S = jacp_ic(0.0, y_zero, inputs_float) + # set initial conditions + self._set_initial_conditions(model, 0.0, inputs) if ics_only: pybamm.logger.info("Finish solver set-up") @@ -283,6 +258,37 @@ def set_up(self, model, inputs=None, t_eval=None, ics_only=False): pybamm.logger.info("Finish solver set-up") + def _set_initial_conditions(self, model, time, inputs): + len_tot = model.len_rhs_and_alg + model.len_rhs_sens + model.len_alg_sens + y_zero = np.zeros((len_tot, 1)) + + casadi_format = model.convert_to_format == "casadi" + if casadi_format: + # stack inputs + inputs_y0_ics = casadi.vertcat(*[x for x in inputs.values()]) + else: + inputs_y0_ics = inputs + + model.y0 = model.initial_conditions_eval(time, y_zero, inputs_y0_ics) + + if model.jacp_initial_conditions_eval is None: + model.y0S = None + return + + if casadi_format: + inputs_jacp_ics = inputs_y0_ics + else: + # we are calculating the derivative wrt the inputs + # so need to make sure we convert int -> float + # This is to satisfy JAX jacfwd function which requires + # float inputs + inputs_jacp_ics = { + key: float(value) if isinstance(value, int) else value + for key, value in inputs.items() + } + + model.y0S = model.jacp_initial_conditions_eval(time, y_zero, inputs_jacp_ics) + @classmethod def _wrangle_name(cls, name: str) -> str: """ @@ -434,6 +440,12 @@ def _set_up_model_sensitivities_inplace( model.len_rhs_sens = 0 model.len_alg_sens = 0 + has_mass_matrix = model.mass_matrix is not None + has_mass_matrix_inv = model.mass_matrix_inv is not None + + if not has_mass_matrix: + return + # if we will change the equations to include the explicit sensitivity # equations, then we also need to update the mass matrix and bounds. # First, we reset the mass matrix and bounds back to their original form @@ -443,48 +455,38 @@ def _set_up_model_sensitivities_inplace( model.bounds[0][: model.len_rhs_and_alg], model.bounds[1][: model.len_rhs_and_alg], ) - if ( - model.mass_matrix is not None - and model.mass_matrix.shape[0] > model.len_rhs_and_alg - ): - if model.mass_matrix_inv is not None: - model.mass_matrix_inv = pybamm.Matrix( - model.mass_matrix_inv.entries[: model.len_rhs, : model.len_rhs] - ) - model.mass_matrix = pybamm.Matrix( - model.mass_matrix.entries[ - : model.len_rhs_and_alg, : model.len_rhs_and_alg - ] + model.mass_matrix = pybamm.Matrix( + model.mass_matrix.entries[: model.len_rhs_and_alg, : model.len_rhs_and_alg] + ) + if has_mass_matrix_inv: + model.mass_matrix_inv = pybamm.Matrix( + model.mass_matrix_inv.entries[: model.len_rhs, : model.len_rhs] ) # now we can extend them by the number of sensitivity parameters - # if needed - if calculate_sensitivities_explicit: - if model.len_rhs != 0: - n_inputs = model.len_rhs_sens // model.len_rhs - elif model.len_alg != 0: - n_inputs = model.len_alg_sens // model.len_alg - if model.bounds[0].shape[0] == model.len_rhs_and_alg: - model.bounds = ( - np.repeat(model.bounds[0], n_inputs + 1), - np.repeat(model.bounds[1], n_inputs + 1), - ) - if ( - model.mass_matrix is not None - and model.mass_matrix.shape[0] == model.len_rhs_and_alg - ): - if model.mass_matrix_inv is not None: - model.mass_matrix_inv = pybamm.Matrix( - block_diag( - [model.mass_matrix_inv.entries] * (n_inputs + 1), - format="csr", - ) - ) - model.mass_matrix = pybamm.Matrix( - block_diag( - [model.mass_matrix.entries] * (n_inputs + 1), format="csr" - ) - ) + # if necessary + if not calculate_sensitivities_explicit: + return + + if model.bounds[0].shape[0] == model.len_rhs_and_alg: + model.bounds = ( + np.repeat(model.bounds[0], num_parameters + 1), + np.repeat(model.bounds[1], num_parameters + 1), + ) + + # if we have a mass matrix, we need to extend it + def extend_mass_matrix(M): + M_extend = [M.entries] * (num_parameters + 1) + M_extend_pybamm = pybamm.Matrix(block_diag(M_extend, format="csr")) + return M_extend_pybamm + + model.mass_matrix = extend_mass_matrix(model.mass_matrix) + model.mass_matrix = extend_mass_matrix(model.mass_matrix) + + model.mass_matrix = extend_mass_matrix(model.mass_matrix) + + if has_mass_matrix_inv: + model.mass_matrix_inv = extend_mass_matrix(model.mass_matrix_inv) def _set_up_events(self, model, t_eval, inputs, vars_for_processing): # Check for heaviside and modulo functions in rhs and algebraic and add @@ -493,45 +495,44 @@ def _set_up_events(self, model, t_eval, inputs, vars_for_processing): # but also accounts for the fact that t might be dimensional # Only do this for DAE models as ODE models can deal with discontinuities # fine + if len(model.algebraic) > 0: for symbol in itertools.chain( model.concatenated_rhs.pre_order(), model.concatenated_algebraic.pre_order(), ): if isinstance(symbol, _Heaviside): - expr = None if symbol.right == pybamm.t: expr = symbol.left + elif symbol.left == pybamm.t: + expr = symbol.right else: - if symbol.left == pybamm.t: - expr = symbol.right + # Heaviside function does not contain pybamm.t as an argument. + # Do not create an event + continue # pragma: no cover + + model.events.append( + pybamm.Event( + str(symbol), + expr, + pybamm.EventType.DISCONTINUITY, + ) + ) + + elif isinstance(symbol, pybamm.Modulo) and symbol.left == pybamm.t: + expr = symbol.right + num_events = 200 if (t_eval is None) else (t_eval[-1] // expr.value) - # Update the events if the heaviside function depended on t - if expr is not None: + for i in np.arange(num_events): model.events.append( pybamm.Event( str(symbol), - expr, + expr * pybamm.Scalar(i + 1), pybamm.EventType.DISCONTINUITY, ) ) - elif isinstance(symbol, pybamm.Modulo): - if symbol.left == pybamm.t: - expr = symbol.right - num_events = 200 - if t_eval is not None: - num_events = t_eval[-1] // expr.value - - for i in np.arange(num_events): - model.events.append( - pybamm.Event( - str(symbol), - expr * pybamm.Scalar(i + 1), - pybamm.EventType.DISCONTINUITY, - ) - ) else: - pass + continue casadi_switch_events = [] terminate_events = [] @@ -542,40 +543,37 @@ def _set_up_events(self, model, t_eval, inputs, vars_for_processing): # discontinuity events are evaluated before the solver is called, # so don't need to process them discontinuity_events.append(event) - elif event.event_type == pybamm.EventType.SWITCH: - if ( - isinstance(self, pybamm.CasadiSolver) - and self.mode == "fast with events" - and model.algebraic != {} - ): - # Save some events to casadi_switch_events for the 'fast with - # events' mode of the casadi solver - # We only need to do this if the model is a DAE model - # see #1082 - k = 20 - # address numpy 1.25 deprecation warning: array should have - # ndim=0 before conversion - init_sign = float( - np.sign( - event.evaluate(0, model.y0.full(), inputs=inputs) - ).item() - ) - # We create a sigmoid for each event which will multiply the - # rhs. Doing * 2 - 1 ensures that when the event is crossed, - # the sigmoid is zero. Hence the rhs is zero and the solution - # stays constant for the rest of the simulation period - # We can then cut off the part after the event was crossed - event_sigmoid = ( - pybamm.sigmoid(0, init_sign * event.expression, k) * 2 - 1 - ) - event_casadi = process( - event_sigmoid, - f"event_{n}", - vars_for_processing, - use_jacobian=False, - )[0] - # use the actual casadi object as this will go into the rhs - casadi_switch_events.append(event_casadi) + elif event.event_type == pybamm.EventType.SWITCH and ( + isinstance(self, pybamm.CasadiSolver) + and self.mode == "fast with events" + and model.algebraic != {} + ): + # Save some events to casadi_switch_events for the 'fast with + # events' mode of the casadi solver + # We only need to do this if the model is a DAE model + # see #1082 + k = 20 + # address numpy 1.25 deprecation warning: array should have + # ndim=0 before conversion + init_sign = float( + np.sign(event.evaluate(0, model.y0.full(), inputs=inputs)).item() + ) + # We create a sigmoid for each event which will multiply the + # rhs. Doing * 2 - 1 ensures that when the event is crossed, + # the sigmoid is zero. Hence the rhs is zero and the solution + # stays constant for the rest of the simulation period + # We can then cut off the part after the event was crossed + event_sigmoid = ( + pybamm.sigmoid(0, init_sign * event.expression, k) * 2 - 1 + ) + event_casadi = process( + event_sigmoid, + f"event_{n}", + vars_for_processing, + use_jacobian=False, + )[0] + # use the actual casadi object as this will go into the rhs + casadi_switch_events.append(event_casadi) else: # use the function call event_call = process( @@ -596,9 +594,9 @@ def _set_up_events(self, model, t_eval, inputs, vars_for_processing): discontinuity_events, ) - def _set_initial_conditions(self, model, time, inputs_dict, update_rhs): + def _set_consistent_initialization(self, model, time, inputs_dict): """ - Set initial conditions for the model. This is skipped if the solver is an + Set initialized states for the model. This is skipped if the solver is an algebraic solver (since this would make the algebraic solver redundant), and if the model doesn't have any algebraic equations (since there are no initial conditions to be calculated in this case). @@ -607,6 +605,8 @@ def _set_initial_conditions(self, model, time, inputs_dict, update_rhs): ---------- model : :class:`pybamm.BaseModel` The model for which to calculate initial conditions. + time : numeric type + The time at which to calculate the initial conditions inputs_dict : dict Any input parameters to pass to the model when solving update_rhs : bool @@ -614,48 +614,12 @@ def _set_initial_conditions(self, model, time, inputs_dict, update_rhs): """ - y0_total_size = ( - model.len_rhs + model.len_rhs_sens + model.len_alg + model.len_alg_sens - ) - y_zero = np.zeros((y0_total_size, 1)) - - if model.convert_to_format == "casadi": - # stack inputs - inputs = casadi.vertcat(*[x for x in inputs_dict.values()]) - else: - inputs = inputs_dict - - if self.algebraic_solver is True: + if self.algebraic_solver or model.len_alg == 0: # Don't update model.y0 return - elif len(model.algebraic) == 0: - if update_rhs is True: - # Recalculate initial conditions for the rhs equations - y0 = model.initial_conditions_eval(time, y_zero, inputs) - else: - # Don't update model.y0 - return - else: - if update_rhs is True: - # Recalculate initial conditions for the rhs equations - y0_from_inputs = model.initial_conditions_eval(time, y_zero, inputs) - # Reuse old solution for algebraic equations - y0_from_model = model.y0 - len_rhs = model.len_rhs - # update model.y0, which is used for initialising the algebraic solver - if len_rhs == 0: - model.y0 = y0_from_model - elif isinstance(y0_from_inputs, casadi.DM): - model.y0 = casadi.vertcat( - y0_from_inputs[:len_rhs], y0_from_model[len_rhs:] - ) - else: - model.y0 = np.vstack( - (y0_from_inputs[:len_rhs], y0_from_model[len_rhs:]) - ) - y0 = self.calculate_consistent_state(model, time, inputs_dict) - # Make y0 a function of inputs if doing symbolic with casadi - model.y0 = y0 + + # Calculate consistent states for the algebraic equations + model.y0 = self.calculate_consistent_state(model, time, inputs_dict) def calculate_consistent_state(self, model, time=0, inputs=None): """ @@ -667,7 +631,7 @@ def calculate_consistent_state(self, model, time=0, inputs=None): model : :class:`pybamm.BaseModel` The model for which to calculate initial conditions. time : float - The time at which to calculate the states + The time at which to calculate the initial conditions inputs: dict, optional Any input parameters to pass to the model when solving @@ -751,24 +715,14 @@ def solve( calculate_sensitivities_list = calculate_sensitivities # Make sure model isn't empty - if len(model.rhs) == 0 and len(model.algebraic) == 0: - if not isinstance(self, pybamm.DummySolver): - # check for a discretised model without original parameters - if not ( - model.concatenated_rhs is not None - or model.concatenated_algebraic is not None - ): - raise pybamm.ModelError( - "Cannot solve empty model, use `pybamm.DummySolver` instead" - ) + self._check_empty_model(model) # t_eval can only be None if the solver is an algebraic solver. In that case # set it to 0 if t_eval is None: - if self.algebraic_solver is True: - t_eval = np.array([0]) - else: + if self.algebraic_solver is False: raise ValueError("t_eval cannot be None") + t_eval = np.array([0]) # If t_eval is provided as [t0, tf] return the solution at 100 points elif isinstance(t_eval, list): @@ -799,11 +753,32 @@ def solve( self._set_up_model_inputs(model, inputs) for inputs in inputs_list ] + # (Re-)calculate consistent initialization + # Assuming initial conditions do not depend on input parameters + # when len(inputs_list) > 1, only `model_inputs_list[0]` + # is passed to `_set_consistent_initialization`. + # See https://github.com/pybamm-team/PyBaMM/pull/1261 + if len(inputs_list) > 1: + all_inputs_names = set( + itertools.chain.from_iterable( + [model_inputs.keys() for model_inputs in model_inputs_list] + ) + ) + initial_conditions_node_names = set( + [it.name for it in model.concatenated_initial_conditions.pre_order()] + ) + if all_inputs_names.issubset(initial_conditions_node_names): + raise pybamm.SolverError( + "Input parameters cannot appear in expression " + "for initial conditions." + ) + # Check that calculate_sensitivites have not been updated calculate_sensitivities_list.sort() - if not hasattr(model, "calculate_sensitivities"): + if hasattr(model, "calculate_sensitivities"): + model.calculate_sensitivities.sort() + else: model.calculate_sensitivities = [] - model.calculate_sensitivities.sort() if calculate_sensitivities_list != model.calculate_sensitivities: self._model_set_up.pop(model, None) # CasadiSolver caches its integrators using model, so delete this too @@ -816,6 +791,7 @@ def solve( # Set up (if not done already) timer = pybamm.Timer() + # Set the initial conditions if model not in self._model_set_up: if len(self._model_set_up) > 0: existing_model = next(iter(self._model_set_up)) @@ -833,52 +809,34 @@ def solve( self._model_set_up.update( {model: {"initial conditions": model.concatenated_initial_conditions}} ) + elif ( + self._model_set_up[model]["initial conditions"] + != model.concatenated_initial_conditions + ): + if self.algebraic_solver: + # For an algebraic solver, we don't need to set up the initial + # conditions function and we can just evaluate + # model.concatenated_initial_conditions + model.y0 = model.concatenated_initial_conditions.evaluate() + else: + # If the new initial conditions are different + # and cannot be evaluated directly, set up again + self.set_up(model, model_inputs_list[0], t_eval, ics_only=True) + self._model_set_up[model]["initial conditions"] = ( + model.concatenated_initial_conditions + ) else: - ics_set_up = self._model_set_up[model]["initial conditions"] - # Check that initial conditions have not been updated - if ics_set_up != model.concatenated_initial_conditions: - if self.algebraic_solver is True: - # For an algebraic solver, we don't need to set up the initial - # conditions function and we can just evaluate - # model.concatenated_initial_conditions - model.y0 = model.concatenated_initial_conditions.evaluate() - else: - # If the new initial conditions are different - # and cannot be evaluated directly, set up again - self.set_up(model, model_inputs_list[0], t_eval, ics_only=True) - self._model_set_up[model]["initial conditions"] = ( - model.concatenated_initial_conditions - ) + # Set the standard initial conditions + self._set_initial_conditions(model, t_eval[0], model_inputs_list[0]) + + # Solve for the consistent initialization + self._set_consistent_initialization(model, t_eval[0], model_inputs_list[0]) set_up_time = timer.time() timer.reset() - # (Re-)calculate consistent initial conditions - # Assuming initial conditions do not depend on input parameters - # when len(inputs_list) > 1, only `model_inputs_list[0]` - # is passed to `_set_initial_conditions`. - # See https://github.com/pybamm-team/PyBaMM/pull/1261 - if len(inputs_list) > 1: - all_inputs_names = set( - itertools.chain.from_iterable( - [model_inputs.keys() for model_inputs in model_inputs_list] - ) - ) - initial_conditions_node_names = set( - [it.name for it in model.concatenated_initial_conditions.pre_order()] - ) - if all_inputs_names.issubset(initial_conditions_node_names): - raise pybamm.SolverError( - "Input parameters cannot appear in expression " - "for initial conditions." - ) - - self._set_initial_conditions( - model, t_eval[0], model_inputs_list[0], update_rhs=True - ) - # Check initial conditions don't violate events - self._check_events_with_initial_conditions(t_eval, model, model_inputs_list[0]) + self._check_events_with_initialization(t_eval, model, model_inputs_list[0]) # Process discontinuities ( @@ -904,26 +862,25 @@ def solve( model_inputs_list[0], ) new_solutions = [new_solution] + elif model.convert_to_format == "jax": + # Jax can parallelize over the inputs efficiently + new_solutions = self._integrate( + model, + t_eval[start_index:end_index], + model_inputs_list, + ) else: - if model.convert_to_format == "jax": - # Jax can parallelize over the inputs efficiently - new_solutions = self._integrate( - model, - t_eval[start_index:end_index], - model_inputs_list, + with mp.get_context(self._mp_context).Pool(processes=nproc) as p: + new_solutions = p.starmap( + self._integrate, + zip( + [model] * ninputs, + [t_eval[start_index:end_index]] * ninputs, + model_inputs_list, + ), ) - else: - with mp.get_context(self._mp_context).Pool(processes=nproc) as p: - new_solutions = p.starmap( - self._integrate, - zip( - [model] * ninputs, - [t_eval[start_index:end_index]] * ninputs, - model_inputs_list, - ), - ) - p.close() - p.join() + p.close() + p.join() # Setting the solve time for each segment. # pybamm.Solution.__add__ assumes attribute solve_time. solve_time = timer.time() @@ -1059,7 +1016,7 @@ def _get_discontinuity_start_end_indices(model, inputs, t_eval): return start_indices, end_indices, t_eval @staticmethod - def _check_events_with_initial_conditions(t_eval, model, inputs_dict): + def _check_events_with_initialization(t_eval, model, inputs_dict): num_terminate_events = len(model.terminate_events_eval) if num_terminate_events == 0: return @@ -1140,11 +1097,7 @@ def step( return old_solution # Make sure model isn't empty - if len(model.rhs) == 0 and len(model.algebraic) == 0: - if not isinstance(self, pybamm.DummySolver): - raise pybamm.ModelError( - "Cannot step empty model, use `pybamm.DummySolver` instead" - ) + self._check_empty_model(model) # Make sure dt is greater than the offset step_start_offset = pybamm.settings.step_start_offset @@ -1161,16 +1114,14 @@ def step( stacklevel=2, ) t_eval = np.linspace(0, dt, npts) - - if t_eval is not None: - # Checking if t_eval lies within range - if t_eval[0] != 0 or t_eval[-1] != dt: - raise pybamm.SolverError( - "Elements inside array t_eval must lie in the closed interval 0 to dt" - ) - - else: + elif t_eval is None: t_eval = np.array([0, dt]) + elif t_eval[0] != 0 or t_eval[-1] != dt: + raise pybamm.SolverError( + "Elements inside array t_eval must lie in the closed interval 0 to dt" + ) + else: + pass t_start = old_solution.t[-1] t_eval = t_start + t_eval @@ -1192,9 +1143,8 @@ def step( # Set up inputs model_inputs = self._set_up_model_inputs(model, inputs) - first_step_this_model = False - if model not in self._model_set_up: - first_step_this_model = True + first_step_this_model = model not in self._model_set_up + if first_step_this_model: if len(self._model_set_up) > 0: existing_model = next(iter(self._model_set_up)) raise RuntimeError( @@ -1217,27 +1167,22 @@ def step( if not first_step_this_model: # reset y0 to original initial conditions self.set_up(model, model_inputs, ics_only=True) + elif old_solution.all_models[-1] == model: + # initialize with old solution + model.y0 = old_solution.all_ys[-1][:, -1] else: - if old_solution.all_models[-1] == model: - # initialize with old solution - model.y0 = old_solution.all_ys[-1][:, -1] - else: - _, concatenated_initial_conditions = model.set_initial_conditions_from( - old_solution, return_type="ics" - ) - model.y0 = concatenated_initial_conditions.evaluate( - 0, inputs=model_inputs - ) + _, concatenated_initial_conditions = model.set_initial_conditions_from( + old_solution, return_type="ics" + ) + model.y0 = concatenated_initial_conditions.evaluate(0, inputs=model_inputs) set_up_time = timer.time() - # (Re-)calculate consistent initial conditions - self._set_initial_conditions( - model, t_start_shifted, model_inputs, update_rhs=False - ) + # (Re-)calculate consistent initialization + self._set_consistent_initialization(model, t_start_shifted, model_inputs) - # Check initial conditions don't violate events - self._check_events_with_initial_conditions(t_eval, model, model_inputs) + # Check consistent initialization doesn't violate events + self._check_events_with_initialization(t_eval, model, model_inputs) # Step pybamm.logger.verbose(f"Stepping for {t_start_shifted:.0f} < t < {t_end:.0f}") @@ -1293,52 +1238,53 @@ def get_termination_reason(solution, events): solution, "the solver successfully reached the end of the integration interval", ) - elif solution.termination == "event": - pybamm.logger.debug("Start post-processing events") - if solution.closest_event_idx is not None: - solution.termination = ( - f"event: {termination_events[solution.closest_event_idx].name}" - ) - else: - # Get final event value - final_event_values = {} - - for event in termination_events: - final_event_values[event.name] = event.expression.evaluate( - solution.t_event, - solution.y_event, - inputs=solution.all_inputs[-1], - ) - termination_event = min(final_event_values, key=final_event_values.get) - - # Check that it's actually an event - if final_event_values[termination_event] > 0.1: # pragma: no cover - # Hard to test this - raise pybamm.SolverError( - "Could not determine which event was triggered " - "(possibly due to NaNs)" - ) - # Add the event to the solution object - solution.termination = f"event: {termination_event}" - # Update t, y and inputs to include event time and state - # Note: if the final entry of t is equal to the event time we skip - # this (having duplicate entries causes an error later in ProcessedVariable) - if solution.t_event != solution.all_ts[-1][-1]: - event_sol = pybamm.Solution( - solution.t_event, - solution.y_event, - solution.all_models[-1], - solution.all_inputs[-1], + + # solution.termination == "event": + pybamm.logger.debug("Start post-processing events") + if solution.closest_event_idx is not None: + solution.termination = ( + f"event: {termination_events[solution.closest_event_idx].name}" + ) + else: + # Get final event value + final_event_values = {} + + for event in termination_events: + final_event_values[event.name] = event.expression.evaluate( solution.t_event, solution.y_event, - solution.termination, + inputs=solution.all_inputs[-1], ) - event_sol.solve_time = 0 - event_sol.integration_time = 0 - solution = solution + event_sol + termination_event = min(final_event_values, key=final_event_values.get) - pybamm.logger.debug("Finish post-processing events") - return solution, solution.termination + # Check that it's actually an event + if final_event_values[termination_event] > 0.1: # pragma: no cover + # Hard to test this + raise pybamm.SolverError( + "Could not determine which event was triggered " + "(possibly due to NaNs)" + ) + # Add the event to the solution object + solution.termination = f"event: {termination_event}" + # Update t, y and inputs to include event time and state + # Note: if the final entry of t is equal to the event time we skip + # this (having duplicate entries causes an error later in ProcessedVariable) + if solution.t_event != solution.all_ts[-1][-1]: + event_sol = pybamm.Solution( + solution.t_event, + solution.y_event, + solution.all_models[-1], + solution.all_inputs[-1], + solution.t_event, + solution.y_event, + solution.termination, + ) + event_sol.solve_time = 0 + event_sol.integration_time = 0 + solution = solution + event_sol + + pybamm.logger.debug("Finish post-processing events") + return solution, solution.termination def check_extrapolation(self, solution, events): """ @@ -1356,40 +1302,63 @@ def check_extrapolation(self, solution, events): """ extrap_events = [] - if any( - event.event_type == pybamm.EventType.INTERPOLANT_EXTRAPOLATION + # Add the event dictionary to the solution object + solution.extrap_events = extrap_events + + # first pass: check if any events are extrapolation events + if all( + event.event_type != pybamm.EventType.INTERPOLANT_EXTRAPOLATION for event in events ): - last_state = solution.last_state - t = last_state.all_ts[0][0] - y = last_state.all_ys[0][:, 0] - inputs = last_state.all_inputs[0] - - if isinstance(y, casadi.DM): - y = y.full() - for event in events: - if event.event_type == pybamm.EventType.INTERPOLANT_EXTRAPOLATION: - if event.expression.evaluate(t, y, inputs=inputs) < self.extrap_tol: - extrap_events.append(event.name) - - if any(extrap_events): - if self._on_extrapolation == "warn": - name = solution.all_models[-1].name - warnings.warn( - f"While solving {name} extrapolation occurred " - f"for {extrap_events}", - pybamm.SolverWarning, - stacklevel=2, - ) - # Add the event dictionary to the solution object - solution.extrap_events = extrap_events - elif self._on_extrapolation == "error": - raise pybamm.SolverError( - "Solver failed because the following " - f"interpolation bounds were exceeded: {extrap_events}. " - "You may need to provide additional interpolation points " - "outside these bounds." - ) + # no extrapolation events to check + return + + # second pass: check if the extrapolation events are within the tolerance + last_state = solution.last_state + t = last_state.all_ts[0][0] + y = last_state.all_ys[0][:, 0] + inputs = last_state.all_inputs[0] + + if isinstance(y, casadi.DM): + y = y.full() + + for event in events: + if ( + event.event_type == pybamm.EventType.INTERPOLANT_EXTRAPOLATION + and event.expression.evaluate(t, y, inputs=inputs) < self.extrap_tol + ): + extrap_events.append(event.name) + + if len(extrap_events) == 0: + # no extrapolation events are within the tolerance + return + + if self._on_extrapolation == "error": + raise pybamm.SolverError( + "Solver failed because the following " + f"interpolation bounds were exceeded: {extrap_events}. " + "You may need to provide additional interpolation points " + "outside these bounds." + ) + elif self._on_extrapolation == "warn": + name = solution.all_models[-1].name + warnings.warn( + f"While solving {name} extrapolation occurred " f"for {extrap_events}", + pybamm.SolverWarning, + stacklevel=2, + ) + + def _check_empty_model(self, model): + # Make sure model isn't empty + if ( + (len(model.rhs) == 0 and len(model.algebraic) == 0) + and model.concatenated_rhs is None + and model.concatenated_algebraic is None + and not isinstance(self, pybamm.DummySolver) + ): + raise pybamm.ModelError( + "Cannot simulate an empty model, use `pybamm.DummySolver` instead" + ) def get_platform_context(self, system_type: str): # Set context for parallel processing depending on the platform @@ -1411,10 +1380,10 @@ def _set_up_model_inputs(model, inputs): inputs_in_model = {} for input_param in model.input_parameters: name = input_param.name - if name in inputs: - inputs_in_model[name] = inputs[name] - else: + if name not in inputs: raise pybamm.SolverError(f"No value provided for input '{name}'") + inputs_in_model[name] = inputs[name] + inputs = inputs_in_model ordered_inputs_names = list(inputs.keys()) diff --git a/pybamm/solvers/idaklu_solver.py b/pybamm/solvers/idaklu_solver.py index fa92563e3b..34f7c1abaa 100644 --- a/pybamm/solvers/idaklu_solver.py +++ b/pybamm/solvers/idaklu_solver.py @@ -54,7 +54,7 @@ class IDAKLUSolver(pybamm.BaseSolver): rtol : float, optional The relative tolerance for the solver (default is 1e-6). atol : float, optional - The absolute tolerance for the solver (default is 1e-6). + The absolute tolerance for the solver (default is 1e-4). root_method : str or pybamm algebraic solver class, optional The method to use to find initial conditions (for DAE solvers). If a solver class, must be an algebraic solver class. @@ -120,9 +120,11 @@ class IDAKLUSolver(pybamm.BaseSolver): # Maximum number of error test failures in attempting one step "max_error_test_failures": 10, # Maximum number of nonlinear solver iterations at one step - "max_nonlinear_iterations": 4, + # Note: this value differs from the IDA default of 4 + "max_nonlinear_iterations": 40, # Maximum number of nonlinear solver convergence failures at one step - "max_convergence_failures": 10, + # Note: this value differs from the IDA default of 10 + "max_convergence_failures": 100, # Safety factor in the nonlinear convergence test "nonlinear_convergence_coefficient": 0.33, # Suppress algebraic variables from error test @@ -132,7 +134,8 @@ class IDAKLUSolver(pybamm.BaseSolver): # initial condition calculation "nonlinear_convergence_coefficient_ic": 0.0033, # Maximum number of steps allowed when `init_all_y_ic = False` - "max_num_steps_ic": 5, + # Note: this value differs from the IDA default of 5 + "max_num_steps_ic": 50, # Maximum number of the approximate Jacobian or preconditioner evaluations # allowed when the Newton iteration appears to be slowly converging # Note: this value differs from the IDA default of 4 @@ -194,9 +197,9 @@ def __init__( "nonlinear_convergence_coefficient": 0.33, "suppress_algebraic_error": False, "nonlinear_convergence_coefficient_ic": 0.0033, - "max_num_steps_ic": 5, - "max_num_jacobians_ic": 4, - "max_num_iterations_ic": 10, + "max_num_steps_ic": 50, + "max_num_jacobians_ic": 40, + "max_num_iterations_ic": 100, "max_linesearch_backtracks_ic": 100, "linesearch_off_ic": False, "init_all_y_ic": False, @@ -281,26 +284,6 @@ def inputs_to_dict(inputs): y0 = y0.full() y0 = y0.flatten() - y0S = model.y0S - # only casadi solver needs sensitivity ics - if model.convert_to_format != "casadi": - y0S = None - if self.output_variables and not ( - model.convert_to_format == "jax" - and self._options["jax_evaluator"] == "iree" - ): - raise pybamm.SolverError( - "output_variables can only be specified " - 'with convert_to_format="casadi", or convert_to_format="jax" ' - 'with jax_evaluator="iree"' - ) # pragma: no cover - if y0S is not None: - if isinstance(y0S, casadi.DM): - y0S = (y0S,) - - y0S = (x.full() for x in y0S) - y0S = [x.flatten() for x in y0S] - if ics_only: return base_set_up_return @@ -535,13 +518,10 @@ def sensfn(resvalS, t, y, inputs, yp, yS, ypS): for i, dFdp_i in enumerate(dFdp.values()): resvalS[i][:] = dFdy @ yS[i] - dFdyd @ ypS[i] + dFdp_i - try: - atol = model.atol - except AttributeError: - atol = self.atol + atol = getattr(model, "atol", self.atol) + atol = self._check_atol_type(atol, y0.size) rtol = self.rtol - atol = self._check_atol_type(atol, y0.size) if model.convert_to_format == "casadi" or ( model.convert_to_format == "jax" @@ -859,72 +839,22 @@ def _integrate(self, model, t_eval, inputs_dict=None): t_eval : numeric type The times at which to compute the solution inputs_dict : dict, optional - Any input parameters to pass to the model when solving + Any input parameters to pass to the model when solving. """ inputs_dict = inputs_dict or {} # stack inputs if inputs_dict: - inputs_dict_keys = list(inputs_dict.keys()) # save order arrays_to_stack = [np.array(x).reshape(-1, 1) for x in inputs_dict.values()] inputs = np.vstack(arrays_to_stack) else: inputs = np.array([[]]) - inputs_dict_keys = [] - - # do this here cause y0 is set after set_up (calc consistent conditions) - y0 = model.y0 - if isinstance(y0, casadi.DM): - y0 = y0.full() - y0 = y0.flatten() - - y0S = model.y0S - if ( - model.convert_to_format == "jax" - and self._options["jax_evaluator"] == "iree" - ): - if y0S is not None: - pybamm.demote_expressions_to_32bit = True - # preserve order of inputs - y0S = self._demote_64_to_32( - np.concatenate([y0S[k] for k in inputs_dict_keys]).flatten() - ) - y0full = self._demote_64_to_32(np.concatenate([y0, y0S]).flatten()) - ydot0S = self._demote_64_to_32(np.zeros_like(y0S)) - ydot0full = self._demote_64_to_32( - np.concatenate([np.zeros_like(y0), ydot0S]).flatten() - ) - pybamm.demote_expressions_to_32bit = False - else: - y0full = y0 - ydot0full = np.zeros_like(y0) - else: - # only casadi solver needs sensitivity ics - if model.convert_to_format != "casadi": - y0S = None - if y0S is not None: - if isinstance(y0S, casadi.DM): - y0S = (y0S,) - - y0S = (x.full() for x in y0S) - y0S = [x.flatten() for x in y0S] - - # solver works with ydot0 set to zero - ydot0 = np.zeros_like(y0) - if y0S is not None: - ydot0S = [np.zeros_like(y0S_i) for y0S_i in y0S] - y0full = np.concatenate([y0, *y0S]) - ydot0full = np.concatenate([ydot0, *ydot0S]) - else: - y0full = y0 - ydot0full = ydot0 - try: - atol = model.atol - except AttributeError: - atol = self.atol + y0full = model.y0full + ydot0full = model.ydot0full + atol = getattr(model, "atol", self.atol) + atol = self._check_atol_type(atol, y0full.size) rtol = self.rtol - atol = self._check_atol_type(atol, y0.size) timer = pybamm.Timer() if model.convert_to_format == "casadi" or ( @@ -940,8 +870,8 @@ def _integrate(self, model, t_eval, inputs_dict=None): else: sol = idaklu.solve_python( t_eval, - y0, - ydot0, + y0full, + ydot0full, self._setup["resfn"], self._setup["jac_class"].jac_res, self._setup["sensfn"], @@ -964,9 +894,8 @@ def _integrate(self, model, t_eval, inputs_dict=None): "number_of_sensitivity_parameters" ] sensitivity_names = self._setup["sensitivity_names"] - t = sol.t - number_of_timesteps = t.size - number_of_states = y0.size + number_of_timesteps = sol.t.size + number_of_states = model.len_rhs_and_alg if self.output_variables: # Substitute empty vectors for state vector 'y' y_out = np.zeros((number_of_timesteps * number_of_states, 0)) @@ -997,64 +926,211 @@ def _integrate(self, model, t_eval, inputs_dict=None): termination = "event" else: raise pybamm.SolverError("idaklu solver failed") + newsol = pybamm.Solution( sol.t, np.transpose(y_out), model, inputs_dict, - np.array([t[-1]]), + np.array([sol.t[-1]]), np.transpose(y_event)[:, np.newaxis], termination, sensitivities=yS_out, ) newsol.integration_time = integration_time - if self.output_variables: - # Populate variables and sensititivies dictionaries directly - number_of_samples = sol.y.shape[0] // number_of_timesteps - sol.y = sol.y.reshape((number_of_timesteps, number_of_samples)) - startk = 0 - for var in self.output_variables: - # ExplicitTimeIntegral's are not computed as part of the solver and - # do not need to be converted - if isinstance( - model.variables_and_events[var], pybamm.ExplicitTimeIntegral - ): - continue - if model.convert_to_format == "casadi": - len_of_var = ( - self._setup["var_fcns"][var](0.0, 0.0, 0.0).sparsity().nnz() - ) - base_variables = [self._setup["var_fcns"][var]] - elif ( - model.convert_to_format == "jax" - and self._options["jax_evaluator"] == "iree" - ): - idx = self.output_variables.index(var) - len_of_var = self._setup["var_idaklu_fcns"][idx].nnz - base_variables = [self._setup["var_idaklu_fcns"][idx]] - else: # pragma: no cover - raise pybamm.SolverError( - "Unsupported evaluation engine for convert_to_format=" - + f"{model.convert_to_format} " - + f"(jax_evaluator={self._options['jax_evaluator']})" - ) - newsol._variables[var] = pybamm.ProcessedVariableComputed( - [model.variables_and_events[var]], - base_variables, - [sol.y[:, startk : (startk + len_of_var)]], - newsol, + if not self.output_variables: + return newsol + + # Populate variables and sensititivies dictionaries directly + number_of_samples = sol.y.shape[0] // number_of_timesteps + sol.y = sol.y.reshape((number_of_timesteps, number_of_samples)) + startk = 0 + for var in self.output_variables: + # ExplicitTimeIntegral's are not computed as part of the solver and + # do not need to be converted + if isinstance(model.variables_and_events[var], pybamm.ExplicitTimeIntegral): + continue + if model.convert_to_format == "casadi": + len_of_var = ( + self._setup["var_fcns"][var](0.0, 0.0, 0.0).sparsity().nnz() ) - # Add sensitivities - newsol[var]._sensitivities = {} - if model.calculate_sensitivities: - for paramk, param in enumerate(inputs_dict.keys()): - newsol[var].add_sensitivity( - param, - [sol.yS[:, startk : (startk + len_of_var), paramk]], - ) - startk += len_of_var + base_variables = [self._setup["var_fcns"][var]] + elif ( + model.convert_to_format == "jax" + and self._options["jax_evaluator"] == "iree" + ): + idx = self.output_variables.index(var) + len_of_var = self._setup["var_idaklu_fcns"][idx].nnz + base_variables = [self._setup["var_idaklu_fcns"][idx]] + else: # pragma: no cover + raise pybamm.SolverError( + "Unsupported evaluation engine for convert_to_format=" + + f"{model.convert_to_format} " + + f"(jax_evaluator={self._options['jax_evaluator']})" + ) + newsol._variables[var] = pybamm.ProcessedVariableComputed( + [model.variables_and_events[var]], + base_variables, + [sol.y[:, startk : (startk + len_of_var)]], + newsol, + ) + # Add sensitivities + newsol[var]._sensitivities = {} + if model.calculate_sensitivities: + for paramk, param in enumerate(inputs_dict.keys()): + newsol[var].add_sensitivity( + param, + [sol.yS[:, startk : (startk + len_of_var), paramk]], + ) + startk += len_of_var return newsol + def _set_consistent_initialization(self, model, time, inputs_dict): + """ + Initialize y0 and ydot0 for the solver. In addition to calculating + y0 from BaseSolver, we also calculate ydot0 for semi-explicit DAEs + + Parameters + ---------- + model : :class:`pybamm.BaseModel` + The model for which to calculate initial conditions. + time : numeric type + The time at which to calculate the initial conditions. + inputs_dict : dict + Any input parameters to pass to the model when solving. + """ + + # set model.y0 + super()._set_consistent_initialization(model, time, inputs_dict) + + casadi_format = model.convert_to_format == "casadi" + jax_iree_format = ( + model.convert_to_format == "jax" + and self._options["jax_evaluator"] == "iree" + ) + + y0 = model.y0 + if isinstance(y0, casadi.DM): + y0 = y0.full() + y0 = y0.flatten() + + # calculate the time derivatives of the differential equations + # for semi-explicit DAEs + if model.len_rhs > 0: + ydot0 = self._rhs_dot_consistent_initialization( + y0, model, time, inputs_dict + ) + else: + ydot0 = np.zeros_like(y0) + + sensitivity = (model.y0S is not None) and (jax_iree_format or casadi_format) + if sensitivity: + y0full, ydot0full = self._sensitivity_consistent_initialization( + y0, ydot0, model, time, inputs_dict + ) + else: + y0full = y0 + ydot0full = ydot0 + + if jax_iree_format: + pybamm.demote_expressions_to_32bit = True + y0full = self._demote_64_to_32(y0full) + ydot0full = self._demote_64_to_32(ydot0full) + pybamm.demote_expressions_to_32bit = False + + model.y0full = y0full + model.ydot0full = ydot0full + + def _rhs_dot_consistent_initialization(self, y0, model, time, inputs_dict): + """ + Compute the consistent initialization of ydot0 for the differential terms + for the solver. If we have a semi-explicit DAE, we can explicitly solve + for this value using the consistently initialized y0 vector. + + Parameters + ---------- + y0 : :class:`numpy.array` + The initial values of the state vector. + model : :class:`pybamm.BaseModel` + The model for which to calculate initial conditions. + time : numeric type + The time at which to calculate the initial conditions. + inputs_dict : dict + Any input parameters to pass to the model when solving. + + """ + casadi_format = model.convert_to_format == "casadi" + + inputs_dict = inputs_dict or {} + # stack inputs + if inputs_dict: + arrays_to_stack = [np.array(x).reshape(-1, 1) for x in inputs_dict.values()] + inputs = np.vstack(arrays_to_stack) + else: + inputs = np.array([[]]) + + ydot0 = np.zeros_like(y0) + # calculate the time derivatives of the differential equations + input_eval = inputs if casadi_format else inputs_dict + + rhs_alg0 = model.rhs_algebraic_eval(time, y0, input_eval) + if isinstance(rhs_alg0, casadi.DM): + rhs_alg0 = rhs_alg0.full() + rhs_alg0 = rhs_alg0.flatten() + + rhs0 = rhs_alg0[: model.len_rhs] + + # for the differential terms, ydot = -M^-1 * (rhs) + ydot0[: model.len_rhs] = model.mass_matrix_inv.entries @ rhs0 + + return ydot0 + + def _sensitivity_consistent_initialization( + self, y0, ydot0, model, time, inputs_dict + ): + """ + Extend the consistent initialization to include the sensitivty equations + + Parameters + ---------- + y0 : :class:`numpy.array` + The initial values of the state vector. + ydot0 : :class:`numpy.array` + The initial values of the time derivatives of the state vector. + time : numeric type + The time at which to calculate the initial conditions. + model : :class:`pybamm.BaseModel` + The model for which to calculate initial conditions. + inputs_dict : dict + Any input parameters to pass to the model when solving. + + """ + + jax_iree_format = ( + model.convert_to_format == "jax" + and self._options["jax_evaluator"] == "iree" + ) + + y0S = model.y0S + + if jax_iree_format: + inputs_dict = inputs_dict or {} + inputs_dict_keys = list(inputs_dict.keys()) + y0S = np.concatenate([y0S[k] for k in inputs_dict_keys]) + elif isinstance(y0S, casadi.DM): + y0S = (y0S,) + + if isinstance(y0S[0], casadi.DM): + y0S = (x.full() for x in y0S) + y0S = [x.flatten() for x in y0S] + + y0full = np.concatenate([y0, *y0S]) + + ydot0S = [np.zeros_like(y0S_i) for y0S_i in y0S] + ydot0full = np.concatenate([ydot0, *ydot0S]) + + return y0full, ydot0full + def jaxify( self, model, diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_tests.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_tests.py index 1fff91b476..eddf2aa1e4 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_tests.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_tests.py @@ -133,6 +133,8 @@ def test_surface_form_algebraic(self): def test_kinetics_asymmetric_butler_volmer(self): options = {"intercalation kinetics": "asymmetric Butler-Volmer"} + solver = pybamm.CasadiSolver(atol=1e-14, rtol=1e-14) + parameter_values = pybamm.ParameterValues("Marquis2019") parameter_values.update( { @@ -141,7 +143,9 @@ def test_kinetics_asymmetric_butler_volmer(self): }, check_already_exists=False, ) - self.run_basic_processing_test(options, parameter_values=parameter_values) + self.run_basic_processing_test( + options, parameter_values=parameter_values, solver=solver + ) def test_kinetics_linear(self): options = {"intercalation kinetics": "linear"} diff --git a/tests/unit/test_solvers/test_base_solver.py b/tests/unit/test_solvers/test_base_solver.py index 9a6dec0eaf..c444722929 100644 --- a/tests/unit/test_solvers/test_base_solver.py +++ b/tests/unit/test_solvers/test_base_solver.py @@ -41,9 +41,10 @@ def test_root_method_init(self): def test_step_or_solve_empty_model(self): model = pybamm.BaseModel() solver = pybamm.BaseSolver() - with self.assertRaisesRegex(pybamm.ModelError, "Cannot step empty model"): + error = "Cannot simulate an empty model" + with self.assertRaisesRegex(pybamm.ModelError, error): solver.step(None, model, None) - with self.assertRaisesRegex(pybamm.ModelError, "Cannot solve empty model"): + with self.assertRaisesRegex(pybamm.ModelError, error): solver.solve(model, None) def test_t_eval_none(self): @@ -122,7 +123,7 @@ def test_ode_solver_fail_with_dae(self): with self.assertRaisesRegex(pybamm.SolverError, "Cannot use ODE solver"): solver.set_up(model) - def test_find_consistent_initial_conditions(self): + def test_find_consistent_initialization(self): # Simple system: a single algebraic equation class ScalarModel: def __init__(self): @@ -148,13 +149,13 @@ def algebraic_eval(self, t, y, inputs): solver = pybamm.BaseSolver(root_method="lm") model = ScalarModel() - init_cond = solver.calculate_consistent_state(model) - np.testing.assert_array_equal(init_cond, -2) + init_states = solver.calculate_consistent_state(model) + np.testing.assert_array_equal(init_states, -2) # with casadi solver_with_casadi = pybamm.BaseSolver(root_method="casadi", root_tol=1e-12) model = ScalarModel() - init_cond = solver_with_casadi.calculate_consistent_state(model) - np.testing.assert_array_equal(init_cond, -2) + init_states = solver_with_casadi.calculate_consistent_state(model) + np.testing.assert_array_equal(init_states, -2) # More complicated system vec = np.array([0.0, 1.0, 1.5, 2.0]) @@ -184,19 +185,19 @@ def algebraic_eval(self, t, y, inputs): return (y[1:] - vec[1:]) ** 2 model = VectorModel() - init_cond = solver.calculate_consistent_state(model) - np.testing.assert_array_almost_equal(init_cond.flatten(), vec) + init_states = solver.calculate_consistent_state(model) + np.testing.assert_array_almost_equal(init_states.flatten(), vec) # with casadi - init_cond = solver_with_casadi.calculate_consistent_state(model) - np.testing.assert_array_almost_equal(init_cond.full().flatten(), vec) + init_states = solver_with_casadi.calculate_consistent_state(model) + np.testing.assert_array_almost_equal(init_states.full().flatten(), vec) # With Jacobian def jac_dense(t, y, inputs): return 2 * np.hstack([np.zeros((3, 1)), np.diag(y[1:] - vec[1:])]) model.jac_algebraic_eval = jac_dense - init_cond = solver.calculate_consistent_state(model) - np.testing.assert_array_almost_equal(init_cond.flatten(), vec) + init_states = solver.calculate_consistent_state(model) + np.testing.assert_array_almost_equal(init_states.flatten(), vec) # With sparse Jacobian def jac_sparse(t, y, inputs): @@ -205,10 +206,10 @@ def jac_sparse(t, y, inputs): ) model.jac_algebraic_eval = jac_sparse - init_cond = solver.calculate_consistent_state(model) - np.testing.assert_array_almost_equal(init_cond.flatten(), vec) + init_states = solver.calculate_consistent_state(model) + np.testing.assert_array_almost_equal(init_states.flatten(), vec) - def test_fail_consistent_initial_conditions(self): + def test_fail_consistent_initialization(self): class Model: def __init__(self): self.y0 = np.array([2]) diff --git a/tests/unit/test_solvers/test_idaklu_solver.py b/tests/unit/test_solvers/test_idaklu_solver.py index 0f67385017..02303a364e 100644 --- a/tests/unit/test_solvers/test_idaklu_solver.py +++ b/tests/unit/test_solvers/test_idaklu_solver.py @@ -346,6 +346,46 @@ def test_ida_roberts_klu_sensitivities(self): 2 * dyda_ida[0:200:2], d2uda, decimal=decimal ) + def test_ida_roberts_consistent_initialization(self): + # this test implements a python version of the ida Roberts + # example provided in sundials + # see sundials ida examples pdf + for form in ["python", "casadi", "jax", "iree"]: + if (form == "jax" or form == "iree") and not pybamm.have_jax(): + continue + if (form == "iree") and not pybamm.have_iree(): + continue + if form == "casadi": + root_method = "casadi" + else: + root_method = "lm" + model = pybamm.BaseModel() + model.convert_to_format = "jax" if form == "iree" else form + u = pybamm.Variable("u") + v = pybamm.Variable("v") + model.rhs = {u: 0.1 * v} + model.algebraic = {v: 1 - v} + model.initial_conditions = {u: 0, v: 2} + + disc = pybamm.Discretisation() + disc.process_model(model) + + solver = pybamm.IDAKLUSolver( + root_method=root_method, + options={"jax_evaluator": "iree"} if form == "iree" else {}, + ) + + # Set up and model consistently initializate the model + solver.set_up(model) + t0 = 0.0 + solver._set_consistent_initialization(model, t0, inputs_dict={}) + + # u(t0) = 0, v(t0) = 1 + np.testing.assert_array_almost_equal(model.y0full, [0, 1]) + # u'(t0) = 0.1 * v(t0) = 0.1 + # Since v is algebraic, the initial derivative is set to 0 + np.testing.assert_array_almost_equal(model.ydot0full, [0.1, 0]) + def test_sensitivities_with_events(self): # this test implements a python version of the ida Roberts # example provided in sundials @@ -819,7 +859,7 @@ def test_with_output_variables_and_sensitivities(self): disc = pybamm.Discretisation(mesh, model.default_spatial_methods) disc.process_model(model) - t_eval = np.linspace(0, 3600, 100) + t_eval = np.linspace(0, 100, 100) options = { "linear_solver": "SUNLinSol_KLU", From 789dc49791dcc057eed9c3d607e8baa82f08789f Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Wed, 31 Jul 2024 17:20:45 +0530 Subject: [PATCH 41/82] Address RTD deprecation for build-time Sphinx context injection (#4305) * Set HTML baseurl and add RTD env vars to context * Fix failing links * suggestions from Arjun Co-authored-by: Arjun Verma --------- Co-authored-by: Ferran Brosa Planella Co-authored-by: Arjun Verma --- CONTRIBUTING.md | 4 ++-- docs/conf.py | 20 ++++++++++++++------ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f0f2274080..1a1b015c09 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -153,9 +153,9 @@ All code requires testing. We use the [pytest](https://docs.pytest.org/en/stable We use following plugins for various needs: -[nbmake](https://github.com/treebeardtech/nbmake)) : plugins to test the example notebooks. +[nbmake](https://github.com/treebeardtech/nbmake/) : plugins to test the example notebooks. -[pytest-xdist](https://pypi.org/project/pytest-xdist/)) : plugins to run tests in parallel. +[pytest-xdist](https://pypi.org/project/pytest-xdist/) : plugins to run tests in parallel. If you have `nox` installed, to run unit tests, type diff --git a/docs/conf.py b/docs/conf.py index 1d26e7ce38..a2a12bf04f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -176,13 +176,21 @@ html_sidebars = {"**": ["sidebar-nav-bs.html", "sidebar-ethical-ads.html"]} # For edit button -html_context = { - "github_user": "pybamm-team", - "github_repo": "pybamm", - "github_version": "develop", - "doc_path": "docs/", -} +html_context.update( + { + "github_user": "pybamm-team", + "github_repo": "pybamm", + "github_version": "develop", + "doc_path": "docs/", + } +) + +# Set canonical URL from the Read the Docs Domain +html_baseurl = os.getenv("READTHEDOCS_CANONICAL_URL", "") +# Tell Jinja2 templates the build is running on Read the Docs +if os.getenv("READTHEDOCS") == "True": + html_context["READTHEDOCS"] = True # -- Options for HTMLHelp output --------------------------------------------- From 8983f43a9f2ce54c6afa14e2e06edb250d426dc4 Mon Sep 17 00:00:00 2001 From: Dion Wilde <91852142+dion-w@users.noreply.github.com> Date: Wed, 31 Jul 2024 16:19:06 +0200 Subject: [PATCH 42/82] replacing rounded faraday constant with exact value in bpx.py (#4290) * updated faraday constant to exact value in bpx.py * Update CHANGELOG.md * Update CHANGELOG.md Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> * Update CHANGELOG.md * Update pybamm/parameters/bpx.py Co-authored-by: Eric G. Kratz * Update CHANGELOG.md Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> --------- Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Co-authored-by: Eric G. Kratz Co-authored-by: Arjun Verma --- CHANGELOG.md | 2 ++ pybamm/parameters/bpx.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ab711cf21..b2f8637442 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # [Unreleased](https://github.com/pybamm-team/PyBaMM/) +- Replaced rounded faraday constant with its exact value in `bpx.py` for better comparison between different tools + ## Features - Added additional user-configurable options to the (`IDAKLUSolver`) and adjusted the default values to improve performance. ([#4282](https://github.com/pybamm-team/PyBaMM/pull/4282)) diff --git a/pybamm/parameters/bpx.py b/pybamm/parameters/bpx.py index 680477a74b..2f259a8f52 100644 --- a/pybamm/parameters/bpx.py +++ b/pybamm/parameters/bpx.py @@ -240,7 +240,7 @@ def _entropic_change(sto, c_s_max, dUdT, constant=False): # (cs-cs_max)) in BPX exchange current is defined j0 = F * k_norm * sqrt((ce/ce0) * # (cs/cs_max) * (1-cs/cs_max)) c_e = pybamm_dict["Initial concentration in electrolyte [mol.m-3]"] - F = 96485 + F = pybamm.constants.F.value def _exchange_current_density(c_e, c_s_surf, c_s_max, T, k_ref, Ea): return ( From 109b9c442e8df77224bbfcf2a7909c69ec11e848 Mon Sep 17 00:00:00 2001 From: Pradyot Ranjan <99216956+prady0t@users.noreply.github.com> Date: Fri, 2 Aug 2024 16:18:12 +0530 Subject: [PATCH 43/82] Removing `testcase.py` file (#4231) * Removing testcase.py file Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * style: pre-commit fixes * Testcase error fix Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> --------- Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> Co-authored-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- tests/__init__.py | 2 +- tests/integration/test_experiments.py | 4 +- .../test_lithium_ion/test_compare_outputs.py | 4 +- .../test_compare_outputs_two_phase.py | 3 +- .../test_lithium_ion/test_dfn.py | 6 +-- .../test_lithium_ion/test_mpm.py | 4 +- .../test_lithium_ion/test_thermal_models.py | 3 +- .../test_function_control.py | 4 +- .../test_interface/test_butler_volmer.py | 4 +- .../test_interface/test_lead_acid.py | 4 +- .../test_interface/test_lithium_ion.py | 4 +- tests/integration/test_solvers/test_idaklu.py | 4 +- .../integration/test_solvers/test_solution.py | 4 +- .../test_finite_volume.py | 8 ++-- .../test_spectral_volume.py | 4 +- tests/shared.py | 7 ++++ tests/unit/test_batch_study.py | 3 +- tests/unit/test_callbacks.py | 4 +- .../test_discretisation.py | 4 +- .../unit/test_experiments/test_experiment.py | 4 +- .../test_simulation_with_experiment.py | 3 +- .../test_expression_tree/test_averages.py | 6 +-- .../test_binary_operators.py | 4 +- .../test_expression_tree/test_broadcasts.py | 18 ++++----- .../test_concatenations.py | 9 +++-- .../test_expression_tree/test_functions.py | 8 ++-- .../test_expression_tree/test_interpolant.py | 4 +- .../test_operations/test_convert_to_casadi.py | 4 +- .../test_operations/test_copy.py | 4 +- .../test_operations/test_evaluate_python.py | 4 +- .../test_operations/test_jac.py | 4 +- .../test_operations/test_jac_2D.py | 4 +- .../test_operations/test_latexify.py | 3 +- .../test_expression_tree/test_parameter.py | 6 +-- .../test_expression_tree/test_state_vector.py | 6 +-- .../unit/test_expression_tree/test_symbol.py | 6 +-- .../test_symbolic_diff.py | 4 +- .../test_unary_operators.py | 39 +++++++++---------- .../test_expression_tree/test_variable.py | 6 +-- .../test_geometry/test_battery_geometry.py | 6 +-- tests/unit/test_meshes/test_meshes.py | 6 +-- .../test_one_dimensional_submesh.py | 13 +++---- .../test_meshes/test_scikit_fem_submesh.py | 10 ++--- tests/unit/test_models/test_base_model.py | 4 +- .../test_base_battery_model.py | 6 +-- .../test_equivalent_circuit/test_thevenin.py | 4 +- .../test_lead_acid/test_loqs.py | 10 ++--- .../test_lithium_ion/test_electrode_soh.py | 16 ++++---- .../test_lithium_ion/test_mpm.py | 12 +++--- tests/unit/test_parameters/test_bpx.py | 4 +- .../test_parameters/test_current_functions.py | 4 +- .../test_parameters/test_ecm_parameters.py | 4 +- .../test_lead_acid_parameters.py | 4 +- .../test_lithium_ion_parameters.py | 4 +- .../test_parameter_sets/test_Ecker2015.py | 4 +- .../test_parameter_sets/test_OKane2022.py | 4 +- .../test_parameters_with_default_models.py | 4 +- .../test_parameters/test_parameter_values.py | 4 +- .../test_process_parameter_data.py | 4 +- tests/unit/test_plotting/test_quick_plot.py | 4 +- .../test_serialisation/test_serialisation.py | 6 +-- tests/unit/test_simulation.py | 4 +- .../test_solvers/test_algebraic_solver.py | 4 +- tests/unit/test_solvers/test_base_solver.py | 4 +- .../test_casadi_algebraic_solver.py | 5 +-- tests/unit/test_solvers/test_casadi_solver.py | 7 ++-- tests/unit/test_solvers/test_idaklu_jax.py | 6 +-- tests/unit/test_solvers/test_idaklu_solver.py | 4 +- .../unit/test_solvers/test_jax_bdf_solver.py | 4 +- tests/unit/test_solvers/test_jax_solver.py | 4 +- .../test_solvers/test_processed_variable.py | 4 +- .../test_processed_variable_computed.py | 4 +- tests/unit/test_solvers/test_scipy_solver.py | 6 +-- tests/unit/test_solvers/test_solution.py | 4 +- .../test_base_spatial_method.py | 4 +- .../test_finite_volume/test_extrapolation.py | 4 +- .../test_finite_volume/test_finite_volume.py | 4 +- .../test_ghost_nodes_and_neumann.py | 4 +- .../test_grad_div_shapes.py | 4 +- .../test_finite_volume/test_integration.py | 4 +- .../test_scikit_finite_element.py | 4 +- .../test_spectral_volume.py | 4 +- 82 files changed, 227 insertions(+), 228 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index f23a008ce0..9ca19981bb 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -44,5 +44,5 @@ get_optional_distribution_deps, get_present_optional_import_deps, no_internet_connection, + assert_domain_equal, ) -from .testcase import TestCase diff --git a/tests/integration/test_experiments.py b/tests/integration/test_experiments.py index 30db3e446c..f43c7293d2 100644 --- a/tests/integration/test_experiments.py +++ b/tests/integration/test_experiments.py @@ -1,13 +1,13 @@ # # Test some experiments # -from tests import TestCase + import pybamm import numpy as np import unittest -class TestExperiments(TestCase): +class TestExperiments(unittest.TestCase): def test_discharge_rest_charge(self): experiment = pybamm.Experiment( [ diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_compare_outputs.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_compare_outputs.py index 136350fe65..57067d6e3b 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_compare_outputs.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_compare_outputs.py @@ -1,14 +1,14 @@ # # Tests for the surface formulation # -from tests import TestCase + import pybamm import numpy as np import unittest from tests import StandardOutputComparison -class TestCompareOutputs(TestCase): +class TestCompareOutputs(unittest.TestCase): def test_compare_outputs_surface_form(self): # load models options = [ diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_compare_outputs_two_phase.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_compare_outputs_two_phase.py index b0c0fe5898..ed6d707d77 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_compare_outputs_two_phase.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_compare_outputs_two_phase.py @@ -4,10 +4,9 @@ import pybamm import numpy as np import unittest -from tests import TestCase -class TestCompareOutputsTwoPhase(TestCase): +class TestCompareOutputsTwoPhase(unittest.TestCase): def compare_outputs_two_phase_graphite_graphite(self, model_class): """ Check that a two-phase graphite-graphite model gives the same results as a diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py index 083e3b648b..1b6b29ea10 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py @@ -1,7 +1,7 @@ # # Tests for the lithium-ion DFN model # -from tests import TestCase + import pybamm import tests import numpy as np @@ -9,7 +9,7 @@ from tests import BaseIntegrationTestLithiumIon -class TestDFN(BaseIntegrationTestLithiumIon, TestCase): +class TestDFN(BaseIntegrationTestLithiumIon, unittest.TestCase): def setUp(self): self.model = pybamm.lithium_ion.DFN @@ -35,7 +35,7 @@ def positive_radius(x): self.run_basic_processing_test({}, parameter_values=param) -class TestDFNWithSizeDistribution(TestCase): +class TestDFNWithSizeDistribution(unittest.TestCase): def setUp(self): params = pybamm.ParameterValues("Marquis2019") self.params = pybamm.get_size_distribution_parameters(params) diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py index 82d228badb..7f5a5941c8 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py @@ -1,14 +1,14 @@ # # Tests for the lithium-ion MPM model # -from tests import TestCase + import pybamm import tests import numpy as np import unittest -class TestMPM(TestCase): +class TestMPM(unittest.TestCase): def test_basic_processing(self): options = {"thermal": "isothermal"} model = pybamm.lithium_ion.MPM(options) diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_thermal_models.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_thermal_models.py index 7de36da042..2b74366652 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_thermal_models.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_thermal_models.py @@ -5,10 +5,9 @@ import pybamm import numpy as np import unittest -from tests import TestCase -class TestThermal(TestCase): +class TestThermal(unittest.TestCase): def test_consistent_cooling(self): "Test the cooling is consistent between the 1D, 1+1D and 2+1D SPMe models" diff --git a/tests/integration/test_models/test_submodels/test_external_circuit/test_function_control.py b/tests/integration/test_models/test_submodels/test_external_circuit/test_function_control.py index 8909367892..c67604412c 100644 --- a/tests/integration/test_models/test_submodels/test_external_circuit/test_function_control.py +++ b/tests/integration/test_models/test_submodels/test_external_circuit/test_function_control.py @@ -1,13 +1,13 @@ # # Test function control submodel # -from tests import TestCase + import numpy as np import pybamm import unittest -class TestFunctionControl(TestCase): +class TestFunctionControl(unittest.TestCase): def test_constant_current(self): def constant_current(variables): I = variables["Current [A]"] diff --git a/tests/integration/test_models/test_submodels/test_interface/test_butler_volmer.py b/tests/integration/test_models/test_submodels/test_interface/test_butler_volmer.py index 536573bad1..f1f02350cd 100644 --- a/tests/integration/test_models/test_submodels/test_interface/test_butler_volmer.py +++ b/tests/integration/test_models/test_submodels/test_interface/test_butler_volmer.py @@ -1,7 +1,7 @@ # # Tests for the electrode-electrolyte interface equations # -from tests import TestCase + import pybamm from tests import get_discretisation_for_testing @@ -9,7 +9,7 @@ import numpy as np -class TestButlerVolmer(TestCase): +class TestButlerVolmer(unittest.TestCase): def setUp(self): self.delta_phi_s_n = pybamm.Variable( "surface potential difference [V]", diff --git a/tests/integration/test_models/test_submodels/test_interface/test_lead_acid.py b/tests/integration/test_models/test_submodels/test_interface/test_lead_acid.py index daf05ff6ca..ca41414c70 100644 --- a/tests/integration/test_models/test_submodels/test_interface/test_lead_acid.py +++ b/tests/integration/test_models/test_submodels/test_interface/test_lead_acid.py @@ -1,13 +1,13 @@ # # Tests for the electrode-electrolyte interface equations for lead-acid models # -from tests import TestCase + import pybamm from tests import get_discretisation_for_testing import unittest -class TestMainReaction(TestCase): +class TestMainReaction(unittest.TestCase): def setUp(self): c_e_n = pybamm.Variable( "Negative electrolyte concentration [mol.m-3]", diff --git a/tests/integration/test_models/test_submodels/test_interface/test_lithium_ion.py b/tests/integration/test_models/test_submodels/test_interface/test_lithium_ion.py index 90d27e2de7..135e29ad8b 100644 --- a/tests/integration/test_models/test_submodels/test_interface/test_lithium_ion.py +++ b/tests/integration/test_models/test_submodels/test_interface/test_lithium_ion.py @@ -1,7 +1,7 @@ # # Tests for the electrode-electrolyte interface equations for lithium-ion models # -from tests import TestCase + import pybamm from tests import get_discretisation_for_testing @@ -9,7 +9,7 @@ import numpy as np -class TestExchangeCurrentDensity(TestCase): +class TestExchangeCurrentDensity(unittest.TestCase): def setUp(self): c_e_n = pybamm.Variable("concentration", domain=["negative electrode"]) c_e_s = pybamm.Variable("concentration", domain=["separator"]) diff --git a/tests/integration/test_solvers/test_idaklu.py b/tests/integration/test_solvers/test_idaklu.py index eecc5dfe2b..693ec849a9 100644 --- a/tests/integration/test_solvers/test_idaklu.py +++ b/tests/integration/test_solvers/test_idaklu.py @@ -1,12 +1,12 @@ import pybamm import numpy as np import sys -from tests import TestCase + import unittest @unittest.skipIf(not pybamm.have_idaklu(), "idaklu solver is not installed") -class TestIDAKLUSolver(TestCase): +class TestIDAKLUSolver(unittest.TestCase): def test_on_spme(self): model = pybamm.lithium_ion.SPMe() geometry = model.default_geometry diff --git a/tests/integration/test_solvers/test_solution.py b/tests/integration/test_solvers/test_solution.py index d45df9fd4c..bb276a62a2 100644 --- a/tests/integration/test_solvers/test_solution.py +++ b/tests/integration/test_solvers/test_solution.py @@ -1,13 +1,13 @@ # # Tests for the Solution class # -from tests import TestCase + import pybamm import unittest import numpy as np -class TestSolution(TestCase): +class TestSolution(unittest.TestCase): def test_append(self): model = pybamm.lithium_ion.SPMe() # create geometry diff --git a/tests/integration/test_spatial_methods/test_finite_volume.py b/tests/integration/test_spatial_methods/test_finite_volume.py index ce2d07c2de..d331554853 100644 --- a/tests/integration/test_spatial_methods/test_finite_volume.py +++ b/tests/integration/test_spatial_methods/test_finite_volume.py @@ -1,7 +1,7 @@ # # Test for the operator class # -from tests import TestCase + import pybamm from tests import ( get_mesh_for_testing, @@ -13,7 +13,7 @@ import unittest -class TestFiniteVolumeConvergence(TestCase): +class TestFiniteVolumeConvergence(unittest.TestCase): def test_grad_div_broadcast(self): # create mesh and discretisation spatial_methods = {"macroscale": pybamm.FiniteVolume()} @@ -319,7 +319,7 @@ def solve_laplace_equation(coord_sys="cartesian"): return solver.solve(model) -class TestFiniteVolumeLaplacian(TestCase): +class TestFiniteVolumeLaplacian(unittest.TestCase): def test_laplacian_cartesian(self): solution = solve_laplace_equation(coord_sys="cartesian") np.testing.assert_array_almost_equal( @@ -374,7 +374,7 @@ def solve_advection_equation(direction="upwind", source=1, bc=0): return solver.solve(model, [0, 1]) -class TestUpwindDownwind(TestCase): +class TestUpwindDownwind(unittest.TestCase): def test_upwind(self): solution = solve_advection_equation("upwind") np.testing.assert_array_almost_equal( diff --git a/tests/integration/test_spatial_methods/test_spectral_volume.py b/tests/integration/test_spatial_methods/test_spectral_volume.py index 4b998cb0ed..deba95ebac 100644 --- a/tests/integration/test_spatial_methods/test_spectral_volume.py +++ b/tests/integration/test_spatial_methods/test_spectral_volume.py @@ -1,7 +1,7 @@ # # Test for the operator class # -from tests import TestCase + import pybamm import numpy as np @@ -76,7 +76,7 @@ def get_p2d_mesh_for_testing(xpts=None, rpts=10): return get_mesh_for_testing(xpts=xpts, rpts=rpts, geometry=geometry) -class TestSpectralVolumeConvergence(TestCase): +class TestSpectralVolumeConvergence(unittest.TestCase): def test_grad_div_broadcast(self): # create mesh and discretisation spatial_methods = {"macroscale": pybamm.SpectralVolume()} diff --git a/tests/shared.py b/tests/shared.py index 2fa9c24960..48e54e19d8 100644 --- a/tests/shared.py +++ b/tests/shared.py @@ -338,3 +338,10 @@ def no_internet_connection(): return False except socket.gaierror: return True + + +def assert_domain_equal(a, b): + "Check that two domains are equal, ignoring empty domains" + a_dict = {k: v for k, v in a.items() if v != []} + b_dict = {k: v for k, v in b.items() if v != []} + assert a_dict == b_dict diff --git a/tests/unit/test_batch_study.py b/tests/unit/test_batch_study.py index 2713a3f35b..9d13d71d9d 100644 --- a/tests/unit/test_batch_study.py +++ b/tests/unit/test_batch_study.py @@ -2,14 +2,13 @@ Tests for the batch_study.py """ -from tests import TestCase import os import pybamm import unittest from tempfile import TemporaryDirectory -class TestBatchStudy(TestCase): +class TestBatchStudy(unittest.TestCase): def test_solve(self): spm = pybamm.lithium_ion.SPM() spm_uniform = pybamm.lithium_ion.SPM({"particle": "uniform profile"}) diff --git a/tests/unit/test_callbacks.py b/tests/unit/test_callbacks.py index 649c7d9ec8..bc523d404f 100644 --- a/tests/unit/test_callbacks.py +++ b/tests/unit/test_callbacks.py @@ -1,7 +1,7 @@ # # Tests the citations class. # -from tests import TestCase + import pybamm import unittest import os @@ -18,7 +18,7 @@ def on_experiment_end(self, logs): print(self.name, file=f) -class TestCallbacks(TestCase): +class TestCallbacks(unittest.TestCase): def tearDown(self): # Remove any test log files that were created, even if the test fails for logfile in ["test_callback.log", "test_callback_2.log"]: diff --git a/tests/unit/test_discretisations/test_discretisation.py b/tests/unit/test_discretisations/test_discretisation.py index 61e3c2bc6e..bf698dd39c 100644 --- a/tests/unit/test_discretisations/test_discretisation.py +++ b/tests/unit/test_discretisations/test_discretisation.py @@ -1,7 +1,7 @@ # # Tests for the base model class # -from tests import TestCase + import pybamm import numpy as np @@ -18,7 +18,7 @@ from scipy.sparse.linalg import inv -class TestDiscretise(TestCase): +class TestDiscretise(unittest.TestCase): def test_concatenate_in_order(self): a = pybamm.Variable("a") b = pybamm.Variable("b") diff --git a/tests/unit/test_experiments/test_experiment.py b/tests/unit/test_experiments/test_experiment.py index 6c342bd269..87672ee0aa 100644 --- a/tests/unit/test_experiments/test_experiment.py +++ b/tests/unit/test_experiments/test_experiment.py @@ -1,13 +1,13 @@ # # Test the base experiment class # -from tests import TestCase + from datetime import datetime import pybamm import unittest -class TestExperiment(TestCase): +class TestExperiment(unittest.TestCase): def test_cycle_unpacking(self): experiment = pybamm.Experiment( [ diff --git a/tests/unit/test_experiments/test_simulation_with_experiment.py b/tests/unit/test_experiments/test_simulation_with_experiment.py index 4b3fa3366a..fb9e2b2f70 100644 --- a/tests/unit/test_experiments/test_simulation_with_experiment.py +++ b/tests/unit/test_experiments/test_simulation_with_experiment.py @@ -1,7 +1,6 @@ # # Test setting up a simulation with an experiment # -from tests import TestCase import casadi import pybamm import numpy as np @@ -16,7 +15,7 @@ def default_duration(self, value): return 1 -class TestSimulationExperiment(TestCase): +class TestSimulationExperiment(unittest.TestCase): def test_set_up(self): experiment = pybamm.Experiment( [ diff --git a/tests/unit/test_expression_tree/test_averages.py b/tests/unit/test_expression_tree/test_averages.py index 1351ee3c05..1f1385db65 100644 --- a/tests/unit/test_expression_tree/test_averages.py +++ b/tests/unit/test_expression_tree/test_averages.py @@ -2,12 +2,12 @@ # Tests for the Unary Operator classes # import unittest -from tests import TestCase import numpy as np import pybamm +from tests import assert_domain_equal -class TestUnaryOperators(TestCase): +class TestUnaryOperators(unittest.TestCase): def test_x_average(self): a = pybamm.Scalar(4) average_a = pybamm.x_average(a) @@ -112,7 +112,7 @@ def test_x_average(self): ) average_conc_broad = pybamm.x_average(conc_broad) self.assertIsInstance(average_conc_broad, pybamm.FullBroadcast) - self.assertDomainEqual( + assert_domain_equal( average_conc_broad.domains, {"primary": ["current collector"], "secondary": ["test"]}, ) diff --git a/tests/unit/test_expression_tree/test_binary_operators.py b/tests/unit/test_expression_tree/test_binary_operators.py index 9efcbb90f2..e1a14206b4 100644 --- a/tests/unit/test_expression_tree/test_binary_operators.py +++ b/tests/unit/test_expression_tree/test_binary_operators.py @@ -1,7 +1,7 @@ # # Tests for the Binary Operator classes # -from tests import TestCase + import unittest import unittest.mock as mock @@ -19,7 +19,7 @@ } -class TestBinaryOperators(TestCase): +class TestBinaryOperators(unittest.TestCase): def test_binary_operator(self): a = pybamm.Symbol("a") b = pybamm.Symbol("b") diff --git a/tests/unit/test_expression_tree/test_broadcasts.py b/tests/unit/test_expression_tree/test_broadcasts.py index be8fe1a677..e5516395e9 100644 --- a/tests/unit/test_expression_tree/test_broadcasts.py +++ b/tests/unit/test_expression_tree/test_broadcasts.py @@ -2,13 +2,13 @@ # Tests for the Broadcast class # import unittest -from tests import TestCase +from tests import assert_domain_equal import numpy as np import pybamm -class TestBroadcasts(TestCase): +class TestBroadcasts(unittest.TestCase): def test_primary_broadcast(self): a = pybamm.Symbol("a") broad_a = pybamm.PrimaryBroadcast(a, ["negative electrode"]) @@ -24,7 +24,7 @@ def test_primary_broadcast(self): auxiliary_domains={"secondary": "current collector"}, ) broad_a = pybamm.PrimaryBroadcast(a, ["negative particle"]) - self.assertDomainEqual( + assert_domain_equal( broad_a.domains, { "primary": ["negative particle"], @@ -41,7 +41,7 @@ def test_primary_broadcast(self): }, ) broad_a = pybamm.PrimaryBroadcast(a, ["negative particle"]) - self.assertDomainEqual( + assert_domain_equal( broad_a.domains, { "primary": ["negative particle"], @@ -83,7 +83,7 @@ def test_secondary_broadcast(self): auxiliary_domains={"secondary": "current collector"}, ) broad_a = pybamm.SecondaryBroadcast(a, ["negative electrode"]) - self.assertDomainEqual( + assert_domain_equal( broad_a.domains, { "primary": ["negative particle"], @@ -93,7 +93,7 @@ def test_secondary_broadcast(self): ) self.assertTrue(broad_a.broadcasts_to_nodes) broadbroad_a = pybamm.SecondaryBroadcast(broad_a, ["negative particle size"]) - self.assertDomainEqual( + assert_domain_equal( broadbroad_a.domains, { "primary": ["negative particle"], @@ -140,7 +140,7 @@ def test_tertiary_broadcast(self): }, ) broad_a = pybamm.TertiaryBroadcast(a, "negative electrode") - self.assertDomainEqual( + assert_domain_equal( broad_a.domains, { "primary": ["negative particle"], @@ -269,7 +269,7 @@ def test_broadcast_to_edges(self): auxiliary_domains={"secondary": "current collector"}, ) broad_a = pybamm.SecondaryBroadcastToEdges(a, ["negative electrode"]) - self.assertDomainEqual( + assert_domain_equal( broad_a.domains, { "primary": ["negative particle"], @@ -290,7 +290,7 @@ def test_broadcast_to_edges(self): }, ) broad_a = pybamm.TertiaryBroadcastToEdges(a, ["negative electrode"]) - self.assertDomainEqual( + assert_domain_equal( broad_a.domains, { "primary": ["negative particle"], diff --git a/tests/unit/test_expression_tree/test_concatenations.py b/tests/unit/test_expression_tree/test_concatenations.py index 03a4b5a894..a609c73be8 100644 --- a/tests/unit/test_expression_tree/test_concatenations.py +++ b/tests/unit/test_expression_tree/test_concatenations.py @@ -3,7 +3,8 @@ # import unittest import unittest.mock as mock -from tests import TestCase +from tests import assert_domain_equal + import numpy as np @@ -12,7 +13,7 @@ from tests import get_discretisation_for_testing, get_mesh_for_testing -class TestConcatenations(TestCase): +class TestConcatenations(unittest.TestCase): def test_base_concatenation(self): a = pybamm.Symbol("a", domain="test a") b = pybamm.Symbol("b", domain="test b") @@ -81,7 +82,7 @@ def test_concatenation_auxiliary_domains(self): auxiliary_domains={"secondary": "current collector"}, ) conc = pybamm.concatenation(a, b) - self.assertDomainEqual( + assert_domain_equal( conc.domains, { "primary": ["negative electrode", "separator", "positive electrode"], @@ -158,7 +159,7 @@ def test_concatenation_simplify(self): concat = pybamm.concatenation(a, b, c) self.assertIsInstance(concat, pybamm.FullBroadcast) self.assertEqual(concat.orphans[0], pybamm.Scalar(0)) - self.assertDomainEqual( + assert_domain_equal( concat.domains, { "primary": ["negative electrode", "separator", "positive electrode"], diff --git a/tests/unit/test_expression_tree/test_functions.py b/tests/unit/test_expression_tree/test_functions.py index c4b5fb4368..4d401d74bc 100644 --- a/tests/unit/test_expression_tree/test_functions.py +++ b/tests/unit/test_expression_tree/test_functions.py @@ -1,7 +1,7 @@ # # Tests for the Function classes # -from tests import TestCase + import unittest import unittest.mock as mock @@ -16,7 +16,7 @@ ) -class TestFunction(TestCase): +class TestFunction(unittest.TestCase): def test_number_input(self): # with numbers log = pybamm.Function(np.log, 10) @@ -106,7 +106,7 @@ def test_to_from_json_error(self): pybamm.Function._from_json({}) -class TestSpecificFunctions(TestCase): +class TestSpecificFunctions(unittest.TestCase): def test_to_json(self): a = pybamm.InputParameter("a") fun = pybamm.cos(a) @@ -452,7 +452,7 @@ def test_erfc(self): ) -class TestNonObjectFunctions(TestCase): +class TestNonObjectFunctions(unittest.TestCase): def test_normal_pdf(self): x = pybamm.InputParameter("x") mu = pybamm.InputParameter("mu") diff --git a/tests/unit/test_expression_tree/test_interpolant.py b/tests/unit/test_expression_tree/test_interpolant.py index f5ded9cf8e..e40b9daab0 100644 --- a/tests/unit/test_expression_tree/test_interpolant.py +++ b/tests/unit/test_expression_tree/test_interpolant.py @@ -1,7 +1,7 @@ # # Tests for the Function classes # -from tests import TestCase + import pybamm import unittest @@ -9,7 +9,7 @@ import numpy as np -class TestInterpolant(TestCase): +class TestInterpolant(unittest.TestCase): def test_errors(self): with self.assertRaisesRegex(ValueError, "x1"): pybamm.Interpolant(np.ones(10), np.ones(11), pybamm.Symbol("a")) diff --git a/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py b/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py index 20c2e40db0..e3301fdcc3 100644 --- a/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py +++ b/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py @@ -1,7 +1,7 @@ # # Test for the Simplify class # -from tests import TestCase + import casadi import numpy as np import pybamm @@ -10,7 +10,7 @@ from scipy import special -class TestCasadiConverter(TestCase): +class TestCasadiConverter(unittest.TestCase): def assert_casadi_equal(self, a, b, evalf=False): if evalf is True: self.assertTrue((casadi.evalf(a) - casadi.evalf(b)).is_zero()) diff --git a/tests/unit/test_expression_tree/test_operations/test_copy.py b/tests/unit/test_expression_tree/test_operations/test_copy.py index ca6ac6c448..5c1e3b29ca 100644 --- a/tests/unit/test_expression_tree/test_operations/test_copy.py +++ b/tests/unit/test_expression_tree/test_operations/test_copy.py @@ -1,14 +1,14 @@ # # Test for making copies # -from tests import TestCase + import numpy as np import pybamm import unittest from tests import get_mesh_for_testing -class TestCopy(TestCase): +class TestCopy(unittest.TestCase): def test_symbol_create_copy(self): a = pybamm.Parameter("a") b = pybamm.Parameter("b") diff --git a/tests/unit/test_expression_tree/test_operations/test_evaluate_python.py b/tests/unit/test_expression_tree/test_operations/test_evaluate_python.py index 6e1b155eca..667522c286 100644 --- a/tests/unit/test_expression_tree/test_operations/test_evaluate_python.py +++ b/tests/unit/test_expression_tree/test_operations/test_evaluate_python.py @@ -1,7 +1,7 @@ # # Test for the evaluate-to-python functions # -from tests import TestCase + import pybamm from tests import get_discretisation_for_testing, get_1p1d_discretisation_for_testing @@ -18,7 +18,7 @@ ) -class TestEvaluate(TestCase): +class TestEvaluate(unittest.TestCase): def test_find_symbols(self): a = pybamm.StateVector(slice(0, 1)) b = pybamm.StateVector(slice(1, 2)) diff --git a/tests/unit/test_expression_tree/test_operations/test_jac.py b/tests/unit/test_expression_tree/test_operations/test_jac.py index bec38d7243..34a65f007b 100644 --- a/tests/unit/test_expression_tree/test_operations/test_jac.py +++ b/tests/unit/test_expression_tree/test_operations/test_jac.py @@ -1,7 +1,7 @@ # # Tests for the jacobian methods # -from tests import TestCase + import pybamm import numpy as np @@ -10,7 +10,7 @@ from tests import get_mesh_for_testing -class TestJacobian(TestCase): +class TestJacobian(unittest.TestCase): def test_variable_is_statevector(self): a = pybamm.Symbol("a") with self.assertRaisesRegex( diff --git a/tests/unit/test_expression_tree/test_operations/test_jac_2D.py b/tests/unit/test_expression_tree/test_operations/test_jac_2D.py index 2be71d85e4..e7001b184b 100644 --- a/tests/unit/test_expression_tree/test_operations/test_jac_2D.py +++ b/tests/unit/test_expression_tree/test_operations/test_jac_2D.py @@ -1,7 +1,7 @@ # # Tests for the jacobian methods for two-dimensional objects # -from tests import TestCase + import pybamm import numpy as np @@ -12,7 +12,7 @@ ) -class TestJacobian(TestCase): +class TestJacobian(unittest.TestCase): def test_linear(self): y = pybamm.StateVector(slice(0, 8)) u = pybamm.StateVector(slice(0, 2), slice(4, 6)) diff --git a/tests/unit/test_expression_tree/test_operations/test_latexify.py b/tests/unit/test_expression_tree/test_operations/test_latexify.py index 0340a1d53f..746efb4269 100644 --- a/tests/unit/test_expression_tree/test_operations/test_latexify.py +++ b/tests/unit/test_expression_tree/test_operations/test_latexify.py @@ -2,7 +2,6 @@ Tests for the latexify.py """ -from tests import TestCase import os import platform import unittest @@ -11,7 +10,7 @@ import pybamm -class TestLatexify(TestCase): +class TestLatexify(unittest.TestCase): def test_latexify(self): model_dfn = pybamm.lithium_ion.DFN() func_dfn = str(model_dfn.latexify()) diff --git a/tests/unit/test_expression_tree/test_parameter.py b/tests/unit/test_expression_tree/test_parameter.py index efd9dcbfba..e2eadffba1 100644 --- a/tests/unit/test_expression_tree/test_parameter.py +++ b/tests/unit/test_expression_tree/test_parameter.py @@ -1,7 +1,7 @@ # # Tests for the Parameter class # -from tests import TestCase + import numbers import unittest @@ -9,7 +9,7 @@ import sympy -class TestParameter(TestCase): +class TestParameter(unittest.TestCase): def test_parameter_init(self): a = pybamm.Parameter("a") self.assertEqual(a.name, "a") @@ -40,7 +40,7 @@ def test_to_json_error(self): pybamm.Parameter._from_json({}) -class TestFunctionParameter(TestCase): +class TestFunctionParameter(unittest.TestCase): def test_function_parameter_init(self): var = pybamm.Variable("var") func = pybamm.FunctionParameter("func", {"var": var}) diff --git a/tests/unit/test_expression_tree/test_state_vector.py b/tests/unit/test_expression_tree/test_state_vector.py index 18025c0aa3..0cbc6faa4d 100644 --- a/tests/unit/test_expression_tree/test_state_vector.py +++ b/tests/unit/test_expression_tree/test_state_vector.py @@ -1,7 +1,7 @@ # # Tests for the State Vector class # -from tests import TestCase + import pybamm import numpy as np @@ -9,7 +9,7 @@ import unittest.mock as mock -class TestStateVector(TestCase): +class TestStateVector(unittest.TestCase): def test_evaluate(self): sv = pybamm.StateVector(slice(0, 10)) y = np.linspace(0, 2, 19) @@ -97,7 +97,7 @@ def test_to_from_json(self): pybamm.settings.debug_mode = original_debug_mode -class TestStateVectorDot(TestCase): +class TestStateVectorDot(unittest.TestCase): def test_evaluate(self): sv = pybamm.StateVectorDot(slice(0, 10)) y_dot = np.linspace(0, 2, 19) diff --git a/tests/unit/test_expression_tree/test_symbol.py b/tests/unit/test_expression_tree/test_symbol.py index e42f8dc8ef..f1090fe7dd 100644 --- a/tests/unit/test_expression_tree/test_symbol.py +++ b/tests/unit/test_expression_tree/test_symbol.py @@ -1,7 +1,7 @@ # # Test for the Symbol class # -from tests import TestCase + import os import unittest import unittest.mock as mock @@ -15,7 +15,7 @@ import sympy -class TestSymbol(TestCase): +class TestSymbol(unittest.TestCase): def test_symbol_init(self): sym = pybamm.Symbol("a symbol") with self.assertRaises(TypeError): @@ -520,7 +520,7 @@ def test_to_from_json(self): self.assertEqual(pybamm.Symbol._from_json(json_dict), symp) -class TestIsZero(TestCase): +class TestIsZero(unittest.TestCase): def test_is_scalar_zero(self): a = pybamm.Scalar(0) b = pybamm.Scalar(2) diff --git a/tests/unit/test_expression_tree/test_symbolic_diff.py b/tests/unit/test_expression_tree/test_symbolic_diff.py index a6493b66ce..fb08740305 100644 --- a/tests/unit/test_expression_tree/test_symbolic_diff.py +++ b/tests/unit/test_expression_tree/test_symbolic_diff.py @@ -1,14 +1,14 @@ # # Tests for the symbolic differentiation methods # -from tests import TestCase + import numpy as np import pybamm import unittest from numpy import testing -class TestSymbolicDifferentiation(TestCase): +class TestSymbolicDifferentiation(unittest.TestCase): def test_advanced(self): a = pybamm.StateVector(slice(0, 1)) b = pybamm.StateVector(slice(1, 2)) diff --git a/tests/unit/test_expression_tree/test_unary_operators.py b/tests/unit/test_expression_tree/test_unary_operators.py index 2f476e3d09..39cd05cf1d 100644 --- a/tests/unit/test_expression_tree/test_unary_operators.py +++ b/tests/unit/test_expression_tree/test_unary_operators.py @@ -2,7 +2,7 @@ # Tests for the Unary Operator classes # import unittest -from tests import TestCase + import unittest.mock as mock import numpy as np @@ -10,11 +10,12 @@ import sympy from sympy.vector.operators import Divergence as sympy_Divergence from sympy.vector.operators import Gradient as sympy_Gradient +from tests import assert_domain_equal import pybamm -class TestUnaryOperators(TestCase): +class TestUnaryOperators(unittest.TestCase): def test_unary_operator(self): a = pybamm.Symbol("a", domain=["test"]) un = pybamm.UnaryOperator("unary test", a) @@ -290,7 +291,7 @@ def test_integral(self): self.assertEqual(inta.name, "integral dx ['negative electrode']") self.assertEqual(inta.children[0].name, a.name) self.assertEqual(inta.integration_variable[0], x) - self.assertDomainEqual(inta.domains, {}) + assert_domain_equal(inta.domains, {}) # space integral with secondary domain a_sec = pybamm.Symbol( "a", @@ -299,7 +300,7 @@ def test_integral(self): ) x = pybamm.SpatialVariable("x", ["negative electrode"]) inta_sec = pybamm.Integral(a_sec, x) - self.assertDomainEqual(inta_sec.domains, {"primary": ["current collector"]}) + assert_domain_equal(inta_sec.domains, {"primary": ["current collector"]}) # space integral with tertiary domain a_tert = pybamm.Symbol( "a", @@ -311,7 +312,7 @@ def test_integral(self): ) x = pybamm.SpatialVariable("x", ["negative electrode"]) inta_tert = pybamm.Integral(a_tert, x) - self.assertDomainEqual( + assert_domain_equal( inta_tert.domains, {"primary": ["current collector"], "secondary": ["some extra domain"]}, ) @@ -326,7 +327,7 @@ def test_integral(self): }, ) inta_quat = pybamm.Integral(a_quat, x) - self.assertDomainEqual( + assert_domain_equal( inta_quat.domains, { "primary": ["current collector"], @@ -339,16 +340,16 @@ def test_integral(self): y = pybamm.SpatialVariable("y", ["current collector"]) # without a tertiary domain inta_sec_y = pybamm.Integral(a_sec, y) - self.assertDomainEqual(inta_sec_y.domains, {"primary": ["negative electrode"]}) + assert_domain_equal(inta_sec_y.domains, {"primary": ["negative electrode"]}) # with a tertiary domain inta_tert_y = pybamm.Integral(a_tert, y) - self.assertDomainEqual( + assert_domain_equal( inta_tert_y.domains, {"primary": ["negative electrode"], "secondary": ["some extra domain"]}, ) # with a quaternary domain inta_quat_y = pybamm.Integral(a_quat, y) - self.assertDomainEqual( + assert_domain_equal( inta_quat_y.domains, { "primary": ["negative electrode"], @@ -360,13 +361,13 @@ def test_integral(self): # space integral *in* tertiary domain z = pybamm.SpatialVariable("z", ["some extra domain"]) inta_tert_z = pybamm.Integral(a_tert, z) - self.assertDomainEqual( + assert_domain_equal( inta_tert_z.domains, {"primary": ["negative electrode"], "secondary": ["current collector"]}, ) # with a quaternary domain inta_quat_z = pybamm.Integral(a_quat, z) - self.assertDomainEqual( + assert_domain_equal( inta_quat_z.domains, { "primary": ["negative electrode"], @@ -378,7 +379,7 @@ def test_integral(self): # space integral *in* quaternary domain Z = pybamm.SpatialVariable("Z", ["another extra domain"]) inta_quat_Z = pybamm.Integral(a_quat, Z) - self.assertDomainEqual( + assert_domain_equal( inta_quat_Z.domains, { "primary": ["negative electrode"], @@ -405,7 +406,7 @@ def test_integral(self): self.assertEqual(inta.integration_variable[0], x) self.assertEqual(inta.domain, ["negative electrode"]) inta_sec = pybamm.IndefiniteIntegral(a_sec, x) - self.assertDomainEqual( + assert_domain_equal( inta_sec.domains, {"primary": ["negative electrode"], "secondary": ["current collector"]}, ) @@ -556,7 +557,7 @@ def test_delta_function(self): a = pybamm.Symbol("a", domain="some domain") delta_a = pybamm.DeltaFunction(a, "left", "another domain") self.assertEqual(delta_a.side, "left") - self.assertDomainEqual( + assert_domain_equal( delta_a.domains, {"primary": ["another domain"], "secondary": ["some domain"]}, ) @@ -595,7 +596,7 @@ def test_boundary_value(self): boundary_a = pybamm.boundary_value(a, "right") self.assertIsInstance(boundary_a, pybamm.BoundaryValue) self.assertEqual(boundary_a.side, "right") - self.assertDomainEqual(boundary_a.domains, {}) + assert_domain_equal(boundary_a.domains, {}) # test with secondary domain a_sec = pybamm.Symbol( "a", @@ -603,9 +604,7 @@ def test_boundary_value(self): auxiliary_domains={"secondary": "current collector"}, ) boundary_a_sec = pybamm.boundary_value(a_sec, "right") - self.assertDomainEqual( - boundary_a_sec.domains, {"primary": ["current collector"]} - ) + assert_domain_equal(boundary_a_sec.domains, {"primary": ["current collector"]}) # test with secondary domain and tertiary domain a_tert = pybamm.Symbol( "a", @@ -613,7 +612,7 @@ def test_boundary_value(self): auxiliary_domains={"secondary": "current collector", "tertiary": "bla"}, ) boundary_a_tert = pybamm.boundary_value(a_tert, "right") - self.assertDomainEqual( + assert_domain_equal( boundary_a_tert.domains, {"primary": ["current collector"], "secondary": ["bla"]}, ) @@ -629,7 +628,7 @@ def test_boundary_value(self): ) boundary_a_quat = pybamm.boundary_value(a_quat, "right") self.assertEqual(boundary_a_quat.domain, ["current collector"]) - self.assertDomainEqual( + assert_domain_equal( boundary_a_quat.domains, { "primary": ["current collector"], diff --git a/tests/unit/test_expression_tree/test_variable.py b/tests/unit/test_expression_tree/test_variable.py index 42ab2c0e22..fb17968ca8 100644 --- a/tests/unit/test_expression_tree/test_variable.py +++ b/tests/unit/test_expression_tree/test_variable.py @@ -1,7 +1,7 @@ # # Tests for the Variable class # -from tests import TestCase + import unittest import numpy as np @@ -10,7 +10,7 @@ import sympy -class TestVariable(TestCase): +class TestVariable(unittest.TestCase): def test_variable_init(self): a = pybamm.Variable("a") self.assertEqual(a.name, "a") @@ -69,7 +69,7 @@ def test_to_json_error(self): func.to_json() -class TestVariableDot(TestCase): +class TestVariableDot(unittest.TestCase): def test_variable_init(self): a = pybamm.VariableDot("a'") self.assertEqual(a.name, "a'") diff --git a/tests/unit/test_geometry/test_battery_geometry.py b/tests/unit/test_geometry/test_battery_geometry.py index 57d638e5ef..38e1ce1908 100644 --- a/tests/unit/test_geometry/test_battery_geometry.py +++ b/tests/unit/test_geometry/test_battery_geometry.py @@ -1,12 +1,12 @@ # # Tests for the base model class # -from tests import TestCase + import pybamm import unittest -class TestBatteryGeometry(TestCase): +class TestBatteryGeometry(unittest.TestCase): def test_geometry_keys(self): for cc_dimension in [0, 1, 2]: geometry = pybamm.battery_geometry( @@ -84,7 +84,7 @@ def test_geometry_error(self): pybamm.battery_geometry(form_factor="triangle") -class TestReadParameters(TestCase): +class TestReadParameters(unittest.TestCase): # This is the most complicated geometry and should test the parameters are # all returned for the deepest dict def test_read_parameters(self): diff --git a/tests/unit/test_meshes/test_meshes.py b/tests/unit/test_meshes/test_meshes.py index 3066d14534..2f3bffddfb 100644 --- a/tests/unit/test_meshes/test_meshes.py +++ b/tests/unit/test_meshes/test_meshes.py @@ -1,7 +1,7 @@ # # Test for the Finite Volume Mesh class # -from tests import TestCase + import pybamm import numpy as np import unittest @@ -19,7 +19,7 @@ def get_param(): ) -class TestMesh(TestCase): +class TestMesh(unittest.TestCase): def test_mesh_creation_no_parameters(self): r = pybamm.SpatialVariable( "r", domain=["negative particle"], coord_sys="spherical polar" @@ -415,7 +415,7 @@ def test_to_json(self): self.assertEqual(mesh_json, expected_json) -class TestMeshGenerator(TestCase): +class TestMeshGenerator(unittest.TestCase): def test_init_name(self): mesh_generator = pybamm.MeshGenerator(pybamm.SubMesh0D) self.assertEqual(mesh_generator.__repr__(), "Generator for SubMesh0D") diff --git a/tests/unit/test_meshes/test_one_dimensional_submesh.py b/tests/unit/test_meshes/test_one_dimensional_submesh.py index 514de4248b..82429e475c 100644 --- a/tests/unit/test_meshes/test_one_dimensional_submesh.py +++ b/tests/unit/test_meshes/test_one_dimensional_submesh.py @@ -1,10 +1,9 @@ import pybamm import unittest import numpy as np -from tests import TestCase -class TestSubMesh1D(TestCase): +class TestSubMesh1D(unittest.TestCase): def test_tabs(self): edges = np.linspace(0, 1, 10) tabs = {"negative": {"z_centre": 0}, "positive": {"z_centre": 1}} @@ -49,7 +48,7 @@ def test_to_json(self): self.assertEqual(mesh.tabs, new_mesh.tabs) -class TestUniform1DSubMesh(TestCase): +class TestUniform1DSubMesh(unittest.TestCase): def test_exceptions(self): lims = {"a": 1, "b": 2} with self.assertRaises(pybamm.GeometryError): @@ -82,7 +81,7 @@ def test_symmetric_mesh_creation_no_parameters(self): ) -class TestExponential1DSubMesh(TestCase): +class TestExponential1DSubMesh(unittest.TestCase): def test_symmetric_mesh_creation_no_parameters_even(self): r = pybamm.SpatialVariable( "r", domain=["negative particle"], coord_sys="spherical polar" @@ -208,7 +207,7 @@ def test_right_mesh_creation_no_parameters(self): ) -class TestChebyshev1DSubMesh(TestCase): +class TestChebyshev1DSubMesh(unittest.TestCase): def test_mesh_creation_no_parameters(self): r = pybamm.SpatialVariable( "r", domain=["negative particle"], coord_sys="spherical polar" @@ -236,7 +235,7 @@ def test_mesh_creation_no_parameters(self): ) -class TestUser1DSubMesh(TestCase): +class TestUser1DSubMesh(unittest.TestCase): def test_exceptions(self): edges = np.array([0, 0.3, 1]) submesh_params = {"edges": edges} @@ -298,7 +297,7 @@ def test_mesh_creation_no_parameters(self): ) -class TestSpectralVolume1DSubMesh(TestCase): +class TestSpectralVolume1DSubMesh(unittest.TestCase): def test_exceptions(self): edges = np.array([0, 0.3, 1]) submesh_params = {"edges": edges} diff --git a/tests/unit/test_meshes/test_scikit_fem_submesh.py b/tests/unit/test_meshes/test_scikit_fem_submesh.py index 83c0192d30..07e7dd016a 100644 --- a/tests/unit/test_meshes/test_scikit_fem_submesh.py +++ b/tests/unit/test_meshes/test_scikit_fem_submesh.py @@ -1,7 +1,7 @@ # # Test for the scikit-fem Finite Element Mesh class # -from tests import TestCase + import pybamm import unittest import numpy as np @@ -25,7 +25,7 @@ def get_param(): ) -class TestScikitFiniteElement2DSubMesh(TestCase): +class TestScikitFiniteElement2DSubMesh(unittest.TestCase): def test_mesh_creation(self): param = get_param() geometry = pybamm.battery_geometry( @@ -284,7 +284,7 @@ def test_to_json(self): np.testing.assert_array_equal(x, y) -class TestScikitFiniteElementChebyshev2DSubMesh(TestCase): +class TestScikitFiniteElementChebyshev2DSubMesh(unittest.TestCase): def test_mesh_creation(self): param = get_param() @@ -347,7 +347,7 @@ def test_init_failure(self): pybamm.ScikitChebyshev2DSubMesh(lims, None) -class TestScikitExponential2DSubMesh(TestCase): +class TestScikitExponential2DSubMesh(unittest.TestCase): def test_mesh_creation(self): param = get_param() @@ -416,7 +416,7 @@ def test_init_failure(self): pybamm.ScikitExponential2DSubMesh(None, None, side="bottom") -class TestScikitUser2DSubMesh(TestCase): +class TestScikitUser2DSubMesh(unittest.TestCase): def test_mesh_creation(self): param = get_param() diff --git a/tests/unit/test_models/test_base_model.py b/tests/unit/test_models/test_base_model.py index 7caa4c94b8..4d5f71201a 100644 --- a/tests/unit/test_models/test_base_model.py +++ b/tests/unit/test_models/test_base_model.py @@ -1,7 +1,7 @@ # # Tests for the base model class # -from tests import TestCase + import os import platform import subprocess # nosec @@ -16,7 +16,7 @@ import pybamm -class TestBaseModel(TestCase): +class TestBaseModel(unittest.TestCase): def test_rhs_set_get(self): model = pybamm.BaseModel() rhs = { diff --git a/tests/unit/test_models/test_full_battery_models/test_base_battery_model.py b/tests/unit/test_models/test_full_battery_models/test_base_battery_model.py index 057d6d2ad9..6ee38faf9a 100644 --- a/tests/unit/test_models/test_full_battery_models/test_base_battery_model.py +++ b/tests/unit/test_models/test_full_battery_models/test_base_battery_model.py @@ -1,7 +1,7 @@ # # Tests for the base battery model class # -from tests import TestCase + from pybamm.models.full_battery_models.base_battery_model import BatteryModelOptions import pybamm import unittest @@ -55,7 +55,7 @@ """ -class TestBaseBatteryModel(TestCase): +class TestBaseBatteryModel(unittest.TestCase): def test_process_parameters_and_discretise(self): model = pybamm.lithium_ion.SPM() # Set up geometry and parameters @@ -486,7 +486,7 @@ def test_save_load_model(self): os.remove("test_base_battery_model.json") -class TestOptions(TestCase): +class TestOptions(unittest.TestCase): def test_print_options(self): with io.StringIO() as buffer, redirect_stdout(buffer): BatteryModelOptions(OPTIONS_DICT).print_options() diff --git a/tests/unit/test_models/test_full_battery_models/test_equivalent_circuit/test_thevenin.py b/tests/unit/test_models/test_full_battery_models/test_equivalent_circuit/test_thevenin.py index 95365fb42b..f6abf592f8 100644 --- a/tests/unit/test_models/test_full_battery_models/test_equivalent_circuit/test_thevenin.py +++ b/tests/unit/test_models/test_full_battery_models/test_equivalent_circuit/test_thevenin.py @@ -1,12 +1,12 @@ # # Tests for the Thevenin equivalant circuit model # -from tests import TestCase + import pybamm import unittest -class TestThevenin(TestCase): +class TestThevenin(unittest.TestCase): def test_standard_model(self): model = pybamm.equivalent_circuit.Thevenin() model.check_well_posedness() diff --git a/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_loqs.py b/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_loqs.py index c20626856b..d68686936c 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_loqs.py +++ b/tests/unit/test_models/test_full_battery_models/test_lead_acid/test_loqs.py @@ -1,12 +1,12 @@ # # Tests for the lead-acid LOQS model # -from tests import TestCase + import pybamm import unittest -class TestLeadAcidLOQS(TestCase): +class TestLeadAcidLOQS(unittest.TestCase): def test_well_posed(self): options = {"thermal": "isothermal"} model = pybamm.lead_acid.LOQS(options) @@ -80,7 +80,7 @@ def test_well_posed_2plus1D(self): ) -class TestLeadAcidLOQSWithSideReactions(TestCase): +class TestLeadAcidLOQSWithSideReactions(unittest.TestCase): def test_well_posed_differential(self): options = {"surface form": "differential", "hydrolysis": "true"} model = pybamm.lead_acid.LOQS(options) @@ -92,7 +92,7 @@ def test_well_posed_algebraic(self): model.check_well_posedness() -class TestLeadAcidLOQSSurfaceForm(TestCase): +class TestLeadAcidLOQSSurfaceForm(unittest.TestCase): def test_well_posed_differential(self): options = {"surface form": "differential"} model = pybamm.lead_acid.LOQS(options) @@ -121,7 +121,7 @@ def test_default_geometry(self): self.assertIn("current collector", model.default_geometry) -class TestLeadAcidLOQSExternalCircuits(TestCase): +class TestLeadAcidLOQSExternalCircuits(unittest.TestCase): def test_well_posed_voltage(self): options = {"operating mode": "voltage"} model = pybamm.lead_acid.LOQS(options) diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_electrode_soh.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_electrode_soh.py index dd7d35b683..9f044b0566 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_electrode_soh.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_electrode_soh.py @@ -1,12 +1,12 @@ # # Tests for the lithium-ion electrode-specific SOH model # -from tests import TestCase + import pybamm import unittest -class TestElectrodeSOH(TestCase): +class TestElectrodeSOH(unittest.TestCase): def test_known_solution(self): param = pybamm.LithiumIonParameters() parameter_values = pybamm.ParameterValues("Mohtat2020") @@ -146,7 +146,7 @@ def test_error(self): esoh_solver.solve(inputs) -class TestElectrodeSOHMSMR(TestCase): +class TestElectrodeSOHMSMR(unittest.TestCase): def test_known_solution(self): options = { "open-circuit potential": "MSMR", @@ -232,7 +232,7 @@ def test_error(self): esoh_solver._get_electrode_soh_sims_split() -class TestElectrodeSOHHalfCell(TestCase): +class TestElectrodeSOHHalfCell(unittest.TestCase): def test_known_solution(self): model = pybamm.lithium_ion.ElectrodeSOHHalfCell() param = pybamm.LithiumIonParameters({"working electrode": "positive"}) @@ -247,7 +247,7 @@ def test_known_solution(self): self.assertAlmostEqual(sol["Uw(x_0)"].data[0], V_min, places=5) -class TestCalculateTheoreticalEnergy(TestCase): +class TestCalculateTheoreticalEnergy(unittest.TestCase): def test_efficiency(self): model = pybamm.lithium_ion.DFN(options={"calculate discharge energy": "true"}) parameter_values = pybamm.ParameterValues("Chen2020") @@ -266,7 +266,7 @@ def test_efficiency(self): self.assertLess(0, theoretical_energy) -class TestGetInitialSOC(TestCase): +class TestGetInitialSOC(unittest.TestCase): def test_initial_soc(self): param = pybamm.LithiumIonParameters() parameter_values = pybamm.ParameterValues("Mohtat2020") @@ -390,7 +390,7 @@ def test_error(self): ) -class TestGetInitialOCP(TestCase): +class TestGetInitialOCP(unittest.TestCase): def test_get_initial_ocp(self): param = pybamm.LithiumIonParameters() parameter_values = pybamm.ParameterValues("Mohtat2020") @@ -412,7 +412,7 @@ def test_min_max_ocp(self): self.assertAlmostEqual(Up_0 - Un_0, 2.8) -class TestGetInitialOCPMSMR(TestCase): +class TestGetInitialOCPMSMR(unittest.TestCase): def test_get_initial_ocp(self): options = { "open-circuit potential": "MSMR", diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py index 389aa55849..e5147f01e2 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py @@ -1,12 +1,12 @@ # # Tests for the lithium-ion MPM model # -from tests import TestCase + import pybamm import unittest -class TestMPM(TestCase): +class TestMPM(unittest.TestCase): def test_well_posed(self): options = {"thermal": "isothermal"} model = pybamm.lithium_ion.MPM(options) @@ -124,7 +124,7 @@ def test_wycisk_ocp(self): model.check_well_posedness() -class TestMPMExternalCircuits(TestCase): +class TestMPMExternalCircuits(unittest.TestCase): def test_well_posed_voltage(self): options = {"operating mode": "voltage"} model = pybamm.lithium_ion.MPM(options) @@ -146,7 +146,7 @@ def external_circuit_function(variables): model.check_well_posedness() -class TestMPMWithSEI(TestCase): +class TestMPMWithSEI(unittest.TestCase): def test_reaction_limited_not_implemented(self): options = {"SEI": "reaction limited"} with self.assertRaises(NotImplementedError): @@ -176,7 +176,7 @@ def test_ec_reaction_limited_not_implemented(self): pybamm.lithium_ion.MPM(options) -class TestMPMWithMechanics(TestCase): +class TestMPMWithMechanics(unittest.TestCase): def test_well_posed_negative_cracking_not_implemented(self): options = {"particle mechanics": ("swelling and cracking", "none")} with self.assertRaises(NotImplementedError): @@ -198,7 +198,7 @@ def test_well_posed_both_swelling_only_not_implemented(self): pybamm.lithium_ion.MPM(options) -class TestMPMWithPlating(TestCase): +class TestMPMWithPlating(unittest.TestCase): def test_well_posed_reversible_plating_not_implemented(self): options = {"lithium plating": "reversible"} with self.assertRaises(NotImplementedError): diff --git a/tests/unit/test_parameters/test_bpx.py b/tests/unit/test_parameters/test_bpx.py index 916eb8d161..f57fe8f7fa 100644 --- a/tests/unit/test_parameters/test_bpx.py +++ b/tests/unit/test_parameters/test_bpx.py @@ -1,7 +1,7 @@ # # Tests for the create_from_bpx function # -from tests import TestCase + import tempfile import unittest @@ -12,7 +12,7 @@ import pytest -class TestBPX(TestCase): +class TestBPX(unittest.TestCase): def setUp(self): self.base = { "Header": { diff --git a/tests/unit/test_parameters/test_current_functions.py b/tests/unit/test_parameters/test_current_functions.py index 8a8cc266ce..b00cba0b89 100644 --- a/tests/unit/test_parameters/test_current_functions.py +++ b/tests/unit/test_parameters/test_current_functions.py @@ -1,7 +1,7 @@ # # Tests for current input functions # -from tests import TestCase + import pybamm import numbers import unittest @@ -11,7 +11,7 @@ from tests import no_internet_connection -class TestCurrentFunctions(TestCase): +class TestCurrentFunctions(unittest.TestCase): def test_constant_current(self): # test simplify param = pybamm.electrical_parameters diff --git a/tests/unit/test_parameters/test_ecm_parameters.py b/tests/unit/test_parameters/test_ecm_parameters.py index 4764409e4e..543b4f4e5b 100644 --- a/tests/unit/test_parameters/test_ecm_parameters.py +++ b/tests/unit/test_parameters/test_ecm_parameters.py @@ -1,7 +1,7 @@ # # Tests for the equivalent circuit parameters # -from tests import TestCase + import pybamm import unittest @@ -33,7 +33,7 @@ parameter_values = pybamm.ParameterValues(values) -class TestEcmParameters(TestCase): +class TestEcmParameters(unittest.TestCase): def test_init_parameters(self): param = pybamm.EcmParameters() diff --git a/tests/unit/test_parameters/test_lead_acid_parameters.py b/tests/unit/test_parameters/test_lead_acid_parameters.py index ddc73f61ee..3fc62fde93 100644 --- a/tests/unit/test_parameters/test_lead_acid_parameters.py +++ b/tests/unit/test_parameters/test_lead_acid_parameters.py @@ -2,14 +2,14 @@ # Test for the standard lead acid parameters # import os -from tests import TestCase + import pybamm from tests import get_discretisation_for_testing from tempfile import TemporaryDirectory import unittest -class TestStandardParametersLeadAcid(TestCase): +class TestStandardParametersLeadAcid(unittest.TestCase): def test_scipy_constants(self): constants = pybamm.constants self.assertAlmostEqual(constants.R.evaluate(), 8.314, places=3) diff --git a/tests/unit/test_parameters/test_lithium_ion_parameters.py b/tests/unit/test_parameters/test_lithium_ion_parameters.py index 0c46eec16e..66c4ea398e 100644 --- a/tests/unit/test_parameters/test_lithium_ion_parameters.py +++ b/tests/unit/test_parameters/test_lithium_ion_parameters.py @@ -2,14 +2,14 @@ # Tests lithium-ion parameters load and give expected values # import os -from tests import TestCase + import pybamm from tempfile import TemporaryDirectory import unittest import numpy as np -class TestLithiumIonParameterValues(TestCase): +class TestLithiumIonParameterValues(unittest.TestCase): def test_print_parameters(self): with TemporaryDirectory() as dir_name: parameters = pybamm.LithiumIonParameters() diff --git a/tests/unit/test_parameters/test_parameter_sets/test_Ecker2015.py b/tests/unit/test_parameters/test_parameter_sets/test_Ecker2015.py index 2dc73d4484..4be67175d7 100644 --- a/tests/unit/test_parameters/test_parameter_sets/test_Ecker2015.py +++ b/tests/unit/test_parameters/test_parameter_sets/test_Ecker2015.py @@ -1,12 +1,12 @@ # # Tests for O'Kane (2022) parameter set # -from tests import TestCase + import pybamm import unittest -class TestEcker2015(TestCase): +class TestEcker2015(unittest.TestCase): def test_functions(self): param = pybamm.ParameterValues("Ecker2015") sto = pybamm.Scalar(0.5) diff --git a/tests/unit/test_parameters/test_parameter_sets/test_OKane2022.py b/tests/unit/test_parameters/test_parameter_sets/test_OKane2022.py index 1ab7e7930e..e34f837b38 100644 --- a/tests/unit/test_parameters/test_parameter_sets/test_OKane2022.py +++ b/tests/unit/test_parameters/test_parameter_sets/test_OKane2022.py @@ -1,12 +1,12 @@ # # Tests for O'Kane (2022) parameter set # -from tests import TestCase + import pybamm import unittest -class TestOKane2022(TestCase): +class TestOKane2022(unittest.TestCase): def test_functions(self): param = pybamm.ParameterValues("OKane2022") sto = pybamm.Scalar(0.9) diff --git a/tests/unit/test_parameters/test_parameter_sets/test_parameters_with_default_models.py b/tests/unit/test_parameters/test_parameter_sets/test_parameters_with_default_models.py index 4e18f1ef50..d7133a73e0 100644 --- a/tests/unit/test_parameters/test_parameter_sets/test_parameters_with_default_models.py +++ b/tests/unit/test_parameters/test_parameter_sets/test_parameters_with_default_models.py @@ -1,12 +1,12 @@ # # Tests each parameter set with the standard model associated with that parameter set # -from tests import TestCase + import pybamm import unittest -class TestParameterValuesWithModel(TestCase): +class TestParameterValuesWithModel(unittest.TestCase): def test_parameter_values_with_model(self): param_to_model = { "Ai2020": pybamm.lithium_ion.DFN( diff --git a/tests/unit/test_parameters/test_parameter_values.py b/tests/unit/test_parameters/test_parameter_values.py index 28b8aa2ef9..4826885f1c 100644 --- a/tests/unit/test_parameters/test_parameter_values.py +++ b/tests/unit/test_parameters/test_parameter_values.py @@ -1,7 +1,7 @@ # # Tests for the Base Parameter Values class # -from tests import TestCase + import os import unittest @@ -19,7 +19,7 @@ import casadi -class TestParameterValues(TestCase): +class TestParameterValues(unittest.TestCase): def test_init(self): # from dict param = pybamm.ParameterValues({"a": 1}) diff --git a/tests/unit/test_parameters/test_process_parameter_data.py b/tests/unit/test_parameters/test_process_parameter_data.py index 22f83a6bdb..bf27f71e4f 100644 --- a/tests/unit/test_parameters/test_process_parameter_data.py +++ b/tests/unit/test_parameters/test_process_parameter_data.py @@ -1,7 +1,7 @@ # # Tests for the parameter processing functions # -from tests import TestCase + import os import numpy as np @@ -10,7 +10,7 @@ import unittest -class TestProcessParameterData(TestCase): +class TestProcessParameterData(unittest.TestCase): def test_process_1D_data(self): name = "lico2_ocv_example" path = os.path.join(pybamm.root_dir(), "tests", "unit", "test_parameters") diff --git a/tests/unit/test_plotting/test_quick_plot.py b/tests/unit/test_plotting/test_quick_plot.py index e9e5dd810b..eb6c0607e3 100644 --- a/tests/unit/test_plotting/test_quick_plot.py +++ b/tests/unit/test_plotting/test_quick_plot.py @@ -1,12 +1,12 @@ import os import pybamm import unittest -from tests import TestCase + import numpy as np from tempfile import TemporaryDirectory -class TestQuickPlot(TestCase): +class TestQuickPlot(unittest.TestCase): def test_simple_ode_model(self): model = pybamm.lithium_ion.BaseModel(name="Simple ODE Model") diff --git a/tests/unit/test_serialisation/test_serialisation.py b/tests/unit/test_serialisation/test_serialisation.py index e7dcba6702..a1286cad26 100644 --- a/tests/unit/test_serialisation/test_serialisation.py +++ b/tests/unit/test_serialisation/test_serialisation.py @@ -1,7 +1,7 @@ # # Tests for the serialisation class # -from tests import TestCase + import json import os import unittest @@ -86,7 +86,7 @@ def mesh_var_dict(): return mesh, mesh_json -class TestSerialiseModels(TestCase): +class TestSerialiseModels(unittest.TestCase): def test_user_defined_model_recreaction(self): # Start with a base model model = pybamm.BaseModel() @@ -146,7 +146,7 @@ def test_user_defined_model_recreaction(self): os.remove("heat_equation.json") -class TestSerialise(TestCase): +class TestSerialise(unittest.TestCase): # test the symbol encoder def test_symbol_encoder_symbol(self): diff --git a/tests/unit/test_simulation.py b/tests/unit/test_simulation.py index 368c84607c..744ea2457c 100644 --- a/tests/unit/test_simulation.py +++ b/tests/unit/test_simulation.py @@ -1,7 +1,7 @@ import pybamm import numpy as np import pandas as pd -from tests import TestCase + import os import sys import unittest @@ -12,7 +12,7 @@ from tests import no_internet_connection -class TestSimulation(TestCase): +class TestSimulation(unittest.TestCase): def test_simple_model(self): model = pybamm.BaseModel() v = pybamm.Variable("v") diff --git a/tests/unit/test_solvers/test_algebraic_solver.py b/tests/unit/test_solvers/test_algebraic_solver.py index 22017f8cf8..6e8b3a3d80 100644 --- a/tests/unit/test_solvers/test_algebraic_solver.py +++ b/tests/unit/test_solvers/test_algebraic_solver.py @@ -1,14 +1,14 @@ # # Tests for the Algebraic Solver class # -from tests import TestCase + import pybamm import unittest import numpy as np from tests import get_discretisation_for_testing -class TestAlgebraicSolver(TestCase): +class TestAlgebraicSolver(unittest.TestCase): def test_algebraic_solver_init(self): solver = pybamm.AlgebraicSolver( method="hybr", tol=1e-4, extra_options={"maxfev": 100} diff --git a/tests/unit/test_solvers/test_base_solver.py b/tests/unit/test_solvers/test_base_solver.py index c444722929..884c85f87f 100644 --- a/tests/unit/test_solvers/test_base_solver.py +++ b/tests/unit/test_solvers/test_base_solver.py @@ -1,7 +1,7 @@ # # Tests for the Base Solver class # -from tests import TestCase + import casadi import pybamm import numpy as np @@ -10,7 +10,7 @@ import unittest -class TestBaseSolver(TestCase): +class TestBaseSolver(unittest.TestCase): def test_base_solver_init(self): solver = pybamm.BaseSolver(rtol=1e-2, atol=1e-4) self.assertEqual(solver.rtol, 1e-2) diff --git a/tests/unit/test_solvers/test_casadi_algebraic_solver.py b/tests/unit/test_solvers/test_casadi_algebraic_solver.py index 5001b37d82..b85f4292b9 100644 --- a/tests/unit/test_solvers/test_casadi_algebraic_solver.py +++ b/tests/unit/test_solvers/test_casadi_algebraic_solver.py @@ -1,4 +1,3 @@ -from tests import TestCase import casadi import pybamm import unittest @@ -7,7 +6,7 @@ import tests -class TestCasadiAlgebraicSolver(TestCase): +class TestCasadiAlgebraicSolver(unittest.TestCase): def test_algebraic_solver_init(self): solver = pybamm.CasadiAlgebraicSolver(tol=1e-4) self.assertEqual(solver.tol, 1e-4) @@ -171,7 +170,7 @@ def test_solve_with_input(self): np.testing.assert_array_equal(solution.y, -7) -class TestCasadiAlgebraicSolverSensitivity(TestCase): +class TestCasadiAlgebraicSolverSensitivity(unittest.TestCase): def test_solve_with_symbolic_input(self): # Simple system: a single algebraic equation var = pybamm.Variable("var") diff --git a/tests/unit/test_solvers/test_casadi_solver.py b/tests/unit/test_solvers/test_casadi_solver.py index c798024579..5ce29a365d 100644 --- a/tests/unit/test_solvers/test_casadi_solver.py +++ b/tests/unit/test_solvers/test_casadi_solver.py @@ -1,4 +1,3 @@ -from tests import TestCase import pybamm import unittest import numpy as np @@ -6,7 +5,7 @@ from scipy.sparse import eye -class TestCasadiSolver(TestCase): +class TestCasadiSolver(unittest.TestCase): def test_bad_mode(self): with self.assertRaisesRegex(ValueError, "invalid mode"): pybamm.CasadiSolver(mode="bad mode") @@ -582,7 +581,7 @@ def test_modulo_non_smooth_events(self): ) -class TestCasadiSolverODEsWithForwardSensitivityEquations(TestCase): +class TestCasadiSolverODEsWithForwardSensitivityEquations(unittest.TestCase): def test_solve_sensitivity_scalar_var_scalar_input(self): # Create model model = pybamm.BaseModel() @@ -963,7 +962,7 @@ def test_solve_sensitivity_subset(self): ) -class TestCasadiSolverDAEsWithForwardSensitivityEquations(TestCase): +class TestCasadiSolverDAEsWithForwardSensitivityEquations(unittest.TestCase): def test_solve_sensitivity_scalar_var_scalar_input(self): # Create model model = pybamm.BaseModel() diff --git a/tests/unit/test_solvers/test_idaklu_jax.py b/tests/unit/test_solvers/test_idaklu_jax.py index 6d891010d6..7bae5d74e9 100644 --- a/tests/unit/test_solvers/test_idaklu_jax.py +++ b/tests/unit/test_solvers/test_idaklu_jax.py @@ -1,7 +1,7 @@ # # Tests for the KLU-Jax interface class # -from tests import TestCase + from parameterized import parameterized import pybamm @@ -87,7 +87,7 @@ def no_jit(f): pybamm.have_idaklu() and pybamm.have_jax(), "Both IDAKLU and JAX are available", ) -class TestIDAKLUJax_NoJax(TestCase): +class TestIDAKLUJax_NoJax(unittest.TestCase): def test_instantiate_fails(self): with self.assertRaises(ModuleNotFoundError): pybamm.IDAKLUJax([], [], []) @@ -97,7 +97,7 @@ def test_instantiate_fails(self): not pybamm.have_idaklu() or not pybamm.have_jax(), "IDAKLU Solver and/or JAX are not available", ) -class TestIDAKLUJax(TestCase): +class TestIDAKLUJax(unittest.TestCase): # Initialisation tests def test_initialise_twice(self): diff --git a/tests/unit/test_solvers/test_idaklu_solver.py b/tests/unit/test_solvers/test_idaklu_solver.py index 02303a364e..c14c29fbb6 100644 --- a/tests/unit/test_solvers/test_idaklu_solver.py +++ b/tests/unit/test_solvers/test_idaklu_solver.py @@ -1,7 +1,7 @@ # # Tests for the KLU Solver class # -from tests import TestCase + from contextlib import redirect_stdout import io import unittest @@ -13,7 +13,7 @@ @unittest.skipIf(not pybamm.have_idaklu(), "idaklu solver is not installed") -class TestIDAKLUSolver(TestCase): +class TestIDAKLUSolver(unittest.TestCase): def test_ida_roberts_klu(self): # this test implements a python version of the ida Roberts # example provided in sundials diff --git a/tests/unit/test_solvers/test_jax_bdf_solver.py b/tests/unit/test_solvers/test_jax_bdf_solver.py index 854a618fba..e02bdb2510 100644 --- a/tests/unit/test_solvers/test_jax_bdf_solver.py +++ b/tests/unit/test_solvers/test_jax_bdf_solver.py @@ -1,7 +1,7 @@ import pybamm import unittest from tests import get_mesh_for_testing -from tests import TestCase + import sys import numpy as np @@ -10,7 +10,7 @@ @unittest.skipIf(not pybamm.have_jax(), "jax or jaxlib is not installed") -class TestJaxBDFSolver(TestCase): +class TestJaxBDFSolver(unittest.TestCase): def test_solver_(self): # Trailing _ manipulates the random seed # Create model model = pybamm.BaseModel() diff --git a/tests/unit/test_solvers/test_jax_solver.py b/tests/unit/test_solvers/test_jax_solver.py index 9df28e8ac2..4f34497626 100644 --- a/tests/unit/test_solvers/test_jax_solver.py +++ b/tests/unit/test_solvers/test_jax_solver.py @@ -1,7 +1,7 @@ import pybamm import unittest from tests import get_mesh_for_testing -from tests import TestCase + import sys import numpy as np @@ -10,7 +10,7 @@ @unittest.skipIf(not pybamm.have_jax(), "jax or jaxlib is not installed") -class TestJaxSolver(TestCase): +class TestJaxSolver(unittest.TestCase): def test_model_solver(self): # Create model model = pybamm.BaseModel() diff --git a/tests/unit/test_solvers/test_processed_variable.py b/tests/unit/test_solvers/test_processed_variable.py index 4cf3f9392e..b6ae669878 100644 --- a/tests/unit/test_solvers/test_processed_variable.py +++ b/tests/unit/test_solvers/test_processed_variable.py @@ -1,7 +1,7 @@ # # Tests for the Processed Variable class # -from tests import TestCase + import casadi import pybamm import tests @@ -61,7 +61,7 @@ def process_and_check_2D_variable( return y_sol, first_sol, second_sol, t_sol -class TestProcessedVariable(TestCase): +class TestProcessedVariable(unittest.TestCase): def test_processed_variable_0D(self): # without space t = pybamm.t diff --git a/tests/unit/test_solvers/test_processed_variable_computed.py b/tests/unit/test_solvers/test_processed_variable_computed.py index 7e0616c81b..407d422e4c 100644 --- a/tests/unit/test_solvers/test_processed_variable_computed.py +++ b/tests/unit/test_solvers/test_processed_variable_computed.py @@ -5,7 +5,7 @@ # by the idaklu solver, and does not possesses any capability to calculate # values itself since it does not have access to the full state vector # -from tests import TestCase + import casadi import pybamm import tests @@ -68,7 +68,7 @@ def process_and_check_2D_variable( return y_sol, first_sol, second_sol, t_sol -class TestProcessedVariableComputed(TestCase): +class TestProcessedVariableComputed(unittest.TestCase): def test_processed_variable_0D(self): # without space y = pybamm.StateVector(slice(0, 1)) diff --git a/tests/unit/test_solvers/test_scipy_solver.py b/tests/unit/test_solvers/test_scipy_solver.py index fad6651d55..c6afd16704 100644 --- a/tests/unit/test_solvers/test_scipy_solver.py +++ b/tests/unit/test_solvers/test_scipy_solver.py @@ -1,7 +1,7 @@ # Tests for the Scipy Solver class # import pybamm -from tests import TestCase + import unittest import numpy as np from tests import get_mesh_for_testing, get_discretisation_for_testing @@ -9,7 +9,7 @@ import sys -class TestScipySolver(TestCase): +class TestScipySolver(unittest.TestCase): def test_model_solver_python_and_jax(self): if pybamm.have_jax(): formats = ["python", "jax"] @@ -492,7 +492,7 @@ def test_scale_and_reference(self): ) -class TestScipySolverWithSensitivity(TestCase): +class TestScipySolverWithSensitivity(unittest.TestCase): def test_solve_sensitivity_scalar_var_scalar_input(self): # Create model model = pybamm.BaseModel() diff --git a/tests/unit/test_solvers/test_solution.py b/tests/unit/test_solvers/test_solution.py index ecc8d1bd8e..995898e8dd 100644 --- a/tests/unit/test_solvers/test_solution.py +++ b/tests/unit/test_solvers/test_solution.py @@ -2,7 +2,7 @@ # Tests for the Solution class # import os -from tests import TestCase + import json import pybamm import unittest @@ -13,7 +13,7 @@ from tempfile import TemporaryDirectory -class TestSolution(TestCase): +class TestSolution(unittest.TestCase): def test_init(self): t = np.linspace(0, 1) y = np.tile(t, (20, 1)) diff --git a/tests/unit/test_spatial_methods/test_base_spatial_method.py b/tests/unit/test_spatial_methods/test_base_spatial_method.py index d48ea69a7b..647616c924 100644 --- a/tests/unit/test_spatial_methods/test_base_spatial_method.py +++ b/tests/unit/test_spatial_methods/test_base_spatial_method.py @@ -1,7 +1,7 @@ # # Test for the base Spatial Method class # -from tests import TestCase + import numpy as np import pybamm import unittest @@ -12,7 +12,7 @@ ) -class TestSpatialMethod(TestCase): +class TestSpatialMethod(unittest.TestCase): def test_basics(self): mesh = get_mesh_for_testing() spatial_method = pybamm.SpatialMethod() diff --git a/tests/unit/test_spatial_methods/test_finite_volume/test_extrapolation.py b/tests/unit/test_spatial_methods/test_finite_volume/test_extrapolation.py index 86597e6a1c..c51e2d9a13 100644 --- a/tests/unit/test_spatial_methods/test_finite_volume/test_extrapolation.py +++ b/tests/unit/test_spatial_methods/test_finite_volume/test_extrapolation.py @@ -1,7 +1,7 @@ # # Test for the extrapolations in the finite volume class # -from tests import TestCase + import pybamm from tests import ( get_mesh_for_testing, @@ -57,7 +57,7 @@ def get_errors(function, method_options, pts, bcs=None): return l_errors, r_errors -class TestExtrapolation(TestCase): +class TestExtrapolation(unittest.TestCase): def test_convergence_without_bcs(self): # all tests are performed on x in [0, 1] linear = {"extrapolation": {"order": "linear"}} diff --git a/tests/unit/test_spatial_methods/test_finite_volume/test_finite_volume.py b/tests/unit/test_spatial_methods/test_finite_volume/test_finite_volume.py index 1d5844d7b0..de31b770ff 100644 --- a/tests/unit/test_spatial_methods/test_finite_volume/test_finite_volume.py +++ b/tests/unit/test_spatial_methods/test_finite_volume/test_finite_volume.py @@ -1,7 +1,7 @@ # # Tests for the Finite Volume Method # -from tests import TestCase + import pybamm from tests import ( get_mesh_for_testing, @@ -13,7 +13,7 @@ import unittest -class TestFiniteVolume(TestCase): +class TestFiniteVolume(unittest.TestCase): def test_node_to_edge_to_node(self): # Create discretisation mesh = get_mesh_for_testing() diff --git a/tests/unit/test_spatial_methods/test_finite_volume/test_ghost_nodes_and_neumann.py b/tests/unit/test_spatial_methods/test_finite_volume/test_ghost_nodes_and_neumann.py index 47e1a2fda1..ba82f2fb09 100644 --- a/tests/unit/test_spatial_methods/test_finite_volume/test_ghost_nodes_and_neumann.py +++ b/tests/unit/test_spatial_methods/test_finite_volume/test_ghost_nodes_and_neumann.py @@ -1,14 +1,14 @@ # # Test for adding ghost nodes in finite volumes class # -from tests import TestCase + import pybamm from tests import get_mesh_for_testing, get_p2d_mesh_for_testing import numpy as np import unittest -class TestGhostNodes(TestCase): +class TestGhostNodes(unittest.TestCase): def test_add_ghost_nodes(self): # Set up diff --git a/tests/unit/test_spatial_methods/test_finite_volume/test_grad_div_shapes.py b/tests/unit/test_spatial_methods/test_finite_volume/test_grad_div_shapes.py index 24132f5cc1..a1dd402f56 100644 --- a/tests/unit/test_spatial_methods/test_finite_volume/test_grad_div_shapes.py +++ b/tests/unit/test_spatial_methods/test_finite_volume/test_grad_div_shapes.py @@ -1,7 +1,7 @@ # # Test for the gradient and divergence in Finite Volumes # -from tests import TestCase + import pybamm from tests import ( get_mesh_for_testing, @@ -13,7 +13,7 @@ import unittest -class TestFiniteVolumeGradDiv(TestCase): +class TestFiniteVolumeGradDiv(unittest.TestCase): def test_grad_div_shapes_Dirichlet_bcs(self): """ Test grad and div with Dirichlet boundary conditions in Cartesian coordinates diff --git a/tests/unit/test_spatial_methods/test_finite_volume/test_integration.py b/tests/unit/test_spatial_methods/test_finite_volume/test_integration.py index 57113259c1..e9730a8eb7 100644 --- a/tests/unit/test_spatial_methods/test_finite_volume/test_integration.py +++ b/tests/unit/test_spatial_methods/test_finite_volume/test_integration.py @@ -1,7 +1,7 @@ # # Tests for integration using Finite Volume method # -from tests import TestCase + import pybamm from tests import ( get_mesh_for_testing, @@ -12,7 +12,7 @@ import unittest -class TestFiniteVolumeIntegration(TestCase): +class TestFiniteVolumeIntegration(unittest.TestCase): def test_definite_integral(self): # create discretisation mesh = get_mesh_for_testing(xpts=200, rpts=200) diff --git a/tests/unit/test_spatial_methods/test_scikit_finite_element.py b/tests/unit/test_spatial_methods/test_scikit_finite_element.py index 05b424e053..18c941517b 100644 --- a/tests/unit/test_spatial_methods/test_scikit_finite_element.py +++ b/tests/unit/test_spatial_methods/test_scikit_finite_element.py @@ -1,14 +1,14 @@ # # Test for the operator class # -from tests import TestCase + import pybamm from tests import get_2p1d_mesh_for_testing, get_unit_2p1D_mesh_for_testing import numpy as np import unittest -class TestScikitFiniteElement(TestCase): +class TestScikitFiniteElement(unittest.TestCase): def test_not_implemented(self): mesh = get_2p1d_mesh_for_testing(include_particles=False) spatial_method = pybamm.ScikitFiniteElement() diff --git a/tests/unit/test_spatial_methods/test_spectral_volume.py b/tests/unit/test_spatial_methods/test_spectral_volume.py index 3988bdd266..f6a631e84c 100644 --- a/tests/unit/test_spatial_methods/test_spectral_volume.py +++ b/tests/unit/test_spatial_methods/test_spectral_volume.py @@ -1,7 +1,7 @@ # # Test for the operator class # -from tests import TestCase + import pybamm import numpy as np import unittest @@ -87,7 +87,7 @@ def get_1p1d_mesh_for_testing( ) -class TestSpectralVolume(TestCase): +class TestSpectralVolume(unittest.TestCase): def test_exceptions(self): sp_meth = pybamm.SpectralVolume() with self.assertRaises(ValueError): From 2d3db215d58ecdab8072aa7e4219aaf601fb46cc Mon Sep 17 00:00:00 2001 From: Mehrdad Babazadeh Date: Mon, 5 Aug 2024 13:32:27 +0100 Subject: [PATCH 44/82] Ecm with diffusion (#4254) * add diffusion element for ECM * fix bugs * An example to compare ECM with ECMD model * style: pre-commit fixes * We removed the example to compare two models. * Added citation. * Fixed typo. * Added entry to change log. * style: pre-commit fixes * Added test * style: pre-commit fixes * Fixed citation error. * Changed line 39. * New test_default_properties definition added. * style: pre-commit fixes * Increased cover * style: pre-commit fixes --------- Co-authored-by: Ferran Brosa Planella Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Eric G. Kratz Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> --- CHANGELOG.md | 5 +- examples/scripts/run_ecmd.py | 28 +++++ pybamm/CITATIONS.bib | 10 ++ .../equivalent_circuit/thevenin.py | 39 +++++++ .../equivalent_circuit_elements/__init__.py | 1 + .../diffusion_element.py | 110 ++++++++++++++++++ .../ocv_element.py | 2 +- .../voltage_model.py | 4 +- pybamm/parameters/ecm_parameters.py | 1 + .../test_equivalent_circuit/test_thevenin.py | 12 ++ tests/unit/test_citations.py | 12 ++ .../test_equivalent_circuit/test_thevenin.py | 27 +++++ 12 files changed, 247 insertions(+), 4 deletions(-) create mode 100644 examples/scripts/run_ecmd.py create mode 100644 pybamm/models/submodels/equivalent_circuit_elements/diffusion_element.py diff --git a/CHANGELOG.md b/CHANGELOG.md index b2f8637442..57e0aad007 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,15 @@ # [Unreleased](https://github.com/pybamm-team/PyBaMM/) -- Replaced rounded faraday constant with its exact value in `bpx.py` for better comparison between different tools - ## Features - Added additional user-configurable options to the (`IDAKLUSolver`) and adjusted the default values to improve performance. ([#4282](https://github.com/pybamm-team/PyBaMM/pull/4282)) +- Added the diffusion element to be used in the Thevenin model. ([#4254](https://github.com/pybamm-team/PyBaMM/pull/4254)) ## Optimizations - Improved performance and reliability of DAE consistent initialization. ([#4301](https://github.com/pybamm-team/PyBaMM/pull/4301)) +- Replaced rounded Faraday constant with its exact value in `bpx.py` for better comparison between different tools. ([#4290](https://github.com/pybamm-team/PyBaMM/pull/4290)) + ## Bug Fixes - Fixed bug where IDAKLU solver failed when `output variables` were specified and an event triggered. ([#4300](https://github.com/pybamm-team/PyBaMM/pull/4300)) diff --git a/examples/scripts/run_ecmd.py b/examples/scripts/run_ecmd.py new file mode 100644 index 0000000000..c12fef9cfb --- /dev/null +++ b/examples/scripts/run_ecmd.py @@ -0,0 +1,28 @@ +import pybamm + +pybamm.set_logging_level("INFO") + +model = pybamm.equivalent_circuit.Thevenin(options={"diffusion element": "true"}) +parameter_values = model.default_parameter_values + +parameter_values.update( + {"Diffusion time constant [s]": 580}, check_already_exists=False +) + +experiment = pybamm.Experiment( + [ + ( + "Discharge at C/10 for 10 hours or until 3.3 V", + "Rest for 30 minutes", + "Rest for 2 hours", + "Charge at 100 A until 4.1 V", + "Hold at 4.1 V until 5 A", + "Rest for 30 minutes", + "Rest for 1 hour", + ), + ] +) + +sim = pybamm.Simulation(model, experiment=experiment, parameter_values=parameter_values) +sim.solve() +sim.plot() diff --git a/pybamm/CITATIONS.bib b/pybamm/CITATIONS.bib index 33ac6055d3..8fb9c6dc98 100644 --- a/pybamm/CITATIONS.bib +++ b/pybamm/CITATIONS.bib @@ -794,3 +794,13 @@ @article{Wycisk2022 author = {Dominik Wycisk and Marc Oldenburger and Marc Gerry Stoye and Toni Mrkonjic and Arnulf Latz}, keywords = {Lithium-ion battery, Voltage hysteresis, Plett-model, Silicon–graphite anode}, } + +@article{Fan2022, + title={Data-driven identification of lithium-ion batteries: A nonlinear equivalent circuit model with diffusion dynamics}, + author={Fan, Chuanxin and O’Regan, Kieran and Li, Liuying and Higgins, Matthew D and Kendrick, Emma and Widanage, Widanalage D}, + journal={Applied Energy}, + volume={321}, + pages={119336}, + year={2022}, + publisher={Elsevier} +} diff --git a/pybamm/models/full_battery_models/equivalent_circuit/thevenin.py b/pybamm/models/full_battery_models/equivalent_circuit/thevenin.py index 9aaf747a4c..48c0c1d1ac 100644 --- a/pybamm/models/full_battery_models/equivalent_circuit/thevenin.py +++ b/pybamm/models/full_battery_models/equivalent_circuit/thevenin.py @@ -30,6 +30,9 @@ class Thevenin(pybamm.BaseModel): throughput capacity in addition to discharge capacity. Must be one of "true" or "false". "false" is the default, since calculating discharge energy can be computationally expensive for simple models like SPM. + * "diffusion element" : str + Whether to include the diffusion element to the model. Must be one of + "true" or "false". "false" is the default. * "operating mode" : str Sets the operating mode for the model. This determines how the current is set. Can be: @@ -73,6 +76,7 @@ def __init__( def set_options(self, extra_options=None): possible_options = { "calculate discharge energy": ["false", "true"], + "diffusion element": ["false", "true"], "operating mode": OperatingModes("current"), "number of rc elements": NaturalNumberOption(1), } @@ -165,6 +169,18 @@ def set_rc_submodels(self): ) self.element_counter += 1 + def set_diffusion_submodel(self): + if self.options["diffusion element"] == "false": + self.submodels["Diffusion"] = ( + pybamm.equivalent_circuit_elements.NoDiffusion(self.param, self.options) + ) + elif self.options["diffusion element"] == "true": + self.submodels["Diffusion"] = ( + pybamm.equivalent_circuit_elements.DiffusionElement( + self.param, self.options + ) + ) + def set_thermal_submodel(self): self.submodels["Thermal"] = pybamm.equivalent_circuit_elements.ThermalSubModel( self.param, self.options @@ -180,6 +196,7 @@ def set_submodels(self, build): self.set_ocv_submodel() self.set_resistor_submodel() self.set_rc_submodels() + self.set_diffusion_submodel() self.set_thermal_submodel() self.set_voltage_submodel() @@ -227,3 +244,25 @@ def default_quick_plot_variables(self): "Irreversible heat generation [W]", ], ] + + @property + def default_var_pts(self): + x = pybamm.SpatialVariable( + "x ECMD", domain=["ECMD particle"], coord_sys="Cartesian" + ) + return {x: 20} + + @property + def default_geometry(self): + x = pybamm.SpatialVariable( + "x ECMD", domain=["ECMD particle"], coord_sys="Cartesian" + ) + return {"ECMD particle": {x: {"min": 0, "max": 1}}} + + @property + def default_submesh_types(self): + return {"ECMD particle": pybamm.Uniform1DSubMesh} + + @property + def default_spatial_methods(self): + return {"ECMD particle": pybamm.FiniteVolume()} diff --git a/pybamm/models/submodels/equivalent_circuit_elements/__init__.py b/pybamm/models/submodels/equivalent_circuit_elements/__init__.py index 4ff6ee7c62..16d57c691c 100644 --- a/pybamm/models/submodels/equivalent_circuit_elements/__init__.py +++ b/pybamm/models/submodels/equivalent_circuit_elements/__init__.py @@ -3,6 +3,7 @@ from .rc_element import RCElement from .thermal import ThermalSubModel from .voltage_model import VoltageModel +from .diffusion_element import NoDiffusion, DiffusionElement __all__ = ['ocv_element', 'rc_element', 'resistor_element', 'thermal', 'voltage_model'] diff --git a/pybamm/models/submodels/equivalent_circuit_elements/diffusion_element.py b/pybamm/models/submodels/equivalent_circuit_elements/diffusion_element.py new file mode 100644 index 0000000000..c7f1b4bcd5 --- /dev/null +++ b/pybamm/models/submodels/equivalent_circuit_elements/diffusion_element.py @@ -0,0 +1,110 @@ +import pybamm + + +class NoDiffusion(pybamm.BaseSubModel): + """ + Without Diffusion element for + equivalent circuits. + + Parameters + ---------- + param : parameter class + The parameters to use for this submodel + options : dict, optional + A dictionary of options to be passed to the model. + """ + + def __init__(self, param, options=None): + super().__init__(param) + self.model_options = options + + def get_coupled_variables(self, variables): + z = pybamm.PrimaryBroadcast(variables["SoC"], "ECMD particle") + x = pybamm.SpatialVariable( + "x ECMD", domain=["ECMD particle"], coord_sys="Cartesian" + ) + z_surf = pybamm.surf(z) + eta_diffusion = pybamm.Scalar(0) + + variables.update( + { + "Distributed SoC": z, + "x ECMD": x, + "Diffusion overpotential [V]": eta_diffusion, + "Surface SoC": z_surf, + } + ) + + return variables + + +class DiffusionElement(pybamm.BaseSubModel): + """ + With Diffusion element for + equivalent circuits. + + Parameters + ---------- + param : parameter class + The parameters to use for this submodel + options : dict, optional + A dictionary of options to be passed to the model. + """ + + def __init__(self, param, options=None): + super().__init__(param) + pybamm.citations.register("Fan2022") + self.model_options = options + + def get_fundamental_variables(self): + z = pybamm.Variable("Distributed SoC", domain="ECMD particle") + x = pybamm.SpatialVariable( + "x ECMD", domain=["ECMD particle"], coord_sys="Cartesian" + ) + variables = { + "Distributed SoC": z, + "x ECMD": x, + } + return variables + + def get_coupled_variables(self, variables): + z = variables["Distributed SoC"] + soc = variables["SoC"] + z_surf = pybamm.surf(z) + eta_diffusion = -(self.param.ocv(z_surf) - self.param.ocv(soc)) + + variables.update( + { + "Diffusion overpotential [V]": eta_diffusion, + "Surface SoC": z_surf, + } + ) + + return variables + + def set_rhs(self, variables): + cell_capacity = self.param.cell_capacity + current = variables["Current [A]"] + z = variables["Distributed SoC"] + + # governing equations + dzdt = pybamm.div(pybamm.grad(z)) / self.param.tau_D + self.rhs = {z: dzdt} + + # boundary conditions + lbc = pybamm.Scalar(0) + rbc = -self.param.tau_D * current / (cell_capacity * 3600) + self.boundary_conditions = { + z: {"left": (lbc, "Neumann"), "right": (rbc, "Neumann")} + } + + def set_initial_conditions(self, variables): + z = variables["Distributed SoC"] + self.initial_conditions = {z: self.param.initial_soc} + + def set_events(self, variables): + z_surf = variables["Surface SoC"] + self.events += [ + pybamm.Event("Minimum surface SoC", z_surf), + pybamm.Event("Maximum surface SoC", 1 - z_surf), + ] diff --git a/pybamm/models/submodels/equivalent_circuit_elements/ocv_element.py b/pybamm/models/submodels/equivalent_circuit_elements/ocv_element.py index 4c570b3cf4..9d1adf3d57 100644 --- a/pybamm/models/submodels/equivalent_circuit_elements/ocv_element.py +++ b/pybamm/models/submodels/equivalent_circuit_elements/ocv_element.py @@ -56,7 +56,7 @@ def set_initial_conditions(self, variables): def set_events(self, variables): soc = variables["SoC"] - self.events = [ + self.events += [ pybamm.Event("Minimum SoC", soc), pybamm.Event("Maximum SoC", 1 - soc), ] diff --git a/pybamm/models/submodels/equivalent_circuit_elements/voltage_model.py b/pybamm/models/submodels/equivalent_circuit_elements/voltage_model.py index d976cd0747..380902fca5 100644 --- a/pybamm/models/submodels/equivalent_circuit_elements/voltage_model.py +++ b/pybamm/models/submodels/equivalent_circuit_elements/voltage_model.py @@ -30,7 +30,9 @@ def get_coupled_variables(self, variables): for i in range(number_of_elements): overpotential += variables[f"Element-{i} overpotential [V]"] - voltage = ocv + overpotential + diffusion_overpotential = variables["Diffusion overpotential [V]"] + + voltage = ocv + overpotential + diffusion_overpotential # Power and Resistance current = variables["Current [A]"] diff --git a/pybamm/parameters/ecm_parameters.py b/pybamm/parameters/ecm_parameters.py index 79bb42e318..4d5855c2fd 100644 --- a/pybamm/parameters/ecm_parameters.py +++ b/pybamm/parameters/ecm_parameters.py @@ -4,6 +4,7 @@ class EcmParameters: def __init__(self): self.cell_capacity = pybamm.Parameter("Cell capacity [A.h]") + self.tau_D = pybamm.Parameter("Diffusion time constant [s]") self._set_current_parameters() self._set_voltage_parameters() diff --git a/tests/integration/test_models/test_full_battery_models/test_equivalent_circuit/test_thevenin.py b/tests/integration/test_models/test_full_battery_models/test_equivalent_circuit/test_thevenin.py index e01cf1a7d7..c8c68ea7f0 100644 --- a/tests/integration/test_models/test_full_battery_models/test_equivalent_circuit/test_thevenin.py +++ b/tests/integration/test_models/test_full_battery_models/test_equivalent_circuit/test_thevenin.py @@ -7,3 +7,15 @@ def test_basic_processing(self): model = pybamm.equivalent_circuit.Thevenin() modeltest = tests.StandardModelTest(model) modeltest.test_all() + + def test_diffusion(self): + model = pybamm.equivalent_circuit.Thevenin( + options={"diffusion element": "true"} + ) + parameter_values = model.default_parameter_values + + parameter_values.update( + {"Diffusion time constant [s]": 580}, check_already_exists=False + ) + modeltest = tests.StandardModelTest(model, parameter_values=parameter_values) + modeltest.test_all() diff --git a/tests/unit/test_citations.py b/tests/unit/test_citations.py index ba216e62ff..978569d864 100644 --- a/tests/unit/test_citations.py +++ b/tests/unit/test_citations.py @@ -345,6 +345,18 @@ def test_msmr(self): self.assertIn("Verbrugge2017", citations._papers_to_cite) self.assertIn("Verbrugge2017", citations._citation_tags.keys()) + def test_thevenin(self): + citations = pybamm.citations + + citations._reset() + pybamm.equivalent_circuit.Thevenin() + self.assertNotIn("Fan2022", citations._papers_to_cite) + self.assertNotIn("Fan2022", citations._citation_tags.keys()) + + pybamm.equivalent_circuit.Thevenin(options={"diffusion element": "true"}) + self.assertIn("Fan2022", citations._papers_to_cite) + self.assertIn("Fan2022", citations._citation_tags.keys()) + def test_parameter_citations(self): citations = pybamm.citations diff --git a/tests/unit/test_models/test_full_battery_models/test_equivalent_circuit/test_thevenin.py b/tests/unit/test_models/test_full_battery_models/test_equivalent_circuit/test_thevenin.py index f6abf592f8..39bfaf1145 100644 --- a/tests/unit/test_models/test_full_battery_models/test_equivalent_circuit/test_thevenin.py +++ b/tests/unit/test_models/test_full_battery_models/test_equivalent_circuit/test_thevenin.py @@ -11,6 +11,28 @@ def test_standard_model(self): model = pybamm.equivalent_circuit.Thevenin() model.check_well_posedness() + def test_default_properties(self): + model = pybamm.equivalent_circuit.Thevenin() + x = model.variables["x ECMD"] + + # test var_pts + self.assertEqual(model.default_var_pts, {x: 20}) + + # test geometry + self.assertEqual( + model.default_geometry, {"ECMD particle": {x: {"min": 0, "max": 1}}} + ) + + # test spatial methods + self.assertIsInstance( + model.default_spatial_methods["ECMD particle"], pybamm.FiniteVolume + ) + + # test submesh types + self.assertEqual( + model.default_submesh_types, {"ECMD particle": pybamm.Uniform1DSubMesh} + ) + def test_changing_number_of_rcs(self): options = {"number of rc elements": 0} model = pybamm.equivalent_circuit.Thevenin(options=options) @@ -33,6 +55,11 @@ def test_changing_number_of_rcs(self): model = pybamm.equivalent_circuit.Thevenin(options=options) model.check_well_posedness() + def test_diffusion_element(self): + options = {"diffusion element": "true"} + model = pybamm.equivalent_circuit.Thevenin(options=options) + model.check_well_posedness(post_discretisation=True) + def test_calculate_discharge_energy(self): options = {"calculate discharge energy": "true"} model = pybamm.equivalent_circuit.Thevenin(options=options) From e989323c8bf6b1dbeb482ae711214a0bd2ec56e7 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Mon, 5 Aug 2024 14:50:43 +0200 Subject: [PATCH 45/82] docs: add MehrdadBabazadeh as a contributor for code, and test (#4316) * docs: update all_contributors.md [skip ci] * docs: update README.md [skip ci] * docs: update .all-contributorsrc [skip ci] --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 10 ++++++++++ README.md | 2 +- all_contributors.md | 1 + 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 72be42003c..963256650d 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -940,6 +940,16 @@ "contributions": [ "code" ] + }, + { + "login": "MehrdadBabazadeh", + "name": "Mehrdad Babazadeh", + "avatar_url": "https://avatars.githubusercontent.com/u/30574522?v=4", + "profile": "https://github.com/MehrdadBabazadeh", + "contributions": [ + "code", + "test" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 85fc55c2c3..961c2d5f71 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/pybamm-team/PyBaMM/badge)](https://scorecard.dev/viewer/?uri=github.com/pybamm-team/PyBaMM) -[![All Contributors](https://img.shields.io/badge/all_contributors-88-orange.svg)](#-contributors) +[![All Contributors](https://img.shields.io/badge/all_contributors-89-orange.svg)](#-contributors) diff --git a/all_contributors.md b/all_contributors.md index c41e5ec36e..3cc69655e0 100644 --- a/all_contributors.md +++ b/all_contributors.md @@ -118,6 +118,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Santhosh
Santhosh

💻 🚇 Smita Sahu
Smita Sahu

💻 Ubham16
Ubham16

💻 + Mehrdad Babazadeh
Mehrdad Babazadeh

💻 ⚠️ From e1fe3fe7237fb75635e331a4d50c0961246f23e4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Aug 2024 15:39:07 -0400 Subject: [PATCH 46/82] Bump actions/upload-artifact from 4.3.4 to 4.3.5 in the actions group (#4318) Bumps the actions group with 1 update: [actions/upload-artifact](https://github.com/actions/upload-artifact). Updates `actions/upload-artifact` from 4.3.4 to 4.3.5 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4.3.4...v4.3.5) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/periodic_benchmarks.yml | 2 +- .github/workflows/publish_pypi.yml | 8 ++++---- .github/workflows/run_benchmarks_over_history.yml | 2 +- .github/workflows/scorecard.yml | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/periodic_benchmarks.yml b/.github/workflows/periodic_benchmarks.yml index fe40bb0248..69921f210e 100644 --- a/.github/workflows/periodic_benchmarks.yml +++ b/.github/workflows/periodic_benchmarks.yml @@ -48,7 +48,7 @@ jobs: LD_LIBRARY_PATH: $HOME/.local/lib - name: Upload results as artifact - uses: actions/upload-artifact@v4.3.4 + uses: actions/upload-artifact@v4.3.5 with: name: asv_periodic_results path: results diff --git a/.github/workflows/publish_pypi.yml b/.github/workflows/publish_pypi.yml index 8d328118a1..740b4580b9 100644 --- a/.github/workflows/publish_pypi.yml +++ b/.github/workflows/publish_pypi.yml @@ -88,7 +88,7 @@ jobs: CIBW_TEST_COMMAND: python -c "import pybamm; print(pybamm.IDAKLUSolver())" - name: Upload Windows wheels - uses: actions/upload-artifact@v4.3.4 + uses: actions/upload-artifact@v4.3.5 with: name: wheels_windows path: ./wheelhouse/*.whl @@ -123,7 +123,7 @@ jobs: python -c "import pybamm; print(pybamm.IDAKLUSolver())" - name: Upload wheels for Linux - uses: actions/upload-artifact@v4.3.4 + uses: actions/upload-artifact@v4.3.5 with: name: wheels_manylinux path: ./wheelhouse/*.whl @@ -254,7 +254,7 @@ jobs: python -c "import pybamm; print(pybamm.IDAKLUSolver())" - name: Upload wheels for macOS (amd64, arm64) - uses: actions/upload-artifact@v4.3.4 + uses: actions/upload-artifact@v4.3.5 with: name: wheels_${{ matrix.os }} path: ./wheelhouse/*.whl @@ -274,7 +274,7 @@ jobs: run: pipx run build --sdist - name: Upload SDist - uses: actions/upload-artifact@v4.3.4 + uses: actions/upload-artifact@v4.3.5 with: name: sdist path: ./dist/*.tar.gz diff --git a/.github/workflows/run_benchmarks_over_history.yml b/.github/workflows/run_benchmarks_over_history.yml index 9af7b5a755..2b0d338c41 100644 --- a/.github/workflows/run_benchmarks_over_history.yml +++ b/.github/workflows/run_benchmarks_over_history.yml @@ -46,7 +46,7 @@ jobs: ${{ github.event.inputs.commit_start }}..${{ github.event.inputs.commit_end }} - name: Upload results as artifact - uses: actions/upload-artifact@v4.3.4 + uses: actions/upload-artifact@v4.3.5 with: name: asv_over_history_results path: results diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index b44c13f92e..d2b2178a9c 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -59,7 +59,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 + uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5 with: name: SARIF file path: results.sarif From 58bbf1321c4ba2ca892f8a0cbc85553d6204cb9b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Aug 2024 17:37:16 -0400 Subject: [PATCH 47/82] chore: update pre-commit hooks (#4320) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.5.5 → v0.5.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.5...v0.5.6) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dc4c7b1c34..24fec627f4 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.5" + rev: "v0.5.6" hooks: - id: ruff args: [--fix, --show-fixes] From fa342768ebc3a11b360f6ab8b804f9152fa99b9b Mon Sep 17 00:00:00 2001 From: Brady Planden <55357039+BradyPlanden@users.noreply.github.com> Date: Tue, 6 Aug 2024 15:58:16 +0100 Subject: [PATCH 48/82] Update simulation class private members (#4319) * fix: use private attributes instead of property * fix: uncommented line --- pybamm/simulation.py | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/pybamm/simulation.py b/pybamm/simulation.py index a55310870e..a54c76ec7d 100644 --- a/pybamm/simulation.py +++ b/pybamm/simulation.py @@ -218,7 +218,7 @@ def set_parameters(self): A method to set the parameters in the model and the associated geometry. """ - if self.model_with_set_params: + if self._model_with_set_params: return self._model_with_set_params = self._parameter_values.process_model( @@ -235,7 +235,7 @@ def set_initial_soc(self, initial_soc, inputs=None): self.steps_to_built_models = None self.steps_to_built_solvers = None - options = self.model.options + options = self._model.options param = self._model.param if options["open-circuit potential"] == "MSMR": self._parameter_values = ( @@ -287,7 +287,7 @@ def build(self, initial_soc=None, inputs=None): if initial_soc is not None: self.set_initial_soc(initial_soc, inputs=inputs) - if self.built_model: + if self._built_model: return elif self._model.is_discretised: self._model_with_set_params = self._model @@ -486,7 +486,7 @@ def solve( ) self._solution = solver.solve( - self.built_model, t_eval, inputs=inputs, **kwargs + self._built_model, t_eval, inputs=inputs, **kwargs ) elif self.operating_mode == "with experiment": @@ -875,17 +875,17 @@ def solve( if feasible is False: break - if self.solution is not None and len(all_cycle_solutions) > 0: - self.solution.cycles = all_cycle_solutions - self.solution.set_summary_variables(all_summary_variables) - self.solution.all_first_states = all_first_states + if self._solution is not None and len(all_cycle_solutions) > 0: + self._solution.cycles = all_cycle_solutions + self._solution.set_summary_variables(all_summary_variables) + self._solution.all_first_states = all_first_states callbacks.on_experiment_end(logs) # record initial_start_time of the solution - self.solution.initial_start_time = initial_start_time + self._solution.initial_start_time = initial_start_time - return self.solution + return self._solution def run_padding_rest(self, kwargs, rest_time, step_solution, inputs): model = self.steps_to_built_models["Rest for padding"] @@ -951,7 +951,7 @@ def step( self._solution = solver.step( starting_solution, - self.built_model, + self._built_model, dt, t_eval=t_eval, save=save, @@ -959,7 +959,7 @@ def step( **kwargs, ) - return self.solution + return self._solution def _get_esoh_solver(self, calc_esoh): if ( @@ -1019,7 +1019,7 @@ def create_gif(self, number_of_images=80, duration=0.1, output_filename="plot.gi Name of the generated GIF file. """ - if self.solution is None: + if self._solution is None: raise ValueError("The simulation has not been solved yet.") if self.quick_plot is None: self.quick_plot = pybamm.QuickPlot(self._solution) @@ -1128,14 +1128,14 @@ def save_model( be available when the model is read back in and solved. variables: bool The discretised variables. Not required to solve a model, but if false - tools will not be availble. Will automatically save meshes as well, required + tools will not be available. Will automatically save meshes as well, required for plotting tools. filename: str, optional The desired name of the JSON file. If no name is provided, one will be created based on the model name, and the current datetime. """ - mesh = self.mesh if (mesh or variables) else None - variables = self.built_model.variables if variables else None + mesh = self._mesh if (mesh or variables) else None + variables = self._built_model.variables if variables else None if self.operating_mode == "with experiment": raise NotImplementedError( @@ -1144,9 +1144,9 @@ def save_model( """ ) - if self.built_model: + if self._built_model: Serialise().save_model( - self.built_model, filename=filename, mesh=mesh, variables=variables + self._built_model, filename=filename, mesh=mesh, variables=variables ) else: raise NotImplementedError( @@ -1183,11 +1183,11 @@ def plot_voltage_components( Keyword arguments, passed to ax.fill_between. """ - if self.solution is None: + if self._solution is None: raise ValueError("The simulation has not been solved yet.") return pybamm.plot_voltage_components( - self.solution, + self._solution, ax=ax, show_legend=show_legend, split_by_electrode=split_by_electrode, From fa68ddc0496853b2abf8ef4ef3cdbca88615befc Mon Sep 17 00:00:00 2001 From: Santhosh <52504160+santacodes@users.noreply.github.com> Date: Thu, 8 Aug 2024 00:32:19 +0530 Subject: [PATCH 49/82] Migrated from miniconda to official python bullseye docker image (#3901) * migrated to python bullseye * added pybamm user to sudoers * updated docker image to python:3.12-slim-bullseye * Apply suggestions from code review Co-authored-by: Eric G. Kratz * Update .github/workflows/docker.yml Co-authored-by: Arjun Verma * reverted the docker workflow * using uv to generate venv and edited docs --------- Co-authored-by: Eric G. Kratz Co-authored-by: Arjun Verma --- .../installation/install-from-docker.rst | 4 ++-- scripts/Dockerfile | 21 ++++++++++--------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/docs/source/user_guide/installation/install-from-docker.rst b/docs/source/user_guide/installation/install-from-docker.rst index f8fe733098..aeffdd25a1 100644 --- a/docs/source/user_guide/installation/install-from-docker.rst +++ b/docs/source/user_guide/installation/install-from-docker.rst @@ -90,11 +90,11 @@ If you want to build the PyBaMM Docker image locally from the PyBaMM source code docker run -it pybamm -5. Activate PyBaMM development environment inside docker container using: +5. Activate PyBaMM development virtual environment inside docker container using: .. code-block:: bash - conda activate pybamm + source /home/pybamm/venv/bin/activate .. note:: diff --git a/scripts/Dockerfile b/scripts/Dockerfile index 4f83b20ba1..e6fac122ff 100644 --- a/scripts/Dockerfile +++ b/scripts/Dockerfile @@ -1,4 +1,5 @@ -FROM continuumio/miniconda3:latest +FROM python:3.12-slim-bullseye +COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv WORKDIR / @@ -21,17 +22,17 @@ ENV CMAKE_CXX_COMPILER=/usr/bin/g++ ENV CMAKE_MAKE_PROGRAM=/usr/bin/make ENV LD_LIBRARY_PATH=/home/pybamm/.local/lib -RUN conda create -n pybamm python=3.11 -RUN conda init --all -SHELL ["conda", "run", "-n", "pybamm", "/bin/bash", "-c"] -RUN conda install -y pip +# Create a virtual environment +ENV VIRTUAL_ENV=/home/pybamm/venv +RUN uv venv $VIRTUAL_ENV +RUN #!/bin/bash && source /home/pybamm/venv/bin/activate; +ENV PATH="$VIRTUAL_ENV/bin:$PATH" -RUN pip install --upgrade --user pip setuptools wheel wget -RUN pip install cmake +RUN uv pip install --upgrade setuptools wheel wget cmake RUN python scripts/install_KLU_Sundials.py && \ - rm -rf pybind11 && \ - git clone https://github.com/pybind/pybind11.git && \ - pip install --user -e ".[all,dev,docs,jax]"; +rm -rf pybind11 && \ +git clone https://github.com/pybind/pybind11.git && \ +uv pip install -e ".[all,dev,docs,jax]"; ENTRYPOINT ["/bin/bash"] From fe6230d6b463cfabd8d6b13455e73b3a2767f138 Mon Sep 17 00:00:00 2001 From: Santhosh <52504160+santacodes@users.noreply.github.com> Date: Thu, 8 Aug 2024 15:40:05 +0530 Subject: [PATCH 50/82] Fixed JAX solver compatibility test (#4323) * fixed JAX solver compatibility test * removed jax compatibility test * Update tests/unit/test_util.py Co-authored-by: Arjun Verma * style: pre-commit fixes * Update tests/unit/test_util.py Co-authored-by: Eric G. Kratz * style: pre-commit fixes * Update tests/unit/test_util.py Co-authored-by: Eric G. Kratz --------- Co-authored-by: Arjun Verma Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Eric G. Kratz --- tests/unit/test_util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_util.py b/tests/unit/test_util.py index abcdb4dcae..c3f53f80be 100644 --- a/tests/unit/test_util.py +++ b/tests/unit/test_util.py @@ -88,9 +88,9 @@ def test_get_parameters_filepath(self): path = os.path.join(package_dir, tempfile_obj.name) assert pybamm.get_parameters_filepath(tempfile_obj.name) == path - @pytest.mark.skipif(pybamm.have_jax(), reason="The JAX solver is not installed") + @pytest.mark.skipif(not pybamm.have_jax(), reason="JAX is not installed") def test_is_jax_compatible(self): - assert True + assert pybamm.is_jax_compatible() def test_git_commit_info(self): git_commit_info = pybamm.get_git_commit_info() From f49189c23d1a129d64fc6608f9b4f31a2e7c31f0 Mon Sep 17 00:00:00 2001 From: Pradyot Ranjan <99216956+prady0t@users.noreply.github.com> Date: Thu, 8 Aug 2024 17:13:56 +0530 Subject: [PATCH 51/82] Moving to src layout (#4311) * Moving to src layout Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * Applying require changes according to src path Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * using src/pybamm Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * changing path of pybamm.root_dir() Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * style: pre-commit fixes * changing path to src/pybamm in CMakeLists.txt Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * fixing RTD failure Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * using pybamm.__path__[0] Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * style: pre-commit fixes * Update noxfile.py Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> * Fix notebook paths --------- Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> Co-authored-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> Co-authored-by: Arjun Verma Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> --- .gitignore | 4 +- CMakeLists.txt | 62 ++++++++--------- CONTRIBUTING.md | 4 +- MANIFEST.in | 2 +- README.md | 2 +- docs/conf.py | 2 +- .../examples/notebooks/models/SPM.ipynb | 5 +- examples/scripts/drive_cycle.py | 2 - examples/scripts/experiment_drive_cycle.py | 2 - noxfile.py | 2 +- pyproject.toml | 9 ++- scripts/update_version.py | 4 +- setup.py | 68 +++++++++---------- {pybamm => src/pybamm}/CITATIONS.bib | 0 {pybamm => src/pybamm}/__init__.py | 0 {pybamm => src/pybamm}/batch_study.py | 0 {pybamm => src/pybamm}/callbacks.py | 0 {pybamm => src/pybamm}/citations.py | 2 +- .../pybamm}/discretisations/__init__.py | 0 .../pybamm}/discretisations/discretisation.py | 0 {pybamm => src/pybamm}/doc_utils.py | 0 {pybamm => src/pybamm}/experiment/__init__.py | 0 .../pybamm}/experiment/experiment.py | 0 .../pybamm}/experiment/step/__init__.py | 0 .../pybamm}/experiment/step/base_step.py | 0 .../experiment/step/step_termination.py | 0 .../pybamm}/experiment/step/steps.py | 0 .../pybamm}/expression_tree/__init__.py | 0 .../pybamm}/expression_tree/array.py | 0 .../pybamm}/expression_tree/averages.py | 0 .../expression_tree/binary_operators.py | 0 .../pybamm}/expression_tree/broadcasts.py | 0 .../pybamm}/expression_tree/concatenations.py | 0 .../pybamm}/expression_tree/exceptions.py | 0 .../pybamm}/expression_tree/functions.py | 0 .../expression_tree/independent_variable.py | 0 .../expression_tree/input_parameter.py | 0 .../pybamm}/expression_tree/interpolant.py | 0 .../pybamm}/expression_tree/matrix.py | 0 .../expression_tree/operations/__init__.py | 0 .../operations/convert_to_casadi.py | 0 .../operations/evaluate_python.py | 0 .../expression_tree/operations/jacobian.py | 0 .../expression_tree/operations/latexify.py | 0 .../expression_tree/operations/serialise.py | 0 .../operations/unpack_symbols.py | 0 .../pybamm}/expression_tree/parameter.py | 0 .../expression_tree/printing/__init__.py | 0 .../expression_tree/printing/print_name.py | 0 .../printing/sympy_overrides.py | 0 .../pybamm}/expression_tree/scalar.py | 0 .../pybamm}/expression_tree/state_vector.py | 0 .../pybamm}/expression_tree/symbol.py | 0 .../expression_tree/unary_operators.py | 0 .../pybamm}/expression_tree/variable.py | 0 .../pybamm}/expression_tree/vector.py | 0 {pybamm => src/pybamm}/geometry/__init__.py | 0 .../pybamm}/geometry/battery_geometry.py | 0 {pybamm => src/pybamm}/geometry/geometry.py | 0 .../pybamm}/geometry/standard_spatial_vars.py | 0 {pybamm => src/pybamm}/input/__init__.py | 0 .../pybamm}/input/parameters/__init__.py | 0 .../pybamm}/input/parameters/ecm/__init__.py | 0 .../parameters/ecm/data/ecm_example_c1.csv | 0 .../parameters/ecm/data/ecm_example_dudt.csv | 0 .../parameters/ecm/data/ecm_example_ocv.csv | 0 .../parameters/ecm/data/ecm_example_r0.csv | 0 .../parameters/ecm/data/ecm_example_r1.csv | 0 .../input/parameters/ecm/example_set.py | 0 .../input/parameters/lead_acid/Sulzer2019.py | 0 .../input/parameters/lead_acid/__init__.py | 0 .../input/parameters/lithium_ion/Ai2020.py | 0 .../input/parameters/lithium_ion/Chen2020.py | 0 .../lithium_ion/Chen2020_composite.py | 0 .../input/parameters/lithium_ion/Ecker2015.py | 0 .../Ecker2015_graphite_halfcell.py | 0 .../lithium_ion/MSMR_example_set.py | 0 .../parameters/lithium_ion/Marquis2019.py | 0 .../parameters/lithium_ion/Mohtat2020.py | 0 .../parameters/lithium_ion/NCA_Kim2011.py | 0 .../input/parameters/lithium_ion/OKane2022.py | 0 .../OKane2022_graphite_SiOx_halfcell.py | 0 .../parameters/lithium_ion/ORegan2022.py | 0 .../input/parameters/lithium_ion/Prada2013.py | 0 .../parameters/lithium_ion/Ramadass2004.py | 0 .../input/parameters/lithium_ion/Xu2019.py | 0 .../input/parameters/lithium_ion/__init__.py | 0 .../data/graphite_LGM50_ocp_Chen2020.csv | 0 .../data/graphite_ocp_Ecker2015.csv | 0 .../data/graphite_ocp_Enertech_Ai2020.csv | 0 .../lithium_ion/data/lico2_ocp_Ai2020.csv | 0 ...easured_graphite_diffusivity_Ecker2015.csv | 0 .../measured_nco_diffusivity_Ecker2015.csv | 0 .../lithium_ion/data/nca_ocp_Kim2011_data.csv | 0 .../lithium_ion/data/nco_ocp_Ecker2015.csv | 0 .../data/nmc_LGM50_ocp_Chen2020.csv | 0 {pybamm => src/pybamm}/logger.py | 0 {pybamm => src/pybamm}/meshes/__init__.py | 0 {pybamm => src/pybamm}/meshes/meshes.py | 0 .../meshes/one_dimensional_submeshes.py | 0 .../pybamm}/meshes/scikit_fem_submeshes.py | 0 .../meshes/zero_dimensional_submesh.py | 0 {pybamm => src/pybamm}/models/__init__.py | 0 {pybamm => src/pybamm}/models/base_model.py | 0 {pybamm => src/pybamm}/models/event.py | 0 .../models/full_battery_models/__init__.py | 0 .../full_battery_models/base_battery_model.py | 0 .../equivalent_circuit/__init__.py | 0 .../equivalent_circuit/ecm_model_options.py | 0 .../equivalent_circuit/thevenin.py | 0 .../full_battery_models/lead_acid/__init__.py | 0 .../lead_acid/base_lead_acid_model.py | 0 .../lead_acid/basic_full.py | 0 .../full_battery_models/lead_acid/full.py | 0 .../full_battery_models/lead_acid/loqs.py | 0 .../lithium_ion/Yang2017.py | 0 .../lithium_ion/__init__.py | 0 .../lithium_ion/base_lithium_ion_model.py | 0 .../lithium_ion/basic_dfn.py | 0 .../lithium_ion/basic_dfn_composite.py | 0 .../lithium_ion/basic_dfn_half_cell.py | 0 .../lithium_ion/basic_spm.py | 0 .../full_battery_models/lithium_ion/dfn.py | 0 .../lithium_ion/electrode_soh.py | 0 .../lithium_ion/electrode_soh_half_cell.py | 0 .../full_battery_models/lithium_ion/mpm.py | 0 .../full_battery_models/lithium_ion/msmr.py | 0 .../lithium_ion/newman_tobias.py | 0 .../full_battery_models/lithium_ion/spm.py | 0 .../full_battery_models/lithium_ion/spme.py | 0 .../full_battery_models/lithium_metal/dfn.py | 0 .../pybamm}/models/submodels/__init__.py | 0 .../submodels/active_material/__init__.py | 0 .../active_material/base_active_material.py | 0 .../constant_active_material.py | 0 .../active_material/loss_active_material.py | 0 .../active_material/total_active_material.py | 0 .../pybamm}/models/submodels/base_submodel.py | 0 .../models/submodels/convection/__init__.py | 0 .../submodels/convection/base_convection.py | 0 .../convection/through_cell/__init__.py | 0 .../base_through_cell_convection.py | 0 .../through_cell/explicit_convection.py | 0 .../through_cell/full_convection.py | 0 .../convection/through_cell/no_convection.py | 0 .../convection/transverse/__init__.py | 0 .../transverse/base_transverse_convection.py | 0 .../convection/transverse/full_convection.py | 0 .../convection/transverse/no_convection.py | 0 .../transverse/uniform_convection.py | 0 .../submodels/current_collector/__init__.py | 0 .../base_current_collector.py | 0 .../effective_resistance_current_collector.py | 0 .../homogeneous_current_collector.py | 0 .../current_collector/potential_pair.py | 0 .../models/submodels/electrode/__init__.py | 0 .../submodels/electrode/base_electrode.py | 0 .../submodels/electrode/ohm/__init__.py | 0 .../submodels/electrode/ohm/base_ohm.py | 0 .../submodels/electrode/ohm/composite_ohm.py | 0 .../submodels/electrode/ohm/full_ohm.py | 0 .../submodels/electrode/ohm/leading_ohm.py | 0 .../submodels/electrode/ohm/li_metal.py | 0 .../electrode/ohm/surface_form_ohm.py | 0 .../electrolyte_conductivity/__init__.py | 0 .../base_electrolyte_conductivity.py | 0 .../composite_conductivity.py | 0 .../full_conductivity.py | 0 .../integrated_conductivity.py | 0 .../leading_order_conductivity.py | 0 .../surface_potential_form/__init__.py | 0 .../composite_surface_form_conductivity.py | 0 .../explicit_surface_form_conductivity.py | 0 .../full_surface_form_conductivity.py | 0 .../leading_surface_form_conductivity.py | 0 .../electrolyte_diffusion/__init__.py | 0 .../base_electrolyte_diffusion.py | 0 .../constant_concentration.py | 0 .../electrolyte_diffusion/full_diffusion.py | 0 .../leading_order_diffusion.py | 0 .../equivalent_circuit_elements/__init__.py | 0 .../diffusion_element.py | 0 .../ocv_element.py | 0 .../equivalent_circuit_elements/rc_element.py | 0 .../resistor_element.py | 0 .../equivalent_circuit_elements/thermal.py | 0 .../voltage_model.py | 0 .../submodels/external_circuit/__init__.py | 0 .../external_circuit/base_external_circuit.py | 0 .../external_circuit/discharge_throughput.py | 0 .../explicit_control_external_circuit.py | 0 .../function_control_external_circuit.py | 0 .../models/submodels/interface/__init__.py | 0 .../submodels/interface/base_interface.py | 0 .../interface_utilisation/__init__.py | 0 .../interface_utilisation/base_utilisation.py | 0 .../constant_utilisation.py | 0 .../current_driven_utilisation.py | 0 .../interface_utilisation/full_utilisation.py | 0 .../submodels/interface/kinetics/__init__.py | 0 .../interface/kinetics/base_kinetics.py | 0 .../interface/kinetics/butler_volmer.py | 0 .../interface/kinetics/diffusion_limited.py | 0 .../kinetics/inverse_kinetics/__init__.py | 0 .../inverse_kinetics/inverse_butler_volmer.py | 0 .../submodels/interface/kinetics/linear.py | 0 .../submodels/interface/kinetics/marcus.py | 0 .../interface/kinetics/msmr_butler_volmer.py | 0 .../interface/kinetics/no_reaction.py | 0 .../submodels/interface/kinetics/tafel.py | 0 .../interface/kinetics/total_main_kinetics.py | 0 .../interface/lithium_plating/__init__.py | 0 .../interface/lithium_plating/base_plating.py | 0 .../interface/lithium_plating/no_plating.py | 0 .../interface/lithium_plating/plating.py | 0 .../lithium_plating/total_lithium_plating.py | 0 .../open_circuit_potential/__init__.py | 0 .../open_circuit_potential/base_ocp.py | 0 .../current_sigmoid_ocp.py | 0 .../open_circuit_potential/msmr_ocp.py | 0 .../open_circuit_potential/single_ocp.py | 0 .../open_circuit_potential/wycisk_ocp.py | 0 .../submodels/interface/sei/__init__.py | 0 .../submodels/interface/sei/base_sei.py | 0 .../submodels/interface/sei/constant_sei.py | 0 .../models/submodels/interface/sei/no_sei.py | 0 .../submodels/interface/sei/sei_growth.py | 0 .../submodels/interface/sei/total_sei.py | 0 .../interface/total_interfacial_current.py | 0 .../submodels/oxygen_diffusion/__init__.py | 0 .../oxygen_diffusion/base_oxygen_diffusion.py | 0 .../oxygen_diffusion/full_oxygen_diffusion.py | 0 .../leading_oxygen_diffusion.py | 0 .../submodels/oxygen_diffusion/no_oxygen.py | 0 .../models/submodels/particle/__init__.py | 0 .../submodels/particle/base_particle.py | 0 .../submodels/particle/fickian_diffusion.py | 0 .../submodels/particle/msmr_diffusion.py | 0 .../submodels/particle/polynomial_profile.py | 0 .../particle/total_particle_concentration.py | 0 .../particle/x_averaged_polynomial_profile.py | 0 .../submodels/particle_mechanics/__init__.py | 0 .../particle_mechanics/base_mechanics.py | 0 .../particle_mechanics/crack_propagation.py | 0 .../particle_mechanics/no_mechanics.py | 0 .../particle_mechanics/swelling_only.py | 0 .../models/submodels/porosity/__init__.py | 0 .../submodels/porosity/base_porosity.py | 0 .../submodels/porosity/constant_porosity.py | 0 .../porosity/reaction_driven_porosity.py | 0 .../porosity/reaction_driven_porosity_ode.py | 0 .../models/submodels/thermal/__init__.py | 0 .../models/submodels/thermal/base_thermal.py | 0 .../models/submodels/thermal/isothermal.py | 0 .../models/submodels/thermal/lumped.py | 0 .../submodels/thermal/pouch_cell/__init__.py | 0 .../pouch_cell_1D_current_collectors.py | 0 .../pouch_cell_2D_current_collectors.py | 0 .../submodels/thermal/pouch_cell/x_full.py | 0 .../transport_efficiency/__init__.py | 0 .../base_transport_efficiency.py | 0 .../transport_efficiency/bruggeman.py | 0 .../cation_exchange_membrane.py | 0 .../heterogeneous_catalyst.py | 0 .../hyperbola_of_revolution.py | 0 .../transport_efficiency/ordered_packing.py | 0 .../overlapping_spheres.py | 0 .../random_overlapping_cylinders.py | 0 .../transport_efficiency/tortuosity_factor.py | 0 {pybamm => src/pybamm}/parameters/__init__.py | 0 .../pybamm}/parameters/base_parameters.py | 0 {pybamm => src/pybamm}/parameters/bpx.py | 0 .../pybamm}/parameters/constants.py | 0 .../pybamm}/parameters/ecm_parameters.py | 0 .../parameters/electrical_parameters.py | 0 .../parameters/geometric_parameters.py | 0 .../parameters/lead_acid_parameters.py | 0 .../parameters/lithium_ion_parameters.py | 0 .../pybamm}/parameters/parameter_sets.py | 0 .../pybamm}/parameters/parameter_values.py | 0 .../parameters/process_parameter_data.py | 0 .../size_distribution_parameters.py | 0 .../pybamm}/parameters/thermal_parameters.py | 0 {pybamm => src/pybamm}/plotting/__init__.py | 0 .../pybamm}/plotting/dynamic_plot.py | 0 {pybamm => src/pybamm}/plotting/plot.py | 0 {pybamm => src/pybamm}/plotting/plot2D.py | 0 .../plotting/plot_summary_variables.py | 0 .../plotting/plot_thermal_components.py | 0 .../plotting/plot_voltage_components.py | 0 {pybamm => src/pybamm}/plotting/quick_plot.py | 0 {pybamm => src/pybamm}/pybamm_data.py | 0 {pybamm => src/pybamm}/settings.py | 0 {pybamm => src/pybamm}/simulation.py | 0 {pybamm => src/pybamm}/solvers/__init__.py | 0 .../pybamm}/solvers/algebraic_solver.py | 0 {pybamm => src/pybamm}/solvers/base_solver.py | 0 .../pybamm}/solvers/c_solvers/__init__.py | 0 .../pybamm}/solvers/c_solvers/idaklu.cpp | 0 .../idaklu/Expressions/Base/Expression.hpp | 0 .../idaklu/Expressions/Base/ExpressionSet.hpp | 0 .../Expressions/Base/ExpressionTypes.hpp | 0 .../Expressions/Casadi/CasadiFunctions.cpp | 0 .../Expressions/Casadi/CasadiFunctions.hpp | 0 .../idaklu/Expressions/Expressions.hpp | 0 .../Expressions/IREE/IREEBaseFunction.hpp | 0 .../idaklu/Expressions/IREE/IREEFunction.hpp | 0 .../idaklu/Expressions/IREE/IREEFunctions.cpp | 0 .../idaklu/Expressions/IREE/IREEFunctions.hpp | 0 .../idaklu/Expressions/IREE/ModuleParser.cpp | 0 .../idaklu/Expressions/IREE/ModuleParser.hpp | 0 .../idaklu/Expressions/IREE/iree_jit.cpp | 0 .../idaklu/Expressions/IREE/iree_jit.hpp | 0 .../solvers/c_solvers/idaklu/IDAKLUSolver.cpp | 0 .../solvers/c_solvers/idaklu/IDAKLUSolver.hpp | 0 .../c_solvers/idaklu/IDAKLUSolverOpenMP.hpp | 0 .../c_solvers/idaklu/IDAKLUSolverOpenMP.inl | 0 .../idaklu/IDAKLUSolverOpenMP_solvers.cpp | 0 .../idaklu/IDAKLUSolverOpenMP_solvers.hpp | 0 .../solvers/c_solvers/idaklu/IdakluJax.cpp | 0 .../solvers/c_solvers/idaklu/IdakluJax.hpp | 0 .../solvers/c_solvers/idaklu/Options.cpp | 0 .../solvers/c_solvers/idaklu/Options.hpp | 0 .../solvers/c_solvers/idaklu/Solution.cpp | 0 .../solvers/c_solvers/idaklu/Solution.hpp | 0 .../solvers/c_solvers/idaklu/common.hpp | 0 .../c_solvers/idaklu/idaklu_solver.hpp | 0 .../solvers/c_solvers/idaklu/python.cpp | 0 .../solvers/c_solvers/idaklu/python.hpp | 0 .../c_solvers/idaklu/sundials_functions.hpp | 0 .../c_solvers/idaklu/sundials_functions.inl | 0 .../idaklu/sundials_legacy_wrapper.hpp | 0 .../solvers/casadi_algebraic_solver.py | 0 .../pybamm}/solvers/casadi_solver.py | 0 .../pybamm}/solvers/dummy_solver.py | 0 {pybamm => src/pybamm}/solvers/idaklu_jax.py | 0 .../pybamm}/solvers/idaklu_solver.py | 0 .../pybamm}/solvers/jax_bdf_solver.py | 0 {pybamm => src/pybamm}/solvers/jax_solver.py | 0 {pybamm => src/pybamm}/solvers/lrudict.py | 0 .../pybamm}/solvers/processed_variable.py | 0 .../solvers/processed_variable_computed.py | 0 .../pybamm}/solvers/scipy_solver.py | 0 {pybamm => src/pybamm}/solvers/solution.py | 0 .../pybamm}/spatial_methods/__init__.py | 0 .../pybamm}/spatial_methods/finite_volume.py | 0 .../spatial_methods/scikit_finite_element.py | 0 .../pybamm}/spatial_methods/spatial_method.py | 0 .../spatial_methods/spectral_volume.py | 0 .../zero_dimensional_method.py | 0 {pybamm => src/pybamm}/type_definitions.py | 0 {pybamm => src/pybamm}/util.py | 2 +- {pybamm => src/pybamm}/version.py | 0 .../test_simulation_with_experiment.py | 2 + .../test_parameters/test_parameter_values.py | 6 +- .../test_process_parameter_data.py | 8 +-- tests/unit/test_util.py | 2 +- 357 files changed, 93 insertions(+), 97 deletions(-) rename {pybamm => src/pybamm}/CITATIONS.bib (100%) rename {pybamm => src/pybamm}/__init__.py (100%) rename {pybamm => src/pybamm}/batch_study.py (100%) rename {pybamm => src/pybamm}/callbacks.py (100%) rename {pybamm => src/pybamm}/citations.py (99%) rename {pybamm => src/pybamm}/discretisations/__init__.py (100%) rename {pybamm => src/pybamm}/discretisations/discretisation.py (100%) rename {pybamm => src/pybamm}/doc_utils.py (100%) rename {pybamm => src/pybamm}/experiment/__init__.py (100%) rename {pybamm => src/pybamm}/experiment/experiment.py (100%) rename {pybamm => src/pybamm}/experiment/step/__init__.py (100%) rename {pybamm => src/pybamm}/experiment/step/base_step.py (100%) rename {pybamm => src/pybamm}/experiment/step/step_termination.py (100%) rename {pybamm => src/pybamm}/experiment/step/steps.py (100%) rename {pybamm => src/pybamm}/expression_tree/__init__.py (100%) rename {pybamm => src/pybamm}/expression_tree/array.py (100%) rename {pybamm => src/pybamm}/expression_tree/averages.py (100%) rename {pybamm => src/pybamm}/expression_tree/binary_operators.py (100%) rename {pybamm => src/pybamm}/expression_tree/broadcasts.py (100%) rename {pybamm => src/pybamm}/expression_tree/concatenations.py (100%) rename {pybamm => src/pybamm}/expression_tree/exceptions.py (100%) rename {pybamm => src/pybamm}/expression_tree/functions.py (100%) rename {pybamm => src/pybamm}/expression_tree/independent_variable.py (100%) rename {pybamm => src/pybamm}/expression_tree/input_parameter.py (100%) rename {pybamm => src/pybamm}/expression_tree/interpolant.py (100%) rename {pybamm => src/pybamm}/expression_tree/matrix.py (100%) rename {pybamm => src/pybamm}/expression_tree/operations/__init__.py (100%) rename {pybamm => src/pybamm}/expression_tree/operations/convert_to_casadi.py (100%) rename {pybamm => src/pybamm}/expression_tree/operations/evaluate_python.py (100%) rename {pybamm => src/pybamm}/expression_tree/operations/jacobian.py (100%) rename {pybamm => src/pybamm}/expression_tree/operations/latexify.py (100%) rename {pybamm => src/pybamm}/expression_tree/operations/serialise.py (100%) rename {pybamm => src/pybamm}/expression_tree/operations/unpack_symbols.py (100%) rename {pybamm => src/pybamm}/expression_tree/parameter.py (100%) rename {pybamm => src/pybamm}/expression_tree/printing/__init__.py (100%) rename {pybamm => src/pybamm}/expression_tree/printing/print_name.py (100%) rename {pybamm => src/pybamm}/expression_tree/printing/sympy_overrides.py (100%) rename {pybamm => src/pybamm}/expression_tree/scalar.py (100%) rename {pybamm => src/pybamm}/expression_tree/state_vector.py (100%) rename {pybamm => src/pybamm}/expression_tree/symbol.py (100%) rename {pybamm => src/pybamm}/expression_tree/unary_operators.py (100%) rename {pybamm => src/pybamm}/expression_tree/variable.py (100%) rename {pybamm => src/pybamm}/expression_tree/vector.py (100%) rename {pybamm => src/pybamm}/geometry/__init__.py (100%) rename {pybamm => src/pybamm}/geometry/battery_geometry.py (100%) rename {pybamm => src/pybamm}/geometry/geometry.py (100%) rename {pybamm => src/pybamm}/geometry/standard_spatial_vars.py (100%) rename {pybamm => src/pybamm}/input/__init__.py (100%) rename {pybamm => src/pybamm}/input/parameters/__init__.py (100%) rename {pybamm => src/pybamm}/input/parameters/ecm/__init__.py (100%) rename {pybamm => src/pybamm}/input/parameters/ecm/data/ecm_example_c1.csv (100%) rename {pybamm => src/pybamm}/input/parameters/ecm/data/ecm_example_dudt.csv (100%) rename {pybamm => src/pybamm}/input/parameters/ecm/data/ecm_example_ocv.csv (100%) rename {pybamm => src/pybamm}/input/parameters/ecm/data/ecm_example_r0.csv (100%) rename {pybamm => src/pybamm}/input/parameters/ecm/data/ecm_example_r1.csv (100%) rename {pybamm => src/pybamm}/input/parameters/ecm/example_set.py (100%) rename {pybamm => src/pybamm}/input/parameters/lead_acid/Sulzer2019.py (100%) rename {pybamm => src/pybamm}/input/parameters/lead_acid/__init__.py (100%) rename {pybamm => src/pybamm}/input/parameters/lithium_ion/Ai2020.py (100%) rename {pybamm => src/pybamm}/input/parameters/lithium_ion/Chen2020.py (100%) rename {pybamm => src/pybamm}/input/parameters/lithium_ion/Chen2020_composite.py (100%) rename {pybamm => src/pybamm}/input/parameters/lithium_ion/Ecker2015.py (100%) rename {pybamm => src/pybamm}/input/parameters/lithium_ion/Ecker2015_graphite_halfcell.py (100%) rename {pybamm => src/pybamm}/input/parameters/lithium_ion/MSMR_example_set.py (100%) rename {pybamm => src/pybamm}/input/parameters/lithium_ion/Marquis2019.py (100%) rename {pybamm => src/pybamm}/input/parameters/lithium_ion/Mohtat2020.py (100%) rename {pybamm => src/pybamm}/input/parameters/lithium_ion/NCA_Kim2011.py (100%) rename {pybamm => src/pybamm}/input/parameters/lithium_ion/OKane2022.py (100%) rename {pybamm => src/pybamm}/input/parameters/lithium_ion/OKane2022_graphite_SiOx_halfcell.py (100%) rename {pybamm => src/pybamm}/input/parameters/lithium_ion/ORegan2022.py (100%) rename {pybamm => src/pybamm}/input/parameters/lithium_ion/Prada2013.py (100%) rename {pybamm => src/pybamm}/input/parameters/lithium_ion/Ramadass2004.py (100%) rename {pybamm => src/pybamm}/input/parameters/lithium_ion/Xu2019.py (100%) rename {pybamm => src/pybamm}/input/parameters/lithium_ion/__init__.py (100%) rename {pybamm => src/pybamm}/input/parameters/lithium_ion/data/graphite_LGM50_ocp_Chen2020.csv (100%) rename {pybamm => src/pybamm}/input/parameters/lithium_ion/data/graphite_ocp_Ecker2015.csv (100%) rename {pybamm => src/pybamm}/input/parameters/lithium_ion/data/graphite_ocp_Enertech_Ai2020.csv (100%) rename {pybamm => src/pybamm}/input/parameters/lithium_ion/data/lico2_ocp_Ai2020.csv (100%) rename {pybamm => src/pybamm}/input/parameters/lithium_ion/data/measured_graphite_diffusivity_Ecker2015.csv (100%) rename {pybamm => src/pybamm}/input/parameters/lithium_ion/data/measured_nco_diffusivity_Ecker2015.csv (100%) rename {pybamm => src/pybamm}/input/parameters/lithium_ion/data/nca_ocp_Kim2011_data.csv (100%) rename {pybamm => src/pybamm}/input/parameters/lithium_ion/data/nco_ocp_Ecker2015.csv (100%) rename {pybamm => src/pybamm}/input/parameters/lithium_ion/data/nmc_LGM50_ocp_Chen2020.csv (100%) rename {pybamm => src/pybamm}/logger.py (100%) rename {pybamm => src/pybamm}/meshes/__init__.py (100%) rename {pybamm => src/pybamm}/meshes/meshes.py (100%) rename {pybamm => src/pybamm}/meshes/one_dimensional_submeshes.py (100%) rename {pybamm => src/pybamm}/meshes/scikit_fem_submeshes.py (100%) rename {pybamm => src/pybamm}/meshes/zero_dimensional_submesh.py (100%) rename {pybamm => src/pybamm}/models/__init__.py (100%) rename {pybamm => src/pybamm}/models/base_model.py (100%) rename {pybamm => src/pybamm}/models/event.py (100%) rename {pybamm => src/pybamm}/models/full_battery_models/__init__.py (100%) rename {pybamm => src/pybamm}/models/full_battery_models/base_battery_model.py (100%) rename {pybamm => src/pybamm}/models/full_battery_models/equivalent_circuit/__init__.py (100%) rename {pybamm => src/pybamm}/models/full_battery_models/equivalent_circuit/ecm_model_options.py (100%) rename {pybamm => src/pybamm}/models/full_battery_models/equivalent_circuit/thevenin.py (100%) rename {pybamm => src/pybamm}/models/full_battery_models/lead_acid/__init__.py (100%) rename {pybamm => src/pybamm}/models/full_battery_models/lead_acid/base_lead_acid_model.py (100%) rename {pybamm => src/pybamm}/models/full_battery_models/lead_acid/basic_full.py (100%) rename {pybamm => src/pybamm}/models/full_battery_models/lead_acid/full.py (100%) rename {pybamm => src/pybamm}/models/full_battery_models/lead_acid/loqs.py (100%) rename {pybamm => src/pybamm}/models/full_battery_models/lithium_ion/Yang2017.py (100%) rename {pybamm => src/pybamm}/models/full_battery_models/lithium_ion/__init__.py (100%) rename {pybamm => src/pybamm}/models/full_battery_models/lithium_ion/base_lithium_ion_model.py (100%) rename {pybamm => src/pybamm}/models/full_battery_models/lithium_ion/basic_dfn.py (100%) rename {pybamm => src/pybamm}/models/full_battery_models/lithium_ion/basic_dfn_composite.py (100%) rename {pybamm => src/pybamm}/models/full_battery_models/lithium_ion/basic_dfn_half_cell.py (100%) rename {pybamm => src/pybamm}/models/full_battery_models/lithium_ion/basic_spm.py (100%) rename {pybamm => src/pybamm}/models/full_battery_models/lithium_ion/dfn.py (100%) rename {pybamm => src/pybamm}/models/full_battery_models/lithium_ion/electrode_soh.py (100%) rename {pybamm => src/pybamm}/models/full_battery_models/lithium_ion/electrode_soh_half_cell.py (100%) rename {pybamm => src/pybamm}/models/full_battery_models/lithium_ion/mpm.py (100%) rename {pybamm => src/pybamm}/models/full_battery_models/lithium_ion/msmr.py (100%) rename {pybamm => src/pybamm}/models/full_battery_models/lithium_ion/newman_tobias.py (100%) rename {pybamm => src/pybamm}/models/full_battery_models/lithium_ion/spm.py (100%) rename {pybamm => src/pybamm}/models/full_battery_models/lithium_ion/spme.py (100%) rename {pybamm => src/pybamm}/models/full_battery_models/lithium_metal/dfn.py (100%) rename {pybamm => src/pybamm}/models/submodels/__init__.py (100%) rename {pybamm => src/pybamm}/models/submodels/active_material/__init__.py (100%) rename {pybamm => src/pybamm}/models/submodels/active_material/base_active_material.py (100%) rename {pybamm => src/pybamm}/models/submodels/active_material/constant_active_material.py (100%) rename {pybamm => src/pybamm}/models/submodels/active_material/loss_active_material.py (100%) rename {pybamm => src/pybamm}/models/submodels/active_material/total_active_material.py (100%) rename {pybamm => src/pybamm}/models/submodels/base_submodel.py (100%) rename {pybamm => src/pybamm}/models/submodels/convection/__init__.py (100%) rename {pybamm => src/pybamm}/models/submodels/convection/base_convection.py (100%) rename {pybamm => src/pybamm}/models/submodels/convection/through_cell/__init__.py (100%) rename {pybamm => src/pybamm}/models/submodels/convection/through_cell/base_through_cell_convection.py (100%) rename {pybamm => src/pybamm}/models/submodels/convection/through_cell/explicit_convection.py (100%) rename {pybamm => src/pybamm}/models/submodels/convection/through_cell/full_convection.py (100%) rename {pybamm => src/pybamm}/models/submodels/convection/through_cell/no_convection.py (100%) rename {pybamm => src/pybamm}/models/submodels/convection/transverse/__init__.py (100%) rename {pybamm => src/pybamm}/models/submodels/convection/transverse/base_transverse_convection.py (100%) rename {pybamm => src/pybamm}/models/submodels/convection/transverse/full_convection.py (100%) rename {pybamm => src/pybamm}/models/submodels/convection/transverse/no_convection.py (100%) rename {pybamm => src/pybamm}/models/submodels/convection/transverse/uniform_convection.py (100%) rename {pybamm => src/pybamm}/models/submodels/current_collector/__init__.py (100%) rename {pybamm => src/pybamm}/models/submodels/current_collector/base_current_collector.py (100%) rename {pybamm => src/pybamm}/models/submodels/current_collector/effective_resistance_current_collector.py (100%) rename {pybamm => src/pybamm}/models/submodels/current_collector/homogeneous_current_collector.py (100%) rename {pybamm => src/pybamm}/models/submodels/current_collector/potential_pair.py (100%) rename {pybamm => src/pybamm}/models/submodels/electrode/__init__.py (100%) rename {pybamm => src/pybamm}/models/submodels/electrode/base_electrode.py (100%) rename {pybamm => src/pybamm}/models/submodels/electrode/ohm/__init__.py (100%) rename {pybamm => src/pybamm}/models/submodels/electrode/ohm/base_ohm.py (100%) rename {pybamm => src/pybamm}/models/submodels/electrode/ohm/composite_ohm.py (100%) rename {pybamm => src/pybamm}/models/submodels/electrode/ohm/full_ohm.py (100%) rename {pybamm => src/pybamm}/models/submodels/electrode/ohm/leading_ohm.py (100%) rename {pybamm => src/pybamm}/models/submodels/electrode/ohm/li_metal.py (100%) rename {pybamm => src/pybamm}/models/submodels/electrode/ohm/surface_form_ohm.py (100%) rename {pybamm => src/pybamm}/models/submodels/electrolyte_conductivity/__init__.py (100%) rename {pybamm => src/pybamm}/models/submodels/electrolyte_conductivity/base_electrolyte_conductivity.py (100%) rename {pybamm => src/pybamm}/models/submodels/electrolyte_conductivity/composite_conductivity.py (100%) rename {pybamm => src/pybamm}/models/submodels/electrolyte_conductivity/full_conductivity.py (100%) rename {pybamm => src/pybamm}/models/submodels/electrolyte_conductivity/integrated_conductivity.py (100%) rename {pybamm => src/pybamm}/models/submodels/electrolyte_conductivity/leading_order_conductivity.py (100%) rename {pybamm => src/pybamm}/models/submodels/electrolyte_conductivity/surface_potential_form/__init__.py (100%) rename {pybamm => src/pybamm}/models/submodels/electrolyte_conductivity/surface_potential_form/composite_surface_form_conductivity.py (100%) rename {pybamm => src/pybamm}/models/submodels/electrolyte_conductivity/surface_potential_form/explicit_surface_form_conductivity.py (100%) rename {pybamm => src/pybamm}/models/submodels/electrolyte_conductivity/surface_potential_form/full_surface_form_conductivity.py (100%) rename {pybamm => src/pybamm}/models/submodels/electrolyte_conductivity/surface_potential_form/leading_surface_form_conductivity.py (100%) rename {pybamm => src/pybamm}/models/submodels/electrolyte_diffusion/__init__.py (100%) rename {pybamm => src/pybamm}/models/submodels/electrolyte_diffusion/base_electrolyte_diffusion.py (100%) rename {pybamm => src/pybamm}/models/submodels/electrolyte_diffusion/constant_concentration.py (100%) rename {pybamm => src/pybamm}/models/submodels/electrolyte_diffusion/full_diffusion.py (100%) rename {pybamm => src/pybamm}/models/submodels/electrolyte_diffusion/leading_order_diffusion.py (100%) rename {pybamm => src/pybamm}/models/submodels/equivalent_circuit_elements/__init__.py (100%) rename {pybamm => src/pybamm}/models/submodels/equivalent_circuit_elements/diffusion_element.py (100%) rename {pybamm => src/pybamm}/models/submodels/equivalent_circuit_elements/ocv_element.py (100%) rename {pybamm => src/pybamm}/models/submodels/equivalent_circuit_elements/rc_element.py (100%) rename {pybamm => src/pybamm}/models/submodels/equivalent_circuit_elements/resistor_element.py (100%) rename {pybamm => src/pybamm}/models/submodels/equivalent_circuit_elements/thermal.py (100%) rename {pybamm => src/pybamm}/models/submodels/equivalent_circuit_elements/voltage_model.py (100%) rename {pybamm => src/pybamm}/models/submodels/external_circuit/__init__.py (100%) rename {pybamm => src/pybamm}/models/submodels/external_circuit/base_external_circuit.py (100%) rename {pybamm => src/pybamm}/models/submodels/external_circuit/discharge_throughput.py (100%) rename {pybamm => src/pybamm}/models/submodels/external_circuit/explicit_control_external_circuit.py (100%) rename {pybamm => src/pybamm}/models/submodels/external_circuit/function_control_external_circuit.py (100%) rename {pybamm => src/pybamm}/models/submodels/interface/__init__.py (100%) rename {pybamm => src/pybamm}/models/submodels/interface/base_interface.py (100%) rename {pybamm => src/pybamm}/models/submodels/interface/interface_utilisation/__init__.py (100%) rename {pybamm => src/pybamm}/models/submodels/interface/interface_utilisation/base_utilisation.py (100%) rename {pybamm => src/pybamm}/models/submodels/interface/interface_utilisation/constant_utilisation.py (100%) rename {pybamm => src/pybamm}/models/submodels/interface/interface_utilisation/current_driven_utilisation.py (100%) rename {pybamm => src/pybamm}/models/submodels/interface/interface_utilisation/full_utilisation.py (100%) rename {pybamm => src/pybamm}/models/submodels/interface/kinetics/__init__.py (100%) rename {pybamm => src/pybamm}/models/submodels/interface/kinetics/base_kinetics.py (100%) rename {pybamm => src/pybamm}/models/submodels/interface/kinetics/butler_volmer.py (100%) rename {pybamm => src/pybamm}/models/submodels/interface/kinetics/diffusion_limited.py (100%) rename {pybamm => src/pybamm}/models/submodels/interface/kinetics/inverse_kinetics/__init__.py (100%) rename {pybamm => src/pybamm}/models/submodels/interface/kinetics/inverse_kinetics/inverse_butler_volmer.py (100%) rename {pybamm => src/pybamm}/models/submodels/interface/kinetics/linear.py (100%) rename {pybamm => src/pybamm}/models/submodels/interface/kinetics/marcus.py (100%) rename {pybamm => src/pybamm}/models/submodels/interface/kinetics/msmr_butler_volmer.py (100%) rename {pybamm => src/pybamm}/models/submodels/interface/kinetics/no_reaction.py (100%) rename {pybamm => src/pybamm}/models/submodels/interface/kinetics/tafel.py (100%) rename {pybamm => src/pybamm}/models/submodels/interface/kinetics/total_main_kinetics.py (100%) rename {pybamm => src/pybamm}/models/submodels/interface/lithium_plating/__init__.py (100%) rename {pybamm => src/pybamm}/models/submodels/interface/lithium_plating/base_plating.py (100%) rename {pybamm => src/pybamm}/models/submodels/interface/lithium_plating/no_plating.py (100%) rename {pybamm => src/pybamm}/models/submodels/interface/lithium_plating/plating.py (100%) rename {pybamm => src/pybamm}/models/submodels/interface/lithium_plating/total_lithium_plating.py (100%) rename {pybamm => src/pybamm}/models/submodels/interface/open_circuit_potential/__init__.py (100%) rename {pybamm => src/pybamm}/models/submodels/interface/open_circuit_potential/base_ocp.py (100%) rename {pybamm => src/pybamm}/models/submodels/interface/open_circuit_potential/current_sigmoid_ocp.py (100%) rename {pybamm => src/pybamm}/models/submodels/interface/open_circuit_potential/msmr_ocp.py (100%) rename {pybamm => src/pybamm}/models/submodels/interface/open_circuit_potential/single_ocp.py (100%) rename {pybamm => src/pybamm}/models/submodels/interface/open_circuit_potential/wycisk_ocp.py (100%) rename {pybamm => src/pybamm}/models/submodels/interface/sei/__init__.py (100%) rename {pybamm => src/pybamm}/models/submodels/interface/sei/base_sei.py (100%) rename {pybamm => src/pybamm}/models/submodels/interface/sei/constant_sei.py (100%) rename {pybamm => src/pybamm}/models/submodels/interface/sei/no_sei.py (100%) rename {pybamm => src/pybamm}/models/submodels/interface/sei/sei_growth.py (100%) rename {pybamm => src/pybamm}/models/submodels/interface/sei/total_sei.py (100%) rename {pybamm => src/pybamm}/models/submodels/interface/total_interfacial_current.py (100%) rename {pybamm => src/pybamm}/models/submodels/oxygen_diffusion/__init__.py (100%) rename {pybamm => src/pybamm}/models/submodels/oxygen_diffusion/base_oxygen_diffusion.py (100%) rename {pybamm => src/pybamm}/models/submodels/oxygen_diffusion/full_oxygen_diffusion.py (100%) rename {pybamm => src/pybamm}/models/submodels/oxygen_diffusion/leading_oxygen_diffusion.py (100%) rename {pybamm => src/pybamm}/models/submodels/oxygen_diffusion/no_oxygen.py (100%) rename {pybamm => src/pybamm}/models/submodels/particle/__init__.py (100%) rename {pybamm => src/pybamm}/models/submodels/particle/base_particle.py (100%) rename {pybamm => src/pybamm}/models/submodels/particle/fickian_diffusion.py (100%) rename {pybamm => src/pybamm}/models/submodels/particle/msmr_diffusion.py (100%) rename {pybamm => src/pybamm}/models/submodels/particle/polynomial_profile.py (100%) rename {pybamm => src/pybamm}/models/submodels/particle/total_particle_concentration.py (100%) rename {pybamm => src/pybamm}/models/submodels/particle/x_averaged_polynomial_profile.py (100%) rename {pybamm => src/pybamm}/models/submodels/particle_mechanics/__init__.py (100%) rename {pybamm => src/pybamm}/models/submodels/particle_mechanics/base_mechanics.py (100%) rename {pybamm => src/pybamm}/models/submodels/particle_mechanics/crack_propagation.py (100%) rename {pybamm => src/pybamm}/models/submodels/particle_mechanics/no_mechanics.py (100%) rename {pybamm => src/pybamm}/models/submodels/particle_mechanics/swelling_only.py (100%) rename {pybamm => src/pybamm}/models/submodels/porosity/__init__.py (100%) rename {pybamm => src/pybamm}/models/submodels/porosity/base_porosity.py (100%) rename {pybamm => src/pybamm}/models/submodels/porosity/constant_porosity.py (100%) rename {pybamm => src/pybamm}/models/submodels/porosity/reaction_driven_porosity.py (100%) rename {pybamm => src/pybamm}/models/submodels/porosity/reaction_driven_porosity_ode.py (100%) rename {pybamm => src/pybamm}/models/submodels/thermal/__init__.py (100%) rename {pybamm => src/pybamm}/models/submodels/thermal/base_thermal.py (100%) rename {pybamm => src/pybamm}/models/submodels/thermal/isothermal.py (100%) rename {pybamm => src/pybamm}/models/submodels/thermal/lumped.py (100%) rename {pybamm => src/pybamm}/models/submodels/thermal/pouch_cell/__init__.py (100%) rename {pybamm => src/pybamm}/models/submodels/thermal/pouch_cell/pouch_cell_1D_current_collectors.py (100%) rename {pybamm => src/pybamm}/models/submodels/thermal/pouch_cell/pouch_cell_2D_current_collectors.py (100%) rename {pybamm => src/pybamm}/models/submodels/thermal/pouch_cell/x_full.py (100%) rename {pybamm => src/pybamm}/models/submodels/transport_efficiency/__init__.py (100%) rename {pybamm => src/pybamm}/models/submodels/transport_efficiency/base_transport_efficiency.py (100%) rename {pybamm => src/pybamm}/models/submodels/transport_efficiency/bruggeman.py (100%) rename {pybamm => src/pybamm}/models/submodels/transport_efficiency/cation_exchange_membrane.py (100%) rename {pybamm => src/pybamm}/models/submodels/transport_efficiency/heterogeneous_catalyst.py (100%) rename {pybamm => src/pybamm}/models/submodels/transport_efficiency/hyperbola_of_revolution.py (100%) rename {pybamm => src/pybamm}/models/submodels/transport_efficiency/ordered_packing.py (100%) rename {pybamm => src/pybamm}/models/submodels/transport_efficiency/overlapping_spheres.py (100%) rename {pybamm => src/pybamm}/models/submodels/transport_efficiency/random_overlapping_cylinders.py (100%) rename {pybamm => src/pybamm}/models/submodels/transport_efficiency/tortuosity_factor.py (100%) rename {pybamm => src/pybamm}/parameters/__init__.py (100%) rename {pybamm => src/pybamm}/parameters/base_parameters.py (100%) rename {pybamm => src/pybamm}/parameters/bpx.py (100%) rename {pybamm => src/pybamm}/parameters/constants.py (100%) rename {pybamm => src/pybamm}/parameters/ecm_parameters.py (100%) rename {pybamm => src/pybamm}/parameters/electrical_parameters.py (100%) rename {pybamm => src/pybamm}/parameters/geometric_parameters.py (100%) rename {pybamm => src/pybamm}/parameters/lead_acid_parameters.py (100%) rename {pybamm => src/pybamm}/parameters/lithium_ion_parameters.py (100%) rename {pybamm => src/pybamm}/parameters/parameter_sets.py (100%) rename {pybamm => src/pybamm}/parameters/parameter_values.py (100%) rename {pybamm => src/pybamm}/parameters/process_parameter_data.py (100%) rename {pybamm => src/pybamm}/parameters/size_distribution_parameters.py (100%) rename {pybamm => src/pybamm}/parameters/thermal_parameters.py (100%) rename {pybamm => src/pybamm}/plotting/__init__.py (100%) rename {pybamm => src/pybamm}/plotting/dynamic_plot.py (100%) rename {pybamm => src/pybamm}/plotting/plot.py (100%) rename {pybamm => src/pybamm}/plotting/plot2D.py (100%) rename {pybamm => src/pybamm}/plotting/plot_summary_variables.py (100%) rename {pybamm => src/pybamm}/plotting/plot_thermal_components.py (100%) rename {pybamm => src/pybamm}/plotting/plot_voltage_components.py (100%) rename {pybamm => src/pybamm}/plotting/quick_plot.py (100%) rename {pybamm => src/pybamm}/pybamm_data.py (100%) rename {pybamm => src/pybamm}/settings.py (100%) rename {pybamm => src/pybamm}/simulation.py (100%) rename {pybamm => src/pybamm}/solvers/__init__.py (100%) rename {pybamm => src/pybamm}/solvers/algebraic_solver.py (100%) rename {pybamm => src/pybamm}/solvers/base_solver.py (100%) rename {pybamm => src/pybamm}/solvers/c_solvers/__init__.py (100%) rename {pybamm => src/pybamm}/solvers/c_solvers/idaklu.cpp (100%) rename {pybamm => src/pybamm}/solvers/c_solvers/idaklu/Expressions/Base/Expression.hpp (100%) rename {pybamm => src/pybamm}/solvers/c_solvers/idaklu/Expressions/Base/ExpressionSet.hpp (100%) rename {pybamm => src/pybamm}/solvers/c_solvers/idaklu/Expressions/Base/ExpressionTypes.hpp (100%) rename {pybamm => src/pybamm}/solvers/c_solvers/idaklu/Expressions/Casadi/CasadiFunctions.cpp (100%) rename {pybamm => src/pybamm}/solvers/c_solvers/idaklu/Expressions/Casadi/CasadiFunctions.hpp (100%) rename {pybamm => src/pybamm}/solvers/c_solvers/idaklu/Expressions/Expressions.hpp (100%) rename {pybamm => src/pybamm}/solvers/c_solvers/idaklu/Expressions/IREE/IREEBaseFunction.hpp (100%) rename {pybamm => src/pybamm}/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunction.hpp (100%) rename {pybamm => src/pybamm}/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunctions.cpp (100%) rename {pybamm => src/pybamm}/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunctions.hpp (100%) rename {pybamm => src/pybamm}/solvers/c_solvers/idaklu/Expressions/IREE/ModuleParser.cpp (100%) rename {pybamm => src/pybamm}/solvers/c_solvers/idaklu/Expressions/IREE/ModuleParser.hpp (100%) rename {pybamm => src/pybamm}/solvers/c_solvers/idaklu/Expressions/IREE/iree_jit.cpp (100%) rename {pybamm => src/pybamm}/solvers/c_solvers/idaklu/Expressions/IREE/iree_jit.hpp (100%) rename {pybamm => src/pybamm}/solvers/c_solvers/idaklu/IDAKLUSolver.cpp (100%) rename {pybamm => src/pybamm}/solvers/c_solvers/idaklu/IDAKLUSolver.hpp (100%) rename {pybamm => src/pybamm}/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.hpp (100%) rename {pybamm => src/pybamm}/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl (100%) rename {pybamm => src/pybamm}/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP_solvers.cpp (100%) rename {pybamm => src/pybamm}/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP_solvers.hpp (100%) rename {pybamm => src/pybamm}/solvers/c_solvers/idaklu/IdakluJax.cpp (100%) rename {pybamm => src/pybamm}/solvers/c_solvers/idaklu/IdakluJax.hpp (100%) rename {pybamm => src/pybamm}/solvers/c_solvers/idaklu/Options.cpp (100%) rename {pybamm => src/pybamm}/solvers/c_solvers/idaklu/Options.hpp (100%) rename {pybamm => src/pybamm}/solvers/c_solvers/idaklu/Solution.cpp (100%) rename {pybamm => src/pybamm}/solvers/c_solvers/idaklu/Solution.hpp (100%) rename {pybamm => src/pybamm}/solvers/c_solvers/idaklu/common.hpp (100%) rename {pybamm => src/pybamm}/solvers/c_solvers/idaklu/idaklu_solver.hpp (100%) rename {pybamm => src/pybamm}/solvers/c_solvers/idaklu/python.cpp (100%) rename {pybamm => src/pybamm}/solvers/c_solvers/idaklu/python.hpp (100%) rename {pybamm => src/pybamm}/solvers/c_solvers/idaklu/sundials_functions.hpp (100%) rename {pybamm => src/pybamm}/solvers/c_solvers/idaklu/sundials_functions.inl (100%) rename {pybamm => src/pybamm}/solvers/c_solvers/idaklu/sundials_legacy_wrapper.hpp (100%) rename {pybamm => src/pybamm}/solvers/casadi_algebraic_solver.py (100%) rename {pybamm => src/pybamm}/solvers/casadi_solver.py (100%) rename {pybamm => src/pybamm}/solvers/dummy_solver.py (100%) rename {pybamm => src/pybamm}/solvers/idaklu_jax.py (100%) rename {pybamm => src/pybamm}/solvers/idaklu_solver.py (100%) rename {pybamm => src/pybamm}/solvers/jax_bdf_solver.py (100%) rename {pybamm => src/pybamm}/solvers/jax_solver.py (100%) rename {pybamm => src/pybamm}/solvers/lrudict.py (100%) rename {pybamm => src/pybamm}/solvers/processed_variable.py (100%) rename {pybamm => src/pybamm}/solvers/processed_variable_computed.py (100%) rename {pybamm => src/pybamm}/solvers/scipy_solver.py (100%) rename {pybamm => src/pybamm}/solvers/solution.py (100%) rename {pybamm => src/pybamm}/spatial_methods/__init__.py (100%) rename {pybamm => src/pybamm}/spatial_methods/finite_volume.py (100%) rename {pybamm => src/pybamm}/spatial_methods/scikit_finite_element.py (100%) rename {pybamm => src/pybamm}/spatial_methods/spatial_method.py (100%) rename {pybamm => src/pybamm}/spatial_methods/spectral_volume.py (100%) rename {pybamm => src/pybamm}/spatial_methods/zero_dimensional_method.py (100%) rename {pybamm => src/pybamm}/type_definitions.py (100%) rename {pybamm => src/pybamm}/util.py (99%) rename {pybamm => src/pybamm}/version.py (100%) diff --git a/.gitignore b/.gitignore index 03750e18b2..42c76b7c55 100644 --- a/.gitignore +++ b/.gitignore @@ -14,8 +14,8 @@ !requirements* !LICENSE.txt !CMakeLists.txt -!pybamm/CITATIONS.bib -!pybamm/input/**/*.csv +!src/pybamm/CITATIONS.bib +!src/pybamm/input/**/*.csv !tests/unit/test_parameters/*.csv !benchmarks/benchmark_images/*.png diff --git a/CMakeLists.txt b/CMakeLists.txt index 661f63457e..42ab10ee69 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,8 +48,8 @@ set(IDAKLU_EXPR_CASADI_SOURCE_FILES "") if(${PYBAMM_IDAKLU_EXPR_CASADI} STREQUAL "ON" ) add_compile_definitions(CASADI_ENABLE) set(IDAKLU_EXPR_CASADI_SOURCE_FILES - pybamm/solvers/c_solvers/idaklu/Expressions/Casadi/CasadiFunctions.cpp - pybamm/solvers/c_solvers/idaklu/Expressions/Casadi/CasadiFunctions.hpp + src/pybamm/solvers/c_solvers/idaklu/Expressions/Casadi/CasadiFunctions.cpp + src/pybamm/solvers/c_solvers/idaklu/Expressions/Casadi/CasadiFunctions.hpp ) endif() @@ -65,43 +65,43 @@ if(${PYBAMM_IDAKLU_EXPR_IREE} STREQUAL "ON" ) add_compile_definitions(IREE_ENABLE) # Source file list set(IDAKLU_EXPR_IREE_SOURCE_FILES - pybamm/solvers/c_solvers/idaklu/Expressions/IREE/iree_jit.cpp - pybamm/solvers/c_solvers/idaklu/Expressions/IREE/iree_jit.hpp - pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunctions.cpp - pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunctions.hpp - pybamm/solvers/c_solvers/idaklu/Expressions/IREE/ModuleParser.cpp - pybamm/solvers/c_solvers/idaklu/Expressions/IREE/ModuleParser.hpp + src/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/iree_jit.cpp + src/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/iree_jit.hpp + src/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunctions.cpp + src/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunctions.hpp + src/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/ModuleParser.cpp + src/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/ModuleParser.hpp ) endif() # The complete (all dependencies) sources list should be mirrored in setup.py pybind11_add_module(idaklu # pybind11 interface - pybamm/solvers/c_solvers/idaklu.cpp + src/pybamm/solvers/c_solvers/idaklu.cpp # IDAKLU solver (SUNDIALS) - pybamm/solvers/c_solvers/idaklu/idaklu_solver.hpp - pybamm/solvers/c_solvers/idaklu/IDAKLUSolver.cpp - pybamm/solvers/c_solvers/idaklu/IDAKLUSolver.hpp - pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl - pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.hpp - pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP_solvers.cpp - pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP_solvers.hpp - pybamm/solvers/c_solvers/idaklu/sundials_functions.inl - pybamm/solvers/c_solvers/idaklu/sundials_functions.hpp - pybamm/solvers/c_solvers/idaklu/IdakluJax.cpp - pybamm/solvers/c_solvers/idaklu/IdakluJax.hpp - pybamm/solvers/c_solvers/idaklu/common.hpp - pybamm/solvers/c_solvers/idaklu/python.hpp - pybamm/solvers/c_solvers/idaklu/python.cpp - pybamm/solvers/c_solvers/idaklu/Solution.cpp - pybamm/solvers/c_solvers/idaklu/Solution.hpp - pybamm/solvers/c_solvers/idaklu/Options.hpp - pybamm/solvers/c_solvers/idaklu/Options.cpp + src/pybamm/solvers/c_solvers/idaklu/idaklu_solver.hpp + src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolver.cpp + src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolver.hpp + src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl + src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.hpp + src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP_solvers.cpp + src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP_solvers.hpp + src/pybamm/solvers/c_solvers/idaklu/sundials_functions.inl + src/pybamm/solvers/c_solvers/idaklu/sundials_functions.hpp + src/pybamm/solvers/c_solvers/idaklu/IdakluJax.cpp + src/pybamm/solvers/c_solvers/idaklu/IdakluJax.hpp + src/pybamm/solvers/c_solvers/idaklu/common.hpp + src/pybamm/solvers/c_solvers/idaklu/python.hpp + src/pybamm/solvers/c_solvers/idaklu/python.cpp + src/pybamm/solvers/c_solvers/idaklu/Solution.cpp + src/pybamm/solvers/c_solvers/idaklu/Solution.hpp + src/pybamm/solvers/c_solvers/idaklu/Options.hpp + src/pybamm/solvers/c_solvers/idaklu/Options.cpp # IDAKLU expressions / function evaluation [abstract] - pybamm/solvers/c_solvers/idaklu/Expressions/Expressions.hpp - pybamm/solvers/c_solvers/idaklu/Expressions/Base/Expression.hpp - pybamm/solvers/c_solvers/idaklu/Expressions/Base/ExpressionSet.hpp - pybamm/solvers/c_solvers/idaklu/Expressions/Base/ExpressionTypes.hpp + src/pybamm/solvers/c_solvers/idaklu/Expressions/Expressions.hpp + src/pybamm/solvers/c_solvers/idaklu/Expressions/Base/Expression.hpp + src/pybamm/solvers/c_solvers/idaklu/Expressions/Base/ExpressionSet.hpp + src/pybamm/solvers/c_solvers/idaklu/Expressions/Base/ExpressionTypes.hpp # IDAKLU expressions - concrete implementations ${IDAKLU_EXPR_CASADI_SOURCE_FILES} ${IDAKLU_EXPR_IREE_SOURCE_FILES} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1a1b015c09..73977370b4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,7 +38,7 @@ We use [GIT](https://en.wikipedia.org/wiki/Git) and [GitHub](https://en.wikipedi 2. Create a [branch](https://help.github.com/articles/creating-and-deleting-branches-within-your-repository/) of this repo (ideally on your own [fork](https://help.github.com/articles/fork-a-repo/)), where all changes will be made 3. Download the source code onto your local system, by [cloning](https://help.github.com/articles/cloning-a-repository/) the repository (or your fork of the repository). 4. [Install](https://docs.pybamm.org/en/latest/source/user_guide/installation/install-from-source.html) PyBaMM with the developer options. -5. [Test](#testing) if your installation worked, using the test script: `$ python run-tests.py --unit`. +5. [Test](#testing) if your installation worked, using pytest: `$ pytest -m unit`. You now have everything you need to start making changes! @@ -405,7 +405,7 @@ pybamm.print_citations() to the end of a script will print all citations that were used by that script. This will print BibTeX information to the terminal; passing a filename to `print_citations` will print the BibTeX information to the specified file instead. -When you contribute code to PyBaMM, you can add your own papers that you would like to be cited if that code is used. First, add the BibTeX for your paper to [CITATIONS.bib](https://github.com/pybamm-team/PyBaMM/blob/develop/pybamm/CITATIONS.bib). Then, add the line +When you contribute code to PyBaMM, you can add your own papers that you would like to be cited if that code is used. First, add the BibTeX for your paper to [CITATIONS.bib](https://github.com/pybamm-team/PyBaMM/blob/develop/src/pybamm/CITATIONS.bib). Then, add the line ```python3 pybamm.citations.register("your_paper_bibtex_identifier") diff --git a/MANIFEST.in b/MANIFEST.in index 9481c2d875..6b0e944a4c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -graft pybamm +graft src include CITATION.cff prune tests diff --git a/README.md b/README.md index 961c2d5f71..1a0f8b4b9b 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,7 @@ We would be grateful if you could also cite the relevant papers. These will chan pybamm.print_citations() ``` -to the end of your script. This will print BibTeX information to the terminal; passing a filename to `print_citations` will print the BibTeX information to the specified file instead. A list of all citations can also be found in the [citations file](https://github.com/pybamm-team/PyBaMM/blob/develop/pybamm/CITATIONS.bib). In particular, PyBaMM relies heavily on [CasADi](https://web.casadi.org/publications/). +to the end of your script. This will print BibTeX information to the terminal; passing a filename to `print_citations` will print the BibTeX information to the specified file instead. A list of all citations can also be found in the [citations file](https://github.com/pybamm-team/PyBaMM/blob/develop/src/pybamm/CITATIONS.bib). In particular, PyBaMM relies heavily on [CasADi](https://web.casadi.org/publications/). See [CONTRIBUTING.md](https://github.com/pybamm-team/PyBaMM/blob/develop/CONTRIBUTING.md#citations) for information on how to add your own citations when you contribute. ## 🛠️ Contributing to PyBaMM diff --git a/docs/conf.py b/docs/conf.py index a2a12bf04f..55a4ac3f61 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -288,7 +288,7 @@ # -- sphinxcontrib-bibtex configuration -------------------------------------- -bibtex_bibfiles = ["../pybamm/CITATIONS.bib"] +bibtex_bibfiles = ["../src/pybamm/CITATIONS.bib"] bibtex_style = "unsrt" bibtex_footbibliography_header = """.. rubric:: References""" bibtex_reference_style = "author_year" diff --git a/docs/source/examples/notebooks/models/SPM.ipynb b/docs/source/examples/notebooks/models/SPM.ipynb index 821580e3d7..2a247f4561 100644 --- a/docs/source/examples/notebooks/models/SPM.ipynb +++ b/docs/source/examples/notebooks/models/SPM.ipynb @@ -124,8 +124,7 @@ "variable = list(model.rhs.keys())[1]\n", "equation = list(model.rhs.values())[1]\n", "print(\"rhs equation for variable '\", variable, \"' is:\")\n", - "path = \"docs/source/examples/notebooks/models/\"\n", - "equation.visualise(path + \"spm1.png\")" + "equation.visualise(\"spm1.png\")" ] }, { @@ -383,7 +382,7 @@ "metadata": {}, "outputs": [], "source": [ - "model.concatenated_rhs.children[1].visualise(path + \"spm2.png\")" + "model.concatenated_rhs.children[1].visualise(\"spm2.png\")" ] }, { diff --git a/examples/scripts/drive_cycle.py b/examples/scripts/drive_cycle.py index e884aa679c..535d06f3e4 100644 --- a/examples/scripts/drive_cycle.py +++ b/examples/scripts/drive_cycle.py @@ -3,9 +3,7 @@ # import pybamm import pandas as pd -import os -os.chdir(pybamm.__path__[0] + "/..") pybamm.set_logging_level("INFO") diff --git a/examples/scripts/experiment_drive_cycle.py b/examples/scripts/experiment_drive_cycle.py index 9e0a9415a0..43db37b749 100644 --- a/examples/scripts/experiment_drive_cycle.py +++ b/examples/scripts/experiment_drive_cycle.py @@ -3,9 +3,7 @@ # import pybamm import pandas as pd -import os -os.chdir(pybamm.__path__[0] + "/..") pybamm.set_logging_level("INFO") diff --git a/noxfile.py b/noxfile.py index c1510379d1..0e9ebc1776 100644 --- a/noxfile.py +++ b/noxfile.py @@ -180,7 +180,7 @@ def run_doctests(session): "-m", "pytest", "--doctest-plus", - "pybamm", + "src", ) diff --git a/pyproject.toml b/pyproject.toml index c71ad237a7..5e9c891877 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -169,13 +169,12 @@ pybamm = [ "*.md", "*.csv", "*.py", - "pybamm/CITATIONS.bib", - "pybamm/plotting/mplstyle", + "src/pybamm/CITATIONS.bib", + "src/pybamm/plotting/mplstyle", ] [tool.setuptools.packages.find] -include = ["pybamm", "pybamm.*"] - +where = ["src"] [tool.ruff] extend-include = ["*.ipynb"] extend-exclude = ["__init__.py"] @@ -264,7 +263,7 @@ log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" log_date_format = "%Y-%m-%d %H:%M:%S" [tool.coverage.run] -source = ["pybamm"] +source = ["src/pybamm"] concurrency = ["multiprocessing"] [tool.repo-review] diff --git a/scripts/update_version.py b/scripts/update_version.py index dfc6b7f32e..3543f0b07b 100644 --- a/scripts/update_version.py +++ b/scripts/update_version.py @@ -24,7 +24,9 @@ def update_version(): ) # pybamm/version.py - with open(os.path.join(pybamm.root_dir(), "pybamm", "version.py"), "r+") as file: + with open( + os.path.join(pybamm.root_dir(), "src", "pybamm", "version.py"), "r+" + ) as file: output = file.read() replace_version = re.sub( '(?<=__version__ = ")(.+)(?=")', release_version, output diff --git a/setup.py b/setup.py index 21dabcebb2..95108454ed 100644 --- a/setup.py +++ b/setup.py @@ -295,40 +295,40 @@ def compile_KLU(): name="pybamm.solvers.idaklu", # The sources list should mirror the list in CMakeLists.txt sources=[ - "pybamm/solvers/c_solvers/idaklu/Expressions/Expressions.hpp", - "pybamm/solvers/c_solvers/idaklu/Expressions/Base/Expression.hpp", - "pybamm/solvers/c_solvers/idaklu/Expressions/Base/ExpressionSet.hpp", - "pybamm/solvers/c_solvers/idaklu/Expressions/Base/ExpressionTypes.hpp", - "pybamm/solvers/c_solvers/idaklu/Expressions/Base/ExpressionSparsity.hpp", - "pybamm/solvers/c_solvers/idaklu/Expressions/Casadi/CasadiFunctions.cpp", - "pybamm/solvers/c_solvers/idaklu/Expressions/Casadi/CasadiFunctions.hpp", - "pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEBaseFunction.hpp", - "pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunction.hpp", - "pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunctions.cpp", - "pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunctions.hpp", - "pybamm/solvers/c_solvers/idaklu/Expressions/IREE/iree_jit.cpp", - "pybamm/solvers/c_solvers/idaklu/Expressions/IREE/iree_jit.hpp", - "pybamm/solvers/c_solvers/idaklu/Expressions/IREE/ModuleParser.cpp", - "pybamm/solvers/c_solvers/idaklu/Expressions/IREE/ModuleParser.hpp", - "pybamm/solvers/c_solvers/idaklu/idaklu_solver.hpp", - "pybamm/solvers/c_solvers/idaklu/IDAKLUSolver.cpp", - "pybamm/solvers/c_solvers/idaklu/IDAKLUSolver.hpp", - "pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl", - "pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.hpp", - "pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP_solvers.cpp", - "pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP_solvers.hpp", - "pybamm/solvers/c_solvers/idaklu/sundials_functions.inl", - "pybamm/solvers/c_solvers/idaklu/sundials_functions.hpp", - "pybamm/solvers/c_solvers/idaklu/IdakluJax.cpp", - "pybamm/solvers/c_solvers/idaklu/IdakluJax.hpp", - "pybamm/solvers/c_solvers/idaklu/common.hpp", - "pybamm/solvers/c_solvers/idaklu/python.hpp", - "pybamm/solvers/c_solvers/idaklu/python.cpp", - "pybamm/solvers/c_solvers/idaklu/Solution.cpp", - "pybamm/solvers/c_solvers/idaklu/Solution.hpp", - "pybamm/solvers/c_solvers/idaklu/Options.hpp", - "pybamm/solvers/c_solvers/idaklu/Options.cpp", - "pybamm/solvers/c_solvers/idaklu.cpp", + "src/pybamm/solvers/c_solvers/idaklu/Expressions/Expressions.hpp", + "src/pybamm/solvers/c_solvers/idaklu/Expressions/Base/Expression.hpp", + "src/pybamm/solvers/c_solvers/idaklu/Expressions/Base/ExpressionSet.hpp", + "src/pybamm/solvers/c_solvers/idaklu/Expressions/Base/ExpressionTypes.hpp", + "src/pybamm/solvers/c_solvers/idaklu/Expressions/Base/ExpressionSparsity.hpp", + "src/pybamm/solvers/c_solvers/idaklu/Expressions/Casadi/CasadiFunctions.cpp", + "src/pybamm/solvers/c_solvers/idaklu/Expressions/Casadi/CasadiFunctions.hpp", + "src/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEBaseFunction.hpp", + "src/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunction.hpp", + "src/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunctions.cpp", + "src/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunctions.hpp", + "src/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/iree_jit.cpp", + "src/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/iree_jit.hpp", + "src/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/ModuleParser.cpp", + "src/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/ModuleParser.hpp", + "src/pybamm/solvers/c_solvers/idaklu/idaklu_solver.hpp", + "src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolver.cpp", + "src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolver.hpp", + "src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl", + "src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.hpp", + "src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP_solvers.cpp", + "src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP_solvers.hpp", + "src/pybamm/solvers/c_solvers/idaklu/sundials_functions.inl", + "src/pybamm/solvers/c_solvers/idaklu/sundials_functions.hpp", + "src/pybamm/solvers/c_solvers/idaklu/IdakluJax.cpp", + "src/pybamm/solvers/c_solvers/idaklu/IdakluJax.hpp", + "src/pybamm/solvers/c_solvers/idaklu/common.hpp", + "src/pybamm/solvers/c_solvers/idaklu/python.hpp", + "src/pybamm/solvers/c_solvers/idaklu/python.cpp", + "src/pybamm/solvers/c_solvers/idaklu/Solution.cpp", + "src/pybamm/solvers/c_solvers/idaklu/Solution.hpp", + "src/pybamm/solvers/c_solvers/idaklu/Options.hpp", + "src/pybamm/solvers/c_solvers/idaklu/Options.cpp", + "src/pybamm/solvers/c_solvers/idaklu.cpp", ], ) ext_modules = [idaklu_ext] if compile_KLU() else [] diff --git a/pybamm/CITATIONS.bib b/src/pybamm/CITATIONS.bib similarity index 100% rename from pybamm/CITATIONS.bib rename to src/pybamm/CITATIONS.bib diff --git a/pybamm/__init__.py b/src/pybamm/__init__.py similarity index 100% rename from pybamm/__init__.py rename to src/pybamm/__init__.py diff --git a/pybamm/batch_study.py b/src/pybamm/batch_study.py similarity index 100% rename from pybamm/batch_study.py rename to src/pybamm/batch_study.py diff --git a/pybamm/callbacks.py b/src/pybamm/callbacks.py similarity index 100% rename from pybamm/callbacks.py rename to src/pybamm/callbacks.py diff --git a/pybamm/citations.py b/src/pybamm/citations.py similarity index 99% rename from pybamm/citations.py rename to src/pybamm/citations.py index b76e9b5c83..9f036b9bfd 100644 --- a/pybamm/citations.py +++ b/src/pybamm/citations.py @@ -68,7 +68,7 @@ def read_citations(self): """ try: parse_file = import_optional_dependency("pybtex.database", "parse_file") - citations_file = os.path.join(pybamm.root_dir(), "pybamm", "CITATIONS.bib") + citations_file = os.path.join(pybamm.__path__[0], "CITATIONS.bib") bib_data = parse_file(citations_file, bib_format="bibtex") for key, entry in bib_data.entries.items(): self._add_citation(key, entry) diff --git a/pybamm/discretisations/__init__.py b/src/pybamm/discretisations/__init__.py similarity index 100% rename from pybamm/discretisations/__init__.py rename to src/pybamm/discretisations/__init__.py diff --git a/pybamm/discretisations/discretisation.py b/src/pybamm/discretisations/discretisation.py similarity index 100% rename from pybamm/discretisations/discretisation.py rename to src/pybamm/discretisations/discretisation.py diff --git a/pybamm/doc_utils.py b/src/pybamm/doc_utils.py similarity index 100% rename from pybamm/doc_utils.py rename to src/pybamm/doc_utils.py diff --git a/pybamm/experiment/__init__.py b/src/pybamm/experiment/__init__.py similarity index 100% rename from pybamm/experiment/__init__.py rename to src/pybamm/experiment/__init__.py diff --git a/pybamm/experiment/experiment.py b/src/pybamm/experiment/experiment.py similarity index 100% rename from pybamm/experiment/experiment.py rename to src/pybamm/experiment/experiment.py diff --git a/pybamm/experiment/step/__init__.py b/src/pybamm/experiment/step/__init__.py similarity index 100% rename from pybamm/experiment/step/__init__.py rename to src/pybamm/experiment/step/__init__.py diff --git a/pybamm/experiment/step/base_step.py b/src/pybamm/experiment/step/base_step.py similarity index 100% rename from pybamm/experiment/step/base_step.py rename to src/pybamm/experiment/step/base_step.py diff --git a/pybamm/experiment/step/step_termination.py b/src/pybamm/experiment/step/step_termination.py similarity index 100% rename from pybamm/experiment/step/step_termination.py rename to src/pybamm/experiment/step/step_termination.py diff --git a/pybamm/experiment/step/steps.py b/src/pybamm/experiment/step/steps.py similarity index 100% rename from pybamm/experiment/step/steps.py rename to src/pybamm/experiment/step/steps.py diff --git a/pybamm/expression_tree/__init__.py b/src/pybamm/expression_tree/__init__.py similarity index 100% rename from pybamm/expression_tree/__init__.py rename to src/pybamm/expression_tree/__init__.py diff --git a/pybamm/expression_tree/array.py b/src/pybamm/expression_tree/array.py similarity index 100% rename from pybamm/expression_tree/array.py rename to src/pybamm/expression_tree/array.py diff --git a/pybamm/expression_tree/averages.py b/src/pybamm/expression_tree/averages.py similarity index 100% rename from pybamm/expression_tree/averages.py rename to src/pybamm/expression_tree/averages.py diff --git a/pybamm/expression_tree/binary_operators.py b/src/pybamm/expression_tree/binary_operators.py similarity index 100% rename from pybamm/expression_tree/binary_operators.py rename to src/pybamm/expression_tree/binary_operators.py diff --git a/pybamm/expression_tree/broadcasts.py b/src/pybamm/expression_tree/broadcasts.py similarity index 100% rename from pybamm/expression_tree/broadcasts.py rename to src/pybamm/expression_tree/broadcasts.py diff --git a/pybamm/expression_tree/concatenations.py b/src/pybamm/expression_tree/concatenations.py similarity index 100% rename from pybamm/expression_tree/concatenations.py rename to src/pybamm/expression_tree/concatenations.py diff --git a/pybamm/expression_tree/exceptions.py b/src/pybamm/expression_tree/exceptions.py similarity index 100% rename from pybamm/expression_tree/exceptions.py rename to src/pybamm/expression_tree/exceptions.py diff --git a/pybamm/expression_tree/functions.py b/src/pybamm/expression_tree/functions.py similarity index 100% rename from pybamm/expression_tree/functions.py rename to src/pybamm/expression_tree/functions.py diff --git a/pybamm/expression_tree/independent_variable.py b/src/pybamm/expression_tree/independent_variable.py similarity index 100% rename from pybamm/expression_tree/independent_variable.py rename to src/pybamm/expression_tree/independent_variable.py diff --git a/pybamm/expression_tree/input_parameter.py b/src/pybamm/expression_tree/input_parameter.py similarity index 100% rename from pybamm/expression_tree/input_parameter.py rename to src/pybamm/expression_tree/input_parameter.py diff --git a/pybamm/expression_tree/interpolant.py b/src/pybamm/expression_tree/interpolant.py similarity index 100% rename from pybamm/expression_tree/interpolant.py rename to src/pybamm/expression_tree/interpolant.py diff --git a/pybamm/expression_tree/matrix.py b/src/pybamm/expression_tree/matrix.py similarity index 100% rename from pybamm/expression_tree/matrix.py rename to src/pybamm/expression_tree/matrix.py diff --git a/pybamm/expression_tree/operations/__init__.py b/src/pybamm/expression_tree/operations/__init__.py similarity index 100% rename from pybamm/expression_tree/operations/__init__.py rename to src/pybamm/expression_tree/operations/__init__.py diff --git a/pybamm/expression_tree/operations/convert_to_casadi.py b/src/pybamm/expression_tree/operations/convert_to_casadi.py similarity index 100% rename from pybamm/expression_tree/operations/convert_to_casadi.py rename to src/pybamm/expression_tree/operations/convert_to_casadi.py diff --git a/pybamm/expression_tree/operations/evaluate_python.py b/src/pybamm/expression_tree/operations/evaluate_python.py similarity index 100% rename from pybamm/expression_tree/operations/evaluate_python.py rename to src/pybamm/expression_tree/operations/evaluate_python.py diff --git a/pybamm/expression_tree/operations/jacobian.py b/src/pybamm/expression_tree/operations/jacobian.py similarity index 100% rename from pybamm/expression_tree/operations/jacobian.py rename to src/pybamm/expression_tree/operations/jacobian.py diff --git a/pybamm/expression_tree/operations/latexify.py b/src/pybamm/expression_tree/operations/latexify.py similarity index 100% rename from pybamm/expression_tree/operations/latexify.py rename to src/pybamm/expression_tree/operations/latexify.py diff --git a/pybamm/expression_tree/operations/serialise.py b/src/pybamm/expression_tree/operations/serialise.py similarity index 100% rename from pybamm/expression_tree/operations/serialise.py rename to src/pybamm/expression_tree/operations/serialise.py diff --git a/pybamm/expression_tree/operations/unpack_symbols.py b/src/pybamm/expression_tree/operations/unpack_symbols.py similarity index 100% rename from pybamm/expression_tree/operations/unpack_symbols.py rename to src/pybamm/expression_tree/operations/unpack_symbols.py diff --git a/pybamm/expression_tree/parameter.py b/src/pybamm/expression_tree/parameter.py similarity index 100% rename from pybamm/expression_tree/parameter.py rename to src/pybamm/expression_tree/parameter.py diff --git a/pybamm/expression_tree/printing/__init__.py b/src/pybamm/expression_tree/printing/__init__.py similarity index 100% rename from pybamm/expression_tree/printing/__init__.py rename to src/pybamm/expression_tree/printing/__init__.py diff --git a/pybamm/expression_tree/printing/print_name.py b/src/pybamm/expression_tree/printing/print_name.py similarity index 100% rename from pybamm/expression_tree/printing/print_name.py rename to src/pybamm/expression_tree/printing/print_name.py diff --git a/pybamm/expression_tree/printing/sympy_overrides.py b/src/pybamm/expression_tree/printing/sympy_overrides.py similarity index 100% rename from pybamm/expression_tree/printing/sympy_overrides.py rename to src/pybamm/expression_tree/printing/sympy_overrides.py diff --git a/pybamm/expression_tree/scalar.py b/src/pybamm/expression_tree/scalar.py similarity index 100% rename from pybamm/expression_tree/scalar.py rename to src/pybamm/expression_tree/scalar.py diff --git a/pybamm/expression_tree/state_vector.py b/src/pybamm/expression_tree/state_vector.py similarity index 100% rename from pybamm/expression_tree/state_vector.py rename to src/pybamm/expression_tree/state_vector.py diff --git a/pybamm/expression_tree/symbol.py b/src/pybamm/expression_tree/symbol.py similarity index 100% rename from pybamm/expression_tree/symbol.py rename to src/pybamm/expression_tree/symbol.py diff --git a/pybamm/expression_tree/unary_operators.py b/src/pybamm/expression_tree/unary_operators.py similarity index 100% rename from pybamm/expression_tree/unary_operators.py rename to src/pybamm/expression_tree/unary_operators.py diff --git a/pybamm/expression_tree/variable.py b/src/pybamm/expression_tree/variable.py similarity index 100% rename from pybamm/expression_tree/variable.py rename to src/pybamm/expression_tree/variable.py diff --git a/pybamm/expression_tree/vector.py b/src/pybamm/expression_tree/vector.py similarity index 100% rename from pybamm/expression_tree/vector.py rename to src/pybamm/expression_tree/vector.py diff --git a/pybamm/geometry/__init__.py b/src/pybamm/geometry/__init__.py similarity index 100% rename from pybamm/geometry/__init__.py rename to src/pybamm/geometry/__init__.py diff --git a/pybamm/geometry/battery_geometry.py b/src/pybamm/geometry/battery_geometry.py similarity index 100% rename from pybamm/geometry/battery_geometry.py rename to src/pybamm/geometry/battery_geometry.py diff --git a/pybamm/geometry/geometry.py b/src/pybamm/geometry/geometry.py similarity index 100% rename from pybamm/geometry/geometry.py rename to src/pybamm/geometry/geometry.py diff --git a/pybamm/geometry/standard_spatial_vars.py b/src/pybamm/geometry/standard_spatial_vars.py similarity index 100% rename from pybamm/geometry/standard_spatial_vars.py rename to src/pybamm/geometry/standard_spatial_vars.py diff --git a/pybamm/input/__init__.py b/src/pybamm/input/__init__.py similarity index 100% rename from pybamm/input/__init__.py rename to src/pybamm/input/__init__.py diff --git a/pybamm/input/parameters/__init__.py b/src/pybamm/input/parameters/__init__.py similarity index 100% rename from pybamm/input/parameters/__init__.py rename to src/pybamm/input/parameters/__init__.py diff --git a/pybamm/input/parameters/ecm/__init__.py b/src/pybamm/input/parameters/ecm/__init__.py similarity index 100% rename from pybamm/input/parameters/ecm/__init__.py rename to src/pybamm/input/parameters/ecm/__init__.py diff --git a/pybamm/input/parameters/ecm/data/ecm_example_c1.csv b/src/pybamm/input/parameters/ecm/data/ecm_example_c1.csv similarity index 100% rename from pybamm/input/parameters/ecm/data/ecm_example_c1.csv rename to src/pybamm/input/parameters/ecm/data/ecm_example_c1.csv diff --git a/pybamm/input/parameters/ecm/data/ecm_example_dudt.csv b/src/pybamm/input/parameters/ecm/data/ecm_example_dudt.csv similarity index 100% rename from pybamm/input/parameters/ecm/data/ecm_example_dudt.csv rename to src/pybamm/input/parameters/ecm/data/ecm_example_dudt.csv diff --git a/pybamm/input/parameters/ecm/data/ecm_example_ocv.csv b/src/pybamm/input/parameters/ecm/data/ecm_example_ocv.csv similarity index 100% rename from pybamm/input/parameters/ecm/data/ecm_example_ocv.csv rename to src/pybamm/input/parameters/ecm/data/ecm_example_ocv.csv diff --git a/pybamm/input/parameters/ecm/data/ecm_example_r0.csv b/src/pybamm/input/parameters/ecm/data/ecm_example_r0.csv similarity index 100% rename from pybamm/input/parameters/ecm/data/ecm_example_r0.csv rename to src/pybamm/input/parameters/ecm/data/ecm_example_r0.csv diff --git a/pybamm/input/parameters/ecm/data/ecm_example_r1.csv b/src/pybamm/input/parameters/ecm/data/ecm_example_r1.csv similarity index 100% rename from pybamm/input/parameters/ecm/data/ecm_example_r1.csv rename to src/pybamm/input/parameters/ecm/data/ecm_example_r1.csv diff --git a/pybamm/input/parameters/ecm/example_set.py b/src/pybamm/input/parameters/ecm/example_set.py similarity index 100% rename from pybamm/input/parameters/ecm/example_set.py rename to src/pybamm/input/parameters/ecm/example_set.py diff --git a/pybamm/input/parameters/lead_acid/Sulzer2019.py b/src/pybamm/input/parameters/lead_acid/Sulzer2019.py similarity index 100% rename from pybamm/input/parameters/lead_acid/Sulzer2019.py rename to src/pybamm/input/parameters/lead_acid/Sulzer2019.py diff --git a/pybamm/input/parameters/lead_acid/__init__.py b/src/pybamm/input/parameters/lead_acid/__init__.py similarity index 100% rename from pybamm/input/parameters/lead_acid/__init__.py rename to src/pybamm/input/parameters/lead_acid/__init__.py diff --git a/pybamm/input/parameters/lithium_ion/Ai2020.py b/src/pybamm/input/parameters/lithium_ion/Ai2020.py similarity index 100% rename from pybamm/input/parameters/lithium_ion/Ai2020.py rename to src/pybamm/input/parameters/lithium_ion/Ai2020.py diff --git a/pybamm/input/parameters/lithium_ion/Chen2020.py b/src/pybamm/input/parameters/lithium_ion/Chen2020.py similarity index 100% rename from pybamm/input/parameters/lithium_ion/Chen2020.py rename to src/pybamm/input/parameters/lithium_ion/Chen2020.py diff --git a/pybamm/input/parameters/lithium_ion/Chen2020_composite.py b/src/pybamm/input/parameters/lithium_ion/Chen2020_composite.py similarity index 100% rename from pybamm/input/parameters/lithium_ion/Chen2020_composite.py rename to src/pybamm/input/parameters/lithium_ion/Chen2020_composite.py diff --git a/pybamm/input/parameters/lithium_ion/Ecker2015.py b/src/pybamm/input/parameters/lithium_ion/Ecker2015.py similarity index 100% rename from pybamm/input/parameters/lithium_ion/Ecker2015.py rename to src/pybamm/input/parameters/lithium_ion/Ecker2015.py diff --git a/pybamm/input/parameters/lithium_ion/Ecker2015_graphite_halfcell.py b/src/pybamm/input/parameters/lithium_ion/Ecker2015_graphite_halfcell.py similarity index 100% rename from pybamm/input/parameters/lithium_ion/Ecker2015_graphite_halfcell.py rename to src/pybamm/input/parameters/lithium_ion/Ecker2015_graphite_halfcell.py diff --git a/pybamm/input/parameters/lithium_ion/MSMR_example_set.py b/src/pybamm/input/parameters/lithium_ion/MSMR_example_set.py similarity index 100% rename from pybamm/input/parameters/lithium_ion/MSMR_example_set.py rename to src/pybamm/input/parameters/lithium_ion/MSMR_example_set.py diff --git a/pybamm/input/parameters/lithium_ion/Marquis2019.py b/src/pybamm/input/parameters/lithium_ion/Marquis2019.py similarity index 100% rename from pybamm/input/parameters/lithium_ion/Marquis2019.py rename to src/pybamm/input/parameters/lithium_ion/Marquis2019.py diff --git a/pybamm/input/parameters/lithium_ion/Mohtat2020.py b/src/pybamm/input/parameters/lithium_ion/Mohtat2020.py similarity index 100% rename from pybamm/input/parameters/lithium_ion/Mohtat2020.py rename to src/pybamm/input/parameters/lithium_ion/Mohtat2020.py diff --git a/pybamm/input/parameters/lithium_ion/NCA_Kim2011.py b/src/pybamm/input/parameters/lithium_ion/NCA_Kim2011.py similarity index 100% rename from pybamm/input/parameters/lithium_ion/NCA_Kim2011.py rename to src/pybamm/input/parameters/lithium_ion/NCA_Kim2011.py diff --git a/pybamm/input/parameters/lithium_ion/OKane2022.py b/src/pybamm/input/parameters/lithium_ion/OKane2022.py similarity index 100% rename from pybamm/input/parameters/lithium_ion/OKane2022.py rename to src/pybamm/input/parameters/lithium_ion/OKane2022.py diff --git a/pybamm/input/parameters/lithium_ion/OKane2022_graphite_SiOx_halfcell.py b/src/pybamm/input/parameters/lithium_ion/OKane2022_graphite_SiOx_halfcell.py similarity index 100% rename from pybamm/input/parameters/lithium_ion/OKane2022_graphite_SiOx_halfcell.py rename to src/pybamm/input/parameters/lithium_ion/OKane2022_graphite_SiOx_halfcell.py diff --git a/pybamm/input/parameters/lithium_ion/ORegan2022.py b/src/pybamm/input/parameters/lithium_ion/ORegan2022.py similarity index 100% rename from pybamm/input/parameters/lithium_ion/ORegan2022.py rename to src/pybamm/input/parameters/lithium_ion/ORegan2022.py diff --git a/pybamm/input/parameters/lithium_ion/Prada2013.py b/src/pybamm/input/parameters/lithium_ion/Prada2013.py similarity index 100% rename from pybamm/input/parameters/lithium_ion/Prada2013.py rename to src/pybamm/input/parameters/lithium_ion/Prada2013.py diff --git a/pybamm/input/parameters/lithium_ion/Ramadass2004.py b/src/pybamm/input/parameters/lithium_ion/Ramadass2004.py similarity index 100% rename from pybamm/input/parameters/lithium_ion/Ramadass2004.py rename to src/pybamm/input/parameters/lithium_ion/Ramadass2004.py diff --git a/pybamm/input/parameters/lithium_ion/Xu2019.py b/src/pybamm/input/parameters/lithium_ion/Xu2019.py similarity index 100% rename from pybamm/input/parameters/lithium_ion/Xu2019.py rename to src/pybamm/input/parameters/lithium_ion/Xu2019.py diff --git a/pybamm/input/parameters/lithium_ion/__init__.py b/src/pybamm/input/parameters/lithium_ion/__init__.py similarity index 100% rename from pybamm/input/parameters/lithium_ion/__init__.py rename to src/pybamm/input/parameters/lithium_ion/__init__.py diff --git a/pybamm/input/parameters/lithium_ion/data/graphite_LGM50_ocp_Chen2020.csv b/src/pybamm/input/parameters/lithium_ion/data/graphite_LGM50_ocp_Chen2020.csv similarity index 100% rename from pybamm/input/parameters/lithium_ion/data/graphite_LGM50_ocp_Chen2020.csv rename to src/pybamm/input/parameters/lithium_ion/data/graphite_LGM50_ocp_Chen2020.csv diff --git a/pybamm/input/parameters/lithium_ion/data/graphite_ocp_Ecker2015.csv b/src/pybamm/input/parameters/lithium_ion/data/graphite_ocp_Ecker2015.csv similarity index 100% rename from pybamm/input/parameters/lithium_ion/data/graphite_ocp_Ecker2015.csv rename to src/pybamm/input/parameters/lithium_ion/data/graphite_ocp_Ecker2015.csv diff --git a/pybamm/input/parameters/lithium_ion/data/graphite_ocp_Enertech_Ai2020.csv b/src/pybamm/input/parameters/lithium_ion/data/graphite_ocp_Enertech_Ai2020.csv similarity index 100% rename from pybamm/input/parameters/lithium_ion/data/graphite_ocp_Enertech_Ai2020.csv rename to src/pybamm/input/parameters/lithium_ion/data/graphite_ocp_Enertech_Ai2020.csv diff --git a/pybamm/input/parameters/lithium_ion/data/lico2_ocp_Ai2020.csv b/src/pybamm/input/parameters/lithium_ion/data/lico2_ocp_Ai2020.csv similarity index 100% rename from pybamm/input/parameters/lithium_ion/data/lico2_ocp_Ai2020.csv rename to src/pybamm/input/parameters/lithium_ion/data/lico2_ocp_Ai2020.csv diff --git a/pybamm/input/parameters/lithium_ion/data/measured_graphite_diffusivity_Ecker2015.csv b/src/pybamm/input/parameters/lithium_ion/data/measured_graphite_diffusivity_Ecker2015.csv similarity index 100% rename from pybamm/input/parameters/lithium_ion/data/measured_graphite_diffusivity_Ecker2015.csv rename to src/pybamm/input/parameters/lithium_ion/data/measured_graphite_diffusivity_Ecker2015.csv diff --git a/pybamm/input/parameters/lithium_ion/data/measured_nco_diffusivity_Ecker2015.csv b/src/pybamm/input/parameters/lithium_ion/data/measured_nco_diffusivity_Ecker2015.csv similarity index 100% rename from pybamm/input/parameters/lithium_ion/data/measured_nco_diffusivity_Ecker2015.csv rename to src/pybamm/input/parameters/lithium_ion/data/measured_nco_diffusivity_Ecker2015.csv diff --git a/pybamm/input/parameters/lithium_ion/data/nca_ocp_Kim2011_data.csv b/src/pybamm/input/parameters/lithium_ion/data/nca_ocp_Kim2011_data.csv similarity index 100% rename from pybamm/input/parameters/lithium_ion/data/nca_ocp_Kim2011_data.csv rename to src/pybamm/input/parameters/lithium_ion/data/nca_ocp_Kim2011_data.csv diff --git a/pybamm/input/parameters/lithium_ion/data/nco_ocp_Ecker2015.csv b/src/pybamm/input/parameters/lithium_ion/data/nco_ocp_Ecker2015.csv similarity index 100% rename from pybamm/input/parameters/lithium_ion/data/nco_ocp_Ecker2015.csv rename to src/pybamm/input/parameters/lithium_ion/data/nco_ocp_Ecker2015.csv diff --git a/pybamm/input/parameters/lithium_ion/data/nmc_LGM50_ocp_Chen2020.csv b/src/pybamm/input/parameters/lithium_ion/data/nmc_LGM50_ocp_Chen2020.csv similarity index 100% rename from pybamm/input/parameters/lithium_ion/data/nmc_LGM50_ocp_Chen2020.csv rename to src/pybamm/input/parameters/lithium_ion/data/nmc_LGM50_ocp_Chen2020.csv diff --git a/pybamm/logger.py b/src/pybamm/logger.py similarity index 100% rename from pybamm/logger.py rename to src/pybamm/logger.py diff --git a/pybamm/meshes/__init__.py b/src/pybamm/meshes/__init__.py similarity index 100% rename from pybamm/meshes/__init__.py rename to src/pybamm/meshes/__init__.py diff --git a/pybamm/meshes/meshes.py b/src/pybamm/meshes/meshes.py similarity index 100% rename from pybamm/meshes/meshes.py rename to src/pybamm/meshes/meshes.py diff --git a/pybamm/meshes/one_dimensional_submeshes.py b/src/pybamm/meshes/one_dimensional_submeshes.py similarity index 100% rename from pybamm/meshes/one_dimensional_submeshes.py rename to src/pybamm/meshes/one_dimensional_submeshes.py diff --git a/pybamm/meshes/scikit_fem_submeshes.py b/src/pybamm/meshes/scikit_fem_submeshes.py similarity index 100% rename from pybamm/meshes/scikit_fem_submeshes.py rename to src/pybamm/meshes/scikit_fem_submeshes.py diff --git a/pybamm/meshes/zero_dimensional_submesh.py b/src/pybamm/meshes/zero_dimensional_submesh.py similarity index 100% rename from pybamm/meshes/zero_dimensional_submesh.py rename to src/pybamm/meshes/zero_dimensional_submesh.py diff --git a/pybamm/models/__init__.py b/src/pybamm/models/__init__.py similarity index 100% rename from pybamm/models/__init__.py rename to src/pybamm/models/__init__.py diff --git a/pybamm/models/base_model.py b/src/pybamm/models/base_model.py similarity index 100% rename from pybamm/models/base_model.py rename to src/pybamm/models/base_model.py diff --git a/pybamm/models/event.py b/src/pybamm/models/event.py similarity index 100% rename from pybamm/models/event.py rename to src/pybamm/models/event.py diff --git a/pybamm/models/full_battery_models/__init__.py b/src/pybamm/models/full_battery_models/__init__.py similarity index 100% rename from pybamm/models/full_battery_models/__init__.py rename to src/pybamm/models/full_battery_models/__init__.py diff --git a/pybamm/models/full_battery_models/base_battery_model.py b/src/pybamm/models/full_battery_models/base_battery_model.py similarity index 100% rename from pybamm/models/full_battery_models/base_battery_model.py rename to src/pybamm/models/full_battery_models/base_battery_model.py diff --git a/pybamm/models/full_battery_models/equivalent_circuit/__init__.py b/src/pybamm/models/full_battery_models/equivalent_circuit/__init__.py similarity index 100% rename from pybamm/models/full_battery_models/equivalent_circuit/__init__.py rename to src/pybamm/models/full_battery_models/equivalent_circuit/__init__.py diff --git a/pybamm/models/full_battery_models/equivalent_circuit/ecm_model_options.py b/src/pybamm/models/full_battery_models/equivalent_circuit/ecm_model_options.py similarity index 100% rename from pybamm/models/full_battery_models/equivalent_circuit/ecm_model_options.py rename to src/pybamm/models/full_battery_models/equivalent_circuit/ecm_model_options.py diff --git a/pybamm/models/full_battery_models/equivalent_circuit/thevenin.py b/src/pybamm/models/full_battery_models/equivalent_circuit/thevenin.py similarity index 100% rename from pybamm/models/full_battery_models/equivalent_circuit/thevenin.py rename to src/pybamm/models/full_battery_models/equivalent_circuit/thevenin.py diff --git a/pybamm/models/full_battery_models/lead_acid/__init__.py b/src/pybamm/models/full_battery_models/lead_acid/__init__.py similarity index 100% rename from pybamm/models/full_battery_models/lead_acid/__init__.py rename to src/pybamm/models/full_battery_models/lead_acid/__init__.py diff --git a/pybamm/models/full_battery_models/lead_acid/base_lead_acid_model.py b/src/pybamm/models/full_battery_models/lead_acid/base_lead_acid_model.py similarity index 100% rename from pybamm/models/full_battery_models/lead_acid/base_lead_acid_model.py rename to src/pybamm/models/full_battery_models/lead_acid/base_lead_acid_model.py diff --git a/pybamm/models/full_battery_models/lead_acid/basic_full.py b/src/pybamm/models/full_battery_models/lead_acid/basic_full.py similarity index 100% rename from pybamm/models/full_battery_models/lead_acid/basic_full.py rename to src/pybamm/models/full_battery_models/lead_acid/basic_full.py diff --git a/pybamm/models/full_battery_models/lead_acid/full.py b/src/pybamm/models/full_battery_models/lead_acid/full.py similarity index 100% rename from pybamm/models/full_battery_models/lead_acid/full.py rename to src/pybamm/models/full_battery_models/lead_acid/full.py diff --git a/pybamm/models/full_battery_models/lead_acid/loqs.py b/src/pybamm/models/full_battery_models/lead_acid/loqs.py similarity index 100% rename from pybamm/models/full_battery_models/lead_acid/loqs.py rename to src/pybamm/models/full_battery_models/lead_acid/loqs.py diff --git a/pybamm/models/full_battery_models/lithium_ion/Yang2017.py b/src/pybamm/models/full_battery_models/lithium_ion/Yang2017.py similarity index 100% rename from pybamm/models/full_battery_models/lithium_ion/Yang2017.py rename to src/pybamm/models/full_battery_models/lithium_ion/Yang2017.py diff --git a/pybamm/models/full_battery_models/lithium_ion/__init__.py b/src/pybamm/models/full_battery_models/lithium_ion/__init__.py similarity index 100% rename from pybamm/models/full_battery_models/lithium_ion/__init__.py rename to src/pybamm/models/full_battery_models/lithium_ion/__init__.py diff --git a/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py b/src/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py similarity index 100% rename from pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py rename to src/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py diff --git a/pybamm/models/full_battery_models/lithium_ion/basic_dfn.py b/src/pybamm/models/full_battery_models/lithium_ion/basic_dfn.py similarity index 100% rename from pybamm/models/full_battery_models/lithium_ion/basic_dfn.py rename to src/pybamm/models/full_battery_models/lithium_ion/basic_dfn.py diff --git a/pybamm/models/full_battery_models/lithium_ion/basic_dfn_composite.py b/src/pybamm/models/full_battery_models/lithium_ion/basic_dfn_composite.py similarity index 100% rename from pybamm/models/full_battery_models/lithium_ion/basic_dfn_composite.py rename to src/pybamm/models/full_battery_models/lithium_ion/basic_dfn_composite.py diff --git a/pybamm/models/full_battery_models/lithium_ion/basic_dfn_half_cell.py b/src/pybamm/models/full_battery_models/lithium_ion/basic_dfn_half_cell.py similarity index 100% rename from pybamm/models/full_battery_models/lithium_ion/basic_dfn_half_cell.py rename to src/pybamm/models/full_battery_models/lithium_ion/basic_dfn_half_cell.py diff --git a/pybamm/models/full_battery_models/lithium_ion/basic_spm.py b/src/pybamm/models/full_battery_models/lithium_ion/basic_spm.py similarity index 100% rename from pybamm/models/full_battery_models/lithium_ion/basic_spm.py rename to src/pybamm/models/full_battery_models/lithium_ion/basic_spm.py diff --git a/pybamm/models/full_battery_models/lithium_ion/dfn.py b/src/pybamm/models/full_battery_models/lithium_ion/dfn.py similarity index 100% rename from pybamm/models/full_battery_models/lithium_ion/dfn.py rename to src/pybamm/models/full_battery_models/lithium_ion/dfn.py diff --git a/pybamm/models/full_battery_models/lithium_ion/electrode_soh.py b/src/pybamm/models/full_battery_models/lithium_ion/electrode_soh.py similarity index 100% rename from pybamm/models/full_battery_models/lithium_ion/electrode_soh.py rename to src/pybamm/models/full_battery_models/lithium_ion/electrode_soh.py diff --git a/pybamm/models/full_battery_models/lithium_ion/electrode_soh_half_cell.py b/src/pybamm/models/full_battery_models/lithium_ion/electrode_soh_half_cell.py similarity index 100% rename from pybamm/models/full_battery_models/lithium_ion/electrode_soh_half_cell.py rename to src/pybamm/models/full_battery_models/lithium_ion/electrode_soh_half_cell.py diff --git a/pybamm/models/full_battery_models/lithium_ion/mpm.py b/src/pybamm/models/full_battery_models/lithium_ion/mpm.py similarity index 100% rename from pybamm/models/full_battery_models/lithium_ion/mpm.py rename to src/pybamm/models/full_battery_models/lithium_ion/mpm.py diff --git a/pybamm/models/full_battery_models/lithium_ion/msmr.py b/src/pybamm/models/full_battery_models/lithium_ion/msmr.py similarity index 100% rename from pybamm/models/full_battery_models/lithium_ion/msmr.py rename to src/pybamm/models/full_battery_models/lithium_ion/msmr.py diff --git a/pybamm/models/full_battery_models/lithium_ion/newman_tobias.py b/src/pybamm/models/full_battery_models/lithium_ion/newman_tobias.py similarity index 100% rename from pybamm/models/full_battery_models/lithium_ion/newman_tobias.py rename to src/pybamm/models/full_battery_models/lithium_ion/newman_tobias.py diff --git a/pybamm/models/full_battery_models/lithium_ion/spm.py b/src/pybamm/models/full_battery_models/lithium_ion/spm.py similarity index 100% rename from pybamm/models/full_battery_models/lithium_ion/spm.py rename to src/pybamm/models/full_battery_models/lithium_ion/spm.py diff --git a/pybamm/models/full_battery_models/lithium_ion/spme.py b/src/pybamm/models/full_battery_models/lithium_ion/spme.py similarity index 100% rename from pybamm/models/full_battery_models/lithium_ion/spme.py rename to src/pybamm/models/full_battery_models/lithium_ion/spme.py diff --git a/pybamm/models/full_battery_models/lithium_metal/dfn.py b/src/pybamm/models/full_battery_models/lithium_metal/dfn.py similarity index 100% rename from pybamm/models/full_battery_models/lithium_metal/dfn.py rename to src/pybamm/models/full_battery_models/lithium_metal/dfn.py diff --git a/pybamm/models/submodels/__init__.py b/src/pybamm/models/submodels/__init__.py similarity index 100% rename from pybamm/models/submodels/__init__.py rename to src/pybamm/models/submodels/__init__.py diff --git a/pybamm/models/submodels/active_material/__init__.py b/src/pybamm/models/submodels/active_material/__init__.py similarity index 100% rename from pybamm/models/submodels/active_material/__init__.py rename to src/pybamm/models/submodels/active_material/__init__.py diff --git a/pybamm/models/submodels/active_material/base_active_material.py b/src/pybamm/models/submodels/active_material/base_active_material.py similarity index 100% rename from pybamm/models/submodels/active_material/base_active_material.py rename to src/pybamm/models/submodels/active_material/base_active_material.py diff --git a/pybamm/models/submodels/active_material/constant_active_material.py b/src/pybamm/models/submodels/active_material/constant_active_material.py similarity index 100% rename from pybamm/models/submodels/active_material/constant_active_material.py rename to src/pybamm/models/submodels/active_material/constant_active_material.py diff --git a/pybamm/models/submodels/active_material/loss_active_material.py b/src/pybamm/models/submodels/active_material/loss_active_material.py similarity index 100% rename from pybamm/models/submodels/active_material/loss_active_material.py rename to src/pybamm/models/submodels/active_material/loss_active_material.py diff --git a/pybamm/models/submodels/active_material/total_active_material.py b/src/pybamm/models/submodels/active_material/total_active_material.py similarity index 100% rename from pybamm/models/submodels/active_material/total_active_material.py rename to src/pybamm/models/submodels/active_material/total_active_material.py diff --git a/pybamm/models/submodels/base_submodel.py b/src/pybamm/models/submodels/base_submodel.py similarity index 100% rename from pybamm/models/submodels/base_submodel.py rename to src/pybamm/models/submodels/base_submodel.py diff --git a/pybamm/models/submodels/convection/__init__.py b/src/pybamm/models/submodels/convection/__init__.py similarity index 100% rename from pybamm/models/submodels/convection/__init__.py rename to src/pybamm/models/submodels/convection/__init__.py diff --git a/pybamm/models/submodels/convection/base_convection.py b/src/pybamm/models/submodels/convection/base_convection.py similarity index 100% rename from pybamm/models/submodels/convection/base_convection.py rename to src/pybamm/models/submodels/convection/base_convection.py diff --git a/pybamm/models/submodels/convection/through_cell/__init__.py b/src/pybamm/models/submodels/convection/through_cell/__init__.py similarity index 100% rename from pybamm/models/submodels/convection/through_cell/__init__.py rename to src/pybamm/models/submodels/convection/through_cell/__init__.py diff --git a/pybamm/models/submodels/convection/through_cell/base_through_cell_convection.py b/src/pybamm/models/submodels/convection/through_cell/base_through_cell_convection.py similarity index 100% rename from pybamm/models/submodels/convection/through_cell/base_through_cell_convection.py rename to src/pybamm/models/submodels/convection/through_cell/base_through_cell_convection.py diff --git a/pybamm/models/submodels/convection/through_cell/explicit_convection.py b/src/pybamm/models/submodels/convection/through_cell/explicit_convection.py similarity index 100% rename from pybamm/models/submodels/convection/through_cell/explicit_convection.py rename to src/pybamm/models/submodels/convection/through_cell/explicit_convection.py diff --git a/pybamm/models/submodels/convection/through_cell/full_convection.py b/src/pybamm/models/submodels/convection/through_cell/full_convection.py similarity index 100% rename from pybamm/models/submodels/convection/through_cell/full_convection.py rename to src/pybamm/models/submodels/convection/through_cell/full_convection.py diff --git a/pybamm/models/submodels/convection/through_cell/no_convection.py b/src/pybamm/models/submodels/convection/through_cell/no_convection.py similarity index 100% rename from pybamm/models/submodels/convection/through_cell/no_convection.py rename to src/pybamm/models/submodels/convection/through_cell/no_convection.py diff --git a/pybamm/models/submodels/convection/transverse/__init__.py b/src/pybamm/models/submodels/convection/transverse/__init__.py similarity index 100% rename from pybamm/models/submodels/convection/transverse/__init__.py rename to src/pybamm/models/submodels/convection/transverse/__init__.py diff --git a/pybamm/models/submodels/convection/transverse/base_transverse_convection.py b/src/pybamm/models/submodels/convection/transverse/base_transverse_convection.py similarity index 100% rename from pybamm/models/submodels/convection/transverse/base_transverse_convection.py rename to src/pybamm/models/submodels/convection/transverse/base_transverse_convection.py diff --git a/pybamm/models/submodels/convection/transverse/full_convection.py b/src/pybamm/models/submodels/convection/transverse/full_convection.py similarity index 100% rename from pybamm/models/submodels/convection/transverse/full_convection.py rename to src/pybamm/models/submodels/convection/transverse/full_convection.py diff --git a/pybamm/models/submodels/convection/transverse/no_convection.py b/src/pybamm/models/submodels/convection/transverse/no_convection.py similarity index 100% rename from pybamm/models/submodels/convection/transverse/no_convection.py rename to src/pybamm/models/submodels/convection/transverse/no_convection.py diff --git a/pybamm/models/submodels/convection/transverse/uniform_convection.py b/src/pybamm/models/submodels/convection/transverse/uniform_convection.py similarity index 100% rename from pybamm/models/submodels/convection/transverse/uniform_convection.py rename to src/pybamm/models/submodels/convection/transverse/uniform_convection.py diff --git a/pybamm/models/submodels/current_collector/__init__.py b/src/pybamm/models/submodels/current_collector/__init__.py similarity index 100% rename from pybamm/models/submodels/current_collector/__init__.py rename to src/pybamm/models/submodels/current_collector/__init__.py diff --git a/pybamm/models/submodels/current_collector/base_current_collector.py b/src/pybamm/models/submodels/current_collector/base_current_collector.py similarity index 100% rename from pybamm/models/submodels/current_collector/base_current_collector.py rename to src/pybamm/models/submodels/current_collector/base_current_collector.py diff --git a/pybamm/models/submodels/current_collector/effective_resistance_current_collector.py b/src/pybamm/models/submodels/current_collector/effective_resistance_current_collector.py similarity index 100% rename from pybamm/models/submodels/current_collector/effective_resistance_current_collector.py rename to src/pybamm/models/submodels/current_collector/effective_resistance_current_collector.py diff --git a/pybamm/models/submodels/current_collector/homogeneous_current_collector.py b/src/pybamm/models/submodels/current_collector/homogeneous_current_collector.py similarity index 100% rename from pybamm/models/submodels/current_collector/homogeneous_current_collector.py rename to src/pybamm/models/submodels/current_collector/homogeneous_current_collector.py diff --git a/pybamm/models/submodels/current_collector/potential_pair.py b/src/pybamm/models/submodels/current_collector/potential_pair.py similarity index 100% rename from pybamm/models/submodels/current_collector/potential_pair.py rename to src/pybamm/models/submodels/current_collector/potential_pair.py diff --git a/pybamm/models/submodels/electrode/__init__.py b/src/pybamm/models/submodels/electrode/__init__.py similarity index 100% rename from pybamm/models/submodels/electrode/__init__.py rename to src/pybamm/models/submodels/electrode/__init__.py diff --git a/pybamm/models/submodels/electrode/base_electrode.py b/src/pybamm/models/submodels/electrode/base_electrode.py similarity index 100% rename from pybamm/models/submodels/electrode/base_electrode.py rename to src/pybamm/models/submodels/electrode/base_electrode.py diff --git a/pybamm/models/submodels/electrode/ohm/__init__.py b/src/pybamm/models/submodels/electrode/ohm/__init__.py similarity index 100% rename from pybamm/models/submodels/electrode/ohm/__init__.py rename to src/pybamm/models/submodels/electrode/ohm/__init__.py diff --git a/pybamm/models/submodels/electrode/ohm/base_ohm.py b/src/pybamm/models/submodels/electrode/ohm/base_ohm.py similarity index 100% rename from pybamm/models/submodels/electrode/ohm/base_ohm.py rename to src/pybamm/models/submodels/electrode/ohm/base_ohm.py diff --git a/pybamm/models/submodels/electrode/ohm/composite_ohm.py b/src/pybamm/models/submodels/electrode/ohm/composite_ohm.py similarity index 100% rename from pybamm/models/submodels/electrode/ohm/composite_ohm.py rename to src/pybamm/models/submodels/electrode/ohm/composite_ohm.py diff --git a/pybamm/models/submodels/electrode/ohm/full_ohm.py b/src/pybamm/models/submodels/electrode/ohm/full_ohm.py similarity index 100% rename from pybamm/models/submodels/electrode/ohm/full_ohm.py rename to src/pybamm/models/submodels/electrode/ohm/full_ohm.py diff --git a/pybamm/models/submodels/electrode/ohm/leading_ohm.py b/src/pybamm/models/submodels/electrode/ohm/leading_ohm.py similarity index 100% rename from pybamm/models/submodels/electrode/ohm/leading_ohm.py rename to src/pybamm/models/submodels/electrode/ohm/leading_ohm.py diff --git a/pybamm/models/submodels/electrode/ohm/li_metal.py b/src/pybamm/models/submodels/electrode/ohm/li_metal.py similarity index 100% rename from pybamm/models/submodels/electrode/ohm/li_metal.py rename to src/pybamm/models/submodels/electrode/ohm/li_metal.py diff --git a/pybamm/models/submodels/electrode/ohm/surface_form_ohm.py b/src/pybamm/models/submodels/electrode/ohm/surface_form_ohm.py similarity index 100% rename from pybamm/models/submodels/electrode/ohm/surface_form_ohm.py rename to src/pybamm/models/submodels/electrode/ohm/surface_form_ohm.py diff --git a/pybamm/models/submodels/electrolyte_conductivity/__init__.py b/src/pybamm/models/submodels/electrolyte_conductivity/__init__.py similarity index 100% rename from pybamm/models/submodels/electrolyte_conductivity/__init__.py rename to src/pybamm/models/submodels/electrolyte_conductivity/__init__.py diff --git a/pybamm/models/submodels/electrolyte_conductivity/base_electrolyte_conductivity.py b/src/pybamm/models/submodels/electrolyte_conductivity/base_electrolyte_conductivity.py similarity index 100% rename from pybamm/models/submodels/electrolyte_conductivity/base_electrolyte_conductivity.py rename to src/pybamm/models/submodels/electrolyte_conductivity/base_electrolyte_conductivity.py diff --git a/pybamm/models/submodels/electrolyte_conductivity/composite_conductivity.py b/src/pybamm/models/submodels/electrolyte_conductivity/composite_conductivity.py similarity index 100% rename from pybamm/models/submodels/electrolyte_conductivity/composite_conductivity.py rename to src/pybamm/models/submodels/electrolyte_conductivity/composite_conductivity.py diff --git a/pybamm/models/submodels/electrolyte_conductivity/full_conductivity.py b/src/pybamm/models/submodels/electrolyte_conductivity/full_conductivity.py similarity index 100% rename from pybamm/models/submodels/electrolyte_conductivity/full_conductivity.py rename to src/pybamm/models/submodels/electrolyte_conductivity/full_conductivity.py diff --git a/pybamm/models/submodels/electrolyte_conductivity/integrated_conductivity.py b/src/pybamm/models/submodels/electrolyte_conductivity/integrated_conductivity.py similarity index 100% rename from pybamm/models/submodels/electrolyte_conductivity/integrated_conductivity.py rename to src/pybamm/models/submodels/electrolyte_conductivity/integrated_conductivity.py diff --git a/pybamm/models/submodels/electrolyte_conductivity/leading_order_conductivity.py b/src/pybamm/models/submodels/electrolyte_conductivity/leading_order_conductivity.py similarity index 100% rename from pybamm/models/submodels/electrolyte_conductivity/leading_order_conductivity.py rename to src/pybamm/models/submodels/electrolyte_conductivity/leading_order_conductivity.py diff --git a/pybamm/models/submodels/electrolyte_conductivity/surface_potential_form/__init__.py b/src/pybamm/models/submodels/electrolyte_conductivity/surface_potential_form/__init__.py similarity index 100% rename from pybamm/models/submodels/electrolyte_conductivity/surface_potential_form/__init__.py rename to src/pybamm/models/submodels/electrolyte_conductivity/surface_potential_form/__init__.py diff --git a/pybamm/models/submodels/electrolyte_conductivity/surface_potential_form/composite_surface_form_conductivity.py b/src/pybamm/models/submodels/electrolyte_conductivity/surface_potential_form/composite_surface_form_conductivity.py similarity index 100% rename from pybamm/models/submodels/electrolyte_conductivity/surface_potential_form/composite_surface_form_conductivity.py rename to src/pybamm/models/submodels/electrolyte_conductivity/surface_potential_form/composite_surface_form_conductivity.py diff --git a/pybamm/models/submodels/electrolyte_conductivity/surface_potential_form/explicit_surface_form_conductivity.py b/src/pybamm/models/submodels/electrolyte_conductivity/surface_potential_form/explicit_surface_form_conductivity.py similarity index 100% rename from pybamm/models/submodels/electrolyte_conductivity/surface_potential_form/explicit_surface_form_conductivity.py rename to src/pybamm/models/submodels/electrolyte_conductivity/surface_potential_form/explicit_surface_form_conductivity.py diff --git a/pybamm/models/submodels/electrolyte_conductivity/surface_potential_form/full_surface_form_conductivity.py b/src/pybamm/models/submodels/electrolyte_conductivity/surface_potential_form/full_surface_form_conductivity.py similarity index 100% rename from pybamm/models/submodels/electrolyte_conductivity/surface_potential_form/full_surface_form_conductivity.py rename to src/pybamm/models/submodels/electrolyte_conductivity/surface_potential_form/full_surface_form_conductivity.py diff --git a/pybamm/models/submodels/electrolyte_conductivity/surface_potential_form/leading_surface_form_conductivity.py b/src/pybamm/models/submodels/electrolyte_conductivity/surface_potential_form/leading_surface_form_conductivity.py similarity index 100% rename from pybamm/models/submodels/electrolyte_conductivity/surface_potential_form/leading_surface_form_conductivity.py rename to src/pybamm/models/submodels/electrolyte_conductivity/surface_potential_form/leading_surface_form_conductivity.py diff --git a/pybamm/models/submodels/electrolyte_diffusion/__init__.py b/src/pybamm/models/submodels/electrolyte_diffusion/__init__.py similarity index 100% rename from pybamm/models/submodels/electrolyte_diffusion/__init__.py rename to src/pybamm/models/submodels/electrolyte_diffusion/__init__.py diff --git a/pybamm/models/submodels/electrolyte_diffusion/base_electrolyte_diffusion.py b/src/pybamm/models/submodels/electrolyte_diffusion/base_electrolyte_diffusion.py similarity index 100% rename from pybamm/models/submodels/electrolyte_diffusion/base_electrolyte_diffusion.py rename to src/pybamm/models/submodels/electrolyte_diffusion/base_electrolyte_diffusion.py diff --git a/pybamm/models/submodels/electrolyte_diffusion/constant_concentration.py b/src/pybamm/models/submodels/electrolyte_diffusion/constant_concentration.py similarity index 100% rename from pybamm/models/submodels/electrolyte_diffusion/constant_concentration.py rename to src/pybamm/models/submodels/electrolyte_diffusion/constant_concentration.py diff --git a/pybamm/models/submodels/electrolyte_diffusion/full_diffusion.py b/src/pybamm/models/submodels/electrolyte_diffusion/full_diffusion.py similarity index 100% rename from pybamm/models/submodels/electrolyte_diffusion/full_diffusion.py rename to src/pybamm/models/submodels/electrolyte_diffusion/full_diffusion.py diff --git a/pybamm/models/submodels/electrolyte_diffusion/leading_order_diffusion.py b/src/pybamm/models/submodels/electrolyte_diffusion/leading_order_diffusion.py similarity index 100% rename from pybamm/models/submodels/electrolyte_diffusion/leading_order_diffusion.py rename to src/pybamm/models/submodels/electrolyte_diffusion/leading_order_diffusion.py diff --git a/pybamm/models/submodels/equivalent_circuit_elements/__init__.py b/src/pybamm/models/submodels/equivalent_circuit_elements/__init__.py similarity index 100% rename from pybamm/models/submodels/equivalent_circuit_elements/__init__.py rename to src/pybamm/models/submodels/equivalent_circuit_elements/__init__.py diff --git a/pybamm/models/submodels/equivalent_circuit_elements/diffusion_element.py b/src/pybamm/models/submodels/equivalent_circuit_elements/diffusion_element.py similarity index 100% rename from pybamm/models/submodels/equivalent_circuit_elements/diffusion_element.py rename to src/pybamm/models/submodels/equivalent_circuit_elements/diffusion_element.py diff --git a/pybamm/models/submodels/equivalent_circuit_elements/ocv_element.py b/src/pybamm/models/submodels/equivalent_circuit_elements/ocv_element.py similarity index 100% rename from pybamm/models/submodels/equivalent_circuit_elements/ocv_element.py rename to src/pybamm/models/submodels/equivalent_circuit_elements/ocv_element.py diff --git a/pybamm/models/submodels/equivalent_circuit_elements/rc_element.py b/src/pybamm/models/submodels/equivalent_circuit_elements/rc_element.py similarity index 100% rename from pybamm/models/submodels/equivalent_circuit_elements/rc_element.py rename to src/pybamm/models/submodels/equivalent_circuit_elements/rc_element.py diff --git a/pybamm/models/submodels/equivalent_circuit_elements/resistor_element.py b/src/pybamm/models/submodels/equivalent_circuit_elements/resistor_element.py similarity index 100% rename from pybamm/models/submodels/equivalent_circuit_elements/resistor_element.py rename to src/pybamm/models/submodels/equivalent_circuit_elements/resistor_element.py diff --git a/pybamm/models/submodels/equivalent_circuit_elements/thermal.py b/src/pybamm/models/submodels/equivalent_circuit_elements/thermal.py similarity index 100% rename from pybamm/models/submodels/equivalent_circuit_elements/thermal.py rename to src/pybamm/models/submodels/equivalent_circuit_elements/thermal.py diff --git a/pybamm/models/submodels/equivalent_circuit_elements/voltage_model.py b/src/pybamm/models/submodels/equivalent_circuit_elements/voltage_model.py similarity index 100% rename from pybamm/models/submodels/equivalent_circuit_elements/voltage_model.py rename to src/pybamm/models/submodels/equivalent_circuit_elements/voltage_model.py diff --git a/pybamm/models/submodels/external_circuit/__init__.py b/src/pybamm/models/submodels/external_circuit/__init__.py similarity index 100% rename from pybamm/models/submodels/external_circuit/__init__.py rename to src/pybamm/models/submodels/external_circuit/__init__.py diff --git a/pybamm/models/submodels/external_circuit/base_external_circuit.py b/src/pybamm/models/submodels/external_circuit/base_external_circuit.py similarity index 100% rename from pybamm/models/submodels/external_circuit/base_external_circuit.py rename to src/pybamm/models/submodels/external_circuit/base_external_circuit.py diff --git a/pybamm/models/submodels/external_circuit/discharge_throughput.py b/src/pybamm/models/submodels/external_circuit/discharge_throughput.py similarity index 100% rename from pybamm/models/submodels/external_circuit/discharge_throughput.py rename to src/pybamm/models/submodels/external_circuit/discharge_throughput.py diff --git a/pybamm/models/submodels/external_circuit/explicit_control_external_circuit.py b/src/pybamm/models/submodels/external_circuit/explicit_control_external_circuit.py similarity index 100% rename from pybamm/models/submodels/external_circuit/explicit_control_external_circuit.py rename to src/pybamm/models/submodels/external_circuit/explicit_control_external_circuit.py diff --git a/pybamm/models/submodels/external_circuit/function_control_external_circuit.py b/src/pybamm/models/submodels/external_circuit/function_control_external_circuit.py similarity index 100% rename from pybamm/models/submodels/external_circuit/function_control_external_circuit.py rename to src/pybamm/models/submodels/external_circuit/function_control_external_circuit.py diff --git a/pybamm/models/submodels/interface/__init__.py b/src/pybamm/models/submodels/interface/__init__.py similarity index 100% rename from pybamm/models/submodels/interface/__init__.py rename to src/pybamm/models/submodels/interface/__init__.py diff --git a/pybamm/models/submodels/interface/base_interface.py b/src/pybamm/models/submodels/interface/base_interface.py similarity index 100% rename from pybamm/models/submodels/interface/base_interface.py rename to src/pybamm/models/submodels/interface/base_interface.py diff --git a/pybamm/models/submodels/interface/interface_utilisation/__init__.py b/src/pybamm/models/submodels/interface/interface_utilisation/__init__.py similarity index 100% rename from pybamm/models/submodels/interface/interface_utilisation/__init__.py rename to src/pybamm/models/submodels/interface/interface_utilisation/__init__.py diff --git a/pybamm/models/submodels/interface/interface_utilisation/base_utilisation.py b/src/pybamm/models/submodels/interface/interface_utilisation/base_utilisation.py similarity index 100% rename from pybamm/models/submodels/interface/interface_utilisation/base_utilisation.py rename to src/pybamm/models/submodels/interface/interface_utilisation/base_utilisation.py diff --git a/pybamm/models/submodels/interface/interface_utilisation/constant_utilisation.py b/src/pybamm/models/submodels/interface/interface_utilisation/constant_utilisation.py similarity index 100% rename from pybamm/models/submodels/interface/interface_utilisation/constant_utilisation.py rename to src/pybamm/models/submodels/interface/interface_utilisation/constant_utilisation.py diff --git a/pybamm/models/submodels/interface/interface_utilisation/current_driven_utilisation.py b/src/pybamm/models/submodels/interface/interface_utilisation/current_driven_utilisation.py similarity index 100% rename from pybamm/models/submodels/interface/interface_utilisation/current_driven_utilisation.py rename to src/pybamm/models/submodels/interface/interface_utilisation/current_driven_utilisation.py diff --git a/pybamm/models/submodels/interface/interface_utilisation/full_utilisation.py b/src/pybamm/models/submodels/interface/interface_utilisation/full_utilisation.py similarity index 100% rename from pybamm/models/submodels/interface/interface_utilisation/full_utilisation.py rename to src/pybamm/models/submodels/interface/interface_utilisation/full_utilisation.py diff --git a/pybamm/models/submodels/interface/kinetics/__init__.py b/src/pybamm/models/submodels/interface/kinetics/__init__.py similarity index 100% rename from pybamm/models/submodels/interface/kinetics/__init__.py rename to src/pybamm/models/submodels/interface/kinetics/__init__.py diff --git a/pybamm/models/submodels/interface/kinetics/base_kinetics.py b/src/pybamm/models/submodels/interface/kinetics/base_kinetics.py similarity index 100% rename from pybamm/models/submodels/interface/kinetics/base_kinetics.py rename to src/pybamm/models/submodels/interface/kinetics/base_kinetics.py diff --git a/pybamm/models/submodels/interface/kinetics/butler_volmer.py b/src/pybamm/models/submodels/interface/kinetics/butler_volmer.py similarity index 100% rename from pybamm/models/submodels/interface/kinetics/butler_volmer.py rename to src/pybamm/models/submodels/interface/kinetics/butler_volmer.py diff --git a/pybamm/models/submodels/interface/kinetics/diffusion_limited.py b/src/pybamm/models/submodels/interface/kinetics/diffusion_limited.py similarity index 100% rename from pybamm/models/submodels/interface/kinetics/diffusion_limited.py rename to src/pybamm/models/submodels/interface/kinetics/diffusion_limited.py diff --git a/pybamm/models/submodels/interface/kinetics/inverse_kinetics/__init__.py b/src/pybamm/models/submodels/interface/kinetics/inverse_kinetics/__init__.py similarity index 100% rename from pybamm/models/submodels/interface/kinetics/inverse_kinetics/__init__.py rename to src/pybamm/models/submodels/interface/kinetics/inverse_kinetics/__init__.py diff --git a/pybamm/models/submodels/interface/kinetics/inverse_kinetics/inverse_butler_volmer.py b/src/pybamm/models/submodels/interface/kinetics/inverse_kinetics/inverse_butler_volmer.py similarity index 100% rename from pybamm/models/submodels/interface/kinetics/inverse_kinetics/inverse_butler_volmer.py rename to src/pybamm/models/submodels/interface/kinetics/inverse_kinetics/inverse_butler_volmer.py diff --git a/pybamm/models/submodels/interface/kinetics/linear.py b/src/pybamm/models/submodels/interface/kinetics/linear.py similarity index 100% rename from pybamm/models/submodels/interface/kinetics/linear.py rename to src/pybamm/models/submodels/interface/kinetics/linear.py diff --git a/pybamm/models/submodels/interface/kinetics/marcus.py b/src/pybamm/models/submodels/interface/kinetics/marcus.py similarity index 100% rename from pybamm/models/submodels/interface/kinetics/marcus.py rename to src/pybamm/models/submodels/interface/kinetics/marcus.py diff --git a/pybamm/models/submodels/interface/kinetics/msmr_butler_volmer.py b/src/pybamm/models/submodels/interface/kinetics/msmr_butler_volmer.py similarity index 100% rename from pybamm/models/submodels/interface/kinetics/msmr_butler_volmer.py rename to src/pybamm/models/submodels/interface/kinetics/msmr_butler_volmer.py diff --git a/pybamm/models/submodels/interface/kinetics/no_reaction.py b/src/pybamm/models/submodels/interface/kinetics/no_reaction.py similarity index 100% rename from pybamm/models/submodels/interface/kinetics/no_reaction.py rename to src/pybamm/models/submodels/interface/kinetics/no_reaction.py diff --git a/pybamm/models/submodels/interface/kinetics/tafel.py b/src/pybamm/models/submodels/interface/kinetics/tafel.py similarity index 100% rename from pybamm/models/submodels/interface/kinetics/tafel.py rename to src/pybamm/models/submodels/interface/kinetics/tafel.py diff --git a/pybamm/models/submodels/interface/kinetics/total_main_kinetics.py b/src/pybamm/models/submodels/interface/kinetics/total_main_kinetics.py similarity index 100% rename from pybamm/models/submodels/interface/kinetics/total_main_kinetics.py rename to src/pybamm/models/submodels/interface/kinetics/total_main_kinetics.py diff --git a/pybamm/models/submodels/interface/lithium_plating/__init__.py b/src/pybamm/models/submodels/interface/lithium_plating/__init__.py similarity index 100% rename from pybamm/models/submodels/interface/lithium_plating/__init__.py rename to src/pybamm/models/submodels/interface/lithium_plating/__init__.py diff --git a/pybamm/models/submodels/interface/lithium_plating/base_plating.py b/src/pybamm/models/submodels/interface/lithium_plating/base_plating.py similarity index 100% rename from pybamm/models/submodels/interface/lithium_plating/base_plating.py rename to src/pybamm/models/submodels/interface/lithium_plating/base_plating.py diff --git a/pybamm/models/submodels/interface/lithium_plating/no_plating.py b/src/pybamm/models/submodels/interface/lithium_plating/no_plating.py similarity index 100% rename from pybamm/models/submodels/interface/lithium_plating/no_plating.py rename to src/pybamm/models/submodels/interface/lithium_plating/no_plating.py diff --git a/pybamm/models/submodels/interface/lithium_plating/plating.py b/src/pybamm/models/submodels/interface/lithium_plating/plating.py similarity index 100% rename from pybamm/models/submodels/interface/lithium_plating/plating.py rename to src/pybamm/models/submodels/interface/lithium_plating/plating.py diff --git a/pybamm/models/submodels/interface/lithium_plating/total_lithium_plating.py b/src/pybamm/models/submodels/interface/lithium_plating/total_lithium_plating.py similarity index 100% rename from pybamm/models/submodels/interface/lithium_plating/total_lithium_plating.py rename to src/pybamm/models/submodels/interface/lithium_plating/total_lithium_plating.py diff --git a/pybamm/models/submodels/interface/open_circuit_potential/__init__.py b/src/pybamm/models/submodels/interface/open_circuit_potential/__init__.py similarity index 100% rename from pybamm/models/submodels/interface/open_circuit_potential/__init__.py rename to src/pybamm/models/submodels/interface/open_circuit_potential/__init__.py diff --git a/pybamm/models/submodels/interface/open_circuit_potential/base_ocp.py b/src/pybamm/models/submodels/interface/open_circuit_potential/base_ocp.py similarity index 100% rename from pybamm/models/submodels/interface/open_circuit_potential/base_ocp.py rename to src/pybamm/models/submodels/interface/open_circuit_potential/base_ocp.py diff --git a/pybamm/models/submodels/interface/open_circuit_potential/current_sigmoid_ocp.py b/src/pybamm/models/submodels/interface/open_circuit_potential/current_sigmoid_ocp.py similarity index 100% rename from pybamm/models/submodels/interface/open_circuit_potential/current_sigmoid_ocp.py rename to src/pybamm/models/submodels/interface/open_circuit_potential/current_sigmoid_ocp.py diff --git a/pybamm/models/submodels/interface/open_circuit_potential/msmr_ocp.py b/src/pybamm/models/submodels/interface/open_circuit_potential/msmr_ocp.py similarity index 100% rename from pybamm/models/submodels/interface/open_circuit_potential/msmr_ocp.py rename to src/pybamm/models/submodels/interface/open_circuit_potential/msmr_ocp.py diff --git a/pybamm/models/submodels/interface/open_circuit_potential/single_ocp.py b/src/pybamm/models/submodels/interface/open_circuit_potential/single_ocp.py similarity index 100% rename from pybamm/models/submodels/interface/open_circuit_potential/single_ocp.py rename to src/pybamm/models/submodels/interface/open_circuit_potential/single_ocp.py diff --git a/pybamm/models/submodels/interface/open_circuit_potential/wycisk_ocp.py b/src/pybamm/models/submodels/interface/open_circuit_potential/wycisk_ocp.py similarity index 100% rename from pybamm/models/submodels/interface/open_circuit_potential/wycisk_ocp.py rename to src/pybamm/models/submodels/interface/open_circuit_potential/wycisk_ocp.py diff --git a/pybamm/models/submodels/interface/sei/__init__.py b/src/pybamm/models/submodels/interface/sei/__init__.py similarity index 100% rename from pybamm/models/submodels/interface/sei/__init__.py rename to src/pybamm/models/submodels/interface/sei/__init__.py diff --git a/pybamm/models/submodels/interface/sei/base_sei.py b/src/pybamm/models/submodels/interface/sei/base_sei.py similarity index 100% rename from pybamm/models/submodels/interface/sei/base_sei.py rename to src/pybamm/models/submodels/interface/sei/base_sei.py diff --git a/pybamm/models/submodels/interface/sei/constant_sei.py b/src/pybamm/models/submodels/interface/sei/constant_sei.py similarity index 100% rename from pybamm/models/submodels/interface/sei/constant_sei.py rename to src/pybamm/models/submodels/interface/sei/constant_sei.py diff --git a/pybamm/models/submodels/interface/sei/no_sei.py b/src/pybamm/models/submodels/interface/sei/no_sei.py similarity index 100% rename from pybamm/models/submodels/interface/sei/no_sei.py rename to src/pybamm/models/submodels/interface/sei/no_sei.py diff --git a/pybamm/models/submodels/interface/sei/sei_growth.py b/src/pybamm/models/submodels/interface/sei/sei_growth.py similarity index 100% rename from pybamm/models/submodels/interface/sei/sei_growth.py rename to src/pybamm/models/submodels/interface/sei/sei_growth.py diff --git a/pybamm/models/submodels/interface/sei/total_sei.py b/src/pybamm/models/submodels/interface/sei/total_sei.py similarity index 100% rename from pybamm/models/submodels/interface/sei/total_sei.py rename to src/pybamm/models/submodels/interface/sei/total_sei.py diff --git a/pybamm/models/submodels/interface/total_interfacial_current.py b/src/pybamm/models/submodels/interface/total_interfacial_current.py similarity index 100% rename from pybamm/models/submodels/interface/total_interfacial_current.py rename to src/pybamm/models/submodels/interface/total_interfacial_current.py diff --git a/pybamm/models/submodels/oxygen_diffusion/__init__.py b/src/pybamm/models/submodels/oxygen_diffusion/__init__.py similarity index 100% rename from pybamm/models/submodels/oxygen_diffusion/__init__.py rename to src/pybamm/models/submodels/oxygen_diffusion/__init__.py diff --git a/pybamm/models/submodels/oxygen_diffusion/base_oxygen_diffusion.py b/src/pybamm/models/submodels/oxygen_diffusion/base_oxygen_diffusion.py similarity index 100% rename from pybamm/models/submodels/oxygen_diffusion/base_oxygen_diffusion.py rename to src/pybamm/models/submodels/oxygen_diffusion/base_oxygen_diffusion.py diff --git a/pybamm/models/submodels/oxygen_diffusion/full_oxygen_diffusion.py b/src/pybamm/models/submodels/oxygen_diffusion/full_oxygen_diffusion.py similarity index 100% rename from pybamm/models/submodels/oxygen_diffusion/full_oxygen_diffusion.py rename to src/pybamm/models/submodels/oxygen_diffusion/full_oxygen_diffusion.py diff --git a/pybamm/models/submodels/oxygen_diffusion/leading_oxygen_diffusion.py b/src/pybamm/models/submodels/oxygen_diffusion/leading_oxygen_diffusion.py similarity index 100% rename from pybamm/models/submodels/oxygen_diffusion/leading_oxygen_diffusion.py rename to src/pybamm/models/submodels/oxygen_diffusion/leading_oxygen_diffusion.py diff --git a/pybamm/models/submodels/oxygen_diffusion/no_oxygen.py b/src/pybamm/models/submodels/oxygen_diffusion/no_oxygen.py similarity index 100% rename from pybamm/models/submodels/oxygen_diffusion/no_oxygen.py rename to src/pybamm/models/submodels/oxygen_diffusion/no_oxygen.py diff --git a/pybamm/models/submodels/particle/__init__.py b/src/pybamm/models/submodels/particle/__init__.py similarity index 100% rename from pybamm/models/submodels/particle/__init__.py rename to src/pybamm/models/submodels/particle/__init__.py diff --git a/pybamm/models/submodels/particle/base_particle.py b/src/pybamm/models/submodels/particle/base_particle.py similarity index 100% rename from pybamm/models/submodels/particle/base_particle.py rename to src/pybamm/models/submodels/particle/base_particle.py diff --git a/pybamm/models/submodels/particle/fickian_diffusion.py b/src/pybamm/models/submodels/particle/fickian_diffusion.py similarity index 100% rename from pybamm/models/submodels/particle/fickian_diffusion.py rename to src/pybamm/models/submodels/particle/fickian_diffusion.py diff --git a/pybamm/models/submodels/particle/msmr_diffusion.py b/src/pybamm/models/submodels/particle/msmr_diffusion.py similarity index 100% rename from pybamm/models/submodels/particle/msmr_diffusion.py rename to src/pybamm/models/submodels/particle/msmr_diffusion.py diff --git a/pybamm/models/submodels/particle/polynomial_profile.py b/src/pybamm/models/submodels/particle/polynomial_profile.py similarity index 100% rename from pybamm/models/submodels/particle/polynomial_profile.py rename to src/pybamm/models/submodels/particle/polynomial_profile.py diff --git a/pybamm/models/submodels/particle/total_particle_concentration.py b/src/pybamm/models/submodels/particle/total_particle_concentration.py similarity index 100% rename from pybamm/models/submodels/particle/total_particle_concentration.py rename to src/pybamm/models/submodels/particle/total_particle_concentration.py diff --git a/pybamm/models/submodels/particle/x_averaged_polynomial_profile.py b/src/pybamm/models/submodels/particle/x_averaged_polynomial_profile.py similarity index 100% rename from pybamm/models/submodels/particle/x_averaged_polynomial_profile.py rename to src/pybamm/models/submodels/particle/x_averaged_polynomial_profile.py diff --git a/pybamm/models/submodels/particle_mechanics/__init__.py b/src/pybamm/models/submodels/particle_mechanics/__init__.py similarity index 100% rename from pybamm/models/submodels/particle_mechanics/__init__.py rename to src/pybamm/models/submodels/particle_mechanics/__init__.py diff --git a/pybamm/models/submodels/particle_mechanics/base_mechanics.py b/src/pybamm/models/submodels/particle_mechanics/base_mechanics.py similarity index 100% rename from pybamm/models/submodels/particle_mechanics/base_mechanics.py rename to src/pybamm/models/submodels/particle_mechanics/base_mechanics.py diff --git a/pybamm/models/submodels/particle_mechanics/crack_propagation.py b/src/pybamm/models/submodels/particle_mechanics/crack_propagation.py similarity index 100% rename from pybamm/models/submodels/particle_mechanics/crack_propagation.py rename to src/pybamm/models/submodels/particle_mechanics/crack_propagation.py diff --git a/pybamm/models/submodels/particle_mechanics/no_mechanics.py b/src/pybamm/models/submodels/particle_mechanics/no_mechanics.py similarity index 100% rename from pybamm/models/submodels/particle_mechanics/no_mechanics.py rename to src/pybamm/models/submodels/particle_mechanics/no_mechanics.py diff --git a/pybamm/models/submodels/particle_mechanics/swelling_only.py b/src/pybamm/models/submodels/particle_mechanics/swelling_only.py similarity index 100% rename from pybamm/models/submodels/particle_mechanics/swelling_only.py rename to src/pybamm/models/submodels/particle_mechanics/swelling_only.py diff --git a/pybamm/models/submodels/porosity/__init__.py b/src/pybamm/models/submodels/porosity/__init__.py similarity index 100% rename from pybamm/models/submodels/porosity/__init__.py rename to src/pybamm/models/submodels/porosity/__init__.py diff --git a/pybamm/models/submodels/porosity/base_porosity.py b/src/pybamm/models/submodels/porosity/base_porosity.py similarity index 100% rename from pybamm/models/submodels/porosity/base_porosity.py rename to src/pybamm/models/submodels/porosity/base_porosity.py diff --git a/pybamm/models/submodels/porosity/constant_porosity.py b/src/pybamm/models/submodels/porosity/constant_porosity.py similarity index 100% rename from pybamm/models/submodels/porosity/constant_porosity.py rename to src/pybamm/models/submodels/porosity/constant_porosity.py diff --git a/pybamm/models/submodels/porosity/reaction_driven_porosity.py b/src/pybamm/models/submodels/porosity/reaction_driven_porosity.py similarity index 100% rename from pybamm/models/submodels/porosity/reaction_driven_porosity.py rename to src/pybamm/models/submodels/porosity/reaction_driven_porosity.py diff --git a/pybamm/models/submodels/porosity/reaction_driven_porosity_ode.py b/src/pybamm/models/submodels/porosity/reaction_driven_porosity_ode.py similarity index 100% rename from pybamm/models/submodels/porosity/reaction_driven_porosity_ode.py rename to src/pybamm/models/submodels/porosity/reaction_driven_porosity_ode.py diff --git a/pybamm/models/submodels/thermal/__init__.py b/src/pybamm/models/submodels/thermal/__init__.py similarity index 100% rename from pybamm/models/submodels/thermal/__init__.py rename to src/pybamm/models/submodels/thermal/__init__.py diff --git a/pybamm/models/submodels/thermal/base_thermal.py b/src/pybamm/models/submodels/thermal/base_thermal.py similarity index 100% rename from pybamm/models/submodels/thermal/base_thermal.py rename to src/pybamm/models/submodels/thermal/base_thermal.py diff --git a/pybamm/models/submodels/thermal/isothermal.py b/src/pybamm/models/submodels/thermal/isothermal.py similarity index 100% rename from pybamm/models/submodels/thermal/isothermal.py rename to src/pybamm/models/submodels/thermal/isothermal.py diff --git a/pybamm/models/submodels/thermal/lumped.py b/src/pybamm/models/submodels/thermal/lumped.py similarity index 100% rename from pybamm/models/submodels/thermal/lumped.py rename to src/pybamm/models/submodels/thermal/lumped.py diff --git a/pybamm/models/submodels/thermal/pouch_cell/__init__.py b/src/pybamm/models/submodels/thermal/pouch_cell/__init__.py similarity index 100% rename from pybamm/models/submodels/thermal/pouch_cell/__init__.py rename to src/pybamm/models/submodels/thermal/pouch_cell/__init__.py diff --git a/pybamm/models/submodels/thermal/pouch_cell/pouch_cell_1D_current_collectors.py b/src/pybamm/models/submodels/thermal/pouch_cell/pouch_cell_1D_current_collectors.py similarity index 100% rename from pybamm/models/submodels/thermal/pouch_cell/pouch_cell_1D_current_collectors.py rename to src/pybamm/models/submodels/thermal/pouch_cell/pouch_cell_1D_current_collectors.py diff --git a/pybamm/models/submodels/thermal/pouch_cell/pouch_cell_2D_current_collectors.py b/src/pybamm/models/submodels/thermal/pouch_cell/pouch_cell_2D_current_collectors.py similarity index 100% rename from pybamm/models/submodels/thermal/pouch_cell/pouch_cell_2D_current_collectors.py rename to src/pybamm/models/submodels/thermal/pouch_cell/pouch_cell_2D_current_collectors.py diff --git a/pybamm/models/submodels/thermal/pouch_cell/x_full.py b/src/pybamm/models/submodels/thermal/pouch_cell/x_full.py similarity index 100% rename from pybamm/models/submodels/thermal/pouch_cell/x_full.py rename to src/pybamm/models/submodels/thermal/pouch_cell/x_full.py diff --git a/pybamm/models/submodels/transport_efficiency/__init__.py b/src/pybamm/models/submodels/transport_efficiency/__init__.py similarity index 100% rename from pybamm/models/submodels/transport_efficiency/__init__.py rename to src/pybamm/models/submodels/transport_efficiency/__init__.py diff --git a/pybamm/models/submodels/transport_efficiency/base_transport_efficiency.py b/src/pybamm/models/submodels/transport_efficiency/base_transport_efficiency.py similarity index 100% rename from pybamm/models/submodels/transport_efficiency/base_transport_efficiency.py rename to src/pybamm/models/submodels/transport_efficiency/base_transport_efficiency.py diff --git a/pybamm/models/submodels/transport_efficiency/bruggeman.py b/src/pybamm/models/submodels/transport_efficiency/bruggeman.py similarity index 100% rename from pybamm/models/submodels/transport_efficiency/bruggeman.py rename to src/pybamm/models/submodels/transport_efficiency/bruggeman.py diff --git a/pybamm/models/submodels/transport_efficiency/cation_exchange_membrane.py b/src/pybamm/models/submodels/transport_efficiency/cation_exchange_membrane.py similarity index 100% rename from pybamm/models/submodels/transport_efficiency/cation_exchange_membrane.py rename to src/pybamm/models/submodels/transport_efficiency/cation_exchange_membrane.py diff --git a/pybamm/models/submodels/transport_efficiency/heterogeneous_catalyst.py b/src/pybamm/models/submodels/transport_efficiency/heterogeneous_catalyst.py similarity index 100% rename from pybamm/models/submodels/transport_efficiency/heterogeneous_catalyst.py rename to src/pybamm/models/submodels/transport_efficiency/heterogeneous_catalyst.py diff --git a/pybamm/models/submodels/transport_efficiency/hyperbola_of_revolution.py b/src/pybamm/models/submodels/transport_efficiency/hyperbola_of_revolution.py similarity index 100% rename from pybamm/models/submodels/transport_efficiency/hyperbola_of_revolution.py rename to src/pybamm/models/submodels/transport_efficiency/hyperbola_of_revolution.py diff --git a/pybamm/models/submodels/transport_efficiency/ordered_packing.py b/src/pybamm/models/submodels/transport_efficiency/ordered_packing.py similarity index 100% rename from pybamm/models/submodels/transport_efficiency/ordered_packing.py rename to src/pybamm/models/submodels/transport_efficiency/ordered_packing.py diff --git a/pybamm/models/submodels/transport_efficiency/overlapping_spheres.py b/src/pybamm/models/submodels/transport_efficiency/overlapping_spheres.py similarity index 100% rename from pybamm/models/submodels/transport_efficiency/overlapping_spheres.py rename to src/pybamm/models/submodels/transport_efficiency/overlapping_spheres.py diff --git a/pybamm/models/submodels/transport_efficiency/random_overlapping_cylinders.py b/src/pybamm/models/submodels/transport_efficiency/random_overlapping_cylinders.py similarity index 100% rename from pybamm/models/submodels/transport_efficiency/random_overlapping_cylinders.py rename to src/pybamm/models/submodels/transport_efficiency/random_overlapping_cylinders.py diff --git a/pybamm/models/submodels/transport_efficiency/tortuosity_factor.py b/src/pybamm/models/submodels/transport_efficiency/tortuosity_factor.py similarity index 100% rename from pybamm/models/submodels/transport_efficiency/tortuosity_factor.py rename to src/pybamm/models/submodels/transport_efficiency/tortuosity_factor.py diff --git a/pybamm/parameters/__init__.py b/src/pybamm/parameters/__init__.py similarity index 100% rename from pybamm/parameters/__init__.py rename to src/pybamm/parameters/__init__.py diff --git a/pybamm/parameters/base_parameters.py b/src/pybamm/parameters/base_parameters.py similarity index 100% rename from pybamm/parameters/base_parameters.py rename to src/pybamm/parameters/base_parameters.py diff --git a/pybamm/parameters/bpx.py b/src/pybamm/parameters/bpx.py similarity index 100% rename from pybamm/parameters/bpx.py rename to src/pybamm/parameters/bpx.py diff --git a/pybamm/parameters/constants.py b/src/pybamm/parameters/constants.py similarity index 100% rename from pybamm/parameters/constants.py rename to src/pybamm/parameters/constants.py diff --git a/pybamm/parameters/ecm_parameters.py b/src/pybamm/parameters/ecm_parameters.py similarity index 100% rename from pybamm/parameters/ecm_parameters.py rename to src/pybamm/parameters/ecm_parameters.py diff --git a/pybamm/parameters/electrical_parameters.py b/src/pybamm/parameters/electrical_parameters.py similarity index 100% rename from pybamm/parameters/electrical_parameters.py rename to src/pybamm/parameters/electrical_parameters.py diff --git a/pybamm/parameters/geometric_parameters.py b/src/pybamm/parameters/geometric_parameters.py similarity index 100% rename from pybamm/parameters/geometric_parameters.py rename to src/pybamm/parameters/geometric_parameters.py diff --git a/pybamm/parameters/lead_acid_parameters.py b/src/pybamm/parameters/lead_acid_parameters.py similarity index 100% rename from pybamm/parameters/lead_acid_parameters.py rename to src/pybamm/parameters/lead_acid_parameters.py diff --git a/pybamm/parameters/lithium_ion_parameters.py b/src/pybamm/parameters/lithium_ion_parameters.py similarity index 100% rename from pybamm/parameters/lithium_ion_parameters.py rename to src/pybamm/parameters/lithium_ion_parameters.py diff --git a/pybamm/parameters/parameter_sets.py b/src/pybamm/parameters/parameter_sets.py similarity index 100% rename from pybamm/parameters/parameter_sets.py rename to src/pybamm/parameters/parameter_sets.py diff --git a/pybamm/parameters/parameter_values.py b/src/pybamm/parameters/parameter_values.py similarity index 100% rename from pybamm/parameters/parameter_values.py rename to src/pybamm/parameters/parameter_values.py diff --git a/pybamm/parameters/process_parameter_data.py b/src/pybamm/parameters/process_parameter_data.py similarity index 100% rename from pybamm/parameters/process_parameter_data.py rename to src/pybamm/parameters/process_parameter_data.py diff --git a/pybamm/parameters/size_distribution_parameters.py b/src/pybamm/parameters/size_distribution_parameters.py similarity index 100% rename from pybamm/parameters/size_distribution_parameters.py rename to src/pybamm/parameters/size_distribution_parameters.py diff --git a/pybamm/parameters/thermal_parameters.py b/src/pybamm/parameters/thermal_parameters.py similarity index 100% rename from pybamm/parameters/thermal_parameters.py rename to src/pybamm/parameters/thermal_parameters.py diff --git a/pybamm/plotting/__init__.py b/src/pybamm/plotting/__init__.py similarity index 100% rename from pybamm/plotting/__init__.py rename to src/pybamm/plotting/__init__.py diff --git a/pybamm/plotting/dynamic_plot.py b/src/pybamm/plotting/dynamic_plot.py similarity index 100% rename from pybamm/plotting/dynamic_plot.py rename to src/pybamm/plotting/dynamic_plot.py diff --git a/pybamm/plotting/plot.py b/src/pybamm/plotting/plot.py similarity index 100% rename from pybamm/plotting/plot.py rename to src/pybamm/plotting/plot.py diff --git a/pybamm/plotting/plot2D.py b/src/pybamm/plotting/plot2D.py similarity index 100% rename from pybamm/plotting/plot2D.py rename to src/pybamm/plotting/plot2D.py diff --git a/pybamm/plotting/plot_summary_variables.py b/src/pybamm/plotting/plot_summary_variables.py similarity index 100% rename from pybamm/plotting/plot_summary_variables.py rename to src/pybamm/plotting/plot_summary_variables.py diff --git a/pybamm/plotting/plot_thermal_components.py b/src/pybamm/plotting/plot_thermal_components.py similarity index 100% rename from pybamm/plotting/plot_thermal_components.py rename to src/pybamm/plotting/plot_thermal_components.py diff --git a/pybamm/plotting/plot_voltage_components.py b/src/pybamm/plotting/plot_voltage_components.py similarity index 100% rename from pybamm/plotting/plot_voltage_components.py rename to src/pybamm/plotting/plot_voltage_components.py diff --git a/pybamm/plotting/quick_plot.py b/src/pybamm/plotting/quick_plot.py similarity index 100% rename from pybamm/plotting/quick_plot.py rename to src/pybamm/plotting/quick_plot.py diff --git a/pybamm/pybamm_data.py b/src/pybamm/pybamm_data.py similarity index 100% rename from pybamm/pybamm_data.py rename to src/pybamm/pybamm_data.py diff --git a/pybamm/settings.py b/src/pybamm/settings.py similarity index 100% rename from pybamm/settings.py rename to src/pybamm/settings.py diff --git a/pybamm/simulation.py b/src/pybamm/simulation.py similarity index 100% rename from pybamm/simulation.py rename to src/pybamm/simulation.py diff --git a/pybamm/solvers/__init__.py b/src/pybamm/solvers/__init__.py similarity index 100% rename from pybamm/solvers/__init__.py rename to src/pybamm/solvers/__init__.py diff --git a/pybamm/solvers/algebraic_solver.py b/src/pybamm/solvers/algebraic_solver.py similarity index 100% rename from pybamm/solvers/algebraic_solver.py rename to src/pybamm/solvers/algebraic_solver.py diff --git a/pybamm/solvers/base_solver.py b/src/pybamm/solvers/base_solver.py similarity index 100% rename from pybamm/solvers/base_solver.py rename to src/pybamm/solvers/base_solver.py diff --git a/pybamm/solvers/c_solvers/__init__.py b/src/pybamm/solvers/c_solvers/__init__.py similarity index 100% rename from pybamm/solvers/c_solvers/__init__.py rename to src/pybamm/solvers/c_solvers/__init__.py diff --git a/pybamm/solvers/c_solvers/idaklu.cpp b/src/pybamm/solvers/c_solvers/idaklu.cpp similarity index 100% rename from pybamm/solvers/c_solvers/idaklu.cpp rename to src/pybamm/solvers/c_solvers/idaklu.cpp diff --git a/pybamm/solvers/c_solvers/idaklu/Expressions/Base/Expression.hpp b/src/pybamm/solvers/c_solvers/idaklu/Expressions/Base/Expression.hpp similarity index 100% rename from pybamm/solvers/c_solvers/idaklu/Expressions/Base/Expression.hpp rename to src/pybamm/solvers/c_solvers/idaklu/Expressions/Base/Expression.hpp diff --git a/pybamm/solvers/c_solvers/idaklu/Expressions/Base/ExpressionSet.hpp b/src/pybamm/solvers/c_solvers/idaklu/Expressions/Base/ExpressionSet.hpp similarity index 100% rename from pybamm/solvers/c_solvers/idaklu/Expressions/Base/ExpressionSet.hpp rename to src/pybamm/solvers/c_solvers/idaklu/Expressions/Base/ExpressionSet.hpp diff --git a/pybamm/solvers/c_solvers/idaklu/Expressions/Base/ExpressionTypes.hpp b/src/pybamm/solvers/c_solvers/idaklu/Expressions/Base/ExpressionTypes.hpp similarity index 100% rename from pybamm/solvers/c_solvers/idaklu/Expressions/Base/ExpressionTypes.hpp rename to src/pybamm/solvers/c_solvers/idaklu/Expressions/Base/ExpressionTypes.hpp diff --git a/pybamm/solvers/c_solvers/idaklu/Expressions/Casadi/CasadiFunctions.cpp b/src/pybamm/solvers/c_solvers/idaklu/Expressions/Casadi/CasadiFunctions.cpp similarity index 100% rename from pybamm/solvers/c_solvers/idaklu/Expressions/Casadi/CasadiFunctions.cpp rename to src/pybamm/solvers/c_solvers/idaklu/Expressions/Casadi/CasadiFunctions.cpp diff --git a/pybamm/solvers/c_solvers/idaklu/Expressions/Casadi/CasadiFunctions.hpp b/src/pybamm/solvers/c_solvers/idaklu/Expressions/Casadi/CasadiFunctions.hpp similarity index 100% rename from pybamm/solvers/c_solvers/idaklu/Expressions/Casadi/CasadiFunctions.hpp rename to src/pybamm/solvers/c_solvers/idaklu/Expressions/Casadi/CasadiFunctions.hpp diff --git a/pybamm/solvers/c_solvers/idaklu/Expressions/Expressions.hpp b/src/pybamm/solvers/c_solvers/idaklu/Expressions/Expressions.hpp similarity index 100% rename from pybamm/solvers/c_solvers/idaklu/Expressions/Expressions.hpp rename to src/pybamm/solvers/c_solvers/idaklu/Expressions/Expressions.hpp diff --git a/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEBaseFunction.hpp b/src/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEBaseFunction.hpp similarity index 100% rename from pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEBaseFunction.hpp rename to src/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEBaseFunction.hpp diff --git a/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunction.hpp b/src/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunction.hpp similarity index 100% rename from pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunction.hpp rename to src/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunction.hpp diff --git a/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunctions.cpp b/src/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunctions.cpp similarity index 100% rename from pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunctions.cpp rename to src/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunctions.cpp diff --git a/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunctions.hpp b/src/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunctions.hpp similarity index 100% rename from pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunctions.hpp rename to src/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunctions.hpp diff --git a/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/ModuleParser.cpp b/src/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/ModuleParser.cpp similarity index 100% rename from pybamm/solvers/c_solvers/idaklu/Expressions/IREE/ModuleParser.cpp rename to src/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/ModuleParser.cpp diff --git a/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/ModuleParser.hpp b/src/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/ModuleParser.hpp similarity index 100% rename from pybamm/solvers/c_solvers/idaklu/Expressions/IREE/ModuleParser.hpp rename to src/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/ModuleParser.hpp diff --git a/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/iree_jit.cpp b/src/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/iree_jit.cpp similarity index 100% rename from pybamm/solvers/c_solvers/idaklu/Expressions/IREE/iree_jit.cpp rename to src/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/iree_jit.cpp diff --git a/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/iree_jit.hpp b/src/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/iree_jit.hpp similarity index 100% rename from pybamm/solvers/c_solvers/idaklu/Expressions/IREE/iree_jit.hpp rename to src/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/iree_jit.hpp diff --git a/pybamm/solvers/c_solvers/idaklu/IDAKLUSolver.cpp b/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolver.cpp similarity index 100% rename from pybamm/solvers/c_solvers/idaklu/IDAKLUSolver.cpp rename to src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolver.cpp diff --git a/pybamm/solvers/c_solvers/idaklu/IDAKLUSolver.hpp b/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolver.hpp similarity index 100% rename from pybamm/solvers/c_solvers/idaklu/IDAKLUSolver.hpp rename to src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolver.hpp diff --git a/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.hpp b/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.hpp similarity index 100% rename from pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.hpp rename to src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.hpp diff --git a/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl b/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl similarity index 100% rename from pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl rename to src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl diff --git a/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP_solvers.cpp b/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP_solvers.cpp similarity index 100% rename from pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP_solvers.cpp rename to src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP_solvers.cpp diff --git a/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP_solvers.hpp b/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP_solvers.hpp similarity index 100% rename from pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP_solvers.hpp rename to src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP_solvers.hpp diff --git a/pybamm/solvers/c_solvers/idaklu/IdakluJax.cpp b/src/pybamm/solvers/c_solvers/idaklu/IdakluJax.cpp similarity index 100% rename from pybamm/solvers/c_solvers/idaklu/IdakluJax.cpp rename to src/pybamm/solvers/c_solvers/idaklu/IdakluJax.cpp diff --git a/pybamm/solvers/c_solvers/idaklu/IdakluJax.hpp b/src/pybamm/solvers/c_solvers/idaklu/IdakluJax.hpp similarity index 100% rename from pybamm/solvers/c_solvers/idaklu/IdakluJax.hpp rename to src/pybamm/solvers/c_solvers/idaklu/IdakluJax.hpp diff --git a/pybamm/solvers/c_solvers/idaklu/Options.cpp b/src/pybamm/solvers/c_solvers/idaklu/Options.cpp similarity index 100% rename from pybamm/solvers/c_solvers/idaklu/Options.cpp rename to src/pybamm/solvers/c_solvers/idaklu/Options.cpp diff --git a/pybamm/solvers/c_solvers/idaklu/Options.hpp b/src/pybamm/solvers/c_solvers/idaklu/Options.hpp similarity index 100% rename from pybamm/solvers/c_solvers/idaklu/Options.hpp rename to src/pybamm/solvers/c_solvers/idaklu/Options.hpp diff --git a/pybamm/solvers/c_solvers/idaklu/Solution.cpp b/src/pybamm/solvers/c_solvers/idaklu/Solution.cpp similarity index 100% rename from pybamm/solvers/c_solvers/idaklu/Solution.cpp rename to src/pybamm/solvers/c_solvers/idaklu/Solution.cpp diff --git a/pybamm/solvers/c_solvers/idaklu/Solution.hpp b/src/pybamm/solvers/c_solvers/idaklu/Solution.hpp similarity index 100% rename from pybamm/solvers/c_solvers/idaklu/Solution.hpp rename to src/pybamm/solvers/c_solvers/idaklu/Solution.hpp diff --git a/pybamm/solvers/c_solvers/idaklu/common.hpp b/src/pybamm/solvers/c_solvers/idaklu/common.hpp similarity index 100% rename from pybamm/solvers/c_solvers/idaklu/common.hpp rename to src/pybamm/solvers/c_solvers/idaklu/common.hpp diff --git a/pybamm/solvers/c_solvers/idaklu/idaklu_solver.hpp b/src/pybamm/solvers/c_solvers/idaklu/idaklu_solver.hpp similarity index 100% rename from pybamm/solvers/c_solvers/idaklu/idaklu_solver.hpp rename to src/pybamm/solvers/c_solvers/idaklu/idaklu_solver.hpp diff --git a/pybamm/solvers/c_solvers/idaklu/python.cpp b/src/pybamm/solvers/c_solvers/idaklu/python.cpp similarity index 100% rename from pybamm/solvers/c_solvers/idaklu/python.cpp rename to src/pybamm/solvers/c_solvers/idaklu/python.cpp diff --git a/pybamm/solvers/c_solvers/idaklu/python.hpp b/src/pybamm/solvers/c_solvers/idaklu/python.hpp similarity index 100% rename from pybamm/solvers/c_solvers/idaklu/python.hpp rename to src/pybamm/solvers/c_solvers/idaklu/python.hpp diff --git a/pybamm/solvers/c_solvers/idaklu/sundials_functions.hpp b/src/pybamm/solvers/c_solvers/idaklu/sundials_functions.hpp similarity index 100% rename from pybamm/solvers/c_solvers/idaklu/sundials_functions.hpp rename to src/pybamm/solvers/c_solvers/idaklu/sundials_functions.hpp diff --git a/pybamm/solvers/c_solvers/idaklu/sundials_functions.inl b/src/pybamm/solvers/c_solvers/idaklu/sundials_functions.inl similarity index 100% rename from pybamm/solvers/c_solvers/idaklu/sundials_functions.inl rename to src/pybamm/solvers/c_solvers/idaklu/sundials_functions.inl diff --git a/pybamm/solvers/c_solvers/idaklu/sundials_legacy_wrapper.hpp b/src/pybamm/solvers/c_solvers/idaklu/sundials_legacy_wrapper.hpp similarity index 100% rename from pybamm/solvers/c_solvers/idaklu/sundials_legacy_wrapper.hpp rename to src/pybamm/solvers/c_solvers/idaklu/sundials_legacy_wrapper.hpp diff --git a/pybamm/solvers/casadi_algebraic_solver.py b/src/pybamm/solvers/casadi_algebraic_solver.py similarity index 100% rename from pybamm/solvers/casadi_algebraic_solver.py rename to src/pybamm/solvers/casadi_algebraic_solver.py diff --git a/pybamm/solvers/casadi_solver.py b/src/pybamm/solvers/casadi_solver.py similarity index 100% rename from pybamm/solvers/casadi_solver.py rename to src/pybamm/solvers/casadi_solver.py diff --git a/pybamm/solvers/dummy_solver.py b/src/pybamm/solvers/dummy_solver.py similarity index 100% rename from pybamm/solvers/dummy_solver.py rename to src/pybamm/solvers/dummy_solver.py diff --git a/pybamm/solvers/idaklu_jax.py b/src/pybamm/solvers/idaklu_jax.py similarity index 100% rename from pybamm/solvers/idaklu_jax.py rename to src/pybamm/solvers/idaklu_jax.py diff --git a/pybamm/solvers/idaklu_solver.py b/src/pybamm/solvers/idaklu_solver.py similarity index 100% rename from pybamm/solvers/idaklu_solver.py rename to src/pybamm/solvers/idaklu_solver.py diff --git a/pybamm/solvers/jax_bdf_solver.py b/src/pybamm/solvers/jax_bdf_solver.py similarity index 100% rename from pybamm/solvers/jax_bdf_solver.py rename to src/pybamm/solvers/jax_bdf_solver.py diff --git a/pybamm/solvers/jax_solver.py b/src/pybamm/solvers/jax_solver.py similarity index 100% rename from pybamm/solvers/jax_solver.py rename to src/pybamm/solvers/jax_solver.py diff --git a/pybamm/solvers/lrudict.py b/src/pybamm/solvers/lrudict.py similarity index 100% rename from pybamm/solvers/lrudict.py rename to src/pybamm/solvers/lrudict.py diff --git a/pybamm/solvers/processed_variable.py b/src/pybamm/solvers/processed_variable.py similarity index 100% rename from pybamm/solvers/processed_variable.py rename to src/pybamm/solvers/processed_variable.py diff --git a/pybamm/solvers/processed_variable_computed.py b/src/pybamm/solvers/processed_variable_computed.py similarity index 100% rename from pybamm/solvers/processed_variable_computed.py rename to src/pybamm/solvers/processed_variable_computed.py diff --git a/pybamm/solvers/scipy_solver.py b/src/pybamm/solvers/scipy_solver.py similarity index 100% rename from pybamm/solvers/scipy_solver.py rename to src/pybamm/solvers/scipy_solver.py diff --git a/pybamm/solvers/solution.py b/src/pybamm/solvers/solution.py similarity index 100% rename from pybamm/solvers/solution.py rename to src/pybamm/solvers/solution.py diff --git a/pybamm/spatial_methods/__init__.py b/src/pybamm/spatial_methods/__init__.py similarity index 100% rename from pybamm/spatial_methods/__init__.py rename to src/pybamm/spatial_methods/__init__.py diff --git a/pybamm/spatial_methods/finite_volume.py b/src/pybamm/spatial_methods/finite_volume.py similarity index 100% rename from pybamm/spatial_methods/finite_volume.py rename to src/pybamm/spatial_methods/finite_volume.py diff --git a/pybamm/spatial_methods/scikit_finite_element.py b/src/pybamm/spatial_methods/scikit_finite_element.py similarity index 100% rename from pybamm/spatial_methods/scikit_finite_element.py rename to src/pybamm/spatial_methods/scikit_finite_element.py diff --git a/pybamm/spatial_methods/spatial_method.py b/src/pybamm/spatial_methods/spatial_method.py similarity index 100% rename from pybamm/spatial_methods/spatial_method.py rename to src/pybamm/spatial_methods/spatial_method.py diff --git a/pybamm/spatial_methods/spectral_volume.py b/src/pybamm/spatial_methods/spectral_volume.py similarity index 100% rename from pybamm/spatial_methods/spectral_volume.py rename to src/pybamm/spatial_methods/spectral_volume.py diff --git a/pybamm/spatial_methods/zero_dimensional_method.py b/src/pybamm/spatial_methods/zero_dimensional_method.py similarity index 100% rename from pybamm/spatial_methods/zero_dimensional_method.py rename to src/pybamm/spatial_methods/zero_dimensional_method.py diff --git a/pybamm/type_definitions.py b/src/pybamm/type_definitions.py similarity index 100% rename from pybamm/type_definitions.py rename to src/pybamm/type_definitions.py diff --git a/pybamm/util.py b/src/pybamm/util.py similarity index 99% rename from pybamm/util.py rename to src/pybamm/util.py index 130cb5ba48..ee1431ecb1 100644 --- a/pybamm/util.py +++ b/src/pybamm/util.py @@ -28,7 +28,7 @@ def root_dir(): """return the root directory of the PyBaMM install directory""" - return str(pathlib.Path(pybamm.__path__[0]).parent) + return str(pathlib.Path(pybamm.__path__[0]).parent.parent) def get_git_commit_info(): diff --git a/pybamm/version.py b/src/pybamm/version.py similarity index 100% rename from pybamm/version.py rename to src/pybamm/version.py diff --git a/tests/unit/test_experiments/test_simulation_with_experiment.py b/tests/unit/test_experiments/test_simulation_with_experiment.py index fb9e2b2f70..defca33b00 100644 --- a/tests/unit/test_experiments/test_simulation_with_experiment.py +++ b/tests/unit/test_experiments/test_simulation_with_experiment.py @@ -485,6 +485,7 @@ def test_cycle_summary_variables(self): # Load negative electrode OCP data filename = os.path.join( pybamm.root_dir(), + "src", "pybamm", "input", "parameters", @@ -499,6 +500,7 @@ def test_cycle_summary_variables(self): # Load positive electrode OCP data filename = os.path.join( pybamm.root_dir(), + "src", "pybamm", "input", "parameters", diff --git a/tests/unit/test_parameters/test_parameter_values.py b/tests/unit/test_parameters/test_parameter_values.py index 4826885f1c..eaeb4a5a42 100644 --- a/tests/unit/test_parameters/test_parameter_values.py +++ b/tests/unit/test_parameters/test_parameter_values.py @@ -674,8 +674,7 @@ def lico2_diffusivity_Dualfoil1998_2D(c_s, T): def test_process_interpolant_3D_from_csv(self): name = "data_for_testing_3D" - path = os.path.join(pybamm.root_dir(), "tests", "unit", "test_parameters") - + path = os.path.abspath(os.path.dirname(__file__)) processed = pybamm.parameters.process_3D_data_csv(name, path) parameter_values = pybamm.ParameterValues({"interpolation": processed}) @@ -719,8 +718,7 @@ def test_process_interpolant_3D_from_csv(self): def test_process_interpolant_2D_from_csv(self): name = "data_for_testing_2D" - path = os.path.join(pybamm.root_dir(), "tests", "unit", "test_parameters") - + path = os.path.abspath(os.path.dirname(__file__)) processed = pybamm.parameters.process_2D_data_csv(name, path) parameter_values = pybamm.ParameterValues({"interpolation": processed}) diff --git a/tests/unit/test_parameters/test_process_parameter_data.py b/tests/unit/test_parameters/test_process_parameter_data.py index bf27f71e4f..3230f374f2 100644 --- a/tests/unit/test_parameters/test_process_parameter_data.py +++ b/tests/unit/test_parameters/test_process_parameter_data.py @@ -13,7 +13,7 @@ class TestProcessParameterData(unittest.TestCase): def test_process_1D_data(self): name = "lico2_ocv_example" - path = os.path.join(pybamm.root_dir(), "tests", "unit", "test_parameters") + path = os.path.abspath(os.path.dirname(__file__)) processed = pybamm.parameters.process_1D_data(name, path) self.assertEqual(processed[0], name) self.assertIsInstance(processed[1], tuple) @@ -22,7 +22,7 @@ def test_process_1D_data(self): def test_process_2D_data(self): name = "lico2_diffusivity_Dualfoil1998_2D" - path = os.path.join(pybamm.root_dir(), "tests", "unit", "test_parameters") + path = os.path.abspath(os.path.dirname(__file__)) processed = pybamm.parameters.process_2D_data(name, path) self.assertEqual(processed[0], name) self.assertIsInstance(processed[1], tuple) @@ -32,7 +32,7 @@ def test_process_2D_data(self): def test_process_2D_data_csv(self): name = "data_for_testing_2D" - path = os.path.join(pybamm.root_dir(), "tests", "unit", "test_parameters") + path = os.path.abspath(os.path.dirname(__file__)) processed = pybamm.parameters.process_2D_data_csv(name, path) self.assertEqual(processed[0], name) @@ -43,7 +43,7 @@ def test_process_2D_data_csv(self): def test_process_3D_data_csv(self): name = "data_for_testing_3D" - path = os.path.join(pybamm.root_dir(), "tests", "unit", "test_parameters") + path = os.path.abspath(os.path.dirname(__file__)) processed = pybamm.parameters.process_3D_data_csv(name, path) self.assertEqual(processed[0], name) diff --git a/tests/unit/test_util.py b/tests/unit/test_util.py index c3f53f80be..1b621d98f0 100644 --- a/tests/unit/test_util.py +++ b/tests/unit/test_util.py @@ -83,7 +83,7 @@ def test_get_parameters_filepath(self): pybamm.get_parameters_filepath(tempfile_obj.name) == tempfile_obj.name ) - package_dir = os.path.join(pybamm.root_dir(), "pybamm") + package_dir = os.path.join(pybamm.root_dir(), "src", "pybamm") with tempfile.NamedTemporaryFile("w", dir=package_dir) as tempfile_obj: path = os.path.join(package_dir, tempfile_obj.name) assert pybamm.get_parameters_filepath(tempfile_obj.name) == path From a726f068650cf06917f886da1ff9657b1aafb98c Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 8 Aug 2024 13:13:26 +0100 Subject: [PATCH 52/82] docs: add pipliggins as a contributor for code, and test (#4325) * docs: update all_contributors.md [skip ci] * docs: update README.md [skip ci] * docs: update .all-contributorsrc [skip ci] --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 10 ++++++++++ README.md | 2 +- all_contributors.md | 1 + 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 963256650d..4366246007 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -950,6 +950,16 @@ "code", "test" ] + }, + { + "login": "pipliggins", + "name": "Pip Liggins", + "avatar_url": "https://avatars.githubusercontent.com/u/55396775?v=4", + "profile": "https://github.com/pipliggins", + "contributions": [ + "code", + "test" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 1a0f8b4b9b..a904e5a67c 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/pybamm-team/PyBaMM/badge)](https://scorecard.dev/viewer/?uri=github.com/pybamm-team/PyBaMM) -[![All Contributors](https://img.shields.io/badge/all_contributors-89-orange.svg)](#-contributors) +[![All Contributors](https://img.shields.io/badge/all_contributors-90-orange.svg)](#-contributors) diff --git a/all_contributors.md b/all_contributors.md index 3cc69655e0..9bb1e373d5 100644 --- a/all_contributors.md +++ b/all_contributors.md @@ -119,6 +119,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Smita Sahu
Smita Sahu

💻 Ubham16
Ubham16

💻 Mehrdad Babazadeh
Mehrdad Babazadeh

💻 ⚠️ + Pip Liggins
Pip Liggins

💻 ⚠️ From 4cb4edd7ee02952a232f0be3f35d661e739e0347 Mon Sep 17 00:00:00 2001 From: Pradyot Ranjan <99216956+prady0t@users.noreply.github.com> Date: Thu, 8 Aug 2024 23:12:11 +0530 Subject: [PATCH 53/82] Migrating rest of integration tests to pytest (#4285) * Migrating rest of integration tests to pytest Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * style: pre-commit fixes * Using Testcase to avoid non deterministic tests failure Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * Changing back Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * Adding back Testcase on account of failing test cases Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * using setup() and reverting changes for test_butler_volmer.py Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * style: pre-commit fixes * Update tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> * Update tests/integration/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_tests.py Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> * Update tests/integration/test_spatial_methods/test_finite_volume.py Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> * Update tests/integration/test_spatial_methods/test_finite_volume.py Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> * using setup as fixture for TestDFNWithSizeDistribution Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * removing testcase Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * style: pre-commit fixes * Resolving failing links Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> --------- Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> Co-authored-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Arjun Verma Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> --- docs/source/api/parameters/parameter_sets.rst | 2 +- .../tutorial-4-setting-parameter-values.ipynb | 2 +- tests/integration/test_experiments.py | 16 ++-------- .../test_lithium_ion/test_basic_models.py | 8 ++--- .../test_compare_outputs_two_phase.py | 14 ++------- .../test_lithium_ion/test_dfn.py | 22 +++++--------- .../test_lithium_ion/test_dfn_half_cell.py | 2 +- .../test_lithium_ion/test_mpm.py | 13 +------- .../test_lithium_ion/test_newman_tobias.py | 2 +- .../test_lithium_ion/test_spm.py | 2 +- .../test_lithium_ion/test_spm_half_cell.py | 2 +- .../test_lithium_ion/test_spme.py | 2 +- .../test_lithium_ion/test_spme_half_cell.py | 2 +- .../test_lithium_ion/test_thermal_models.py | 15 ++-------- .../test_function_control.py | 14 +-------- .../test_interface/test_lead_acid.py | 29 +++++++----------- .../test_interface/test_lithium_ion.py | 30 +++++++------------ tests/integration/test_solvers/test_idaklu.py | 17 ++--------- .../integration/test_solvers/test_solution.py | 14 +-------- .../test_finite_volume.py | 17 ++--------- .../test_spectral_volume.py | 14 +-------- 21 files changed, 55 insertions(+), 184 deletions(-) diff --git a/docs/source/api/parameters/parameter_sets.rst b/docs/source/api/parameters/parameter_sets.rst index 575087f415..f4583b5990 100644 --- a/docs/source/api/parameters/parameter_sets.rst +++ b/docs/source/api/parameters/parameter_sets.rst @@ -33,7 +33,7 @@ package (``cell_parameters``) should consist of the following:: The actual parameter set is defined within ``cell_alpha.py``, as shown below. For an example, see the `Marquis2019`_ parameter sets. -.. _Marquis2019: https://github.com/pybamm-team/PyBaMM/blob/develop/pybamm/input/parameters/lithium_ion/Marquis2019.py +.. _Marquis2019: https://github.com/pybamm-team/PyBaMM/blob/develop/src/pybamm/input/parameters/lithium_ion/Marquis2019.py .. code-block:: python :linenos: diff --git a/docs/source/examples/notebooks/getting_started/tutorial-4-setting-parameter-values.ipynb b/docs/source/examples/notebooks/getting_started/tutorial-4-setting-parameter-values.ipynb index 01ae1864b6..a35a81932f 100644 --- a/docs/source/examples/notebooks/getting_started/tutorial-4-setting-parameter-values.ipynb +++ b/docs/source/examples/notebooks/getting_started/tutorial-4-setting-parameter-values.ipynb @@ -573,7 +573,7 @@ "source": [ "Note how, when we pass a function as a parameter, we pass the object without calling it, i.e. we pass `cube` rather than `cube(t)`. This new `parameter_values` variable could now be passed to a simulation, but note that it is incomplete as it does not include all the parameters that the model needs to run (see the parameters needed by calling `model.print_parameter_info()`, as done above). \n", "\n", - "It is often convenient to define the parameter set in a separate file, and then call the parameters into your notebook or script. You can find some examples on how to do so in [PyBaMM's parameter library](https://github.com/pybamm-team/PyBaMM/tree/develop/pybamm/input/parameters/lithium_ion). You can copy one of the parameter sets available into a new file and modify it accordingly for the new parameter set. Then, whenever the set is needed, one can import the `get_parameter_values` method from the corresponding file and call it to obtain a copy of the parameter values." + "It is often convenient to define the parameter set in a separate file, and then call the parameters into your notebook or script. You can find some examples on how to do so in [PyBaMM's parameter library](https://github.com/pybamm-team/PyBaMM/tree/develop/src/pybamm/input/parameters/lithium_ion). You can copy one of the parameter sets available into a new file and modify it accordingly for the new parameter set. Then, whenever the set is needed, one can import the `get_parameter_values` method from the corresponding file and call it to obtain a copy of the parameter values." ] }, { diff --git a/tests/integration/test_experiments.py b/tests/integration/test_experiments.py index f43c7293d2..595e5af2bb 100644 --- a/tests/integration/test_experiments.py +++ b/tests/integration/test_experiments.py @@ -1,13 +1,11 @@ # # Test some experiments # - import pybamm import numpy as np -import unittest -class TestExperiments(unittest.TestCase): +class TestExperiments: def test_discharge_rest_charge(self): experiment = pybamm.Experiment( [ @@ -78,7 +76,7 @@ def test_infeasible(self): ) sol = sim.solve() # this experiment fails during the third cycle (i.e. is infeasible) - self.assertEqual(len(sol.cycles), 3) + assert len(sol.cycles) == 3 def test_drive_cycle(self): drive_cycle = np.array([np.arange(100), 5 * np.ones(100)]).T @@ -100,13 +98,3 @@ def test_drive_cycle(self): ) sol = sim.solve() assert np.all(sol["Terminal voltage [V]"].entries >= 4.00) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_basic_models.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_basic_models.py index 83f8dee318..abb0169d06 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_basic_models.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_basic_models.py @@ -21,24 +21,24 @@ def test_with_experiment(self): class TestBasicSPM(BaseBasicModelTest): @pytest.fixture(autouse=True) - def setUp(self): + def setup(self): self.model = pybamm.lithium_ion.BasicSPM() class TestBasicDFN(BaseBasicModelTest): @pytest.fixture(autouse=True) - def setUp(self): + def setup(self): self.model = pybamm.lithium_ion.BasicDFN() class TestBasicDFNComposite(BaseBasicModelTest): @pytest.fixture(autouse=True) - def setUp(self): + def setup(self): self.model = pybamm.lithium_ion.BasicDFNComposite() class TestBasicDFNHalfCell(BaseBasicModelTest): @pytest.fixture(autouse=True) - def setUp(self): + def setup(self): options = {"working electrode": "positive"} self.model = pybamm.lithium_ion.BasicDFNHalfCell(options) diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_compare_outputs_two_phase.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_compare_outputs_two_phase.py index ed6d707d77..b4aed1f741 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_compare_outputs_two_phase.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_compare_outputs_two_phase.py @@ -3,10 +3,9 @@ # import pybamm import numpy as np -import unittest -class TestCompareOutputsTwoPhase(unittest.TestCase): +class TestCompareOutputsTwoPhase: def compare_outputs_two_phase_graphite_graphite(self, model_class): """ Check that a two-phase graphite-graphite model gives the same results as a @@ -158,7 +157,7 @@ def compare_outputs_two_phase_silicon_graphite(self, model_class): ) # More silicon means longer sim - self.assertLess(sol[0]["Time [s]"].data[-1], sol[1]["Time [s]"].data[-1]) + assert sol[0]["Time [s]"].data[-1] < sol[1]["Time [s]"].data[-1] def test_compare_SPM_silicon_graphite(self): model_class = pybamm.lithium_ion.SPM @@ -171,12 +170,3 @@ def test_compare_SPMe_silicon_graphite(self): def test_compare_DFN_silicon_graphite(self): model_class = pybamm.lithium_ion.DFN self.compare_outputs_two_phase_silicon_graphite(model_class) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - unittest.main() diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py index 1b6b29ea10..02f8b66f9b 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_dfn.py @@ -1,16 +1,16 @@ # # Tests for the lithium-ion DFN model # - import pybamm import tests import numpy as np -import unittest from tests import BaseIntegrationTestLithiumIon +import pytest -class TestDFN(BaseIntegrationTestLithiumIon, unittest.TestCase): - def setUp(self): +class TestDFN(BaseIntegrationTestLithiumIon): + @pytest.fixture(autouse=True) + def setup(self): self.model = pybamm.lithium_ion.DFN def test_particle_distribution_in_x(self): @@ -35,8 +35,9 @@ def positive_radius(x): self.run_basic_processing_test({}, parameter_values=param) -class TestDFNWithSizeDistribution(unittest.TestCase): - def setUp(self): +class TestDFNWithSizeDistribution: + @pytest.fixture(autouse=True) + def setup(self): params = pybamm.ParameterValues("Marquis2019") self.params = pybamm.get_size_distribution_parameters(params) @@ -120,12 +121,3 @@ def test_conservation_each_electrode(self): # compare np.testing.assert_array_almost_equal(neg_Li[0], neg_Li[1], decimal=12) np.testing.assert_array_almost_equal(pos_Li[0], pos_Li[1], decimal=12) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - unittest.main() diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_dfn_half_cell.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_dfn_half_cell.py index 4c64a35dff..4da2392335 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_dfn_half_cell.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_dfn_half_cell.py @@ -8,5 +8,5 @@ class TestDFNHalfCell(BaseIntegrationTestLithiumIonHalfCell): @pytest.fixture(autouse=True) - def setUp(self): + def setup(self): self.model = pybamm.lithium_ion.DFN diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py index 7f5a5941c8..6e67f349fa 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py @@ -1,14 +1,12 @@ # # Tests for the lithium-ion MPM model # - import pybamm import tests import numpy as np -import unittest -class TestMPM(unittest.TestCase): +class TestMPM: def test_basic_processing(self): options = {"thermal": "isothermal"} model = pybamm.lithium_ion.MPM(options) @@ -124,12 +122,3 @@ def test_conservation_each_electrode(self): # compare np.testing.assert_array_almost_equal(neg_Li[0], neg_Li[1], decimal=13) np.testing.assert_array_almost_equal(pos_Li[0], pos_Li[1], decimal=13) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - unittest.main() diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_newman_tobias.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_newman_tobias.py index e9eead9773..7d457fd7dc 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_newman_tobias.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_newman_tobias.py @@ -8,7 +8,7 @@ class TestNewmanTobias(BaseIntegrationTestLithiumIon): @pytest.fixture(autouse=True) - def setUp(self): + def setup(self): self.model = pybamm.lithium_ion.NewmanTobias def test_basic_processing(self): diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm.py index ecbccf9c3e..e07cd6409b 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm.py @@ -8,5 +8,5 @@ class TestSPM(BaseIntegrationTestLithiumIon): @pytest.fixture(autouse=True) - def setUp(self): + def setup(self): self.model = pybamm.lithium_ion.SPM diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm_half_cell.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm_half_cell.py index cf5c1cbb67..104fe0e775 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm_half_cell.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spm_half_cell.py @@ -8,5 +8,5 @@ class TestSPMHalfCell(BaseIntegrationTestLithiumIonHalfCell): @pytest.fixture(autouse=True) - def setUp(self): + def setup(self): self.model = pybamm.lithium_ion.SPM diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spme.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spme.py index 6b8096dc8b..6360b513c7 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spme.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spme.py @@ -8,7 +8,7 @@ class TestSPMe(BaseIntegrationTestLithiumIon): @pytest.fixture(autouse=True) - def setUp(self): + def setup(self): self.model = pybamm.lithium_ion.SPMe def test_integrated_conductivity(self): diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spme_half_cell.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spme_half_cell.py index 4b4ae8e997..0a9f450bb5 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spme_half_cell.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_spme_half_cell.py @@ -8,5 +8,5 @@ class TestSPMeHalfCell(BaseIntegrationTestLithiumIonHalfCell): @pytest.fixture(autouse=True) - def setUp(self): + def setup(self): self.model = pybamm.lithium_ion.SPMe diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_thermal_models.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_thermal_models.py index 2b74366652..603f64d716 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_thermal_models.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_thermal_models.py @@ -4,10 +4,9 @@ # import pybamm import numpy as np -import unittest -class TestThermal(unittest.TestCase): +class TestThermal: def test_consistent_cooling(self): "Test the cooling is consistent between the 1D, 1+1D and 2+1D SPMe models" @@ -100,7 +99,7 @@ def test_consistent_cooling(self): def err(a, b): return np.max(np.abs(a - b)) / np.max(np.abs(a)) - self.assertGreater(1e-5, err(solutions["SPMe 1+1D"], solutions["SPMe 2+1D"])) + assert 1e-5 > err(solutions["SPMe 1+1D"], solutions["SPMe 2+1D"]) def test_lumped_contact_resistance(self): # Test that the heating with contact resistance is greater than without @@ -149,13 +148,3 @@ def test_lumped_contact_resistance(self): # with contact resistance is higher than without contact resistance # skip the first entry because they are the same due to initial conditions np.testing.assert_array_less(avg_cell_temp[1:], avg_cell_temp_cr[1:]) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/integration/test_models/test_submodels/test_external_circuit/test_function_control.py b/tests/integration/test_models/test_submodels/test_external_circuit/test_function_control.py index c67604412c..b2c46eed5b 100644 --- a/tests/integration/test_models/test_submodels/test_external_circuit/test_function_control.py +++ b/tests/integration/test_models/test_submodels/test_external_circuit/test_function_control.py @@ -1,13 +1,11 @@ # # Test function control submodel # - import numpy as np import pybamm -import unittest -class TestFunctionControl(unittest.TestCase): +class TestFunctionControl: def test_constant_current(self): def constant_current(variables): I = variables["Current [A]"] @@ -193,13 +191,3 @@ def test_cccv(self): # solve model t_eval = np.linspace(0, 3600, 100) model.default_solver.solve(model, t_eval) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/integration/test_models/test_submodels/test_interface/test_lead_acid.py b/tests/integration/test_models/test_submodels/test_interface/test_lead_acid.py index ca41414c70..f0418f4bf7 100644 --- a/tests/integration/test_models/test_submodels/test_interface/test_lead_acid.py +++ b/tests/integration/test_models/test_submodels/test_interface/test_lead_acid.py @@ -1,14 +1,14 @@ # # Tests for the electrode-electrolyte interface equations for lead-acid models # - import pybamm from tests import get_discretisation_for_testing -import unittest +import pytest -class TestMainReaction(unittest.TestCase): - def setUp(self): +class TestMainReaction: + @pytest.fixture(autouse=True) + def setup(self): c_e_n = pybamm.Variable( "Negative electrolyte concentration [mol.m-3]", domain=["negative electrode"], @@ -51,8 +51,8 @@ def test_creation_main_reaction(self): param, "positive", "lead-acid main", {} ) j0_p = model_p._get_exchange_current_density(self.variables) - self.assertEqual(j0_n.domain, ["negative electrode"]) - self.assertEqual(j0_p.domain, ["positive electrode"]) + assert j0_n.domain == ["negative electrode"] + assert j0_p.domain == ["positive electrode"] def test_set_parameters_main_reaction(self): # With intercalation @@ -71,9 +71,9 @@ def test_set_parameters_main_reaction(self): j0_p = parameter_values.process_symbol(j0_p) # Test for x in j0_n.pre_order(): - self.assertNotIsInstance(x, pybamm.Parameter) + assert not isinstance(x, pybamm.Parameter) for x in j0_p.pre_order(): - self.assertNotIsInstance(x, pybamm.Parameter) + assert not isinstance(x, pybamm.Parameter) def test_discretisation_main_reaction(self): # With intercalation @@ -99,14 +99,5 @@ def test_discretisation_main_reaction(self): submesh = mesh[whole_cell] y = submesh.nodes**2 # should evaluate to vectors with the right shape - self.assertEqual(j0_n.evaluate(y=y).shape, (mesh["negative electrode"].npts, 1)) - self.assertEqual(j0_p.evaluate(y=y).shape, (mesh["positive electrode"].npts, 1)) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - unittest.main() + assert j0_n.evaluate(y=y).shape == (mesh["negative electrode"].npts, 1) + assert j0_p.evaluate(y=y).shape == (mesh["positive electrode"].npts, 1) diff --git a/tests/integration/test_models/test_submodels/test_interface/test_lithium_ion.py b/tests/integration/test_models/test_submodels/test_interface/test_lithium_ion.py index 135e29ad8b..a6725294d7 100644 --- a/tests/integration/test_models/test_submodels/test_interface/test_lithium_ion.py +++ b/tests/integration/test_models/test_submodels/test_interface/test_lithium_ion.py @@ -1,16 +1,15 @@ # # Tests for the electrode-electrolyte interface equations for lithium-ion models # - import pybamm from tests import get_discretisation_for_testing - -import unittest +import pytest import numpy as np -class TestExchangeCurrentDensity(unittest.TestCase): - def setUp(self): +class TestExchangeCurrentDensity: + @pytest.fixture(autouse=True) + def setup(self): c_e_n = pybamm.Variable("concentration", domain=["negative electrode"]) c_e_s = pybamm.Variable("concentration", domain=["separator"]) c_e_p = pybamm.Variable("concentration", domain=["positive electrode"]) @@ -49,8 +48,8 @@ def test_creation_lithium_ion(self): ) model_p.options = self.options j0_p = model_p._get_exchange_current_density(self.variables) - self.assertEqual(j0_n.domain, ["negative electrode"]) - self.assertEqual(j0_p.domain, ["positive electrode"]) + assert j0_n.domain == ["negative electrode"] + assert j0_p.domain == ["positive electrode"] def test_set_parameters_lithium_ion(self): param = pybamm.LithiumIonParameters() @@ -70,9 +69,9 @@ def test_set_parameters_lithium_ion(self): j0_p = parameter_values.process_symbol(j0_p) # Test for x in j0_n.pre_order(): - self.assertNotIsInstance(x, pybamm.Parameter) + assert not isinstance(x, pybamm.Parameter) for x in j0_p.pre_order(): - self.assertNotIsInstance(x, pybamm.Parameter) + assert not isinstance(x, pybamm.Parameter) def test_discretisation_lithium_ion(self): param = pybamm.LithiumIonParameters() @@ -107,14 +106,5 @@ def test_discretisation_lithium_ion(self): ] ) # should evaluate to vectors with the right shape - self.assertEqual(j0_n.evaluate(y=y).shape, (mesh["negative electrode"].npts, 1)) - self.assertEqual(j0_p.evaluate(y=y).shape, (mesh["positive electrode"].npts, 1)) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - unittest.main() + assert j0_n.evaluate(y=y).shape == (mesh["negative electrode"].npts, 1) + assert j0_p.evaluate(y=y).shape == (mesh["positive electrode"].npts, 1) diff --git a/tests/integration/test_solvers/test_idaklu.py b/tests/integration/test_solvers/test_idaklu.py index 693ec849a9..31083319cf 100644 --- a/tests/integration/test_solvers/test_idaklu.py +++ b/tests/integration/test_solvers/test_idaklu.py @@ -1,12 +1,10 @@ +import pytest import pybamm import numpy as np -import sys -import unittest - -@unittest.skipIf(not pybamm.have_idaklu(), "idaklu solver is not installed") -class TestIDAKLUSolver(unittest.TestCase): +@pytest.mark.skipif(not pybamm.have_idaklu(), reason="idaklu solver is not installed") +class TestIDAKLUSolver: def test_on_spme(self): model = pybamm.lithium_ion.SPMe() geometry = model.default_geometry @@ -89,12 +87,3 @@ def test_changing_grid(self): # solve solver.solve(model_disc, t_eval) - - -if __name__ == "__main__": - print("Add -v for more debug output") - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/integration/test_solvers/test_solution.py b/tests/integration/test_solvers/test_solution.py index bb276a62a2..d0b98e8eb9 100644 --- a/tests/integration/test_solvers/test_solution.py +++ b/tests/integration/test_solvers/test_solution.py @@ -1,13 +1,11 @@ # # Tests for the Solution class # - import pybamm -import unittest import numpy as np -class TestSolution(unittest.TestCase): +class TestSolution: def test_append(self): model = pybamm.lithium_ion.SPMe() # create geometry @@ -49,13 +47,3 @@ def test_append(self): step_solution["Voltage [V]"](solution.t[:-1]), decimal=4, ) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/integration/test_spatial_methods/test_finite_volume.py b/tests/integration/test_spatial_methods/test_finite_volume.py index d331554853..45b30f959d 100644 --- a/tests/integration/test_spatial_methods/test_finite_volume.py +++ b/tests/integration/test_spatial_methods/test_finite_volume.py @@ -1,7 +1,6 @@ # # Test for the operator class # - import pybamm from tests import ( get_mesh_for_testing, @@ -10,10 +9,9 @@ ) import numpy as np -import unittest -class TestFiniteVolumeConvergence(unittest.TestCase): +class TestFiniteVolumeConvergence: def test_grad_div_broadcast(self): # create mesh and discretisation spatial_methods = {"macroscale": pybamm.FiniteVolume()} @@ -319,7 +317,7 @@ def solve_laplace_equation(coord_sys="cartesian"): return solver.solve(model) -class TestFiniteVolumeLaplacian(unittest.TestCase): +class TestFiniteVolumeLaplacian: def test_laplacian_cartesian(self): solution = solve_laplace_equation(coord_sys="cartesian") np.testing.assert_array_almost_equal( @@ -374,7 +372,7 @@ def solve_advection_equation(direction="upwind", source=1, bc=0): return solver.solve(model, [0, 1]) -class TestUpwindDownwind(unittest.TestCase): +class TestUpwindDownwind: def test_upwind(self): solution = solve_advection_equation("upwind") np.testing.assert_array_almost_equal( @@ -386,12 +384,3 @@ def test_downwind(self): np.testing.assert_array_almost_equal( solution["u"].entries, solution["analytical"].entries, decimal=2 ) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - unittest.main() diff --git a/tests/integration/test_spatial_methods/test_spectral_volume.py b/tests/integration/test_spatial_methods/test_spectral_volume.py index deba95ebac..0a7fa18a04 100644 --- a/tests/integration/test_spatial_methods/test_spectral_volume.py +++ b/tests/integration/test_spatial_methods/test_spectral_volume.py @@ -1,11 +1,8 @@ # # Test for the operator class # - import pybamm - import numpy as np -import unittest def get_mesh_for_testing( @@ -76,7 +73,7 @@ def get_p2d_mesh_for_testing(xpts=None, rpts=10): return get_mesh_for_testing(xpts=xpts, rpts=rpts, geometry=geometry) -class TestSpectralVolumeConvergence(unittest.TestCase): +class TestSpectralVolumeConvergence: def test_grad_div_broadcast(self): # create mesh and discretisation spatial_methods = {"macroscale": pybamm.SpectralVolume()} @@ -324,12 +321,3 @@ def get_error(m): err_norm = np.array([np.linalg.norm(errs[n], np.inf) for n in ns]) rates = np.log2(err_norm[:-1] / err_norm[1:]) np.testing.assert_array_less(1.99 * np.ones_like(rates), rates) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - unittest.main() From b1fc5950f0d8e5c8e104e00573fdff5561818014 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Thu, 8 Aug 2024 22:31:51 +0100 Subject: [PATCH 54/82] fix: Expression::get_row / get_col no copy #4314 (#4315) * fix: Expression::get_row / get_col no copy #4314 * fix: const keyword * fix: suppress warnings --- .../idaklu/Expressions/Base/Expression.hpp | 4 ++-- .../Expressions/Casadi/CasadiFunctions.cpp | 24 ++++++++----------- .../Expressions/Casadi/CasadiFunctions.hpp | 8 +++---- .../idaklu/Expressions/IREE/IREEFunction.hpp | 4 ++-- .../idaklu/Expressions/IREE/IREEFunctions.cpp | 4 ++-- .../c_solvers/idaklu/IDAKLUSolverOpenMP.inl | 8 +++---- 6 files changed, 24 insertions(+), 28 deletions(-) diff --git a/src/pybamm/solvers/c_solvers/idaklu/Expressions/Base/Expression.hpp b/src/pybamm/solvers/c_solvers/idaklu/Expressions/Base/Expression.hpp index bbf60b4568..370d2c7427 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/Expressions/Base/Expression.hpp +++ b/src/pybamm/solvers/c_solvers/idaklu/Expressions/Base/Expression.hpp @@ -47,12 +47,12 @@ class Expression { /** * @brief Returns row indices in COO format (where the output data represents sparse matrix elements) */ - virtual std::vector get_row() = 0; + virtual const std::vector& get_row() = 0; /** * @brief Returns column indices in COO format (where the output data represents sparse matrix elements) */ - virtual std::vector get_col() = 0; + virtual const std::vector& get_col() = 0; public: // data members /** diff --git a/src/pybamm/solvers/c_solvers/idaklu/Expressions/Casadi/CasadiFunctions.cpp b/src/pybamm/solvers/c_solvers/idaklu/Expressions/Casadi/CasadiFunctions.cpp index b0c8ab1142..9c4ce0fa33 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/Expressions/Casadi/CasadiFunctions.cpp +++ b/src/pybamm/solvers/c_solvers/idaklu/Expressions/Casadi/CasadiFunctions.cpp @@ -19,6 +19,12 @@ CasadiFunction::CasadiFunction(const BaseFunctionType &f) : Expression(), m_func m_res.resize(sz_res, nullptr); m_iw.resize(sz_iw, 0); m_w.resize(sz_w, 0); + + if (m_func.n_out() > 0) { + casadi::Sparsity casadi_sparsity = m_func.sparsity_out(0); + m_rows = casadi_sparsity.get_row(); + m_cols = casadi_sparsity.get_col(); + } } // only call this once m_arg and m_res have been set appropriately @@ -45,24 +51,14 @@ expr_int CasadiFunction::nnz_out() { return static_cast(m_func.nnz_out()); } -std::vector CasadiFunction::get_row() { - return get_row(0); -} - -std::vector CasadiFunction::get_row(expr_int ind) { +const std::vector& CasadiFunction::get_row() { DEBUG("CasadiFunction get_row(): " << m_func.name()); - casadi::Sparsity casadi_sparsity = m_func.sparsity_out(ind); - return casadi_sparsity.get_row(); -} - -std::vector CasadiFunction::get_col() { - return get_col(0); + return m_rows; } -std::vector CasadiFunction::get_col(expr_int ind) { +const std::vector& CasadiFunction::get_col() { DEBUG("CasadiFunction get_col(): " << m_func.name()); - casadi::Sparsity casadi_sparsity = m_func.sparsity_out(ind); - return casadi_sparsity.get_col(); + return m_cols; } void CasadiFunction::operator()(const std::vector& inputs, diff --git a/src/pybamm/solvers/c_solvers/idaklu/Expressions/Casadi/CasadiFunctions.hpp b/src/pybamm/solvers/c_solvers/idaklu/Expressions/Casadi/CasadiFunctions.hpp index 64db2e6106..b05e429d62 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/Expressions/Casadi/CasadiFunctions.hpp +++ b/src/pybamm/solvers/c_solvers/idaklu/Expressions/Casadi/CasadiFunctions.hpp @@ -29,10 +29,8 @@ class CasadiFunction : public Expression expr_int out_shape(int k) override; expr_int nnz() override; expr_int nnz_out() override; - std::vector get_row() override; - std::vector get_row(expr_int ind); - std::vector get_col() override; - std::vector get_col(expr_int ind); + const std::vector& get_row() override; + const std::vector& get_col() override; public: /* @@ -43,6 +41,8 @@ class CasadiFunction : public Expression private: std::vector m_iw; // cppcheck-suppress unusedStructMember std::vector m_w; // cppcheck-suppress unusedStructMember + std::vector m_rows; // cppcheck-suppress unusedStructMember + std::vector m_cols; // cppcheck-suppress unusedStructMember }; /** diff --git a/src/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunction.hpp b/src/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunction.hpp index 26f81c8f98..bcdae5eabf 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunction.hpp +++ b/src/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunction.hpp @@ -27,8 +27,8 @@ class IREEFunction : public Expression expr_int out_shape(int k) override; expr_int nnz() override; expr_int nnz_out() override; - std::vector get_col() override; - std::vector get_row() override; + const std::vector& get_col() override; + const std::vector& get_row() override; /* * @brief Evaluate the MLIR function diff --git a/src/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunctions.cpp b/src/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunctions.cpp index 6837d21198..3bde647113 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunctions.cpp +++ b/src/pybamm/solvers/c_solvers/idaklu/Expressions/IREE/IREEFunctions.cpp @@ -203,12 +203,12 @@ expr_int IREEFunction::nnz_out() { return m_func.nnz; } -std::vector IREEFunction::get_row() { +const std::vector& IREEFunction::get_row() { DEBUG("IreeFunction get_row" << m_func.row.size()); return m_func.row; } -std::vector IREEFunction::get_col() { +const std::vector& IREEFunction::get_col() { DEBUG("IreeFunction get_col" << m_func.col.size()); return m_func.col; } diff --git a/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl b/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl index 96ebb40d10..b309fd6028 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl +++ b/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl @@ -297,7 +297,7 @@ void IDAKLUSolverOpenMP::CalcVarsSensitivities( DEBUG("IDAKLUSolver::CalcVarsSensitivities"); // Calculate sensitivities std::vector dens_dvar_dp = std::vector(number_of_parameters, 0); - for (size_t dvar_k=0; dvar_kdvar_dy_fcns.size(); dvar_k++) { + for (size_t dvar_k = 0; dvar_k < functions->dvar_dy_fcns.size(); dvar_k++) { // Isolate functions Expression* dvar_dy = functions->dvar_dy_fcns[dvar_k]; Expression* dvar_dp = functions->dvar_dp_fcns[dvar_k]; @@ -306,15 +306,15 @@ void IDAKLUSolverOpenMP::CalcVarsSensitivities( // Calculate dvar/dp and convert to dense array for indexing (*dvar_dp)({tret, yval, functions->inputs.data()}, {&res_dvar_dp[0]}); for (int k=0; knnz_out(); k++) { dens_dvar_dp[dvar_dp->get_row()[k]] = res_dvar_dp[k]; } // Calculate sensitivities - for (int paramk=0; paramknnz_out(); spk++) { + for (int spk = 0; spk < dvar_dy->nnz_out(); spk++) { yS_return[*ySk] += res_dvar_dy[spk] * ySval[paramk][dvar_dy->get_col()[spk]]; } (*ySk)++; From 83115e8bda9a86008cdf9e1a4cb74a119fd460b0 Mon Sep 17 00:00:00 2001 From: Pradyot Ranjan <99216956+prady0t@users.noreply.github.com> Date: Tue, 13 Aug 2024 02:23:57 +0530 Subject: [PATCH 55/82] Migrating unit tests to pytest (Part 5) (#4333) * Migrating unit tests to pytest (Part 5) Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * style: pre-commit fixes * using is instead of == Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * using is instead of == Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * Update tests/unit/test_discretisations/test_discretisation.py Co-authored-by: Saransh Chopra * Update tests/unit/test_discretisations/test_discretisation.py Co-authored-by: Saransh Chopra * Update tests/unit/test_discretisations/test_discretisation.py Co-authored-by: Saransh Chopra * Update tests/unit/test_discretisations/test_discretisation.py Co-authored-by: Saransh Chopra * Update tests/unit/test_batch_study.py Co-authored-by: Saransh Chopra * Update tests/unit/test_expression_tree/test_operations/test_evaluate_python.py Co-authored-by: Saransh Chopra * Adding suggestions Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> --------- Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> Co-authored-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Saransh Chopra --- tests/unit/test_batch_study.py | 40 +- tests/unit/test_callbacks.py | 57 ++- tests/unit/test_citations.py | 302 +++++++------ .../test_discretisation.py | 317 +++++++------- .../unit/test_experiments/test_experiment.py | 173 ++++---- .../test_experiments/test_experiment_steps.py | 154 ++++--- .../test_expression_tree/test_averages.py | 167 ++++---- .../test_expression_tree/test_broadcasts.py | 200 ++++----- .../test_concatenations.py | 190 ++++----- .../test_expression_tree/test_functions.py | 305 ++++++-------- .../test_expression_tree/test_interpolant.py | 85 ++-- .../test_operations/test_convert_to_casadi.py | 78 ++-- .../test_operations/test_copy.py | 170 +++----- .../test_operations/test_evaluate_python.py | 226 +++++----- .../test_operations/test_jac.py | 66 ++- .../test_operations/test_jac_2D.py | 16 +- .../test_operations/test_latexify.py | 54 +-- .../test_expression_tree/test_parameter.py | 64 ++- .../test_expression_tree/test_state_vector.py | 49 +-- .../unit/test_expression_tree/test_symbol.py | 398 +++++++++--------- 20 files changed, 1406 insertions(+), 1705 deletions(-) diff --git a/tests/unit/test_batch_study.py b/tests/unit/test_batch_study.py index 9d13d71d9d..779845df5f 100644 --- a/tests/unit/test_batch_study.py +++ b/tests/unit/test_batch_study.py @@ -2,13 +2,13 @@ Tests for the batch_study.py """ +import pytest import os import pybamm -import unittest from tempfile import TemporaryDirectory -class TestBatchStudy(unittest.TestCase): +class TestBatchStudy: def test_solve(self): spm = pybamm.lithium_ion.SPM() spm_uniform = pybamm.lithium_ion.SPM({"particle": "uniform profile"}) @@ -41,62 +41,62 @@ def test_solve(self): # Tests for exceptions for name in pybamm.BatchStudy.INPUT_LIST: - with self.assertRaises(ValueError): + with pytest.raises(ValueError): pybamm.BatchStudy( models={"SPM": spm, "SPM uniform": spm_uniform}, **{name: {None}} ) # Tests for None when only models are given with permutations=False bs_false_only_models.solve(t_eval=[0, 3600]) - self.assertEqual(2, len(bs_false_only_models.sims)) + assert len(bs_false_only_models.sims) == 2 # Tests for None when only models are given with permutations=True bs_true_only_models.solve(t_eval=[0, 3600]) - self.assertEqual(2, len(bs_true_only_models.sims)) + assert 2 == len(bs_true_only_models.sims) # Tests for BatchStudy when permutations=False bs_false.solve() bs_false.plot(show_plot=False) - self.assertEqual(2, len(bs_false.sims)) + assert len(bs_false.sims) == 2 for num in range(len(bs_false.sims)): output_model = bs_false.sims[num].model.name models_list = [model.name for model in bs_false.models.values()] - self.assertIn(output_model, models_list) + assert output_model in models_list output_solver = bs_false.sims[num].solver.name solvers_list = [solver.name for solver in bs_false.solvers.values()] - self.assertIn(output_solver, solvers_list) + assert output_solver in solvers_list output_experiment = bs_false.sims[num].experiment.steps experiments_list = [ experiment.steps for experiment in bs_false.experiments.values() ] - self.assertIn(output_experiment, experiments_list) + assert output_experiment in experiments_list # Tests for BatchStudy when permutations=True bs_true.solve() bs_true.plot(show_plot=False) - self.assertEqual(4, len(bs_true.sims)) + assert len(bs_true.sims) == 4 for num in range(len(bs_true.sims)): output_model = bs_true.sims[num].model.name models_list = [model.name for model in bs_true.models.values()] - self.assertIn(output_model, models_list) + assert output_model in models_list output_solver = bs_true.sims[num].solver.name solvers_list = [solver.name for solver in bs_true.solvers.values()] - self.assertIn(output_solver, solvers_list) + assert output_solver in solvers_list output_experiment = bs_true.sims[num].experiment.steps experiments_list = [ experiment.steps for experiment in bs_true.experiments.values() ] - self.assertIn(output_experiment, experiments_list) + assert output_experiment in experiments_list def test_create_gif(self): with TemporaryDirectory() as dir_name: bs = pybamm.BatchStudy({"spm": pybamm.lithium_ion.SPM()}) - with self.assertRaisesRegex( - ValueError, "The simulations have not been solved yet." + with pytest.raises( + ValueError, match="The simulations have not been solved yet." ): pybamm.BatchStudy( models={ @@ -117,13 +117,3 @@ def test_create_gif(self): # create a GIF after calling the plot method bs.plot(show_plot=False) bs.create_gif(number_of_images=3, duration=1, output_filename=test_file) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_callbacks.py b/tests/unit/test_callbacks.py index bc523d404f..414b2ecebe 100644 --- a/tests/unit/test_callbacks.py +++ b/tests/unit/test_callbacks.py @@ -1,9 +1,9 @@ # # Tests the citations class. # +import pytest import pybamm -import unittest import os from pybamm import callbacks @@ -18,7 +18,8 @@ def on_experiment_end(self, logs): print(self.name, file=f) -class TestCallbacks(unittest.TestCase): +class TestCallbacks: + @pytest.fixture(autouse=True) def tearDown(self): # Remove any test log files that were created, even if the test fails for logfile in ["test_callback.log", "test_callback_2.log"]: @@ -32,22 +33,22 @@ def tearDown(self): def test_setup_callbacks(self): # No callbacks, LoggingCallback should be added callbacks = pybamm.callbacks.setup_callbacks(None) - self.assertIsInstance(callbacks, pybamm.callbacks.CallbackList) - self.assertEqual(len(callbacks), 1) - self.assertIsInstance(callbacks[0], pybamm.callbacks.LoggingCallback) + assert isinstance(callbacks, pybamm.callbacks.CallbackList) + assert len(callbacks) == 1 + assert isinstance(callbacks[0], pybamm.callbacks.LoggingCallback) # Single object, transformed to list callbacks = pybamm.callbacks.setup_callbacks(1) - self.assertIsInstance(callbacks, pybamm.callbacks.CallbackList) - self.assertEqual(len(callbacks), 2) - self.assertEqual(callbacks.callbacks[0], 1) - self.assertIsInstance(callbacks[-1], pybamm.callbacks.LoggingCallback) + assert isinstance(callbacks, pybamm.callbacks.CallbackList) + assert len(callbacks) == 2 + assert callbacks.callbacks[0] == 1 + assert isinstance(callbacks[-1], pybamm.callbacks.LoggingCallback) # List callbacks = pybamm.callbacks.setup_callbacks([1, 2, 3]) - self.assertIsInstance(callbacks, pybamm.callbacks.CallbackList) - self.assertEqual(callbacks.callbacks[:3], [1, 2, 3]) - self.assertIsInstance(callbacks[-1], pybamm.callbacks.LoggingCallback) + assert isinstance(callbacks, pybamm.callbacks.CallbackList) + assert callbacks.callbacks[:3] == [1, 2, 3] + assert isinstance(callbacks[-1], pybamm.callbacks.LoggingCallback) def test_callback_list(self): "Tests multiple callbacks in a list" @@ -64,18 +65,18 @@ def test_callback_list(self): ) callback.on_experiment_end(None) with open("test_callback.log") as f: - self.assertEqual(f.read(), "first\n") + assert f.read() == "first\n" with open("test_callback_2.log") as f: - self.assertEqual(f.read(), "second\n") + assert f.read() == "second\n" def test_logging_callback(self): # No argument, should use pybamm's logger callback = pybamm.callbacks.LoggingCallback() - self.assertEqual(callback.logger, pybamm.logger) + assert callback.logger == pybamm.logger pybamm.set_logging_level("NOTICE") callback = pybamm.callbacks.LoggingCallback("test_callback.log") - self.assertEqual(callback.logfile, "test_callback.log") + assert callback.logfile == "test_callback.log" logs = { "cycle number": (5, 12), @@ -87,39 +88,29 @@ def test_logging_callback(self): } callback.on_experiment_start(logs) with open("test_callback.log") as f: - self.assertEqual(f.read(), "") + assert f.read() == "" callback.on_cycle_start(logs) with open("test_callback.log") as f: - self.assertIn("Cycle 5/12", f.read()) + assert "Cycle 5/12" in f.read() callback.on_step_start(logs) with open("test_callback.log") as f: - self.assertIn("Cycle 5/12, step 1/4", f.read()) + assert "Cycle 5/12, step 1/4" in f.read() callback.on_experiment_infeasible_event(logs) with open("test_callback.log") as f: - self.assertIn("Experiment is infeasible: 'event'", f.read()) + assert "Experiment is infeasible: 'event'" in f.read() callback.on_experiment_infeasible_time(logs) with open("test_callback.log") as f: - self.assertIn("Experiment is infeasible: default duration", f.read()) + assert "Experiment is infeasible: default duration" in f.read() callback.on_experiment_end(logs) with open("test_callback.log") as f: - self.assertIn("took 0.45", f.read()) + assert "took 0.45" in f.read() # Calling start again should clear the log callback.on_experiment_start(logs) with open("test_callback.log") as f: - self.assertEqual(f.read(), "") - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() + assert f.read() == "" diff --git a/tests/unit/test_citations.py b/tests/unit/test_citations.py index 978569d864..442bb7d50e 100644 --- a/tests/unit/test_citations.py +++ b/tests/unit/test_citations.py @@ -1,10 +1,10 @@ # # Tests the citations class. # +import pytest import pybamm import os import io -import unittest import contextlib import warnings from pybtex.database import Entry @@ -23,26 +23,26 @@ def temporary_filename(): os.remove(f.name) -class TestCitations(unittest.TestCase): +class TestCitations: def test_citations(self): citations = pybamm.citations # Default papers should be in both _all_citations dict and in the papers to cite - self.assertIn("Sulzer2021", citations._all_citations.keys()) - self.assertIn("Sulzer2021", citations._papers_to_cite) - self.assertIn("Harris2020", citations._papers_to_cite) + assert "Sulzer2021" in citations._all_citations.keys() + assert "Sulzer2021" in citations._papers_to_cite + assert "Harris2020" in citations._papers_to_cite # Non-default papers should only be in the _all_citations dict - self.assertIn("Sulzer2019physical", citations._all_citations.keys()) - self.assertNotIn("Sulzer2019physical", citations._papers_to_cite) + assert "Sulzer2019physical" in citations._all_citations.keys() + assert "Sulzer2019physical" not in citations._papers_to_cite # Register a citation that does not exist citations.register("not a citation") # Test key error - with self.assertRaises(KeyError): + with pytest.raises(KeyError): citations._parse_citation("not a citation") # this should raise key error # Test unknown citations at registration - self.assertIn("not a citation", citations._unknown_citations) + assert "not a citation" in citations._unknown_citations def test_print_citations(self): pybamm.citations._reset() @@ -51,29 +51,27 @@ def test_print_citations(self): with temporary_filename() as filename: pybamm.print_citations(filename, "text") with open(filename) as f: - self.assertTrue(len(f.readlines()) > 0) + assert len(f.readlines()) > 0 # Bibtext Style with temporary_filename() as filename: pybamm.print_citations(filename, "bibtex") with open(filename) as f: - self.assertTrue(len(f.readlines()) > 0) + assert len(f.readlines()) > 0 # Write to stdout f = io.StringIO() with contextlib.redirect_stdout(f): pybamm.print_citations() - self.assertTrue( - "Python Battery Mathematical Modelling (PyBaMM)." in f.getvalue() - ) + assert "Python Battery Mathematical Modelling (PyBaMM)." in f.getvalue() - with self.assertRaisesRegex(pybamm.OptionError, "'text' or 'bibtex'"): + with pytest.raises(pybamm.OptionError, match="'text' or 'bibtex'"): pybamm.print_citations("test_citations.txt", "bad format") # Test that unknown citation raises warning message on printing pybamm.citations._reset() pybamm.citations.register("not a citation") - with self.assertWarnsRegex(UserWarning, "not a citation"): + with pytest.warns(UserWarning, match="not a citation"): pybamm.print_citations() def test_overwrite_citation(self): @@ -82,188 +80,186 @@ def test_overwrite_citation(self): with warnings.catch_warnings(): pybamm.citations.register(fake_citation) pybamm.citations._parse_citation(fake_citation) - self.assertIn("NotACitation", pybamm.citations._papers_to_cite) + assert "NotACitation" in pybamm.citations._papers_to_cite # Same NotACitation with warnings.catch_warnings(): pybamm.citations.register(fake_citation) pybamm.citations._parse_citation(fake_citation) - self.assertIn("NotACitation", pybamm.citations._papers_to_cite) + assert "NotACitation" in pybamm.citations._papers_to_cite # Overwrite NotACitation old_citation = pybamm.citations._all_citations["NotACitation"] - with self.assertWarns(Warning): + with pytest.warns(Warning): pybamm.citations.register(r"@article{NotACitation, title = {A New Title}}") pybamm.citations._parse_citation( r"@article{NotACitation, title = {A New Title}}" ) - self.assertIn("NotACitation", pybamm.citations._papers_to_cite) - self.assertNotEqual( - pybamm.citations._all_citations["NotACitation"], old_citation - ) + assert "NotACitation" in pybamm.citations._papers_to_cite + assert pybamm.citations._all_citations["NotACitation"] != old_citation def test_input_validation(self): """Test type validation of ``_add_citation``""" pybamm.citations.register(1) - with self.assertRaises(TypeError): + with pytest.raises(TypeError): pybamm.citations._parse_citation(1) - with self.assertRaises(TypeError): + with pytest.raises(TypeError): pybamm.citations._add_citation("NotACitation", "NotAEntry") - with self.assertRaises(TypeError): + with pytest.raises(TypeError): pybamm.citations._add_citation(1001, Entry("misc")) def test_andersson_2019(self): citations = pybamm.citations citations._reset() - self.assertNotIn("Andersson2019", citations._papers_to_cite) + assert "Andersson2019" not in citations._papers_to_cite pybamm.CasadiConverter() - self.assertIn("Andersson2019", citations._papers_to_cite) + assert "Andersson2019" in citations._papers_to_cite def test_marquis_2019(self): # Test that calling relevant bits of code adds the right paper to citations citations = pybamm.citations citations._reset() - self.assertNotIn("Marquis2019", citations._papers_to_cite) + assert "Marquis2019" not in citations._papers_to_cite pybamm.lithium_ion.SPM(build=False) - self.assertIn("Marquis2019", citations._papers_to_cite) - self.assertIn("Marquis2019", citations._citation_tags.keys()) + assert "Marquis2019" in citations._papers_to_cite + assert "Marquis2019" in citations._citation_tags.keys() citations._reset() pybamm.lithium_ion.SPMe(build=False) - self.assertIn("Marquis2019", citations._papers_to_cite) + assert "Marquis2019" in citations._papers_to_cite def test_doyle_1993(self): citations = pybamm.citations citations._reset() - self.assertNotIn("Doyle1993", citations._papers_to_cite) + assert "Doyle1993" not in citations._papers_to_cite pybamm.lithium_ion.DFN(build=False) - self.assertIn("Doyle1993", citations._papers_to_cite) - self.assertIn("Doyle1993", citations._citation_tags.keys()) + assert "Doyle1993" in citations._papers_to_cite + assert "Doyle1993" in citations._citation_tags.keys() def test_sulzer_2019(self): # Test that calling relevant bits of code adds the right paper to citations citations = pybamm.citations citations._reset() - self.assertNotIn("Sulzer2019asymptotic", citations._papers_to_cite) + assert "Sulzer2019asymptotic" not in citations._papers_to_cite pybamm.lead_acid.LOQS(build=False) - self.assertIn("Sulzer2019asymptotic", citations._papers_to_cite) - self.assertIn("Sulzer2019asymptotic", citations._citation_tags.keys()) + assert "Sulzer2019asymptotic" in citations._papers_to_cite + assert "Sulzer2019asymptotic" in citations._citation_tags.keys() citations._reset() pybamm.lead_acid.Full(build=False) - self.assertIn("Sulzer2019physical", citations._papers_to_cite) - self.assertIn("Sulzer2019physical", citations._citation_tags.keys()) + assert "Sulzer2019physical" in citations._papers_to_cite + assert "Sulzer2019physical" in citations._citation_tags.keys() def test_timms_2021(self): # Test that calling relevant bits of code adds the right paper to citations citations = pybamm.citations citations._reset() - self.assertNotIn("Timms2021", citations._papers_to_cite) + assert "Timms2021" not in citations._papers_to_cite pybamm.current_collector.BasePotentialPair(param=None) - self.assertIn("Timms2021", citations._papers_to_cite) - self.assertIn("Timms2021", citations._citation_tags.keys()) + assert "Timms2021" in citations._papers_to_cite + assert "Timms2021" in citations._citation_tags.keys() citations._reset() - self.assertNotIn("Timms2021", citations._papers_to_cite) + assert "Timms2021" not in citations._papers_to_cite pybamm.current_collector.EffectiveResistance() - self.assertIn("Timms2021", citations._papers_to_cite) - self.assertIn("Timms2021", citations._citation_tags.keys()) + assert "Timms2021" in citations._papers_to_cite + assert "Timms2021" in citations._citation_tags.keys() citations._reset() - self.assertNotIn("Timms2021", citations._papers_to_cite) + assert "Timms2021" not in citations._papers_to_cite pybamm.current_collector.AlternativeEffectiveResistance2D() - self.assertIn("Timms2021", citations._papers_to_cite) - self.assertIn("Timms2021", citations._citation_tags.keys()) + assert "Timms2021" in citations._papers_to_cite + assert "Timms2021" in citations._citation_tags.keys() citations._reset() - self.assertNotIn("Timms2021", citations._papers_to_cite) + assert "Timms2021" not in citations._papers_to_cite pybamm.thermal.pouch_cell.CurrentCollector1D(param=None) - self.assertIn("Timms2021", citations._papers_to_cite) - self.assertIn("Timms2021", citations._citation_tags.keys()) + assert "Timms2021" in citations._papers_to_cite + assert "Timms2021" in citations._citation_tags.keys() citations._reset() - self.assertNotIn("Timms2021", citations._papers_to_cite) + assert "Timms2021" not in citations._papers_to_cite pybamm.thermal.pouch_cell.CurrentCollector2D(param=None) - self.assertIn("Timms2021", citations._papers_to_cite) - self.assertIn("Timms2021", citations._citation_tags.keys()) + assert "Timms2021" in citations._papers_to_cite + assert "Timms2021" in citations._citation_tags.keys() citations._reset() - self.assertNotIn("Timms2021", citations._papers_to_cite) + assert "Timms2021" not in citations._papers_to_cite pybamm.thermal.Lumped(param=None) - self.assertIn("Timms2021", citations._papers_to_cite) - self.assertIn("Timms2021", citations._citation_tags.keys()) + assert "Timms2021" in citations._papers_to_cite + assert "Timms2021" in citations._citation_tags.keys() citations._reset() - self.assertNotIn("Timms2021", citations._papers_to_cite) + assert "Timms2021" not in citations._papers_to_cite pybamm.thermal.pouch_cell.OneDimensionalX(param=None) - self.assertIn("Timms2021", citations._papers_to_cite) - self.assertIn("Timms2021", citations._citation_tags.keys()) + assert "Timms2021" in citations._papers_to_cite + assert "Timms2021" in citations._citation_tags.keys() def test_subramanian_2005(self): # Test that calling relevant bits of code adds the right paper to citations citations = pybamm.citations citations._reset() - self.assertNotIn("Subramanian2005", citations._papers_to_cite) + assert "Subramanian2005" not in citations._papers_to_cite pybamm.particle.XAveragedPolynomialProfile( None, "negative", {"particle": "quadratic profile"}, "primary" ) - self.assertIn("Subramanian2005", citations._papers_to_cite) - self.assertIn("Subramanian2005", citations._citation_tags.keys()) + assert "Subramanian2005" in citations._papers_to_cite + assert "Subramanian2005" in citations._citation_tags.keys() citations._reset() - self.assertNotIn("Subramanian2005", citations._papers_to_cite) + assert "Subramanian2005" not in citations._papers_to_cite pybamm.particle.PolynomialProfile( None, "negative", {"particle": "quadratic profile"}, "primary" ) - self.assertIn("Subramanian2005", citations._papers_to_cite) - self.assertIn("Subramanian2005", citations._citation_tags.keys()) + assert "Subramanian2005" in citations._papers_to_cite + assert "Subramanian2005" in citations._citation_tags.keys() def test_brosaplanella_2021(self): # Test that calling relevant bits of code adds the right paper to citations citations = pybamm.citations citations._reset() - self.assertNotIn("BrosaPlanella2021", citations._papers_to_cite) + assert "BrosaPlanella2021" not in citations._papers_to_cite pybamm.electrolyte_conductivity.Integrated(None) - self.assertIn("BrosaPlanella2021", citations._papers_to_cite) - self.assertIn("BrosaPlanella2021", citations._citation_tags.keys()) + assert "BrosaPlanella2021" in citations._papers_to_cite + assert "BrosaPlanella2021" in citations._citation_tags.keys() def test_brosaplanella_2022(self): # Test that calling relevant bits of code adds the right paper to citations citations = pybamm.citations citations._reset() - self.assertNotIn("BrosaPlanella2022", citations._papers_to_cite) + assert "BrosaPlanella2022" not in citations._papers_to_cite pybamm.lithium_ion.SPM(build=False, options={"SEI": "none"}) pybamm.lithium_ion.SPM(build=False, options={"SEI": "constant"}) pybamm.lithium_ion.SPMe(build=False, options={"SEI": "none"}) pybamm.lithium_ion.SPMe(build=False, options={"SEI": "constant"}) - self.assertNotIn("BrosaPlanella2022", citations._papers_to_cite) + assert "BrosaPlanella2022" not in citations._papers_to_cite pybamm.lithium_ion.SPM(build=False, options={"SEI": "ec reaction limited"}) - self.assertIn("BrosaPlanella2022", citations._papers_to_cite) + assert "BrosaPlanella2022" in citations._papers_to_cite citations._reset() pybamm.lithium_ion.SPMe(build=False, options={"SEI": "ec reaction limited"}) - self.assertIn("BrosaPlanella2022", citations._papers_to_cite) + assert "BrosaPlanella2022" in citations._papers_to_cite citations._reset() pybamm.lithium_ion.SPM(build=False, options={"lithium plating": "irreversible"}) - self.assertIn("BrosaPlanella2022", citations._papers_to_cite) + assert "BrosaPlanella2022" in citations._papers_to_cite citations._reset() pybamm.lithium_ion.SPMe( build=False, options={"lithium plating": "irreversible"} ) - self.assertIn("BrosaPlanella2022", citations._papers_to_cite) - self.assertIn("BrosaPlanella2022", citations._citation_tags.keys()) + assert "BrosaPlanella2022" in citations._papers_to_cite + assert "BrosaPlanella2022" in citations._citation_tags.keys() citations._reset() def test_newman_tobias(self): @@ -271,178 +267,168 @@ def test_newman_tobias(self): citations = pybamm.citations citations._reset() - self.assertNotIn("Newman1962", citations._papers_to_cite) - self.assertNotIn("Chu2020", citations._papers_to_cite) + assert "Newman1962" not in citations._papers_to_cite + assert "Chu2020" not in citations._papers_to_cite pybamm.lithium_ion.NewmanTobias() - self.assertIn("Newman1962", citations._papers_to_cite) - self.assertIn("Newman1962", citations._citation_tags.keys()) - self.assertIn("Chu2020", citations._papers_to_cite) - self.assertIn("Chu2020", citations._citation_tags.keys()) + assert "Newman1962" in citations._papers_to_cite + assert "Newman1962" in citations._citation_tags.keys() + assert "Chu2020" in citations._papers_to_cite + assert "Chu2020" in citations._citation_tags.keys() def test_scikit_fem(self): citations = pybamm.citations citations._reset() - self.assertNotIn("Gustafsson2020", citations._papers_to_cite) + assert "Gustafsson2020" not in citations._papers_to_cite pybamm.ScikitFiniteElement() - self.assertIn("Gustafsson2020", citations._papers_to_cite) - self.assertIn("Gustafsson2020", citations._citation_tags.keys()) + assert "Gustafsson2020" in citations._papers_to_cite + assert "Gustafsson2020" in citations._citation_tags.keys() def test_reniers_2019(self): # Test that calling relevant bits of code adds the right paper to citations citations = pybamm.citations citations._reset() - self.assertNotIn("Reniers2019", citations._papers_to_cite) + assert "Reniers2019" not in citations._papers_to_cite pybamm.active_material.LossActiveMaterial(None, "negative", None, True) - self.assertIn("Reniers2019", citations._papers_to_cite) - self.assertIn("Reniers2019", citations._citation_tags.keys()) + assert "Reniers2019" in citations._papers_to_cite + assert "Reniers2019" in citations._citation_tags.keys() def test_mohtat_2019(self): citations = pybamm.citations citations._reset() - self.assertNotIn("Mohtat2019", citations._papers_to_cite) + assert "Mohtat2019" not in citations._papers_to_cite pybamm.lithium_ion.ElectrodeSOHSolver( pybamm.ParameterValues("Marquis2019") )._get_electrode_soh_sims_full() - self.assertIn("Mohtat2019", citations._papers_to_cite) - self.assertIn("Mohtat2019", citations._citation_tags.keys()) + assert "Mohtat2019" in citations._papers_to_cite + assert "Mohtat2019" in citations._citation_tags.keys() def test_mohtat_2021(self): citations = pybamm.citations citations._reset() - self.assertNotIn("Mohtat2021", citations._papers_to_cite) + assert "Mohtat2021" not in citations._papers_to_cite pybamm.external_circuit.CCCVFunctionControl(None, None) - self.assertIn("Mohtat2021", citations._papers_to_cite) - self.assertIn("Mohtat2021", citations._citation_tags.keys()) + assert "Mohtat2021" in citations._papers_to_cite + assert "Mohtat2021" in citations._citation_tags.keys() def test_sripad_2020(self): citations = pybamm.citations citations._reset() - self.assertNotIn("Sripad2020", citations._papers_to_cite) + assert "Sripad2020" not in citations._papers_to_cite pybamm.kinetics.Marcus(None, None, None, None, None) - self.assertIn("Sripad2020", citations._papers_to_cite) - self.assertIn("Sripad2020", citations._citation_tags.keys()) + assert "Sripad2020" in citations._papers_to_cite + assert "Sripad2020" in citations._citation_tags.keys() citations._reset() - self.assertNotIn("Sripad2020", citations._papers_to_cite) + assert "Sripad2020" not in citations._papers_to_cite pybamm.kinetics.MarcusHushChidsey(None, None, None, None, None) - self.assertIn("Sripad2020", citations._papers_to_cite) - self.assertIn("Sripad2020", citations._citation_tags.keys()) + assert "Sripad2020" in citations._papers_to_cite + assert "Sripad2020" in citations._citation_tags.keys() def test_msmr(self): citations = pybamm.citations citations._reset() - self.assertNotIn("Baker2018", citations._papers_to_cite) - self.assertNotIn("Verbrugge2017", citations._papers_to_cite) + assert "Baker2018" not in citations._papers_to_cite + assert "Verbrugge2017" not in citations._papers_to_cite pybamm.particle.MSMRDiffusion(None, "negative", None, None, None) - self.assertIn("Baker2018", citations._papers_to_cite) - self.assertIn("Baker2018", citations._citation_tags.keys()) - self.assertIn("Verbrugge2017", citations._papers_to_cite) - self.assertIn("Verbrugge2017", citations._citation_tags.keys()) + assert "Baker2018" in citations._papers_to_cite + assert "Baker2018" in citations._citation_tags.keys() + assert "Verbrugge2017" in citations._papers_to_cite + assert "Verbrugge2017" in citations._citation_tags.keys() def test_thevenin(self): citations = pybamm.citations citations._reset() pybamm.equivalent_circuit.Thevenin() - self.assertNotIn("Fan2022", citations._papers_to_cite) - self.assertNotIn("Fan2022", citations._citation_tags.keys()) + assert "Fan2022" not in citations._papers_to_cite + assert "Fan2022" not in citations._citation_tags.keys() pybamm.equivalent_circuit.Thevenin(options={"diffusion element": "true"}) - self.assertIn("Fan2022", citations._papers_to_cite) - self.assertIn("Fan2022", citations._citation_tags.keys()) + assert "Fan2022" in citations._papers_to_cite + assert "Fan2022" in citations._citation_tags.keys() def test_parameter_citations(self): citations = pybamm.citations citations._reset() pybamm.ParameterValues("Chen2020") - self.assertIn("Chen2020", citations._papers_to_cite) - self.assertIn("Chen2020", citations._citation_tags.keys()) + assert "Chen2020" in citations._papers_to_cite + assert "Chen2020" in citations._citation_tags.keys() citations._reset() pybamm.ParameterValues("NCA_Kim2011") - self.assertIn("Kim2011", citations._papers_to_cite) - self.assertIn("Kim2011", citations._citation_tags.keys()) + assert "Kim2011" in citations._papers_to_cite + assert "Kim2011" in citations._citation_tags.keys() citations._reset() pybamm.ParameterValues("Marquis2019") - self.assertIn("Marquis2019", citations._papers_to_cite) - self.assertIn("Marquis2019", citations._citation_tags.keys()) + assert "Marquis2019" in citations._papers_to_cite + assert "Marquis2019" in citations._citation_tags.keys() citations._reset() pybamm.ParameterValues("Sulzer2019") - self.assertIn("Sulzer2019physical", citations._papers_to_cite) - self.assertIn("Sulzer2019physical", citations._citation_tags.keys()) + assert "Sulzer2019physical" in citations._papers_to_cite + assert "Sulzer2019physical" in citations._citation_tags.keys() citations._reset() pybamm.ParameterValues("Ecker2015") - self.assertIn("Ecker2015i", citations._papers_to_cite) - self.assertIn("Ecker2015i", citations._citation_tags.keys()) - self.assertIn("Ecker2015ii", citations._papers_to_cite) - self.assertIn("Ecker2015ii", citations._citation_tags.keys()) - self.assertIn("Zhao2018", citations._papers_to_cite) - self.assertIn("Zhao2018", citations._citation_tags.keys()) - self.assertIn("Hales2019", citations._papers_to_cite) - self.assertIn("Hales2019", citations._citation_tags.keys()) - self.assertIn("Richardson2020", citations._papers_to_cite) - self.assertIn("Richardson2020", citations._citation_tags.keys()) + assert "Ecker2015i" in citations._papers_to_cite + assert "Ecker2015i" in citations._citation_tags.keys() + assert "Ecker2015ii" in citations._papers_to_cite + assert "Ecker2015ii" in citations._citation_tags.keys() + assert "Zhao2018" in citations._papers_to_cite + assert "Zhao2018" in citations._citation_tags.keys() + assert "Hales2019" in citations._papers_to_cite + assert "Hales2019" in citations._citation_tags.keys() + assert "Richardson2020" in citations._papers_to_cite + assert "Richardson2020" in citations._citation_tags.keys() citations._reset() pybamm.ParameterValues("ORegan2022") - self.assertIn("ORegan2022", citations._papers_to_cite) - self.assertIn("ORegan2022", citations._citation_tags.keys()) + assert "ORegan2022" in citations._papers_to_cite + assert "ORegan2022" in citations._citation_tags.keys() citations._reset() pybamm.ParameterValues("MSMR_Example") - self.assertIn("Baker2018", citations._papers_to_cite) - self.assertIn("Baker2018", citations._citation_tags.keys()) - self.assertIn("Verbrugge2017", citations._papers_to_cite) - self.assertIn("Verbrugge2017", citations._citation_tags.keys()) + assert "Baker2018" in citations._papers_to_cite + assert "Baker2018" in citations._citation_tags.keys() + assert "Verbrugge2017" in citations._papers_to_cite + assert "Verbrugge2017" in citations._citation_tags.keys() def test_solver_citations(self): # Test that solving each solver adds the right citations citations = pybamm.citations citations._reset() - self.assertNotIn("Virtanen2020", citations._papers_to_cite) + assert "Virtanen2020" not in citations._papers_to_cite pybamm.ScipySolver() - self.assertIn("Virtanen2020", citations._papers_to_cite) - self.assertIn("Virtanen2020", citations._citation_tags.keys()) + assert "Virtanen2020" in citations._papers_to_cite + assert "Virtanen2020" in citations._citation_tags.keys() citations._reset() - self.assertNotIn("Virtanen2020", citations._papers_to_cite) + assert "Virtanen2020" not in citations._papers_to_cite pybamm.AlgebraicSolver() - self.assertIn("Virtanen2020", citations._papers_to_cite) - self.assertIn("Virtanen2020", citations._citation_tags.keys()) + assert "Virtanen2020" in citations._papers_to_cite + assert "Virtanen2020" in citations._citation_tags.keys() if pybamm.have_idaklu(): citations._reset() - self.assertNotIn("Hindmarsh2005", citations._papers_to_cite) + assert "Hindmarsh2005" not in citations._papers_to_cite pybamm.IDAKLUSolver() - self.assertIn("Hindmarsh2005", citations._papers_to_cite) - self.assertIn("Hindmarsh2005", citations._citation_tags.keys()) + assert "Hindmarsh2005" in citations._papers_to_cite + assert "Hindmarsh2005" in citations._citation_tags.keys() - @unittest.skipIf(not pybamm.have_jax(), "jax or jaxlib is not installed") + @pytest.mark.skipif(not pybamm.have_jax(), reason="jax or jaxlib is not installed") def test_jax_citations(self): citations = pybamm.citations citations._reset() - self.assertNotIn("jax2018", citations._papers_to_cite) + assert "jax2018" not in citations._papers_to_cite pybamm.JaxSolver() - self.assertIn("jax2018", citations._papers_to_cite) - self.assertIn("jax2018", citations._citation_tags.keys()) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() + assert "jax2018" in citations._papers_to_cite + assert "jax2018" in citations._citation_tags.keys() diff --git a/tests/unit/test_discretisations/test_discretisation.py b/tests/unit/test_discretisations/test_discretisation.py index bf698dd39c..78b6cc0d59 100644 --- a/tests/unit/test_discretisations/test_discretisation.py +++ b/tests/unit/test_discretisations/test_discretisation.py @@ -2,10 +2,10 @@ # Tests for the base model class # +import pytest import pybamm import numpy as np -import unittest from tests import ( get_mesh_for_testing, get_discretisation_for_testing, @@ -18,7 +18,7 @@ from scipy.sparse.linalg import inv -class TestDiscretise(unittest.TestCase): +class TestDiscretise: def test_concatenate_in_order(self): a = pybamm.Variable("a") b = pybamm.Variable("b") @@ -36,16 +36,16 @@ def test_concatenate_in_order(self): disc.y_slices = {c: [slice(0, 1)], a: [slice(2, 3)], b: [slice(3, 4)]} result = disc._concatenate_in_order(initial_conditions) - self.assertIsInstance(result, pybamm.Vector) + assert isinstance(result, pybamm.Vector) np.testing.assert_array_equal(result.evaluate(), [[1], [2], [3]]) initial_conditions = {a: pybamm.Scalar(2), b: pybamm.Scalar(3)} - with self.assertRaises(pybamm.ModelError): + with pytest.raises(pybamm.ModelError): result = disc._concatenate_in_order(initial_conditions, check_complete=True) def test_no_mesh(self): disc = pybamm.Discretisation(None, None) - self.assertEqual(disc._spatial_methods, {}) + assert disc._spatial_methods == {} def test_add_internal_boundary_conditions(self): model = pybamm.BaseModel() @@ -66,7 +66,7 @@ def test_add_internal_boundary_conditions(self): disc.set_internal_boundary_conditions(model) for child in c_e.children: - self.assertTrue(child in disc.bcs.keys()) + assert child in disc.bcs.keys() def test_discretise_slicing(self): # create discretisation @@ -79,7 +79,7 @@ def test_discretise_slicing(self): variables = [c] disc.set_variable_slices(variables) - self.assertEqual(disc.y_slices, {c: [slice(0, 100)]}) + assert disc.y_slices == {c: [slice(0, 100)]} submesh = mesh[whole_cell] @@ -93,10 +93,11 @@ def test_discretise_slicing(self): variables = [c, d, jn] disc.set_variable_slices(variables) - self.assertEqual( - disc.y_slices, - {c: [slice(0, 100)], d: [slice(100, 200)], jn: [slice(200, 240)]}, - ) + assert disc.y_slices == { + c: [slice(0, 100)], + d: [slice(100, 200)], + jn: [slice(200, 240)], + } np.testing.assert_array_equal( disc.bounds[0], [-np.inf] * 100 + [0] * 100 + [-np.inf] * 40 ) @@ -116,17 +117,14 @@ def test_discretise_slicing(self): j = pybamm.concatenation(jn, js, jp) variables = [c, d, j] disc.set_variable_slices(variables) - self.assertEqual( - disc.y_slices, - { - c: [slice(0, 100)], - d: [slice(100, 200)], - jn: [slice(200, 240)], - js: [slice(240, 265)], - jp: [slice(265, 300)], - j: [slice(200, 300)], - }, - ) + assert disc.y_slices == { + c: [slice(0, 100)], + d: [slice(100, 200)], + jn: [slice(200, 240)], + js: [slice(240, 265)], + jp: [slice(265, 300)], + j: [slice(200, 300)], + } np.testing.assert_array_equal( disc.bounds[0], [-np.inf] * 100 + [0] * 100 + [-np.inf] * 100 ) @@ -140,7 +138,7 @@ def test_discretise_slicing(self): np.testing.assert_array_equal(y[disc.y_slices[d][0]], d_true) np.testing.assert_array_equal(y[disc.y_slices[jn][0]], jn_true) - with self.assertRaisesRegex(TypeError, "y_slices should be"): + with pytest.raises(TypeError, match="y_slices should be"): disc.y_slices = 1 # bounds with an InputParameter @@ -167,46 +165,46 @@ def test_process_symbol_base(self): var_vec = pybamm.Variable("var vec", domain=["negative electrode"]) disc.y_slices = {var: [slice(53)], var_vec: [slice(53, 93)]} var_disc = disc.process_symbol(var) - self.assertIsInstance(var_disc, pybamm.StateVector) - self.assertEqual(var_disc.y_slices[0], disc.y_slices[var][0]) + assert isinstance(var_disc, pybamm.StateVector) + assert var_disc.y_slices[0] == disc.y_slices[var][0] # variable dot var_dot = pybamm.VariableDot("var'") var_dot_disc = disc.process_symbol(var_dot) - self.assertIsInstance(var_dot_disc, pybamm.StateVectorDot) - self.assertEqual(var_dot_disc.y_slices[0], disc.y_slices[var][0]) + assert isinstance(var_dot_disc, pybamm.StateVectorDot) + assert var_dot_disc.y_slices[0] == disc.y_slices[var][0] # scalar scal = pybamm.Scalar(5) scal_disc = disc.process_symbol(scal) - self.assertIsInstance(scal_disc, pybamm.Scalar) - self.assertEqual(scal_disc.value, scal.value) + assert isinstance(scal_disc, pybamm.Scalar) + assert scal_disc.value == scal.value # vector vec = pybamm.Vector([1, 2, 3, 4]) vec_disc = disc.process_symbol(vec) - self.assertIsInstance(vec_disc, pybamm.Vector) + assert isinstance(vec_disc, pybamm.Vector) np.testing.assert_array_equal(vec_disc.entries, vec.entries) # matrix mat = pybamm.Matrix([[1, 2, 3, 4], [5, 6, 7, 8]]) mat_disc = disc.process_symbol(mat) - self.assertIsInstance(mat_disc, pybamm.Matrix) + assert isinstance(mat_disc, pybamm.Matrix) np.testing.assert_array_equal(mat_disc.entries, mat.entries) # binary operator binary = var + scal binary_disc = disc.process_symbol(binary) - self.assertEqual(binary_disc, 5 + pybamm.StateVector(slice(0, 53))) + assert binary_disc == 5 + pybamm.StateVector(slice(0, 53)) # non-spatial unary operator un1 = -var un1_disc = disc.process_symbol(un1) - self.assertIsInstance(un1_disc, pybamm.Negate) - self.assertIsInstance(un1_disc.children[0], pybamm.StateVector) + assert isinstance(un1_disc, pybamm.Negate) + assert isinstance(un1_disc.children[0], pybamm.StateVector) un2 = abs(var) un2_disc = disc.process_symbol(un2) - self.assertIsInstance(un2_disc, pybamm.AbsoluteValue) - self.assertIsInstance(un2_disc.children[0], pybamm.StateVector) + assert isinstance(un2_disc, pybamm.AbsoluteValue) + assert isinstance(un2_disc.children[0], pybamm.StateVector) # function of one variable def myfun(x): @@ -214,13 +212,13 @@ def myfun(x): func = pybamm.Function(myfun, var) func_disc = disc.process_symbol(func) - self.assertIsInstance(func_disc, pybamm.Function) - self.assertIsInstance(func_disc.children[0], pybamm.StateVector) + assert isinstance(func_disc, pybamm.Function) + assert isinstance(func_disc.children[0], pybamm.StateVector) # function of a scalar gets simplified func = pybamm.Function(myfun, scal) func_disc = disc.process_symbol(func) - self.assertIsInstance(func_disc, pybamm.Scalar) + assert isinstance(func_disc, pybamm.Scalar) # function of multiple variables def myfun(x, y): @@ -228,25 +226,25 @@ def myfun(x, y): func = pybamm.Function(myfun, var, scal) func_disc = disc.process_symbol(func) - self.assertIsInstance(func_disc, pybamm.Function) - self.assertIsInstance(func_disc.children[0], pybamm.StateVector) - self.assertIsInstance(func_disc.children[1], pybamm.Scalar) + assert isinstance(func_disc, pybamm.Function) + assert isinstance(func_disc.children[0], pybamm.StateVector) + assert isinstance(func_disc.children[1], pybamm.Scalar) # boundary value bv_left = pybamm.BoundaryValue(var_vec, "left") bv_left_disc = disc.process_symbol(bv_left) - self.assertIsInstance(bv_left_disc, pybamm.MatrixMultiplication) - self.assertIsInstance(bv_left_disc.left, pybamm.Matrix) - self.assertIsInstance(bv_left_disc.right, pybamm.StateVector) + assert isinstance(bv_left_disc, pybamm.MatrixMultiplication) + assert isinstance(bv_left_disc.left, pybamm.Matrix) + assert isinstance(bv_left_disc.right, pybamm.StateVector) bv_right = pybamm.BoundaryValue(var_vec, "left") bv_right_disc = disc.process_symbol(bv_right) - self.assertIsInstance(bv_right_disc, pybamm.MatrixMultiplication) - self.assertIsInstance(bv_right_disc.left, pybamm.Matrix) - self.assertIsInstance(bv_right_disc.right, pybamm.StateVector) + assert isinstance(bv_right_disc, pybamm.MatrixMultiplication) + assert isinstance(bv_right_disc.left, pybamm.Matrix) + assert isinstance(bv_right_disc.right, pybamm.StateVector) # not implemented sym = pybamm.Symbol("sym") - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): disc.process_symbol(sym) def test_process_complex_expression(self): @@ -259,13 +257,9 @@ def test_process_complex_expression(self): disc.y_slices = {var1: [slice(53)], var2: [slice(53, 106)]} exp_disc = disc.process_symbol(expression) - self.assertEqual( - exp_disc, - (5.0 * (3.0 ** pybamm.StateVector(slice(53, 106)))) - / ( - (-4.0 + pybamm.StateVector(slice(0, 53))) - + pybamm.StateVector(slice(53, 106)) - ), + assert exp_disc == (5.0 * (3.0 ** pybamm.StateVector(slice(53, 106)))) / ( + (-4.0 + pybamm.StateVector(slice(0, 53))) + + pybamm.StateVector(slice(53, 106)) ) def test_discretise_spatial_operator(self): @@ -281,8 +275,8 @@ def test_discretise_spatial_operator(self): for eqn in [pybamm.grad(var), pybamm.div(pybamm.grad(var))]: eqn_disc = disc.process_symbol(eqn) - self.assertIsInstance(eqn_disc, pybamm.MatrixMultiplication) - self.assertIsInstance(eqn_disc.children[0], pybamm.Matrix) + assert isinstance(eqn_disc, pybamm.MatrixMultiplication) + assert isinstance(eqn_disc.children[0], pybamm.Matrix) submesh = mesh[whole_cell] y = submesh.nodes**2 @@ -296,10 +290,10 @@ def test_discretise_spatial_operator(self): for eqn in [var * pybamm.grad(var), var * pybamm.div(pybamm.grad(var))]: eqn_disc = disc.process_symbol(eqn) - self.assertIsInstance(eqn_disc, pybamm.Multiplication) - self.assertIsInstance(eqn_disc.children[0], pybamm.StateVector) - self.assertIsInstance(eqn_disc.children[1], pybamm.MatrixMultiplication) - self.assertIsInstance(eqn_disc.children[1].children[0], pybamm.Matrix) + assert isinstance(eqn_disc, pybamm.Multiplication) + assert isinstance(eqn_disc.children[0], pybamm.StateVector) + assert isinstance(eqn_disc.children[1], pybamm.MatrixMultiplication) + assert isinstance(eqn_disc.children[1].children[0], pybamm.Matrix) y = submesh.nodes**2 var_disc = disc.process_symbol(var) @@ -325,8 +319,8 @@ def test_discretise_average(self): disc.set_variable_slices([var]) var_av_proc = disc.process_symbol(var_av) - self.assertIsInstance(var_av_proc, pybamm.MatrixMultiplication) - self.assertIsInstance(var_av_proc.right.right, pybamm.StateVector) + assert isinstance(var_av_proc, pybamm.MatrixMultiplication) + assert isinstance(var_av_proc.right.right, pybamm.StateVector) def test_process_dict(self): # one equation @@ -503,7 +497,7 @@ def test_process_model_ode(self): np.testing.assert_array_equal(np.eye(np.size(y0)), jacobian.toarray()) # test that discretising again gives an error - with self.assertRaisesRegex(pybamm.ModelError, "Cannot re-discretise a model"): + with pytest.raises(pybamm.ModelError, match="Cannot re-discretise a model"): disc.process_model(model) # test that not enough initial conditions raises an error @@ -512,7 +506,7 @@ def test_process_model_ode(self): model.initial_conditions = {T: pybamm.Scalar(5), S: pybamm.Scalar(8)} model.boundary_conditions = {} model.variables = {"ST": S * T} - with self.assertRaises(pybamm.ModelError): + with pytest.raises(pybamm.ModelError): disc.process_model(model) # test that any time derivatives of variables in rhs raises an @@ -534,7 +528,7 @@ def test_process_model_ode(self): S: {"left": (0, "Neumann"), "right": (0, "Neumann")}, } model.variables = {"ST": S * T} - with self.assertRaises(pybamm.ModelError): + with pytest.raises(pybamm.ModelError): disc.process_model(model) def test_process_model_fail(self): @@ -550,7 +544,7 @@ def test_process_model_fail(self): # turn debug mode off to not check well posedness debug_mode = pybamm.settings.debug_mode pybamm.settings.debug_mode = False - with self.assertRaisesRegex(pybamm.ModelError, "No key set for variable"): + with pytest.raises(pybamm.ModelError, match="No key set for variable"): disc.process_model(model) pybamm.settings.debug_mode = debug_mode @@ -645,7 +639,7 @@ def test_process_model_dae(self): } model.variables = {"c": c, "N": N, "d": d} - with self.assertRaises(pybamm.ModelError): + with pytest.raises(pybamm.ModelError): disc.process_model(model) def test_process_model_algebraic(self): @@ -765,15 +759,15 @@ def test_initial_condition_bounds(self): model.initial_conditions = {c: pybamm.Scalar(3)} disc = pybamm.Discretisation() - with self.assertRaisesRegex( - pybamm.ModelError, "initial condition is outside of variable bounds" + with pytest.raises( + pybamm.ModelError, match="initial condition is outside of variable bounds" ): disc.process_model(model) def test_process_empty_model(self): model = pybamm.BaseModel() disc = pybamm.Discretisation() - with self.assertRaisesRegex(pybamm.ModelError, "Cannot discretise empty model"): + with pytest.raises(pybamm.ModelError, match="Cannot discretise empty model"): disc.process_model(model) def test_broadcast(self): @@ -794,20 +788,20 @@ def test_broadcast(self): broad.evaluate(inputs={"a": 7}), 7 * np.ones_like(submesh.nodes[:, np.newaxis]), ) - self.assertEqual(broad.domain, whole_cell) + assert broad.domain == whole_cell broad_disc = disc.process_symbol(broad) - self.assertIsInstance(broad_disc, pybamm.Multiplication) - self.assertIsInstance(broad_disc.children[0], pybamm.Vector) - self.assertIsInstance(broad_disc.children[1], pybamm.InputParameter) + assert isinstance(broad_disc, pybamm.Multiplication) + assert isinstance(broad_disc.children[0], pybamm.Vector) + assert isinstance(broad_disc.children[1], pybamm.InputParameter) # process Broadcast variable disc.y_slices = {var: [slice(1)]} broad1 = pybamm.FullBroadcast(var, ["negative electrode"], None) broad1_disc = disc.process_symbol(broad1) - self.assertIsInstance(broad1_disc, pybamm.Multiplication) - self.assertIsInstance(broad1_disc.children[0], pybamm.Vector) - self.assertIsInstance(broad1_disc.children[1], pybamm.StateVector) + assert isinstance(broad1_disc, pybamm.Multiplication) + assert isinstance(broad1_disc.children[0], pybamm.Vector) + assert isinstance(broad1_disc.children[1], pybamm.StateVector) # broadcast to edges broad_to_edges = pybamm.FullBroadcastToEdges(a, ["negative electrode"], None) @@ -826,12 +820,12 @@ def test_broadcast_2D(self): disc.set_variable_slices([var]) broad_disc = disc.process_symbol(broad) - self.assertIsInstance(broad_disc, pybamm.MatrixMultiplication) - self.assertIsInstance(broad_disc.children[0], pybamm.Matrix) - self.assertIsInstance(broad_disc.children[1], pybamm.StateVector) - self.assertEqual( - broad_disc.shape, - (mesh["separator"].npts * mesh["current collector"].npts, 1), + assert isinstance(broad_disc, pybamm.MatrixMultiplication) + assert isinstance(broad_disc.children[0], pybamm.Matrix) + assert isinstance(broad_disc.children[1], pybamm.StateVector) + assert broad_disc.shape == ( + mesh["separator"].npts * mesh["current collector"].npts, + 1, ) y_test = np.linspace(0, 1, mesh["current collector"].npts) np.testing.assert_array_equal( @@ -842,12 +836,12 @@ def test_broadcast_2D(self): # test broadcast to edges broad_to_edges = pybamm.PrimaryBroadcastToEdges(var, "separator") broad_to_edges_disc = disc.process_symbol(broad_to_edges) - self.assertIsInstance(broad_to_edges_disc, pybamm.MatrixMultiplication) - self.assertIsInstance(broad_to_edges_disc.children[0], pybamm.Matrix) - self.assertIsInstance(broad_to_edges_disc.children[1], pybamm.StateVector) - self.assertEqual( - broad_to_edges_disc.shape, - ((mesh["separator"].npts + 1) * mesh["current collector"].npts, 1), + assert isinstance(broad_to_edges_disc, pybamm.MatrixMultiplication) + assert isinstance(broad_to_edges_disc.children[0], pybamm.Matrix) + assert isinstance(broad_to_edges_disc.children[1], pybamm.StateVector) + assert broad_to_edges_disc.shape == ( + (mesh["separator"].npts + 1) * mesh["current collector"].npts, + 1, ) y_test = np.linspace(0, 1, mesh["current collector"].npts) np.testing.assert_array_equal( @@ -864,12 +858,12 @@ def test_secondary_broadcast_2D(self): disc.set_variable_slices([var]) broad_disc = disc.process_symbol(broad) - self.assertIsInstance(broad_disc, pybamm.MatrixMultiplication) - self.assertIsInstance(broad_disc.children[0], pybamm.Matrix) - self.assertIsInstance(broad_disc.children[1], pybamm.StateVector) - self.assertEqual( - broad_disc.shape, - (mesh["negative particle"].npts * mesh["negative electrode"].npts, 1), + assert isinstance(broad_disc, pybamm.MatrixMultiplication) + assert isinstance(broad_disc.children[0], pybamm.Matrix) + assert isinstance(broad_disc.children[1], pybamm.StateVector) + assert broad_disc.shape == ( + mesh["negative particle"].npts * mesh["negative electrode"].npts, + 1, ) broad = pybamm.SecondaryBroadcast(var, "negative electrode") @@ -877,12 +871,12 @@ def test_secondary_broadcast_2D(self): broad_to_edges = pybamm.SecondaryBroadcastToEdges(var, "negative electrode") disc.set_variable_slices([var]) broad_to_edges_disc = disc.process_symbol(broad_to_edges) - self.assertIsInstance(broad_to_edges_disc, pybamm.MatrixMultiplication) - self.assertIsInstance(broad_to_edges_disc.children[0], pybamm.Matrix) - self.assertIsInstance(broad_to_edges_disc.children[1], pybamm.StateVector) - self.assertEqual( - broad_to_edges_disc.shape, - (mesh["negative particle"].npts * (mesh["negative electrode"].npts + 1), 1), + assert isinstance(broad_to_edges_disc, pybamm.MatrixMultiplication) + assert isinstance(broad_to_edges_disc.children[0], pybamm.Matrix) + assert isinstance(broad_to_edges_disc.children[1], pybamm.StateVector) + assert broad_to_edges_disc.shape == ( + mesh["negative particle"].npts * (mesh["negative electrode"].npts + 1), + 1, ) def test_tertiary_broadcast_3D(self): @@ -897,34 +891,28 @@ def test_tertiary_broadcast_3D(self): disc.set_variable_slices([var]) broad_disc = disc.process_symbol(broad) - self.assertIsInstance(broad_disc, pybamm.MatrixMultiplication) - self.assertIsInstance(broad_disc.children[0], pybamm.Matrix) - self.assertIsInstance(broad_disc.children[1], pybamm.StateVector) - self.assertEqual( - broad_disc.shape, - ( - mesh["negative particle"].npts - * mesh["negative electrode"].npts - * mesh["current collector"].npts, - 1, - ), + assert isinstance(broad_disc, pybamm.MatrixMultiplication) + assert isinstance(broad_disc.children[0], pybamm.Matrix) + assert isinstance(broad_disc.children[1], pybamm.StateVector) + assert broad_disc.shape == ( + mesh["negative particle"].npts + * mesh["negative electrode"].npts + * mesh["current collector"].npts, + 1, ) # test broadcast to edges broad_to_edges = pybamm.TertiaryBroadcastToEdges(var, "current collector") disc.set_variable_slices([var]) broad_to_edges_disc = disc.process_symbol(broad_to_edges) - self.assertIsInstance(broad_to_edges_disc, pybamm.MatrixMultiplication) - self.assertIsInstance(broad_to_edges_disc.children[0], pybamm.Matrix) - self.assertIsInstance(broad_to_edges_disc.children[1], pybamm.StateVector) - self.assertEqual( - broad_to_edges_disc.shape, - ( - mesh["negative particle"].npts - * mesh["negative electrode"].npts - * (mesh["current collector"].npts + 1), - 1, - ), + assert isinstance(broad_to_edges_disc, pybamm.MatrixMultiplication) + assert isinstance(broad_to_edges_disc.children[0], pybamm.Matrix) + assert isinstance(broad_to_edges_disc.children[1], pybamm.StateVector) + assert broad_to_edges_disc.shape == ( + mesh["negative particle"].npts + * mesh["negative electrode"].npts + * (mesh["current collector"].npts + 1), + 1, ) def test_concatenation(self): @@ -936,7 +924,7 @@ def test_concatenation(self): disc = get_discretisation_for_testing() conc = disc.concatenate(a, b, c) - self.assertIsInstance(conc, pybamm.NumpyConcatenation) + assert isinstance(conc, pybamm.NumpyConcatenation) def test_concatenation_of_scalars(self): whole_cell = ["negative electrode", "separator", "positive electrode"] @@ -985,21 +973,15 @@ def test_concatenation_2D(self): # With simplification conc = pybamm.concatenation(a, b, c) disc.set_variable_slices([conc]) - self.assertEqual( - disc.y_slices[a], [slice(0, 40), slice(100, 140), slice(200, 240)] - ) - self.assertEqual( - disc.y_slices[b], [slice(40, 65), slice(140, 165), slice(240, 265)] - ) - self.assertEqual( - disc.y_slices[c], [slice(65, 100), slice(165, 200), slice(265, 300)] - ) + assert disc.y_slices[a] == [slice(0, 40), slice(100, 140), slice(200, 240)] + assert disc.y_slices[b] == [slice(40, 65), slice(140, 165), slice(240, 265)] + assert disc.y_slices[c] == [slice(65, 100), slice(165, 200), slice(265, 300)] expr = disc.process_symbol(conc) - self.assertIsInstance(expr, pybamm.StateVector) + assert isinstance(expr, pybamm.StateVector) # Evaulate y = np.linspace(0, 1, 300) - self.assertEqual(expr.evaluate(0, y).shape, (120 + 75 + 105, 1)) + assert expr.evaluate(0, y).shape == (120 + 75 + 105, 1) np.testing.assert_equal(expr.evaluate(0, y), y[:, np.newaxis]) # Without simplification @@ -1007,13 +989,13 @@ def test_concatenation_2D(self): conc.bounds = (-np.inf, np.inf) disc.set_variable_slices([a, b, c]) expr = disc.process_symbol(conc) - self.assertIsInstance(expr, pybamm.DomainConcatenation) + assert isinstance(expr, pybamm.DomainConcatenation) # Evaulate y = np.linspace(0, 1, 300) - self.assertEqual(expr.children[0].evaluate(0, y).shape, (120, 1)) - self.assertEqual(expr.children[1].evaluate(0, y).shape, (75, 1)) - self.assertEqual(expr.children[2].evaluate(0, y).shape, (105, 1)) + assert expr.children[0].evaluate(0, y).shape == (120, 1) + assert expr.children[1].evaluate(0, y).shape == (75, 1) + assert expr.children[2].evaluate(0, y).shape == (105, 1) def test_exceptions(self): c_n = pybamm.Variable("c", domain=["negative electrode"]) @@ -1032,7 +1014,7 @@ def test_exceptions(self): # check raises error if different sized key and output var model.variables = {c_n.name: c_s} - with self.assertRaisesRegex(pybamm.ModelError, "variable and its eqn"): + with pytest.raises(pybamm.ModelError, match="variable and its eqn"): disc.process_model(model) # check doesn't raise if concatenation @@ -1054,8 +1036,8 @@ def test_exceptions(self): "negative particle": pybamm.FiniteVolume(), "positive particle": pybamm.ZeroDimensionalSpatialMethod(), } - with self.assertRaisesRegex( - pybamm.DiscretisationError, "Zero-dimensional spatial method for the " + with pytest.raises( + pybamm.DiscretisationError, match="Zero-dimensional spatial method for the " ): pybamm.Discretisation(mesh, spatial_methods) @@ -1068,10 +1050,10 @@ def test_check_tab_bcs_error(self): # for 0D bcs keys should be unchanged new_bcs = disc.check_tab_conditions(a, bcs) - self.assertListEqual(list(bcs.keys()), list(new_bcs.keys())) + assert list(bcs.keys()) == list(new_bcs.keys()) # error if domain not "current collector" - with self.assertRaisesRegex(pybamm.ModelError, "Boundary conditions"): + with pytest.raises(pybamm.ModelError, match="Boundary conditions"): disc.check_tab_conditions(b, bcs) def test_mass_matrix_inverse(self): @@ -1114,19 +1096,19 @@ def test_process_input_variable(self): a = pybamm.InputParameter("a") a_disc = disc.process_symbol(a) - self.assertEqual(a_disc._expected_size, 1) + assert a_disc._expected_size == 1 a = pybamm.InputParameter("a", ["negative electrode", "separator"]) a_disc = disc.process_symbol(a) n = disc.mesh[a.domain].npts - self.assertEqual(a_disc._expected_size, n) + assert a_disc._expected_size == n def test_process_not_constant(self): disc = pybamm.Discretisation() a = pybamm.NotConstant(pybamm.Scalar(1)) - self.assertEqual(disc.process_symbol(a), pybamm.Scalar(1)) - self.assertEqual(disc.process_symbol(2 * a), pybamm.Scalar(2)) + assert disc.process_symbol(a) == pybamm.Scalar(1) + assert disc.process_symbol(2 * a) == pybamm.Scalar(2) def test_bc_symmetry(self): # define model @@ -1163,7 +1145,7 @@ def test_bc_symmetry(self): # discretise disc = pybamm.Discretisation(mesh, spatial_methods) - with self.assertRaisesRegex(pybamm.ModelError, "Boundary condition at r = 0"): + with pytest.raises(pybamm.ModelError, match="Boundary condition at r = 0"): disc.process_model(model) # boundary conditions (non-homog Neumann) @@ -1175,7 +1157,7 @@ def test_bc_symmetry(self): # discretise disc = pybamm.Discretisation(mesh, spatial_methods) - with self.assertRaisesRegex(pybamm.ModelError, "Boundary condition at r = 0"): + with pytest.raises(pybamm.ModelError, match="Boundary condition at r = 0"): disc.process_model(model) def test_check_model_errors(self): @@ -1184,20 +1166,21 @@ def test_check_model_errors(self): var = pybamm.Variable("var") model.rhs = {var: pybamm.Vector([1, 1])} model.initial_conditions = {var: 1} - with self.assertRaisesRegex( - pybamm.ModelError, "initial conditions must be numpy array" + with pytest.raises( + pybamm.ModelError, match="initial conditions must be numpy array" ): disc.check_model(model) model.initial_conditions = {var: pybamm.Vector([1, 1, 1])} - with self.assertRaisesRegex( - pybamm.ModelError, "rhs and initial conditions must have the same shape" + with pytest.raises( + pybamm.ModelError, + match="rhs and initial conditions must have the same shape", ): disc.check_model(model) model.rhs = {} model.algebraic = {var: pybamm.Vector([1, 1])} - with self.assertRaisesRegex( + with pytest.raises( pybamm.ModelError, - "algebraic and initial conditions must have the same shape", + match="algebraic and initial conditions must have the same shape", ): disc.check_model(model) @@ -1228,8 +1211,8 @@ def test_independent_rhs(self): mesh, spatial_methods, remove_independent_variables_from_rhs=True ) disc.process_model(model) - self.assertEqual(len(model.rhs), 2) - self.assertEqual(model.variables["a"], model.variables["a again"]) + assert len(model.rhs) == 2 + assert model.variables["a"] == model.variables["a again"] def test_independent_rhs_one_equation(self): # Test that if there is only one equation, it is not removed @@ -1239,7 +1222,7 @@ def test_independent_rhs_one_equation(self): model.initial_conditions = {a: 0} disc = pybamm.Discretisation(remove_independent_variables_from_rhs=True) disc.process_model(model) - self.assertEqual(len(model.rhs), 1) + assert len(model.rhs) == 1 def test_independent_rhs_with_event(self): a = pybamm.Variable("a") @@ -1255,14 +1238,4 @@ def test_independent_rhs_with_event(self): model.events = [pybamm.Event("a=1", a - 1)] disc = pybamm.Discretisation(remove_independent_variables_from_rhs=True) disc.process_model(model) - self.assertEqual(len(model.rhs), 3) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() + assert len(model.rhs) == 3 diff --git a/tests/unit/test_experiments/test_experiment.py b/tests/unit/test_experiments/test_experiment.py index 87672ee0aa..e445b05e31 100644 --- a/tests/unit/test_experiments/test_experiment.py +++ b/tests/unit/test_experiments/test_experiment.py @@ -4,10 +4,10 @@ from datetime import datetime import pybamm -import unittest +import pytest -class TestExperiment(unittest.TestCase): +class TestExperiment: def test_cycle_unpacking(self): experiment = pybamm.Experiment( [ @@ -16,123 +16,120 @@ def test_cycle_unpacking(self): "Charge at C/5 for 45 minutes", ] ) - self.assertEqual( - [step.to_dict() for step in experiment.steps], - [ - { - "value": 0.05, - "type": "CRate", - "duration": 1800.0, - "period": 60.0, - "temperature": None, - "description": "Discharge at C/20 for 0.5 hours", - "termination": [], - "tags": [], - "start_time": None, - }, - { - "value": -0.2, - "type": "CRate", - "duration": 2700.0, - "period": 60.0, - "temperature": None, - "description": "Charge at C/5 for 45 minutes", - "termination": [], - "tags": [], - "start_time": None, - }, - { - "value": 0.05, - "type": "CRate", - "duration": 1800.0, - "period": 60.0, - "temperature": None, - "description": "Discharge at C/20 for 0.5 hours", - "termination": [], - "tags": [], - "start_time": None, - }, - { - "value": -0.2, - "type": "CRate", - "duration": 2700.0, - "period": 60.0, - "temperature": None, - "description": "Charge at C/5 for 45 minutes", - "termination": [], - "tags": [], - "start_time": None, - }, - ], - ) - self.assertEqual(experiment.cycle_lengths, [2, 1, 1]) + assert [step.to_dict() for step in experiment.steps] == [ + { + "value": 0.05, + "type": "CRate", + "duration": 1800.0, + "period": 60.0, + "temperature": None, + "description": "Discharge at C/20 for 0.5 hours", + "termination": [], + "tags": [], + "start_time": None, + }, + { + "value": -0.2, + "type": "CRate", + "duration": 2700.0, + "period": 60.0, + "temperature": None, + "description": "Charge at C/5 for 45 minutes", + "termination": [], + "tags": [], + "start_time": None, + }, + { + "value": 0.05, + "type": "CRate", + "duration": 1800.0, + "period": 60.0, + "temperature": None, + "description": "Discharge at C/20 for 0.5 hours", + "termination": [], + "tags": [], + "start_time": None, + }, + { + "value": -0.2, + "type": "CRate", + "duration": 2700.0, + "period": 60.0, + "temperature": None, + "description": "Charge at C/5 for 45 minutes", + "termination": [], + "tags": [], + "start_time": None, + }, + ] + assert experiment.cycle_lengths == [2, 1, 1] def test_invalid_step_type(self): unprocessed = {1.0} period = 1 temperature = 300.0 - with self.assertRaisesRegex( - TypeError, "Operating conditions must be a Step object or string." + with pytest.raises( + TypeError, match="Operating conditions must be a Step object or string." ): pybamm.Experiment.process_steps(unprocessed, period, temperature) def test_str_repr(self): conds = ["Discharge at 1 C for 20 seconds", "Charge at 0.5 W for 10 minutes"] experiment = pybamm.Experiment(conds) - self.assertEqual( - str(experiment), - "[('Discharge at 1 C for 20 seconds',)" - + ", ('Charge at 0.5 W for 10 minutes',)]", + assert ( + str(experiment) + == "[('Discharge at 1 C for 20 seconds',)" + + ", ('Charge at 0.5 W for 10 minutes',)]" ) - self.assertEqual( - repr(experiment), - "pybamm.Experiment([('Discharge at 1 C for 20 seconds',)" - + ", ('Charge at 0.5 W for 10 minutes',)])", + assert ( + repr(experiment) + == "pybamm.Experiment([('Discharge at 1 C for 20 seconds',)" + + ", ('Charge at 0.5 W for 10 minutes',)])" ) def test_bad_strings(self): - with self.assertRaisesRegex( - TypeError, "Operating conditions must be a Step object or string." + with pytest.raises( + TypeError, match="Operating conditions must be a Step object or string." ): pybamm.Experiment([1, 2, 3]) - with self.assertRaisesRegex( - TypeError, "Operating conditions must be a Step object or string." + with pytest.raises( + TypeError, match="Operating conditions must be a Step object or string." ): pybamm.Experiment([(1, 2, 3)]) def test_termination(self): experiment = pybamm.Experiment(["Discharge at 1 C for 20 seconds"]) - self.assertEqual(experiment.termination, {}) + assert experiment.termination == {} experiment = pybamm.Experiment( ["Discharge at 1 C for 20 seconds"], termination=["80.7% capacity"] ) - self.assertEqual(experiment.termination, {"capacity": (80.7, "%")}) + assert experiment.termination == {"capacity": (80.7, "%")} experiment = pybamm.Experiment( ["Discharge at 1 C for 20 seconds"], termination=["80.7 % capacity"] ) - self.assertEqual(experiment.termination, {"capacity": (80.7, "%")}) + assert experiment.termination == {"capacity": (80.7, "%")} experiment = pybamm.Experiment( ["Discharge at 1 C for 20 seconds"], termination=["4.1Ah capacity"] ) - self.assertEqual(experiment.termination, {"capacity": (4.1, "Ah")}) + assert experiment.termination == {"capacity": (4.1, "Ah")} experiment = pybamm.Experiment( ["Discharge at 1 C for 20 seconds"], termination=["4.1 A.h capacity"] ) - self.assertEqual(experiment.termination, {"capacity": (4.1, "Ah")}) + assert experiment.termination == {"capacity": (4.1, "Ah")} - with self.assertRaisesRegex(ValueError, "Only capacity"): + with pytest.raises(ValueError, match="Only capacity"): experiment = pybamm.Experiment( ["Discharge at 1 C for 20 seconds"], termination="bla bla capacity bla" ) - with self.assertRaisesRegex(ValueError, "Only capacity"): + with pytest.raises(ValueError, match="Only capacity"): experiment = pybamm.Experiment( ["Discharge at 1 C for 20 seconds"], termination="4 A.h something else" ) - with self.assertRaisesRegex(ValueError, "Capacity termination"): + with pytest.raises(ValueError, match="Capacity termination"): experiment = pybamm.Experiment( ["Discharge at 1 C for 20 seconds"], termination="1 capacity" ) @@ -156,16 +153,16 @@ def test_search_tag(self): ] ) - self.assertEqual(experiment.search_tag("tag1"), [0, 5]) - self.assertEqual(experiment.search_tag("tag2"), [1, 2]) - self.assertEqual(experiment.search_tag("tag3"), [1, 2, 5]) - self.assertEqual(experiment.search_tag("tag4"), [4, 5]) - self.assertEqual(experiment.search_tag("tag5"), [3]) - self.assertEqual(experiment.search_tag("no_tag"), []) + assert experiment.search_tag("tag1") == [0, 5] + assert experiment.search_tag("tag2") == [1, 2] + assert experiment.search_tag("tag3") == [1, 2, 5] + assert experiment.search_tag("tag4") == [4, 5] + assert experiment.search_tag("tag5") == [3] + assert experiment.search_tag("no_tag") == [] def test_no_initial_start_time(self): s = pybamm.step.string - with self.assertRaisesRegex(ValueError, "first step must have a `start_time`"): + with pytest.raises(ValueError, match="first step must have a `start_time`"): pybamm.Experiment( [ s("Rest for 1 hour"), @@ -212,18 +209,8 @@ def test_set_next_start_time(self): # Test method directly for next, end, steps in zip(expected_next, expected_end, processed_steps): # useful form for debugging - self.assertEqual(steps.next_start_time, next) - self.assertEqual(steps.end_time, end) + assert steps.next_start_time == next + assert steps.end_time == end # TODO: once #3176 is completed, the test should pass for # operating_conditions_steps (or equivalent) as well - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_experiments/test_experiment_steps.py b/tests/unit/test_experiments/test_experiment_steps.py index 4bb686986f..67f63032b8 100644 --- a/tests/unit/test_experiments/test_experiment_steps.py +++ b/tests/unit/test_experiments/test_experiment_steps.py @@ -1,24 +1,24 @@ # # Test the experiment steps # +import pytest import pybamm -import unittest import numpy as np from datetime import datetime -class TestExperimentSteps(unittest.TestCase): +class TestExperimentSteps: def test_step(self): step = pybamm.step.current(1, duration=3600) - self.assertEqual(step.value, 1) - self.assertEqual(step.duration, 3600) - self.assertEqual(step.termination, []) - self.assertEqual(step.period, None) - self.assertEqual(step.temperature, None) - self.assertEqual(step.tags, []) - self.assertEqual(step.start_time, None) - self.assertEqual(step.end_time, None) - self.assertEqual(step.next_start_time, None) + assert step.value == 1 + assert step.duration == 3600 + assert step.termination == [] + assert step.period is None + assert step.temperature is None + assert step.tags == [] + assert step.start_time is None + assert step.end_time is None + assert step.next_start_time is None step = pybamm.step.voltage( 1, @@ -29,50 +29,50 @@ def test_step(self): tags="test", start_time=datetime(2020, 1, 1, 0, 0, 0), ) - self.assertEqual(step.value, 1) - self.assertEqual(step.duration, 3600) - self.assertEqual(step.termination, [pybamm.step.VoltageTermination(2.5)]) - self.assertEqual(step.period, 60) - self.assertEqual(step.temperature, 298.15) - self.assertEqual(step.tags, ["test"]) - self.assertEqual(step.start_time, datetime(2020, 1, 1, 0, 0, 0)) + assert step.value == 1 + assert step.duration == 3600 + assert step.termination == [pybamm.step.VoltageTermination(2.5)] + assert step.period == 60 + assert step.temperature == 298.15 + assert step.tags == ["test"] + assert step.start_time == datetime(2020, 1, 1, 0, 0, 0) step = pybamm.step.current(1, temperature="298K") - self.assertEqual(step.temperature, 298) + assert step.temperature == 298 - with self.assertRaisesRegex(ValueError, "temperature units"): + with pytest.raises(ValueError, match="temperature units"): step = pybamm.step.current(1, temperature="298T") - with self.assertRaisesRegex(ValueError, "time must be positive"): + with pytest.raises(ValueError, match="time must be positive"): pybamm.step.current(1, duration=0) def test_specific_steps(self): current = pybamm.step.current(1) - self.assertIsInstance(current, pybamm.step.Current) - self.assertEqual(current.value, 1) - self.assertEqual(str(current), repr(current)) - self.assertEqual(current.duration, 24 * 3600) + assert isinstance(current, pybamm.step.Current) + assert current.value == 1 + assert str(current) == repr(current) + assert current.duration == 24 * 3600 c_rate = pybamm.step.c_rate(1) - self.assertIsInstance(c_rate, pybamm.step.CRate) - self.assertEqual(c_rate.value, 1) - self.assertEqual(c_rate.duration, 3600 * 2) + assert isinstance(c_rate, pybamm.step.CRate) + assert c_rate.value == 1 + assert c_rate.duration == 3600 * 2 voltage = pybamm.step.voltage(1) - self.assertIsInstance(voltage, pybamm.step.Voltage) - self.assertEqual(voltage.value, 1) + assert isinstance(voltage, pybamm.step.Voltage) + assert voltage.value == 1 rest = pybamm.step.rest() - self.assertIsInstance(rest, pybamm.step.Current) - self.assertEqual(rest.value, 0) + assert isinstance(rest, pybamm.step.Current) + assert rest.value == 0 power = pybamm.step.power(1) - self.assertIsInstance(power, pybamm.step.Power) - self.assertEqual(power.value, 1) + assert isinstance(power, pybamm.step.Power) + assert power.value == 1 resistance = pybamm.step.resistance(1) - self.assertIsInstance(resistance, pybamm.step.Resistance) - self.assertEqual(resistance.value, 1) + assert isinstance(resistance, pybamm.step.Resistance) + assert resistance.value == 1 def test_step_string(self): steps = [ @@ -177,14 +177,14 @@ def test_step_string(self): actual = pybamm.step.string(step).to_dict() for k in expected.keys(): # useful form for debugging - self.assertEqual([k, expected[k]], [k, actual[k]]) + assert [k, expected[k]] == [k, actual[k]] - with self.assertRaisesRegex(ValueError, "Period cannot be"): + with pytest.raises(ValueError, match="Period cannot be"): pybamm.step.string( "Discharge at 1C for 1 hour (1 minute period)", period=60 ) - with self.assertRaisesRegex(ValueError, "Temperature must be"): + with pytest.raises(ValueError, match="Temperature must be"): pybamm.step.string("Discharge at 1C for 1 hour at 298.15oC") def test_drive_cycle(self): @@ -194,12 +194,12 @@ def test_drive_cycle(self): # Create steps drive_cycle_step = pybamm.step.current(drive_cycle, temperature="-5oC") # Check drive cycle operating conditions - self.assertEqual(drive_cycle_step.duration, 9) - self.assertEqual(drive_cycle_step.period, 1) - self.assertEqual(drive_cycle_step.temperature, 273.15 - 5) + assert drive_cycle_step.duration == 9 + assert drive_cycle_step.period == 1 + assert drive_cycle_step.temperature == 273.15 - 5 bad_drive_cycle = np.ones((10, 3)) - with self.assertRaisesRegex(ValueError, "Drive cycle must be a 2-column array"): + with pytest.raises(ValueError, match="Drive cycle must be a 2-column array"): pybamm.step.current(bad_drive_cycle) def test_drive_cycle_duration(self): @@ -212,9 +212,9 @@ def test_drive_cycle_duration(self): drive_cycle, duration=20, temperature="-5oC" ) # Check drive cycle operating conditions - self.assertEqual(drive_cycle_step.duration, 20) - self.assertEqual(drive_cycle_step.period, 1) - self.assertEqual(drive_cycle_step.temperature, 273.15 - 5) + assert drive_cycle_step.duration == 20 + assert drive_cycle_step.period == 1 + assert drive_cycle_step.temperature == 273.15 - 5 # Check duration shorter than drive cycle data # Create steps @@ -222,28 +222,28 @@ def test_drive_cycle_duration(self): drive_cycle, duration=5, temperature="-5oC" ) # Check drive cycle operating conditions - self.assertEqual(drive_cycle_step.duration, 5) - self.assertEqual(drive_cycle_step.period, 1) - self.assertEqual(drive_cycle_step.temperature, 273.15 - 5) + assert drive_cycle_step.duration == 5 + assert drive_cycle_step.period == 1 + assert drive_cycle_step.temperature == 273.15 - 5 def test_bad_strings(self): - with self.assertRaisesRegex(TypeError, "Input to step.string"): + with pytest.raises(TypeError, match="Input to step.string"): pybamm.step.string(1) - with self.assertRaisesRegex(TypeError, "Input to step.string"): + with pytest.raises(TypeError, match="Input to step.string"): pybamm.step.string((1, 2, 3)) - with self.assertRaisesRegex(ValueError, "Operating conditions must"): + with pytest.raises(ValueError, match="Operating conditions must"): pybamm.step.string("Discharge at 1 A at 2 hours") - with self.assertRaisesRegex(ValueError, "drive cycles"): + with pytest.raises(ValueError, match="drive cycles"): pybamm.step.string("Run at 1 A for 2 hours") - with self.assertRaisesRegex(ValueError, "Instruction must be"): + with pytest.raises(ValueError, match="Instruction must be"): pybamm.step.string("Play at 1 A for 2 hours") - with self.assertRaisesRegex(ValueError, "Operating conditions must"): + with pytest.raises(ValueError, match="Operating conditions must"): pybamm.step.string("Do at 1 A") - with self.assertRaisesRegex(ValueError, "Instruction"): + with pytest.raises(ValueError, match="Instruction"): pybamm.step.string("Cell Charge at 1 A for 2 hours") - with self.assertRaisesRegex(ValueError, "units must be"): + with pytest.raises(ValueError, match="units must be"): pybamm.step.string("Discharge at 1 B for 2 hours") - with self.assertRaisesRegex(ValueError, "time units must be"): + with pytest.raises(ValueError, match="time units must be"): pybamm.step.string("Discharge at 1 A for 2 years") def test_start_times(self): @@ -251,10 +251,10 @@ def test_start_times(self): step = pybamm.step.current( 1, duration=3600, start_time=datetime(2020, 1, 1, 0, 0, 0) ) - self.assertEqual(step.start_time, datetime(2020, 1, 1, 0, 0, 0)) + assert step.start_time == datetime(2020, 1, 1, 0, 0, 0) # Test bad start_times - with self.assertRaisesRegex(TypeError, "`start_time` should be"): + with pytest.raises(TypeError, match="`start_time` should be"): pybamm.step.current(1, duration=3600, start_time="bad start_time") def test_custom_termination(self): @@ -266,20 +266,20 @@ def neg_stoich_cutoff(variables): ) variables = {"Negative electrode stoichiometry": 3} event = neg_stoich_termination.get_event(variables, None) - self.assertEqual(event.name, "Negative stoichiometry cut-off [experiment]") - self.assertEqual(event.expression, 2) + assert event.name == "Negative stoichiometry cut-off [experiment]" + assert event.expression == 2 def test_drive_cycle_start_time(self): # An example where start_time t>0 t = np.array([[1, 1], [2, 2], [3, 3]]) - with self.assertRaisesRegex(ValueError, "Drive cycle must start at t=0"): + with pytest.raises(ValueError, match="Drive cycle must start at t=0"): pybamm.step.current(t) def test_base_custom_steps(self): - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): pybamm.step.BaseStepExplicit(None).current_value(None) - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): pybamm.step.BaseStepImplicit(None).get_submodel(None) def test_custom_steps(self): @@ -288,32 +288,22 @@ def custom_step_constant(variables): custom_constant = pybamm.step.CustomStepExplicit(custom_step_constant) - self.assertEqual(custom_constant.current_value_function({}), 1) + assert custom_constant.current_value_function({}) == 1 def custom_step_voltage(variables): return variables["Voltage [V]"] - 4.1 custom_step_alg = pybamm.step.CustomStepImplicit(custom_step_voltage) - self.assertEqual(custom_step_alg.control, "algebraic") - self.assertAlmostEqual( - custom_step_alg.current_rhs_function({"Voltage [V]": 4.2}), 0.1 - ) + assert custom_step_alg.control == "algebraic" + assert custom_step_alg.current_rhs_function( + {"Voltage [V]": 4.2} + ) == pytest.approx(0.1) custom_step_diff = pybamm.step.CustomStepImplicit( custom_step_voltage, control="differential" ) - self.assertEqual(custom_step_diff.control, "differential") + assert custom_step_diff.control == "differential" - with self.assertRaisesRegex(ValueError, "control must be"): + with pytest.raises(ValueError, match="control must be"): pybamm.step.CustomStepImplicit(custom_step_voltage, control="bla") - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_expression_tree/test_averages.py b/tests/unit/test_expression_tree/test_averages.py index 1f1385db65..5f9736693e 100644 --- a/tests/unit/test_expression_tree/test_averages.py +++ b/tests/unit/test_expression_tree/test_averages.py @@ -1,33 +1,33 @@ # # Tests for the Unary Operator classes # -import unittest +import pytest import numpy as np import pybamm from tests import assert_domain_equal -class TestUnaryOperators(unittest.TestCase): +class TestUnaryOperators: def test_x_average(self): a = pybamm.Scalar(4) average_a = pybamm.x_average(a) - self.assertEqual(average_a, a) + assert average_a == a # average of a broadcast is the child average_broad_a = pybamm.x_average( pybamm.PrimaryBroadcast(a, ["negative electrode"]) ) - self.assertEqual(average_broad_a, pybamm.Scalar(4)) + assert average_broad_a == pybamm.Scalar(4) # average of a number times a broadcast is the number times the child average_two_broad_a = pybamm.x_average( 2 * pybamm.PrimaryBroadcast(a, ["negative electrode"]) ) - self.assertEqual(average_two_broad_a, pybamm.Scalar(8)) + assert average_two_broad_a == pybamm.Scalar(8) average_t_broad_a = pybamm.x_average( pybamm.t * pybamm.PrimaryBroadcast(a, ["negative electrode"]) ) - self.assertEqual(average_t_broad_a, (pybamm.t * pybamm.Scalar(4))) + assert average_t_broad_a == (pybamm.t * pybamm.Scalar(4)) # full broadcasts average_broad_a = pybamm.x_average( @@ -46,7 +46,7 @@ def test_x_average(self): ["negative particle"], {"secondary": "negative particle size", "tertiary": "current collector"}, ) - self.assertEqual(average_broad_a, average_broad_a_simp) + assert average_broad_a == average_broad_a_simp # x-average of concatenation of broadcasts conc_broad = pybamm.concatenation( @@ -55,16 +55,16 @@ def test_x_average(self): pybamm.PrimaryBroadcast(3, ["positive electrode"]), ) average_conc_broad = pybamm.x_average(conc_broad) - self.assertIsInstance(average_conc_broad, pybamm.Division) - self.assertEqual(average_conc_broad.domain, []) + assert isinstance(average_conc_broad, pybamm.Division) + assert average_conc_broad.domain == [] # separator and positive electrode only (half-cell model) conc_broad = pybamm.concatenation( pybamm.PrimaryBroadcast(2, ["separator"]), pybamm.PrimaryBroadcast(3, ["positive electrode"]), ) average_conc_broad = pybamm.x_average(conc_broad) - self.assertIsInstance(average_conc_broad, pybamm.Division) - self.assertEqual(average_conc_broad.domain, []) + assert isinstance(average_conc_broad, pybamm.Division) + assert average_conc_broad.domain == [] # with auxiliary domains conc_broad = pybamm.concatenation( pybamm.FullBroadcast( @@ -82,8 +82,8 @@ def test_x_average(self): ), ) average_conc_broad = pybamm.x_average(conc_broad) - self.assertIsInstance(average_conc_broad, pybamm.PrimaryBroadcast) - self.assertEqual(average_conc_broad.domain, ["current collector"]) + assert isinstance(average_conc_broad, pybamm.PrimaryBroadcast) + assert average_conc_broad.domain == ["current collector"] conc_broad = pybamm.concatenation( pybamm.FullBroadcast( 1, @@ -111,7 +111,7 @@ def test_x_average(self): ), ) average_conc_broad = pybamm.x_average(conc_broad) - self.assertIsInstance(average_conc_broad, pybamm.FullBroadcast) + assert isinstance(average_conc_broad, pybamm.FullBroadcast) assert_domain_equal( average_conc_broad.domains, {"primary": ["current collector"], "secondary": ["test"]}, @@ -122,29 +122,30 @@ def test_x_average(self): a = pybamm.Variable("a", domain=domain) x = pybamm.SpatialVariable("x", domain) av_a = pybamm.x_average(a) - self.assertIsInstance(av_a, pybamm.XAverage) - self.assertEqual(av_a.integration_variable[0].domain, x.domain) - self.assertEqual(av_a.domain, []) + assert isinstance(av_a, pybamm.XAverage) + assert av_a.integration_variable[0].domain == x.domain + assert av_a.domain == [] # whole electrode domain domain = ["negative electrode", "separator", "positive electrode"] a = pybamm.Variable("a", domain=domain) x = pybamm.SpatialVariable("x", domain) av_a = pybamm.x_average(a) - self.assertIsInstance(av_a, pybamm.XAverage) - self.assertEqual(av_a.integration_variable[0].domain, x.domain) - self.assertEqual(av_a.domain, []) + assert isinstance(av_a, pybamm.XAverage) + assert av_a.integration_variable[0].domain == x.domain + assert av_a.domain == [] a = pybamm.Variable("a", domain="new domain") av_a = pybamm.x_average(a) - self.assertEqual(av_a, a) + assert av_a == a # x-average of symbol that evaluates on edges raises error symbol_on_edges = pybamm.SpatialVariableEdge( "x_n", domain=["negative electrode"] ) - with self.assertRaisesRegex( - ValueError, "Can't take the x-average of a symbol that evaluates on edges" + with pytest.raises( + ValueError, + match="Can't take the x-average of a symbol that evaluates on edges", ): pybamm.x_average(symbol_on_edges) @@ -155,8 +156,8 @@ def test_x_average(self): auxiliary_domains={"secondary": "negative electrode"}, ) av_a = pybamm.x_average(a) - self.assertEqual(a.domain, ["negative particle"]) - self.assertIsInstance(av_a, pybamm.XAverage) + assert a.domain == ["negative particle"] + assert isinstance(av_a, pybamm.XAverage) a = pybamm.Symbol( "a", @@ -164,24 +165,20 @@ def test_x_average(self): auxiliary_domains={"secondary": "positive electrode"}, ) av_a = pybamm.x_average(a) - self.assertEqual(a.domain, ["positive particle"]) - self.assertIsInstance(av_a, pybamm.XAverage) + assert a.domain == ["positive particle"] + assert isinstance(av_a, pybamm.XAverage) # Addition or Subtraction a = pybamm.Variable("a", domain="domain") b = pybamm.Variable("b", domain="domain") - self.assertEqual( - pybamm.x_average(a + b), pybamm.x_average(a) + pybamm.x_average(b) - ) - self.assertEqual( - pybamm.x_average(a - b), pybamm.x_average(a) - pybamm.x_average(b) - ) + assert pybamm.x_average(a + b) == pybamm.x_average(a) + pybamm.x_average(b) + assert pybamm.x_average(a - b) == pybamm.x_average(a) - pybamm.x_average(b) def test_size_average(self): # no domain a = pybamm.Scalar(1) average_a = pybamm.size_average(a) - self.assertEqual(average_a, a) + assert average_a == a b = pybamm.FullBroadcast( 1, @@ -190,88 +187,85 @@ def test_size_average(self): ) # no "particle size" domain average_b = pybamm.size_average(b) - self.assertEqual(average_b, b) + assert average_b == b # primary or secondary broadcast to "particle size" domain average_a = pybamm.size_average( pybamm.PrimaryBroadcast(a, "negative particle size") ) - self.assertEqual(average_a.evaluate(), np.array([1])) + assert average_a.evaluate() == np.array([1]) a = pybamm.Symbol("a", domain="negative particle") average_a = pybamm.size_average( pybamm.SecondaryBroadcast(a, "negative particle size") ) - self.assertEqual(average_a, a) + assert average_a == a for domain in [["negative particle size"], ["positive particle size"]]: a = pybamm.Symbol("a", domain=domain) R = pybamm.SpatialVariable("R", domain) av_a = pybamm.size_average(a) - self.assertIsInstance(av_a, pybamm.SizeAverage) - self.assertEqual(av_a.integration_variable[0].domain, R.domain) + assert isinstance(av_a, pybamm.SizeAverage) + assert av_a.integration_variable[0].domain == R.domain # domain list should now be empty - self.assertEqual(av_a.domain, []) + assert av_a.domain == [] # R-average of symbol that evaluates on edges raises error symbol_on_edges = pybamm.PrimaryBroadcastToEdges(1, "domain") - with self.assertRaisesRegex( + with pytest.raises( ValueError, - """Can't take the size-average of a symbol that evaluates on edges""", + match="""Can't take the size-average of a symbol that evaluates on edges""", ): pybamm.size_average(symbol_on_edges) def test_r_average(self): a = pybamm.Scalar(1) average_a = pybamm.r_average(a) - self.assertEqual(average_a, a) + assert average_a == a average_broad_a = pybamm.r_average( pybamm.PrimaryBroadcast(a, ["negative particle"]) ) - self.assertEqual(average_broad_a.evaluate(), np.array([1])) + assert average_broad_a.evaluate() == np.array([1]) for domain in [["negative particle"], ["positive particle"]]: a = pybamm.Symbol("a", domain=domain) r = pybamm.SpatialVariable("r", domain) av_a = pybamm.r_average(a) - self.assertIsInstance(av_a, pybamm.RAverage) - self.assertEqual(av_a.integration_variable[0].domain, r.domain) + assert isinstance(av_a, pybamm.RAverage) + assert av_a.integration_variable[0].domain == r.domain # electrode domains go to current collector when averaged - self.assertEqual(av_a.domain, []) + assert av_a.domain == [] # r-average of a symbol that is broadcast to x # takes the average of the child then broadcasts it a = pybamm.PrimaryBroadcast(1, "positive particle") broad_a = pybamm.SecondaryBroadcast(a, "positive electrode") average_broad_a = pybamm.r_average(broad_a) - self.assertIsInstance(average_broad_a, pybamm.PrimaryBroadcast) - self.assertEqual(average_broad_a.domain, ["positive electrode"]) - self.assertEqual(average_broad_a.children[0], pybamm.r_average(a)) + assert isinstance(average_broad_a, pybamm.PrimaryBroadcast) + assert average_broad_a.domain == ["positive electrode"] + assert average_broad_a.children[0] == pybamm.r_average(a) # r-average of symbol that evaluates on edges raises error symbol_on_edges = pybamm.PrimaryBroadcastToEdges(1, "domain") - with self.assertRaisesRegex( - ValueError, "Can't take the r-average of a symbol that evaluates on edges" + with pytest.raises( + ValueError, + match="Can't take the r-average of a symbol that evaluates on edges", ): pybamm.r_average(symbol_on_edges) # Addition or Subtraction a = pybamm.Variable("a", domain="domain") b = pybamm.Variable("b", domain="domain") - self.assertEqual( - pybamm.r_average(a + b), pybamm.r_average(a) + pybamm.r_average(b) - ) - self.assertEqual( - pybamm.r_average(a - b), pybamm.r_average(a) - pybamm.r_average(b) - ) + assert pybamm.r_average(a + b) == pybamm.r_average(a) + pybamm.r_average(b) + assert pybamm.r_average(a - b) == pybamm.r_average(a) - pybamm.r_average(b) def test_yz_average(self): a = pybamm.Scalar(1) z_average_a = pybamm.z_average(a) yz_average_a = pybamm.yz_average(a) - self.assertEqual(z_average_a, a) - self.assertEqual(yz_average_a, a) + assert z_average_a == a + assert yz_average_a == a z_average_broad_a = pybamm.z_average( pybamm.PrimaryBroadcast(a, ["current collector"]) @@ -279,59 +273,42 @@ def test_yz_average(self): yz_average_broad_a = pybamm.yz_average( pybamm.PrimaryBroadcast(a, ["current collector"]) ) - self.assertEqual(z_average_broad_a.evaluate(), np.array([1])) - self.assertEqual(yz_average_broad_a.evaluate(), np.array([1])) + assert z_average_broad_a.evaluate() == np.array([1]) + assert yz_average_broad_a.evaluate() == np.array([1]) a = pybamm.Variable("a", domain=["current collector"]) y = pybamm.SpatialVariable("y", ["current collector"]) z = pybamm.SpatialVariable("z", ["current collector"]) yz_av_a = pybamm.yz_average(a) - self.assertIsInstance(yz_av_a, pybamm.YZAverage) - self.assertEqual(yz_av_a.integration_variable[0].domain, y.domain) - self.assertEqual(yz_av_a.integration_variable[1].domain, z.domain) - self.assertEqual(yz_av_a.domain, []) + assert isinstance(yz_av_a, pybamm.YZAverage) + assert yz_av_a.integration_variable[0].domain == y.domain + assert yz_av_a.integration_variable[1].domain == z.domain + assert yz_av_a.domain == [] z_av_a = pybamm.z_average(a) - self.assertIsInstance(z_av_a, pybamm.ZAverage) - self.assertEqual(z_av_a.integration_variable[0].domain, a.domain) - self.assertEqual(z_av_a.domain, []) + assert isinstance(z_av_a, pybamm.ZAverage) + assert z_av_a.integration_variable[0].domain == a.domain + assert z_av_a.domain == [] a = pybamm.Symbol("a", domain="bad domain") - with self.assertRaises(pybamm.DomainError): + with pytest.raises(pybamm.DomainError): pybamm.z_average(a) - with self.assertRaises(pybamm.DomainError): + with pytest.raises(pybamm.DomainError): pybamm.yz_average(a) # average of symbol that evaluates on edges raises error symbol_on_edges = pybamm.PrimaryBroadcastToEdges(1, "domain") - with self.assertRaisesRegex( - ValueError, "Can't take the z-average of a symbol that evaluates on edges" + with pytest.raises( + ValueError, + match="Can't take the z-average of a symbol that evaluates on edges", ): pybamm.z_average(symbol_on_edges) # Addition or Subtraction a = pybamm.Variable("a", domain="current collector") b = pybamm.Variable("b", domain="current collector") - self.assertEqual( - pybamm.yz_average(a + b), pybamm.yz_average(a) + pybamm.yz_average(b) - ) - self.assertEqual( - pybamm.yz_average(a - b), pybamm.yz_average(a) - pybamm.yz_average(b) - ) - self.assertEqual( - pybamm.z_average(a + b), pybamm.z_average(a) + pybamm.z_average(b) - ) - self.assertEqual( - pybamm.z_average(a - b), pybamm.z_average(a) - pybamm.z_average(b) - ) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() + assert pybamm.yz_average(a + b) == pybamm.yz_average(a) + pybamm.yz_average(b) + assert pybamm.yz_average(a - b) == pybamm.yz_average(a) - pybamm.yz_average(b) + assert pybamm.z_average(a + b) == pybamm.z_average(a) + pybamm.z_average(b) + assert pybamm.z_average(a - b) == pybamm.z_average(a) - pybamm.z_average(b) diff --git a/tests/unit/test_expression_tree/test_broadcasts.py b/tests/unit/test_expression_tree/test_broadcasts.py index e5516395e9..c1e3815f67 100644 --- a/tests/unit/test_expression_tree/test_broadcasts.py +++ b/tests/unit/test_expression_tree/test_broadcasts.py @@ -1,22 +1,22 @@ # # Tests for the Broadcast class # -import unittest +import pytest from tests import assert_domain_equal import numpy as np import pybamm -class TestBroadcasts(unittest.TestCase): +class TestBroadcasts: def test_primary_broadcast(self): a = pybamm.Symbol("a") broad_a = pybamm.PrimaryBroadcast(a, ["negative electrode"]) - self.assertEqual(broad_a.name, "broadcast") - self.assertEqual(broad_a.children[0].name, a.name) - self.assertEqual(broad_a.domain, ["negative electrode"]) - self.assertTrue(broad_a.broadcasts_to_nodes) - self.assertEqual(broad_a.reduce_one_dimension(), a) + assert broad_a.name == "broadcast" + assert broad_a.children[0].name == a.name + assert broad_a.domain == ["negative electrode"] + assert broad_a.broadcasts_to_nodes + assert broad_a.reduce_one_dimension() == a a = pybamm.Symbol( "a", @@ -52,27 +52,27 @@ def test_primary_broadcast(self): ) a = pybamm.Symbol("a", domain="current collector") - with self.assertRaisesRegex( - pybamm.DomainError, "Cannot Broadcast an object into empty domain" + with pytest.raises( + pybamm.DomainError, match="Cannot Broadcast an object into empty domain" ): pybamm.PrimaryBroadcast(a, []) - with self.assertRaisesRegex( - pybamm.DomainError, "Primary broadcast from current collector" + with pytest.raises( + pybamm.DomainError, match="Primary broadcast from current collector" ): pybamm.PrimaryBroadcast(a, "bad domain") a = pybamm.Symbol("a", domain="negative electrode") - with self.assertRaisesRegex( - pybamm.DomainError, "Primary broadcast from electrode" + with pytest.raises( + pybamm.DomainError, match="Primary broadcast from electrode" ): pybamm.PrimaryBroadcast(a, "current collector") a = pybamm.Symbol("a", domain="negative particle size") - with self.assertRaisesRegex( - pybamm.DomainError, "Primary broadcast from particle size" + with pytest.raises( + pybamm.DomainError, match="Primary broadcast from particle size" ): pybamm.PrimaryBroadcast(a, "negative electrode") a = pybamm.Symbol("a", domain="negative particle") - with self.assertRaisesRegex( - pybamm.DomainError, "Cannot do primary broadcast from particle domain" + with pytest.raises( + pybamm.DomainError, match="Cannot do primary broadcast from particle domain" ): pybamm.PrimaryBroadcast(a, "current collector") @@ -91,7 +91,7 @@ def test_secondary_broadcast(self): "tertiary": ["current collector"], }, ) - self.assertTrue(broad_a.broadcasts_to_nodes) + assert broad_a.broadcasts_to_nodes broadbroad_a = pybamm.SecondaryBroadcast(broad_a, ["negative particle size"]) assert_domain_equal( broadbroad_a.domains, @@ -103,31 +103,29 @@ def test_secondary_broadcast(self): }, ) - self.assertEqual(broad_a.reduce_one_dimension(), a) + assert broad_a.reduce_one_dimension() == a a = pybamm.Symbol("a") - with self.assertRaisesRegex(TypeError, "empty domain"): + with pytest.raises(TypeError, match="empty domain"): pybamm.SecondaryBroadcast(a, "current collector") a = pybamm.Symbol("a", domain="negative particle") - with self.assertRaisesRegex( - pybamm.DomainError, "Secondary broadcast from particle" + with pytest.raises( + pybamm.DomainError, match="Secondary broadcast from particle" ): pybamm.SecondaryBroadcast(a, "negative particle") a = pybamm.Symbol("a", domain="negative particle size") - with self.assertRaisesRegex( - pybamm.DomainError, "Secondary broadcast from particle size" + with pytest.raises( + pybamm.DomainError, match="Secondary broadcast from particle size" ): pybamm.SecondaryBroadcast(a, "negative particle") a = pybamm.Symbol("a", domain="negative electrode") - with self.assertRaisesRegex( - pybamm.DomainError, "Secondary broadcast from electrode" + with pytest.raises( + pybamm.DomainError, match="Secondary broadcast from electrode" ): pybamm.SecondaryBroadcast(a, "negative particle") a = pybamm.Symbol("a", domain="current collector") - with self.assertRaisesRegex( - pybamm.DomainError, "Cannot do secondary broadcast" - ): + with pytest.raises(pybamm.DomainError, match="Cannot do secondary broadcast"): pybamm.SecondaryBroadcast(a, "electrode") def test_tertiary_broadcast(self): @@ -149,16 +147,16 @@ def test_tertiary_broadcast(self): "quaternary": ["current collector"], }, ) - self.assertTrue(broad_a.broadcasts_to_nodes) + assert broad_a.broadcasts_to_nodes - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): broad_a.reduce_one_dimension() a_no_secondary = pybamm.Symbol("a", domain="negative particle") - with self.assertRaisesRegex(TypeError, "without a secondary"): + with pytest.raises(TypeError, match="without a secondary"): pybamm.TertiaryBroadcast(a_no_secondary, "negative electrode") - with self.assertRaisesRegex( - pybamm.DomainError, "Tertiary broadcast from a symbol with particle" + with pytest.raises( + pybamm.DomainError, match="Tertiary broadcast from a symbol with particle" ): pybamm.TertiaryBroadcast(a, "negative particle") a = pybamm.Symbol( @@ -166,8 +164,9 @@ def test_tertiary_broadcast(self): domain=["negative particle"], auxiliary_domains={"secondary": "negative electrode"}, ) - with self.assertRaisesRegex( - pybamm.DomainError, "Tertiary broadcast from a symbol with an electrode" + with pytest.raises( + pybamm.DomainError, + match="Tertiary broadcast from a symbol with an electrode", ): pybamm.TertiaryBroadcast(a, "negative particle size") a = pybamm.Symbol( @@ -175,22 +174,21 @@ def test_tertiary_broadcast(self): domain=["negative particle"], auxiliary_domains={"secondary": "current collector"}, ) - with self.assertRaisesRegex(pybamm.DomainError, "Cannot do tertiary broadcast"): + with pytest.raises(pybamm.DomainError, match="Cannot do tertiary broadcast"): pybamm.TertiaryBroadcast(a, "negative electrode") def test_full_broadcast(self): a = pybamm.Symbol("a") broad_a = pybamm.FullBroadcast(a, ["negative electrode"], "current collector") - self.assertEqual(broad_a.domain, ["negative electrode"]) - self.assertEqual(broad_a.domains["secondary"], ["current collector"]) - self.assertTrue(broad_a.broadcasts_to_nodes) - self.assertEqual( - broad_a.reduce_one_dimension(), - pybamm.PrimaryBroadcast(a, "current collector"), + assert broad_a.domain == ["negative electrode"] + assert broad_a.domains["secondary"] == ["current collector"] + assert broad_a.broadcasts_to_nodes + assert broad_a.reduce_one_dimension() == pybamm.PrimaryBroadcast( + a, "current collector" ) broad_a = pybamm.FullBroadcast(a, ["negative electrode"], {}) - self.assertEqual(broad_a.reduce_one_dimension(), a) + assert broad_a.reduce_one_dimension() == a broad_a = pybamm.FullBroadcast( a, @@ -201,38 +199,36 @@ def test_full_broadcast(self): "quaternary": "current collector", }, ) - self.assertEqual( - broad_a.reduce_one_dimension(), - pybamm.FullBroadcast( - a, - "negative particle size", - { - "secondary": "negative electrode", - "tertiary": "current collector", - }, - ), + assert broad_a.reduce_one_dimension() == pybamm.FullBroadcast( + a, + "negative particle size", + { + "secondary": "negative electrode", + "tertiary": "current collector", + }, ) - with self.assertRaisesRegex( - pybamm.DomainError, "Cannot do full broadcast to an empty primary domain" + with pytest.raises( + pybamm.DomainError, + match="Cannot do full broadcast to an empty primary domain", ): pybamm.FullBroadcast(a, []) def test_full_broadcast_number(self): broad_a = pybamm.FullBroadcast(1, ["negative electrode"], None) - self.assertEqual(broad_a.name, "broadcast") - self.assertIsInstance(broad_a.children[0], pybamm.Symbol) - self.assertEqual(broad_a.children[0].evaluate(), np.array([1])) - self.assertEqual(broad_a.domain, ["negative electrode"]) + assert broad_a.name == "broadcast" + assert isinstance(broad_a.children[0], pybamm.Symbol) + assert broad_a.children[0].evaluate() == np.array([1]) + assert broad_a.domain == ["negative electrode"] a = pybamm.Symbol("a", domain="current collector") - with self.assertRaisesRegex(pybamm.DomainError, "Cannot do full broadcast"): + with pytest.raises(pybamm.DomainError, match="Cannot do full broadcast"): pybamm.FullBroadcast(a, "electrode", None) def test_ones_like(self): a = pybamm.Parameter("a") ones_like_a = pybamm.ones_like(a) - self.assertEqual(ones_like_a, pybamm.Scalar(1)) + assert ones_like_a == pybamm.Scalar(1) a = pybamm.Variable( "a", @@ -240,27 +236,27 @@ def test_ones_like(self): auxiliary_domains={"secondary": "current collector"}, ) ones_like_a = pybamm.ones_like(a) - self.assertIsInstance(ones_like_a, pybamm.FullBroadcast) - self.assertEqual(ones_like_a.name, "broadcast") - self.assertEqual(ones_like_a.domains, a.domains) + assert isinstance(ones_like_a, pybamm.FullBroadcast) + assert ones_like_a.name == "broadcast" + assert ones_like_a.domains == a.domains b = pybamm.Variable("b", domain="current collector") ones_like_ab = pybamm.ones_like(b, a) - self.assertIsInstance(ones_like_ab, pybamm.FullBroadcast) - self.assertEqual(ones_like_ab.name, "broadcast") - self.assertEqual(ones_like_ab.domains, a.domains) + assert isinstance(ones_like_ab, pybamm.FullBroadcast) + assert ones_like_ab.name == "broadcast" + assert ones_like_ab.domains == a.domains def test_broadcast_to_edges(self): a = pybamm.Symbol("a") # primary broad_a = pybamm.PrimaryBroadcastToEdges(a, ["negative electrode"]) - self.assertEqual(broad_a.name, "broadcast to edges") - self.assertEqual(broad_a.children[0].name, a.name) - self.assertEqual(broad_a.domain, ["negative electrode"]) - self.assertTrue(broad_a.evaluates_on_edges("primary")) - self.assertFalse(broad_a.broadcasts_to_nodes) - self.assertEqual(broad_a.reduce_one_dimension(), a) + assert broad_a.name == "broadcast to edges" + assert broad_a.children[0].name == a.name + assert broad_a.domain == ["negative electrode"] + assert broad_a.evaluates_on_edges("primary") + assert not broad_a.broadcasts_to_nodes + assert broad_a.reduce_one_dimension() == a # secondary a = pybamm.Symbol( @@ -277,8 +273,8 @@ def test_broadcast_to_edges(self): "tertiary": ["current collector"], }, ) - self.assertTrue(broad_a.evaluates_on_edges("primary")) - self.assertFalse(broad_a.broadcasts_to_nodes) + assert broad_a.evaluates_on_edges("primary") + assert not broad_a.broadcasts_to_nodes # tertiary a = pybamm.Symbol( @@ -299,38 +295,36 @@ def test_broadcast_to_edges(self): "quaternary": ["current collector"], }, ) - self.assertTrue(broad_a.evaluates_on_edges("primary")) - self.assertFalse(broad_a.broadcasts_to_nodes) + assert broad_a.evaluates_on_edges("primary") + assert not broad_a.broadcasts_to_nodes # full a = pybamm.Symbol("a") broad_a = pybamm.FullBroadcastToEdges( a, ["negative electrode"], "current collector" ) - self.assertEqual(broad_a.domain, ["negative electrode"]) - self.assertEqual(broad_a.domains["secondary"], ["current collector"]) - self.assertTrue(broad_a.evaluates_on_edges("primary")) - self.assertFalse(broad_a.broadcasts_to_nodes) - self.assertEqual( - broad_a.reduce_one_dimension(), - pybamm.PrimaryBroadcastToEdges(a, "current collector"), + assert broad_a.domain == ["negative electrode"] + assert broad_a.domains["secondary"] == ["current collector"] + assert broad_a.evaluates_on_edges("primary") + assert not broad_a.broadcasts_to_nodes + assert broad_a.reduce_one_dimension() == pybamm.PrimaryBroadcastToEdges( + a, "current collector" ) broad_a = pybamm.FullBroadcastToEdges(a, ["negative electrode"], {}) - self.assertEqual(broad_a.reduce_one_dimension(), a) + assert broad_a.reduce_one_dimension() == a broad_a = pybamm.FullBroadcastToEdges( a, "negative particle", {"secondary": "negative electrode", "tertiary": "current collector"}, ) - self.assertEqual( - broad_a.reduce_one_dimension(), - pybamm.FullBroadcastToEdges(a, "negative electrode", "current collector"), + assert broad_a.reduce_one_dimension() == pybamm.FullBroadcastToEdges( + a, "negative electrode", "current collector" ) def test_to_equation(self): a = pybamm.PrimaryBroadcast(0, "test").to_equation() - self.assertEqual(a, 0) + assert a == 0 def test_diff(self): a = pybamm.StateVector(slice(0, 1)) @@ -338,33 +332,23 @@ def test_diff(self): y = np.array([5]) # diff of broadcast is broadcast of diff d = b.diff(a) - self.assertIsInstance(d, pybamm.PrimaryBroadcast) - self.assertEqual(d.child.evaluate(y=y), 1) + assert isinstance(d, pybamm.PrimaryBroadcast) + assert d.child.evaluate(y=y) == 1 # diff of broadcast w.r.t. itself is 1 d = b.diff(b) - self.assertIsInstance(d, pybamm.Scalar) - self.assertEqual(d.evaluate(y=y), 1) + assert isinstance(d, pybamm.Scalar) + assert d.evaluate(y=y) == 1 # diff of broadcast of a constant is 0 c = pybamm.PrimaryBroadcast(pybamm.Scalar(4), "separator") d = c.diff(a) - self.assertIsInstance(d, pybamm.Scalar) - self.assertEqual(d.evaluate(y=y), 0) + assert isinstance(d, pybamm.Scalar) + assert d.evaluate(y=y) == 0 def test_to_from_json_error(self): a = pybamm.StateVector(slice(0, 1)) b = pybamm.PrimaryBroadcast(a, "separator") - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): b.to_json() - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): pybamm.PrimaryBroadcast._from_json({}) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_expression_tree/test_concatenations.py b/tests/unit/test_expression_tree/test_concatenations.py index a609c73be8..1d7ccef610 100644 --- a/tests/unit/test_expression_tree/test_concatenations.py +++ b/tests/unit/test_expression_tree/test_concatenations.py @@ -1,7 +1,7 @@ # # Tests for the Concatenation class and subclasses # -import unittest +import pytest import unittest.mock as mock from tests import assert_domain_equal @@ -13,46 +13,46 @@ from tests import get_discretisation_for_testing, get_mesh_for_testing -class TestConcatenations(unittest.TestCase): +class TestConcatenations: def test_base_concatenation(self): a = pybamm.Symbol("a", domain="test a") b = pybamm.Symbol("b", domain="test b") c = pybamm.Symbol("c", domain="test c") conc = pybamm.concatenation(a, b, c) - self.assertEqual(conc.name, "concatenation") - self.assertEqual(str(conc), "concatenation(a, b, c)") - self.assertIsInstance(conc.children[0], pybamm.Symbol) - self.assertEqual(conc.children[0].name, "a") - self.assertEqual(conc.children[1].name, "b") - self.assertEqual(conc.children[2].name, "c") + assert conc.name == "concatenation" + assert str(conc) == "concatenation(a, b, c)" + assert isinstance(conc.children[0], pybamm.Symbol) + assert conc.children[0].name == "a" + assert conc.children[1].name == "b" + assert conc.children[2].name == "c" d = pybamm.Vector([2], domain="test a") e = pybamm.Vector([1], domain="test b") f = pybamm.Vector([3], domain="test c") conc2 = pybamm.concatenation(d, e, f) - with self.assertRaises(TypeError): + with pytest.raises(TypeError): conc2.evaluate() # trying to concatenate non-pybamm symbols - with self.assertRaises(TypeError): + with pytest.raises(TypeError): pybamm.concatenation(1, 2) # concatenation of length 0 - with self.assertRaisesRegex(ValueError, "Cannot create empty concatenation"): + with pytest.raises(ValueError, match="Cannot create empty concatenation"): pybamm.concatenation() # concatenation of lenght 1 - self.assertEqual(pybamm.concatenation(a), a) + assert pybamm.concatenation(a) == a a = pybamm.Variable("a", domain="test a") b = pybamm.Variable("b", domain="test b") - with self.assertRaisesRegex(TypeError, "ConcatenationVariable"): + with pytest.raises(TypeError, match="ConcatenationVariable"): pybamm.Concatenation(a, b) # base concatenation jacobian a = pybamm.Symbol("a", domain="test a") b = pybamm.Symbol("b", domain="test b") conc3 = pybamm.Concatenation(a, b) - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): conc3._concatenation_jac(None) def test_concatenation_domains(self): @@ -60,14 +60,16 @@ def test_concatenation_domains(self): b = pybamm.Symbol("b", domain=["separator", "positive electrode"]) c = pybamm.Symbol("c", domain=["test"]) conc = pybamm.concatenation(a, b, c) - self.assertEqual( - conc.domain, - ["negative electrode", "separator", "positive electrode", "test"], - ) + assert conc.domain == [ + "negative electrode", + "separator", + "positive electrode", + "test", + ] # Can't concatenate nodes with overlapping domains d = pybamm.Symbol("d", domain=["separator"]) - with self.assertRaises(pybamm.DomainError): + with pytest.raises(pybamm.DomainError): pybamm.concatenation(a, b, d) def test_concatenation_auxiliary_domains(self): @@ -94,8 +96,9 @@ def test_concatenation_auxiliary_domains(self): c = pybamm.Symbol( "c", domain=["test"], auxiliary_domains={"secondary": "something else"} ) - with self.assertRaisesRegex( - pybamm.DomainError, "children must have same or empty auxiliary domains" + with pytest.raises( + pybamm.DomainError, + match="children must have same or empty auxiliary domains", ): pybamm.concatenation(a, b, c) @@ -104,38 +107,38 @@ def test_concatenations_scale(self): b = pybamm.Variable("b", domain="test b") conc = pybamm.concatenation(a, b) - self.assertEqual(conc.scale, 1) - self.assertEqual(conc.reference, 0) + assert conc.scale == 1 + assert conc.reference == 0 a._scale = 2 - with self.assertRaisesRegex( - ValueError, "Cannot concatenate symbols with different scales" + with pytest.raises( + ValueError, match="Cannot concatenate symbols with different scales" ): pybamm.concatenation(a, b) b._scale = 2 conc = pybamm.concatenation(a, b) - self.assertEqual(conc.scale, 2) + assert conc.scale == 2 a._reference = 3 - with self.assertRaisesRegex( - ValueError, "Cannot concatenate symbols with different references" + with pytest.raises( + ValueError, match="Cannot concatenate symbols with different references" ): pybamm.concatenation(a, b) b._reference = 3 conc = pybamm.concatenation(a, b) - self.assertEqual(conc.reference, 3) + assert conc.reference == 3 a.bounds = (0, 1) - with self.assertRaisesRegex( - ValueError, "Cannot concatenate symbols with different bounds" + with pytest.raises( + ValueError, match="Cannot concatenate symbols with different bounds" ): pybamm.concatenation(a, b) b.bounds = (0, 1) conc = pybamm.concatenation(a, b) - self.assertEqual(conc.bounds, (0, 1)) + assert conc.bounds == (0, 1) def test_concatenation_simplify(self): # Primary broadcast @@ -145,11 +148,13 @@ def test_concatenation_simplify(self): c = pybamm.PrimaryBroadcast(var, "positive electrode") concat = pybamm.concatenation(a, b, c) - self.assertIsInstance(concat, pybamm.PrimaryBroadcast) - self.assertEqual(concat.orphans[0], var) - self.assertEqual( - concat.domain, ["negative electrode", "separator", "positive electrode"] - ) + assert isinstance(concat, pybamm.PrimaryBroadcast) + assert concat.orphans[0] == var + assert concat.domain == [ + "negative electrode", + "separator", + "positive electrode", + ] # Full broadcast a = pybamm.FullBroadcast(0, "negative electrode", "current collector") @@ -157,8 +162,8 @@ def test_concatenation_simplify(self): c = pybamm.FullBroadcast(0, "positive electrode", "current collector") concat = pybamm.concatenation(a, b, c) - self.assertIsInstance(concat, pybamm.FullBroadcast) - self.assertEqual(concat.orphans[0], pybamm.Scalar(0)) + assert isinstance(concat, pybamm.FullBroadcast) + assert concat.orphans[0] == pybamm.Scalar(0) assert_domain_equal( concat.domains, { @@ -184,7 +189,7 @@ def test_numpy_concatenation_vectors(self): np.testing.assert_array_equal(conc.evaluate(None, y), y) # empty concatenation conc = pybamm.NumpyConcatenation() - self.assertEqual(conc._concatenation_jac(None), 0) + assert conc._concatenation_jac(None) == 0 def test_numpy_concatenation_vector_scalar(self): # with entries @@ -217,17 +222,14 @@ def test_domain_concatenation_domains(self): a = pybamm.Symbol("a", domain=["negative electrode"]) b = pybamm.Symbol("b", domain=["separator", "positive electrode"]) conc = pybamm.DomainConcatenation([a, b], mesh) - self.assertEqual( - conc.domain, - [ - "negative electrode", - "separator", - "positive electrode", - ], - ) + assert conc.domain == [ + "negative electrode", + "separator", + "positive electrode", + ] conc.secondary_dimensions_npts = 2 - with self.assertRaisesRegex(ValueError, "Concatenation and children must have"): + with pytest.raises(ValueError, match="Concatenation and children must have"): conc.create_slices(None) def test_concatenation_orphans(self): @@ -238,15 +240,15 @@ def test_concatenation_orphans(self): a_new, b_new, c_new = conc.orphans # We should be able to manipulate the children without TreeErrors - self.assertIsInstance(2 * a_new, pybamm.Multiplication) - self.assertIsInstance(3 + b_new, pybamm.Addition) - self.assertIsInstance(4 - c_new, pybamm.Subtraction) + assert isinstance(2 * a_new, pybamm.Multiplication) + assert isinstance(3 + b_new, pybamm.Addition) + assert isinstance(4 - c_new, pybamm.Subtraction) # ids should stay the same - self.assertEqual(a, a_new) - self.assertEqual(b, b_new) - self.assertEqual(c, c_new) - self.assertEqual(conc, pybamm.concatenation(a_new, b_new, c_new)) + assert a == a_new + assert b == b_new + assert c == c_new + assert conc == pybamm.concatenation(a_new, b_new, c_new) def test_broadcast_and_concatenate(self): # create discretisation @@ -259,12 +261,10 @@ def test_broadcast_and_concatenate(self): c = pybamm.PrimaryBroadcast(3, ["positive electrode"]) conc = pybamm.concatenation(a, b, c) - self.assertEqual( - conc.domain, ["negative electrode", "separator", "positive electrode"] - ) - self.assertEqual(conc.children[0].domain, ["negative electrode"]) - self.assertEqual(conc.children[1].domain, ["separator"]) - self.assertEqual(conc.children[2].domain, ["positive electrode"]) + assert conc.domain == ["negative electrode", "separator", "positive electrode"] + assert conc.children[0].domain == ["negative electrode"] + assert conc.children[1].domain == ["separator"] + assert conc.children[2].domain == ["positive electrode"] processed_conc = disc.process_symbol(conc) np.testing.assert_array_equal( processed_conc.evaluate(), @@ -283,12 +283,10 @@ def test_broadcast_and_concatenate(self): c_t = pybamm.PrimaryBroadcast(3 * pybamm.t, ["positive electrode"]) conc = pybamm.concatenation(a_t, b_t, c_t) - self.assertEqual( - conc.domain, ["negative electrode", "separator", "positive electrode"] - ) - self.assertEqual(conc.children[0].domain, ["negative electrode"]) - self.assertEqual(conc.children[1].domain, ["separator"]) - self.assertEqual(conc.children[2].domain, ["positive electrode"]) + assert conc.domain == ["negative electrode", "separator", "positive electrode"] + assert conc.children[0].domain == ["negative electrode"] + assert conc.children[1].domain == ["separator"] + assert conc.children[2].domain == ["positive electrode"] processed_conc = disc.process_symbol(conc) np.testing.assert_array_equal( @@ -312,12 +310,10 @@ def test_broadcast_and_concatenate(self): ) conc = pybamm.concatenation(a_sv, b_sv, c_sv) - self.assertEqual( - conc.domain, ["negative electrode", "separator", "positive electrode"] - ) - self.assertEqual(conc.children[0].domain, ["negative electrode"]) - self.assertEqual(conc.children[1].domain, ["separator"]) - self.assertEqual(conc.children[2].domain, ["positive electrode"]) + assert conc.domain == ["negative electrode", "separator", "positive electrode"] + assert conc.children[0].domain == ["negative electrode"] + assert conc.children[1].domain == ["separator"] + assert conc.children[2].domain == ["positive electrode"] processed_conc = disc.process_symbol(conc) y = np.array([1, 2, 3]) @@ -335,12 +331,10 @@ def test_broadcast_and_concatenate(self): # Mixed conc = pybamm.concatenation(a, b_t, c_sv) - self.assertEqual( - conc.domain, ["negative electrode", "separator", "positive electrode"] - ) - self.assertEqual(conc.children[0].domain, ["negative electrode"]) - self.assertEqual(conc.children[1].domain, ["separator"]) - self.assertEqual(conc.children[2].domain, ["positive electrode"]) + assert conc.domain == ["negative electrode", "separator", "positive electrode"] + assert conc.children[0].domain == ["negative electrode"] + assert conc.children[1].domain == ["separator"] + assert conc.children[2].domain == ["positive electrode"] processed_conc = disc.process_symbol(conc) np.testing.assert_array_equal( @@ -357,8 +351,8 @@ def test_broadcast_and_concatenate(self): def test_domain_error(self): a = pybamm.Symbol("a") b = pybamm.Symbol("b") - with self.assertRaisesRegex( - pybamm.DomainError, "Cannot concatenate child 'a' with empty domain" + with pytest.raises( + pybamm.DomainError, match="Cannot concatenate child 'a' with empty domain" ): pybamm.DomainConcatenation([a, b], None) @@ -366,10 +360,9 @@ def test_numpy_concatenation(self): a = pybamm.Variable("a") b = pybamm.Variable("b") c = pybamm.Variable("c") - self.assertEqual( - pybamm.numpy_concatenation(pybamm.numpy_concatenation(a, b), c), - pybamm.NumpyConcatenation(a, b, c), - ) + assert pybamm.numpy_concatenation( + pybamm.numpy_concatenation(a, b), c + ) == pybamm.NumpyConcatenation(a, b, c) def test_to_equation(self): a = pybamm.Symbol("a", domain="test a") @@ -379,10 +372,10 @@ def test_to_equation(self): # Test print_name func = pybamm.Concatenation(a, b) func.print_name = "test" - self.assertEqual(func.to_equation(), sympy.Symbol("test")) + assert func.to_equation() == sympy.Symbol("test") # Test concat_sym - self.assertEqual(pybamm.Concatenation(a, b).to_equation(), func_symbol) + assert pybamm.Concatenation(a, b).to_equation() == func_symbol def test_to_from_json(self): # test DomainConcatenation @@ -416,16 +409,13 @@ def test_to_from_json(self): "secondary_dimensions_npts": 1, } - self.assertEqual( - conc.to_json(), - json_dict, - ) + assert conc.to_json() == json_dict # manually add children json_dict["children"] = [a, b] # check symbol re-creation - self.assertEqual(pybamm.pybamm.DomainConcatenation._from_json(json_dict), conc) + assert pybamm.pybamm.DomainConcatenation._from_json(json_dict) == conc # ----------------------------- # test NumpyConcatenation ----- @@ -449,20 +439,10 @@ def test_to_from_json(self): } # test to_json - self.assertEqual(conc_np.to_json(), np_json) + assert conc_np.to_json() == np_json # add children np_json["children"] = [a_np, b_np, c_np] # test _from_json - self.assertEqual(pybamm.NumpyConcatenation._from_json(np_json), conc_np) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() + assert pybamm.NumpyConcatenation._from_json(np_json) == conc_np diff --git a/tests/unit/test_expression_tree/test_functions.py b/tests/unit/test_expression_tree/test_functions.py index 4d401d74bc..5f5324c0ae 100644 --- a/tests/unit/test_expression_tree/test_functions.py +++ b/tests/unit/test_expression_tree/test_functions.py @@ -2,7 +2,7 @@ # Tests for the Function classes # -import unittest +import pytest import unittest.mock as mock import numpy as np @@ -16,29 +16,29 @@ ) -class TestFunction(unittest.TestCase): +class TestFunction: def test_number_input(self): # with numbers log = pybamm.Function(np.log, 10) - self.assertIsInstance(log.children[0], pybamm.Scalar) - self.assertEqual(log.evaluate(), np.log(10)) + assert isinstance(log.children[0], pybamm.Scalar) + assert log.evaluate() == np.log(10) summ = pybamm.Function(multi_var_function_test, 1, 2) - self.assertIsInstance(summ.children[0], pybamm.Scalar) - self.assertIsInstance(summ.children[1], pybamm.Scalar) - self.assertEqual(summ.evaluate(), 3) + assert isinstance(summ.children[0], pybamm.Scalar) + assert isinstance(summ.children[1], pybamm.Scalar) + assert summ.evaluate() == 3 def test_function_of_one_variable(self): a = pybamm.Symbol("a") funca = pybamm.Function(function_test, a) - self.assertEqual(funca.name, "function (function_test)") - self.assertEqual(str(funca), "function_test(a)") - self.assertEqual(funca.children[0].name, a.name) + assert funca.name == "function (function_test)" + assert str(funca) == "function_test(a)" + assert funca.children[0].name == a.name b = pybamm.Scalar(1) sina = pybamm.Function(np.sin, b) - self.assertEqual(sina.evaluate(), np.sin(1)) - self.assertEqual(sina.name, f"function ({np.sin.__name__})") + assert sina.evaluate() == np.sin(1) + assert sina.name == f"function ({np.sin.__name__})" c = pybamm.Vector(np.linspace(0, 1)) cosb = pybamm.Function(np.cos, c) @@ -52,20 +52,21 @@ def test_function_of_one_variable(self): def test_diff(self): a = pybamm.StateVector(slice(0, 1)) func = pybamm.Function(function_test, a) - with self.assertRaisesRegex( - NotImplementedError, "Derivative of base Function class is not implemented" + with pytest.raises( + NotImplementedError, + match="Derivative of base Function class is not implemented", ): func.diff(a) def test_exceptions(self): a = pybamm.Variable("a", domain="something") b = pybamm.Variable("b", domain="something else") - with self.assertRaises(pybamm.DomainError): + with pytest.raises(pybamm.DomainError): pybamm.Function(multi_var_function_test, a, b) def test_function_unnamed(self): fun = pybamm.Function(np.cos, pybamm.t) - self.assertEqual(fun.name, "function (cos)") + assert fun.name == "function (cos)" def test_to_equation(self): a = pybamm.Symbol("a", domain="test") @@ -73,224 +74,216 @@ def test_to_equation(self): # Test print_name func = pybamm.Arcsinh(a) func.print_name = "test" - self.assertEqual(func.to_equation(), sympy.Symbol("test")) + assert func.to_equation() == sympy.Symbol("test") # Test Arcsinh - self.assertEqual(pybamm.Arcsinh(a).to_equation(), sympy.asinh("a")) + assert pybamm.Arcsinh(a).to_equation() == sympy.asinh("a") # Test Arctan - self.assertEqual(pybamm.Arctan(a).to_equation(), sympy.atan("a")) + assert pybamm.Arctan(a).to_equation() == sympy.atan("a") # Test Exp - self.assertEqual(pybamm.Exp(a).to_equation(), sympy.exp("a")) + assert pybamm.Exp(a).to_equation() == sympy.exp("a") # Test log value = 54.0 - self.assertEqual(pybamm.Log(value).to_equation(), sympy.log(value)) + assert pybamm.Log(value).to_equation() == sympy.log(value) # Test sinh - self.assertEqual(pybamm.Sinh(a).to_equation(), sympy.sinh("a")) + assert pybamm.Sinh(a).to_equation() == sympy.sinh("a") # Test Function value = 10 - self.assertEqual(pybamm.Function(np.log, value).to_equation(), value) + assert pybamm.Function(np.log, value).to_equation() == value def test_to_from_json_error(self): a = pybamm.Symbol("a") funca = pybamm.Function(function_test, a) - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): funca.to_json() - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): pybamm.Function._from_json({}) -class TestSpecificFunctions(unittest.TestCase): - def test_to_json(self): +class TestSpecificFunctions: + def test_to_json(self, mocker): a = pybamm.InputParameter("a") fun = pybamm.cos(a) expected_json = { "name": "function (cos)", - "id": mock.ANY, + "id": mocker.ANY, "function": "cos", } - self.assertEqual(fun.to_json(), expected_json) + assert fun.to_json() == expected_json - def test_arcsinh(self): + def test_arcsinh(self, mocker): a = pybamm.InputParameter("a") fun = pybamm.arcsinh(a) - self.assertIsInstance(fun, pybamm.Arcsinh) - self.assertEqual(fun.evaluate(inputs={"a": 3}), np.arcsinh(3)) + assert isinstance(fun, pybamm.Arcsinh) + assert fun.evaluate(inputs={"a": 3}) == np.arcsinh(3) h = 0.0000001 - self.assertAlmostEqual( - fun.diff(a).evaluate(inputs={"a": 3}), + assert fun.diff(a).evaluate(inputs={"a": 3}) == pytest.approx( ( pybamm.arcsinh(pybamm.Scalar(3 + h)).evaluate() - fun.evaluate(inputs={"a": 3}) ) / h, - places=5, + abs=1e-05, ) # Test broadcast gets switched broad_a = pybamm.PrimaryBroadcast(a, "test") fun_broad = pybamm.arcsinh(broad_a) - self.assertEqual(fun_broad, pybamm.PrimaryBroadcast(fun, "test")) + assert fun_broad == pybamm.PrimaryBroadcast(fun, "test") broad_a = pybamm.FullBroadcast(a, "test", "test2") fun_broad = pybamm.arcsinh(broad_a) - self.assertEqual(fun_broad, pybamm.FullBroadcast(fun, "test", "test2")) + assert fun_broad == pybamm.FullBroadcast(fun, "test", "test2") # Test recursion broad_a = pybamm.PrimaryBroadcast(pybamm.PrimaryBroadcast(a, "test"), "test2") fun_broad = pybamm.arcsinh(broad_a) - self.assertEqual( - fun_broad, - pybamm.PrimaryBroadcast(pybamm.PrimaryBroadcast(fun, "test"), "test2"), + assert fun_broad == pybamm.PrimaryBroadcast( + pybamm.PrimaryBroadcast(fun, "test"), "test2" ) # test creation from json input_json = { "name": "arcsinh", - "id": mock.ANY, + "id": mocker.ANY, "function": "arcsinh", "children": [a], } - self.assertEqual(pybamm.Arcsinh._from_json(input_json), fun) + assert pybamm.Arcsinh._from_json(input_json) == fun - def test_arctan(self): + def test_arctan(self, mocker): a = pybamm.InputParameter("a") fun = pybamm.arctan(a) - self.assertIsInstance(fun, pybamm.Arctan) - self.assertEqual(fun.evaluate(inputs={"a": 3}), np.arctan(3)) + assert isinstance(fun, pybamm.Arctan) + assert fun.evaluate(inputs={"a": 3}) == np.arctan(3) h = 0.0000001 - self.assertAlmostEqual( - fun.diff(a).evaluate(inputs={"a": 3}), + assert fun.diff(a).evaluate(inputs={"a": 3}) == pytest.approx( ( pybamm.arctan(pybamm.Scalar(3 + h)).evaluate() - fun.evaluate(inputs={"a": 3}) ) / h, - places=5, + abs=1e-05, ) # test creation from json input_json = { "name": "arctan", - "id": mock.ANY, + "id": mocker.ANY, "function": "arctan", "children": [a], } - self.assertEqual(pybamm.Arctan._from_json(input_json), fun) + assert pybamm.Arctan._from_json(input_json) == fun - def test_cos(self): + def test_cos(self, mocker): a = pybamm.InputParameter("a") fun = pybamm.cos(a) - self.assertIsInstance(fun, pybamm.Cos) - self.assertEqual(fun.children[0], a) - self.assertEqual(fun.evaluate(inputs={"a": 3}), np.cos(3)) + assert isinstance(fun, pybamm.Cos) + assert fun.children[0] == a + assert fun.evaluate(inputs={"a": 3}) == np.cos(3) h = 0.0000001 - self.assertAlmostEqual( - fun.diff(a).evaluate(inputs={"a": 3}), + assert fun.diff(a).evaluate(inputs={"a": 3}) == pytest.approx( ( pybamm.cos(pybamm.Scalar(3 + h)).evaluate() - fun.evaluate(inputs={"a": 3}) ) / h, - places=5, + abs=1e-05, ) # test creation from json input_json = { "name": "cos", - "id": mock.ANY, + "id": mocker.ANY, "function": "cos", "children": [a], } - self.assertEqual(pybamm.Cos._from_json(input_json), fun) + assert pybamm.Cos._from_json(input_json) == fun - def test_cosh(self): + def test_cosh(self, mocker): a = pybamm.InputParameter("a") fun = pybamm.cosh(a) - self.assertIsInstance(fun, pybamm.Cosh) - self.assertEqual(fun.children[0], a) - self.assertEqual(fun.evaluate(inputs={"a": 3}), np.cosh(3)) + assert isinstance(fun, pybamm.Cosh) + assert fun.children[0] == a + assert fun.evaluate(inputs={"a": 3}) == np.cosh(3) h = 0.0000001 - self.assertAlmostEqual( - fun.diff(a).evaluate(inputs={"a": 3}), + assert fun.diff(a).evaluate(inputs={"a": 3}) == pytest.approx( ( pybamm.cosh(pybamm.Scalar(3 + h)).evaluate() - fun.evaluate(inputs={"a": 3}) ) / h, - places=5, + abs=1e-05, ) # test creation from json input_json = { "name": "cosh", - "id": mock.ANY, + "id": mocker.ANY, "function": "cosh", "children": [a], } - self.assertEqual(pybamm.Cosh._from_json(input_json), fun) + assert pybamm.Cosh._from_json(input_json) == fun - def test_exp(self): + def test_exp(self, mocker): a = pybamm.InputParameter("a") fun = pybamm.exp(a) - self.assertIsInstance(fun, pybamm.Exp) - self.assertEqual(fun.children[0], a) - self.assertEqual(fun.evaluate(inputs={"a": 3}), np.exp(3)) + assert isinstance(fun, pybamm.Exp) + assert fun.children[0] == a + assert fun.evaluate(inputs={"a": 3}) == np.exp(3) h = 0.0000001 - self.assertAlmostEqual( - fun.diff(a).evaluate(inputs={"a": 3}), + assert fun.diff(a).evaluate(inputs={"a": 3}) == pytest.approx( ( pybamm.exp(pybamm.Scalar(3 + h)).evaluate() - fun.evaluate(inputs={"a": 3}) ) / h, - places=5, + abs=1e-05, ) # test creation from json input_json = { "name": "exp", - "id": mock.ANY, + "id": mocker.ANY, "function": "exp", "children": [a], } - self.assertEqual(pybamm.Exp._from_json(input_json), fun) + assert pybamm.Exp._from_json(input_json) == fun - def test_log(self): + def test_log(self, mocker): a = pybamm.InputParameter("a") fun = pybamm.log(a) - self.assertEqual(fun.evaluate(inputs={"a": 3}), np.log(3)) + assert fun.evaluate(inputs={"a": 3}) == np.log(3) h = 0.0000001 - self.assertAlmostEqual( - fun.diff(a).evaluate(inputs={"a": 3}), + assert fun.diff(a).evaluate(inputs={"a": 3}) == pytest.approx( ( pybamm.log(pybamm.Scalar(3 + h)).evaluate() - fun.evaluate(inputs={"a": 3}) ) / h, - places=5, + abs=1e-05, ) # Base 10 fun = pybamm.log10(a) - self.assertAlmostEqual(fun.evaluate(inputs={"a": 3}), np.log10(3)) + assert fun.evaluate(inputs={"a": 3}) == pytest.approx(np.log10(3)) h = 0.0000001 - self.assertAlmostEqual( - fun.diff(a).evaluate(inputs={"a": 3}), + assert fun.diff(a).evaluate(inputs={"a": 3}) == pytest.approx( ( pybamm.log10(pybamm.Scalar(3 + h)).evaluate() - fun.evaluate(inputs={"a": 3}) ) / h, - places=5, + abs=1e-05, ) # test creation from json @@ -298,131 +291,126 @@ def test_log(self): fun = pybamm.log(a) input_json = { "name": "log", - "id": mock.ANY, + "id": mocker.ANY, "function": "log", "children": [a], } - self.assertEqual(pybamm.Log._from_json(input_json), fun) + assert pybamm.Log._from_json(input_json) == fun def test_max(self): a = pybamm.StateVector(slice(0, 3)) y_test = np.array([1, 2, 3]) fun = pybamm.max(a) - self.assertIsInstance(fun, pybamm.Function) - self.assertEqual(fun.evaluate(y=y_test), 3) + assert isinstance(fun, pybamm.Function) + assert fun.evaluate(y=y_test) == 3 def test_min(self): a = pybamm.StateVector(slice(0, 3)) y_test = np.array([1, 2, 3]) fun = pybamm.min(a) - self.assertIsInstance(fun, pybamm.Function) - self.assertEqual(fun.evaluate(y=y_test), 1) + assert isinstance(fun, pybamm.Function) + assert fun.evaluate(y=y_test) == 1 - def test_sin(self): + def test_sin(self, mocker): a = pybamm.InputParameter("a") fun = pybamm.sin(a) - self.assertIsInstance(fun, pybamm.Sin) - self.assertEqual(fun.children[0], a) - self.assertEqual(fun.evaluate(inputs={"a": 3}), np.sin(3)) + assert isinstance(fun, pybamm.Sin) + assert fun.children[0] == a + assert fun.evaluate(inputs={"a": 3}) == np.sin(3) h = 0.0000001 - self.assertAlmostEqual( - fun.diff(a).evaluate(inputs={"a": 3}), + assert fun.diff(a).evaluate(inputs={"a": 3}) == pytest.approx( ( pybamm.sin(pybamm.Scalar(3 + h)).evaluate() - fun.evaluate(inputs={"a": 3}) ) / h, - places=5, + abs=1e-05, ) # test creation from json input_json = { "name": "sin", - "id": mock.ANY, + "id": mocker.ANY, "function": "sin", "children": [a], } - self.assertEqual(pybamm.Sin._from_json(input_json), fun) + assert pybamm.Sin._from_json(input_json) == fun - def test_sinh(self): + def test_sinh(self, mocker): a = pybamm.InputParameter("a") fun = pybamm.sinh(a) - self.assertIsInstance(fun, pybamm.Sinh) - self.assertEqual(fun.children[0], a) - self.assertEqual(fun.evaluate(inputs={"a": 3}), np.sinh(3)) + assert isinstance(fun, pybamm.Sinh) + assert fun.children[0] == a + assert fun.evaluate(inputs={"a": 3}) == np.sinh(3) h = 0.0000001 - self.assertAlmostEqual( - fun.diff(a).evaluate(inputs={"a": 3}), + assert fun.diff(a).evaluate(inputs={"a": 3}) == pytest.approx( ( pybamm.sinh(pybamm.Scalar(3 + h)).evaluate() - fun.evaluate(inputs={"a": 3}) ) / h, - places=5, + abs=1e-05, ) # test creation from json input_json = { "name": "sinh", - "id": mock.ANY, + "id": mocker.ANY, "function": "sinh", "children": [a], } - self.assertEqual(pybamm.Sinh._from_json(input_json), fun) + assert pybamm.Sinh._from_json(input_json) == fun - def test_sqrt(self): + def test_sqrt(self, mocker): a = pybamm.InputParameter("a") fun = pybamm.sqrt(a) - self.assertIsInstance(fun, pybamm.Sqrt) - self.assertEqual(fun.evaluate(inputs={"a": 3}), np.sqrt(3)) + assert isinstance(fun, pybamm.Sqrt) + assert fun.evaluate(inputs={"a": 3}) == np.sqrt(3) h = 0.0000001 - self.assertAlmostEqual( - fun.diff(a).evaluate(inputs={"a": 3}), + assert fun.diff(a).evaluate(inputs={"a": 3}) == pytest.approx( ( pybamm.sqrt(pybamm.Scalar(3 + h)).evaluate() - fun.evaluate(inputs={"a": 3}) ) / h, - places=5, + abs=1e-05, ) # test creation from json input_json = { "name": "sqrt", - "id": mock.ANY, + "id": mocker.ANY, "function": "sqrt", "children": [a], } - self.assertEqual(pybamm.Sqrt._from_json(input_json), fun) + assert pybamm.Sqrt._from_json(input_json) == fun def test_tanh(self): a = pybamm.InputParameter("a") fun = pybamm.tanh(a) - self.assertEqual(fun.evaluate(inputs={"a": 3}), np.tanh(3)) + assert fun.evaluate(inputs={"a": 3}) == np.tanh(3) h = 0.0000001 - self.assertAlmostEqual( - fun.diff(a).evaluate(inputs={"a": 3}), + assert fun.diff(a).evaluate(inputs={"a": 3}) == pytest.approx( ( pybamm.tanh(pybamm.Scalar(3 + h)).evaluate() - fun.evaluate(inputs={"a": 3}) ) / h, - places=5, + abs=1e-05, ) def test_erf(self): a = pybamm.InputParameter("a") fun = pybamm.erf(a) - self.assertEqual(fun.evaluate(inputs={"a": 3}), special.erf(3)) + assert fun.evaluate(inputs={"a": 3}) == special.erf(3) h = 0.0000001 - self.assertAlmostEqual( - fun.diff(a).evaluate(inputs={"a": 3}), + assert fun.diff(a).evaluate(inputs={"a": 3}) == pytest.approx( ( pybamm.erf(pybamm.Scalar(3 + h)).evaluate() - fun.evaluate(inputs={"a": 3}) ) / h, - places=5, + abs=1e-05, ) # test creation from json @@ -432,48 +420,45 @@ def test_erf(self): "function": "erf", "children": [a], } - self.assertEqual(pybamm.Erf._from_json(input_json), fun) + assert pybamm.Erf._from_json(input_json) == fun def test_erfc(self): a = pybamm.InputParameter("a") fun = pybamm.erfc(a) - self.assertAlmostEqual( - fun.evaluate(inputs={"a": 3}), special.erfc(3), places=15 + assert fun.evaluate(inputs={"a": 3}) == pytest.approx( + special.erfc(3), abs=1e-15 ) h = 0.0000001 - self.assertAlmostEqual( - fun.diff(a).evaluate(inputs={"a": 3}), + assert fun.diff(a).evaluate(inputs={"a": 3}) == pytest.approx( ( pybamm.erfc(pybamm.Scalar(3 + h)).evaluate() - fun.evaluate(inputs={"a": 3}) ) / h, - places=5, + abs=1e-05, ) -class TestNonObjectFunctions(unittest.TestCase): +class TestNonObjectFunctions: def test_normal_pdf(self): x = pybamm.InputParameter("x") mu = pybamm.InputParameter("mu") sigma = pybamm.InputParameter("sigma") fun = pybamm.normal_pdf(x, mu, sigma) - self.assertEqual( - fun.evaluate(inputs={"x": 0, "mu": 0, "sigma": 1}), 1 / np.sqrt(2 * np.pi) + assert fun.evaluate(inputs={"x": 0, "mu": 0, "sigma": 1}) == 1 / np.sqrt( + 2 * np.pi ) - self.assertEqual( - fun.evaluate(inputs={"x": 2, "mu": 2, "sigma": 10}), - 1 / np.sqrt(2 * np.pi) / 10, + assert ( + fun.evaluate(inputs={"x": 2, "mu": 2, "sigma": 10}) + == 1 / np.sqrt(2 * np.pi) / 10 ) - self.assertAlmostEqual(fun.evaluate(inputs={"x": 100, "mu": 0, "sigma": 1}), 0) - self.assertAlmostEqual(fun.evaluate(inputs={"x": -100, "mu": 0, "sigma": 1}), 0) - self.assertGreater( - fun.evaluate(inputs={"x": 1, "mu": 0, "sigma": 1}), - fun.evaluate(inputs={"x": 1, "mu": 0, "sigma": 2}), + assert fun.evaluate(inputs={"x": 100, "mu": 0, "sigma": 1}) == pytest.approx(0) + assert fun.evaluate(inputs={"x": -100, "mu": 0, "sigma": 1}) == pytest.approx(0) + assert fun.evaluate(inputs={"x": 1, "mu": 0, "sigma": 1}) > fun.evaluate( + inputs={"x": 1, "mu": 0, "sigma": 2} ) - self.assertGreater( - fun.evaluate(inputs={"x": -1, "mu": 0, "sigma": 1}), - fun.evaluate(inputs={"x": -1, "mu": 0, "sigma": 2}), + assert fun.evaluate(inputs={"x": -1, "mu": 0, "sigma": 1}) > fun.evaluate( + inputs={"x": -1, "mu": 0, "sigma": 2} ) def test_normal_cdf(self): @@ -481,25 +466,13 @@ def test_normal_cdf(self): mu = pybamm.InputParameter("mu") sigma = pybamm.InputParameter("sigma") fun = pybamm.normal_cdf(x, mu, sigma) - self.assertEqual(fun.evaluate(inputs={"x": 0, "mu": 0, "sigma": 1}), 0.5) - self.assertEqual(fun.evaluate(inputs={"x": 2, "mu": 2, "sigma": 10}), 0.5) - self.assertAlmostEqual(fun.evaluate(inputs={"x": 100, "mu": 0, "sigma": 1}), 1) - self.assertAlmostEqual(fun.evaluate(inputs={"x": -100, "mu": 0, "sigma": 1}), 0) - self.assertGreater( - fun.evaluate(inputs={"x": 1, "mu": 0, "sigma": 1}), - fun.evaluate(inputs={"x": 1, "mu": 0, "sigma": 2}), + assert fun.evaluate(inputs={"x": 0, "mu": 0, "sigma": 1}) == 0.5 + assert fun.evaluate(inputs={"x": 2, "mu": 2, "sigma": 10}) == 0.5 + assert fun.evaluate(inputs={"x": 100, "mu": 0, "sigma": 1}) == pytest.approx(1) + assert fun.evaluate(inputs={"x": -100, "mu": 0, "sigma": 1}) == pytest.approx(0) + assert fun.evaluate(inputs={"x": 1, "mu": 0, "sigma": 1}) > fun.evaluate( + inputs={"x": 1, "mu": 0, "sigma": 2} ) - self.assertLess( - fun.evaluate(inputs={"x": -1, "mu": 0, "sigma": 1}), - fun.evaluate(inputs={"x": -1, "mu": 0, "sigma": 2}), + assert fun.evaluate(inputs={"x": -1, "mu": 0, "sigma": 1}) < fun.evaluate( + inputs={"x": -1, "mu": 0, "sigma": 2} ) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_expression_tree/test_interpolant.py b/tests/unit/test_expression_tree/test_interpolant.py index e40b9daab0..e649da50c4 100644 --- a/tests/unit/test_expression_tree/test_interpolant.py +++ b/tests/unit/test_expression_tree/test_interpolant.py @@ -4,42 +4,41 @@ import pybamm -import unittest -import unittest.mock as mock +import pytest import numpy as np -class TestInterpolant(unittest.TestCase): +class TestInterpolant: def test_errors(self): - with self.assertRaisesRegex(ValueError, "x1"): + with pytest.raises(ValueError, match="x1"): pybamm.Interpolant(np.ones(10), np.ones(11), pybamm.Symbol("a")) - with self.assertRaisesRegex(ValueError, "x2"): + with pytest.raises(ValueError, match="x2"): pybamm.Interpolant( (np.ones(10), np.ones(11)), np.ones((10, 12)), pybamm.Symbol("a") ) - with self.assertRaisesRegex(ValueError, "x1"): + with pytest.raises(ValueError, match="x1"): pybamm.Interpolant( (np.ones(11), np.ones(12)), np.ones((10, 12)), pybamm.Symbol("a") ) - with self.assertRaisesRegex(ValueError, "y should"): + with pytest.raises(ValueError, match="y should"): pybamm.Interpolant( (np.ones(10), np.ones(11)), np.ones(10), pybamm.Symbol("a") ) - with self.assertRaisesRegex(ValueError, "interpolator 'bla' not recognised"): + with pytest.raises(ValueError, match="interpolator 'bla' not recognised"): pybamm.Interpolant( np.ones(10), np.ones(10), pybamm.Symbol("a"), interpolator="bla" ) - with self.assertRaisesRegex(ValueError, "child should have size 1"): + with pytest.raises(ValueError, match="child should have size 1"): pybamm.Interpolant( np.ones(10), np.ones((10, 11)), pybamm.StateVector(slice(0, 2)) ) - with self.assertRaisesRegex(ValueError, "should equal"): + with pytest.raises(ValueError, match="should equal"): pybamm.Interpolant( (np.ones(12), np.ones(10)), np.ones((10, 12)), pybamm.Symbol("a") ) - with self.assertRaisesRegex( - ValueError, "len\\(x\\) should equal len\\(children\\)" + with pytest.raises( + ValueError, match="len\\(x\\) should equal len\\(children\\)" ): pybamm.Interpolant( (np.ones(10), np.ones(12)), np.ones((10, 12)), pybamm.Symbol("a") @@ -74,7 +73,7 @@ def test_interpolation(self): def test_interpolation_float(self): x = np.linspace(0, 1, 200) interp = pybamm.Interpolant(x, 2 * x, 0.5) - self.assertEqual(interp.evaluate(), 1) + assert interp.evaluate() == 1 def test_interpolation_1_x_2d_y(self): x = np.linspace(0, 1, 200) @@ -137,25 +136,25 @@ def f(x, y): # Test raising error if data is not 2D data_3d = np.zeros((11, 22, 33)) - with self.assertRaisesRegex(ValueError, "y should be two-dimensional"): + with pytest.raises(ValueError, match="y should be two-dimensional"): interp = pybamm.Interpolant( x_in, data_3d, (var1, var2), interpolator="linear" ) # Test raising error if wrong shapes - with self.assertRaisesRegex(ValueError, "x1.shape"): + with pytest.raises(ValueError, match="x1.shape"): interp = pybamm.Interpolant( x_in, np.zeros((12, 22)), (var1, var2), interpolator="linear" ) - with self.assertRaisesRegex(ValueError, "x2.shape"): + with pytest.raises(ValueError, match="x2.shape"): interp = pybamm.Interpolant( x_in, np.zeros((11, 23)), (var1, var2), interpolator="linear" ) # Raise error if not linear - with self.assertRaisesRegex( - ValueError, "interpolator should be 'linear' or 'cubic'" + with pytest.raises( + ValueError, match="interpolator should be 'linear' or 'cubic'" ): interp = pybamm.Interpolant(x_in, data, (var1, var2), interpolator="pchip") @@ -180,7 +179,7 @@ def f(x, y): value = interp._function_evaluate(evaluated_children) # Test evaluation fails with different child shapes - with self.assertRaisesRegex(ValueError, "All children must"): + with pytest.raises(ValueError, match="All children must"): evaluated_children = [np.array([[1, 1]]), np.array([7])] value = interp._function_evaluate(evaluated_children) @@ -193,11 +192,11 @@ def f(x, y): evaluated_children = [np.array([[1, 1]]), np.array([[7, 7]])] value = interp._function_evaluate(evaluated_children) - self.assertEqual(value.shape, evaluated_children[0].shape) + assert value.shape == evaluated_children[0].shape evaluated_children = [np.array([[1, 1], [1, 1]]), np.array([[7, 7], [7, 7]])] value = interp._function_evaluate(evaluated_children) - self.assertEqual(value.shape, evaluated_children[0].shape) + assert value.shape == evaluated_children[0].shape def test_interpolation_3_x(self): def f(x, y, z): @@ -235,30 +234,30 @@ def f(x, y, z): # Test raising error if data is not 3D data_4d = np.zeros((11, 22, 33, 5)) - with self.assertRaisesRegex(ValueError, "y should be three-dimensional"): + with pytest.raises(ValueError, match="y should be three-dimensional"): interp = pybamm.Interpolant( x_in, data_4d, (var1, var2, var3), interpolator="linear" ) # Test raising error if wrong shapes - with self.assertRaisesRegex(ValueError, "x1.shape"): + with pytest.raises(ValueError, match="x1.shape"): interp = pybamm.Interpolant( x_in, np.zeros((12, 22, 33)), (var1, var2, var3), interpolator="linear" ) - with self.assertRaisesRegex(ValueError, "x2.shape"): + with pytest.raises(ValueError, match="x2.shape"): interp = pybamm.Interpolant( x_in, np.zeros((11, 23, 33)), (var1, var2, var3), interpolator="linear" ) - with self.assertRaisesRegex(ValueError, "x3.shape"): + with pytest.raises(ValueError, match="x3.shape"): interp = pybamm.Interpolant( x_in, np.zeros((11, 22, 34)), (var1, var2, var3), interpolator="linear" ) # Raise error if not linear - with self.assertRaisesRegex( - ValueError, "interpolator should be 'linear' or 'cubic'" + with pytest.raises( + ValueError, match="interpolator should be 'linear' or 'cubic'" ): interp = pybamm.Interpolant( x_in, data, (var1, var2, var3), interpolator="pchip" @@ -287,7 +286,7 @@ def f(x, y, z): value = interp._function_evaluate(evaluated_children) # Test evaluation fails with different child shapes - with self.assertRaisesRegex(ValueError, "All children must"): + with pytest.raises(ValueError, match="All children must"): evaluated_children = [np.array([[1, 1]]), np.ones(()) * 4, np.array([[7]])] value = interp._function_evaluate(evaluated_children) @@ -299,9 +298,9 @@ def test_name(self): a = pybamm.Symbol("a") x = np.linspace(0, 1, 200) interp = pybamm.Interpolant(x, x, a, "name") - self.assertEqual(interp.name, "name") + assert interp.name == "name" interp = pybamm.Interpolant(x, x, a) - self.assertEqual(interp.name, "interpolating_function") + assert interp.name == "interpolating_function" def test_diff(self): x = np.linspace(0, 1, 200) @@ -334,9 +333,9 @@ def test_diff(self): var2 = pybamm.StateVector(slice(1, 2)) # linear interp = pybamm.Interpolant(x, z, (var1, var2), interpolator="linear") - with self.assertRaisesRegex( + with pytest.raises( NotImplementedError, - "differentiation not implemented for functions with more than one child", + match="differentiation not implemented for functions with more than one child", ): interp.diff(var1) @@ -345,16 +344,16 @@ def test_processing(self): y = pybamm.StateVector(slice(0, 2)) interp = pybamm.Interpolant(x, 2 * x, y) - self.assertEqual(interp, interp.create_copy()) + assert interp == interp.create_copy() - def test_to_from_json(self): + def test_to_from_json(self, mocker): x = np.linspace(0, 1, 10) y = pybamm.StateVector(slice(0, 2)) interp = pybamm.Interpolant(x, 2 * x, y) expected_json = { "name": "interpolating_function", - "id": mock.ANY, + "id": mocker.ANY, "x": [ [ 0.0, @@ -387,11 +386,11 @@ def test_to_from_json(self): } # check correct writing to json - self.assertEqual(interp.to_json(), expected_json) + assert interp.to_json() == expected_json expected_json["children"] = [y] # check correct re-creation - self.assertEqual(pybamm.Interpolant._from_json(expected_json), interp) + assert pybamm.Interpolant._from_json(expected_json) == interp # test to_from_json for 2d x & y x = (np.arange(-5.01, 5.01, 0.05), np.arange(-5.01, 5.01, 0.01)) @@ -405,14 +404,4 @@ def test_to_from_json(self): interp2d_json = interp.to_json() interp2d_json["children"] = (var1, var2) - self.assertEqual(pybamm.Interpolant._from_json(interp2d_json), interp) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() + assert pybamm.Interpolant._from_json(interp2d_json) == interp diff --git a/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py b/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py index e3301fdcc3..cb15d5148c 100644 --- a/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py +++ b/tests/unit/test_expression_tree/test_operations/test_convert_to_casadi.py @@ -5,26 +5,24 @@ import casadi import numpy as np import pybamm -import unittest +import pytest from tests import get_mesh_for_testing, get_1p1d_discretisation_for_testing from scipy import special -class TestCasadiConverter(unittest.TestCase): +class TestCasadiConverter: def assert_casadi_equal(self, a, b, evalf=False): if evalf is True: - self.assertTrue((casadi.evalf(a) - casadi.evalf(b)).is_zero()) + assert (casadi.evalf(a) - casadi.evalf(b)).is_zero() else: - self.assertTrue((a - b).is_zero()) + assert (a - b).is_zero() def assert_casadi_almost_equal(self, a, b, decimal=7, evalf=False): tol = 1.5 * 10 ** (-decimal) if evalf is True: - self.assertTrue( - (casadi.fabs(casadi.evalf(a) - casadi.evalf(b)) < tol).is_one() - ) + assert (casadi.fabs(casadi.evalf(a) - casadi.evalf(b)) < tol).is_one() else: - self.assertTrue((casadi.fabs(a - b) < tol).is_one()) + assert (casadi.fabs(a - b) < tol).is_one() def test_convert_scalar_symbols(self): a = pybamm.Scalar(0) @@ -34,49 +32,49 @@ def test_convert_scalar_symbols(self): e = pybamm.Scalar(3) g = pybamm.Scalar(3.3) - self.assertEqual(a.to_casadi(), casadi.MX(0)) - self.assertEqual(d.to_casadi(), casadi.MX(2)) + assert a.to_casadi() == casadi.MX(0) + assert d.to_casadi() == casadi.MX(2) # negate - self.assertEqual((-b).to_casadi(), casadi.MX(-1)) + assert (-b).to_casadi() == casadi.MX(-1) # absolute value - self.assertEqual(abs(c).to_casadi(), casadi.MX(1)) + assert abs(c).to_casadi() == casadi.MX(1) # floor - self.assertEqual(pybamm.Floor(g).to_casadi(), casadi.MX(3)) + assert pybamm.Floor(g).to_casadi() == casadi.MX(3) # ceiling - self.assertEqual(pybamm.Ceiling(g).to_casadi(), casadi.MX(4)) + assert pybamm.Ceiling(g).to_casadi() == casadi.MX(4) # function def square_plus_one(x): return x**2 + 1 f = pybamm.Function(square_plus_one, b) - self.assertEqual(f.to_casadi(), 2) + assert f.to_casadi() == 2 def myfunction(x, y): return x + y f = pybamm.Function(myfunction, b, d) - self.assertEqual(f.to_casadi(), casadi.MX(3)) + assert f.to_casadi() == casadi.MX(3) # use classes to avoid simplification # addition - self.assertEqual((pybamm.Addition(a, b)).to_casadi(), casadi.MX(1)) + assert (pybamm.Addition(a, b)).to_casadi() == casadi.MX(1) # subtraction - self.assertEqual(pybamm.Subtraction(c, d).to_casadi(), casadi.MX(-3)) + assert pybamm.Subtraction(c, d).to_casadi() == casadi.MX(-3) # multiplication - self.assertEqual(pybamm.Multiplication(c, d).to_casadi(), casadi.MX(-2)) + assert pybamm.Multiplication(c, d).to_casadi() == casadi.MX(-2) # power - self.assertEqual(pybamm.Power(c, d).to_casadi(), casadi.MX(1)) + assert pybamm.Power(c, d).to_casadi() == casadi.MX(1) # division - self.assertEqual(pybamm.Division(b, d).to_casadi(), casadi.MX(1 / 2)) + assert pybamm.Division(b, d).to_casadi() == casadi.MX(1 / 2) # modulo - self.assertEqual(pybamm.Modulo(e, d).to_casadi(), casadi.MX(1)) + assert pybamm.Modulo(e, d).to_casadi() == casadi.MX(1) # minimum and maximum - self.assertEqual(pybamm.Minimum(a, b).to_casadi(), casadi.MX(0)) - self.assertEqual(pybamm.Maximum(a, b).to_casadi(), casadi.MX(1)) + assert pybamm.Minimum(a, b).to_casadi() == casadi.MX(0) + assert pybamm.Maximum(a, b).to_casadi() == casadi.MX(1) def test_convert_array_symbols(self): # Arrays @@ -93,7 +91,7 @@ def test_convert_array_symbols(self): pybamm_y_dot = pybamm.StateVectorDot(slice(0, 10)) # Time - self.assertEqual(pybamm_t.to_casadi(casadi_t, casadi_y), casadi_t) + assert pybamm_t.to_casadi(casadi_t, casadi_y) == casadi_t # State Vector self.assert_casadi_equal(pybamm_y.to_casadi(casadi_t, casadi_y), casadi_y) @@ -186,11 +184,11 @@ def test_interpolation(self): # error for pchip interpolator interp = pybamm.Interpolant(x, data, y, interpolator="pchip") - with self.assertRaisesRegex(NotImplementedError, "The interpolator"): + with pytest.raises(NotImplementedError, match="The interpolator"): interp_casadi = interp.to_casadi(y=casadi_y) # error for not recognized interpolator - with self.assertRaisesRegex(ValueError, "interpolator"): + with pytest.raises(ValueError, match="interpolator"): interp = pybamm.Interpolant(x, data, y, interpolator="idonotexist") interp_casadi = interp.to_casadi(y=casadi_y) @@ -204,7 +202,7 @@ def test_interpolation(self): x4_ = [np.linspace(0, 1) for _ in range(4)] x4 = np.column_stack(x4_) data4 = 2 * x4 # np.tile(2 * x3, (10, 1)).T - with self.assertRaisesRegex(ValueError, "Invalid dimension of x"): + with pytest.raises(ValueError, match="Invalid dimension of x"): interp = pybamm.Interpolant(x4_, data4, y4, interpolator="linear") interp_casadi = interp.to_casadi(y=casadi_y) @@ -244,7 +242,7 @@ def test_interpolation_2d(self): # np.testing.assert_array_almost_equal(interp.evaluate(y=y_test), f(y_test)) # error for pchip interpolator - with self.assertRaisesRegex(ValueError, "interpolator should be"): + with pytest.raises(ValueError, match="interpolator should be"): interp = pybamm.Interpolant(x_, Y, y, interpolator="pchip") interp_casadi = interp.to_casadi(y=casadi_y) @@ -276,7 +274,7 @@ def f(x, y, z): casadi_sol = casadi_f(y_test) true_value = f(1, 5, 8) - self.assertIsInstance(casadi_sol, casadi.DM) + assert isinstance(casadi_sol, casadi.DM) np.testing.assert_equal(true_value, casadi_sol.__float__()) @@ -347,25 +345,15 @@ def test_convert_input_parameter(self): def test_errors(self): y = pybamm.StateVector(slice(0, 10)) - with self.assertRaisesRegex( - ValueError, "Must provide a 'y' for converting state vectors" + with pytest.raises( + ValueError, match="Must provide a 'y' for converting state vectors" ): y.to_casadi() y_dot = pybamm.StateVectorDot(slice(0, 10)) - with self.assertRaisesRegex( - ValueError, "Must provide a 'y_dot' for converting state vectors" + with pytest.raises( + ValueError, match="Must provide a 'y_dot' for converting state vectors" ): y_dot.to_casadi() var = pybamm.Variable("var") - with self.assertRaisesRegex(TypeError, "Cannot convert symbol of type"): + with pytest.raises(TypeError, match="Cannot convert symbol of type"): var.to_casadi() - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_expression_tree/test_operations/test_copy.py b/tests/unit/test_expression_tree/test_operations/test_copy.py index 5c1e3b29ca..f0d59a1fe1 100644 --- a/tests/unit/test_expression_tree/test_operations/test_copy.py +++ b/tests/unit/test_expression_tree/test_operations/test_copy.py @@ -2,13 +2,13 @@ # Test for making copies # +import pytest import numpy as np import pybamm -import unittest from tests import get_mesh_for_testing -class TestCopy(unittest.TestCase): +class TestCopy: def test_symbol_create_copy(self): a = pybamm.Parameter("a") b = pybamm.Parameter("b") @@ -64,8 +64,8 @@ def test_symbol_create_copy(self): pybamm.Equality(a, b), pybamm.EvaluateAt(a, 0), ]: - self.assertEqual(symbol, symbol.create_copy()) - self.assertEqual(symbol.print_name, symbol.create_copy().print_name) + assert symbol == symbol.create_copy() + assert symbol.print_name == symbol.create_copy().print_name def test_symbol_create_copy_new_children(self): a = pybamm.Parameter("a") @@ -95,8 +95,8 @@ def test_symbol_create_copy_new_children(self): ], ): new_symbol = symbol_ab.create_copy(new_children=[b, a]) - self.assertEqual(new_symbol, symbol_ba) - self.assertEqual(new_symbol.print_name, symbol_ba.print_name) + assert new_symbol == symbol_ba + assert new_symbol.print_name == symbol_ba.print_name # unary operations for symbol_a, symbol_b in zip( @@ -120,8 +120,8 @@ def test_symbol_create_copy_new_children(self): ], ): new_symbol = symbol_a.create_copy(new_children=[b]) - self.assertEqual(new_symbol, symbol_b) - self.assertEqual(new_symbol.print_name, symbol_b.print_name) + assert new_symbol == symbol_b + assert new_symbol.print_name == symbol_b.print_name v_n = pybamm.Variable("v", "negative electrode") w_n = pybamm.Variable("w", "negative electrode") @@ -148,13 +148,12 @@ def test_symbol_create_copy_new_children(self): ], ): new_symbol = symbol_v.create_copy(new_children=[w_n]) - self.assertEqual(new_symbol, symbol_w) - self.assertEqual(new_symbol.print_name, symbol_w.print_name) + assert new_symbol == symbol_w + assert new_symbol.print_name == symbol_w.print_name - self.assertEqual( - pybamm.div(pybamm.grad(v_n)).create_copy(new_children=[pybamm.grad(w_n)]), - pybamm.div(pybamm.grad(w_n)), - ) + assert pybamm.div(pybamm.grad(v_n)).create_copy( + new_children=[pybamm.grad(w_n)] + ) == pybamm.div(pybamm.grad(w_n)) v_s = pybamm.Variable("v", "separator") mesh = get_mesh_for_testing() @@ -170,13 +169,12 @@ def test_symbol_create_copy_new_children(self): ], ): new_symbol = symbol_n.create_copy(new_children=[v_s, v_n]) - self.assertEqual(new_symbol, symbol_s) - self.assertEqual(new_symbol.print_name, symbol_s.print_name) + assert new_symbol == symbol_s + assert new_symbol.print_name == symbol_s.print_name - self.assertEqual( - pybamm.NumpyConcatenation(a, b, v_s).create_copy(new_children=[b, a, v_n]), - pybamm.NumpyConcatenation(b, a, v_n), - ) + assert pybamm.NumpyConcatenation(a, b, v_s).create_copy( + new_children=[b, a, v_n] + ) == pybamm.NumpyConcatenation(b, a, v_n) v_n_2D = pybamm.Variable( "v", @@ -193,57 +191,48 @@ def test_symbol_create_copy_new_children(self): mat = pybamm.Matrix([[1, 2], [3, 4]]) mat_b = pybamm.Matrix([[5, 6], [7, 8]]) - self.assertEqual( - pybamm.TertiaryBroadcast(v_n_2D, "current collector").create_copy( - new_children=[w_n_2D] - ), - pybamm.TertiaryBroadcast(w_n_2D, "current collector"), - ) - self.assertEqual( - pybamm.Index(vec, 1).create_copy(new_children=[vec_b]), - pybamm.Index(vec_b, 1), - ) - self.assertEqual( - pybamm.SparseStack(mat, mat).create_copy(new_children=[mat_b, mat_b]), - pybamm.SparseStack(mat_b, mat_b), + assert pybamm.TertiaryBroadcast(v_n_2D, "current collector").create_copy( + new_children=[w_n_2D] + ) == pybamm.TertiaryBroadcast(w_n_2D, "current collector") + assert pybamm.Index(vec, 1).create_copy(new_children=[vec_b]) == pybamm.Index( + vec_b, 1 ) + assert pybamm.SparseStack(mat, mat).create_copy( + new_children=[mat_b, mat_b] + ) == pybamm.SparseStack(mat_b, mat_b) def test_create_copy_new_children_binary_error(self): a = pybamm.Parameter("a") b = pybamm.Parameter("b") - with self.assertRaisesRegex(ValueError, "must have exactly two children"): + with pytest.raises(ValueError, match="must have exactly two children"): (a + b).create_copy(new_children=[a]) def test_create_copy_new_children_scalars(self): a = pybamm.Scalar(2) b = pybamm.Scalar(5) - self.assertEqual((a + b).create_copy(), a + b) + assert (a + b).create_copy() == a + b # a+b produces a scalar, not an addition object. - with self.assertRaisesRegex( - ValueError, "Cannot create a copy of a scalar with new children" + with pytest.raises( + ValueError, match="Cannot create a copy of a scalar with new children" ): (a + b).create_copy(new_children=[a, b]) - self.assertEqual(pybamm.Addition(a, b).create_copy(), pybamm.Scalar(7)) - self.assertEqual( - pybamm.Addition(a, b).create_copy(perform_simplifications=False), - pybamm.Addition(a, b), - ) + assert pybamm.Addition(a, b).create_copy() == pybamm.Scalar(7) + assert pybamm.Addition(a, b).create_copy( + perform_simplifications=False + ) == pybamm.Addition(a, b) c = pybamm.Scalar(4) d = pybamm.Scalar(8) - self.assertEqual( - pybamm.Addition(a, b).create_copy(new_children=[c, d]), pybamm.Scalar(12) - ) - self.assertEqual( - pybamm.Addition(a, b).create_copy( - new_children=[c, d], perform_simplifications=False - ), - pybamm.Addition(c, d), + assert pybamm.Addition(a, b).create_copy(new_children=[c, d]) == pybamm.Scalar( + 12 ) + assert pybamm.Addition(a, b).create_copy( + new_children=[c, d], perform_simplifications=False + ) == pybamm.Addition(c, d) def test_create_copy_new_children_unary_error(self): vec = pybamm.Vector([1, 2, 3, 4, 5]) @@ -251,7 +240,7 @@ def test_create_copy_new_children_unary_error(self): I = pybamm.Index(vec, 1) - with self.assertRaisesRegex(ValueError, "must have exactly one child"): + with pytest.raises(ValueError, match="must have exactly one child"): I.create_copy(new_children=[vec, vec_b]) def test_unary_create_copy_no_simplification(self): @@ -271,36 +260,29 @@ def test_unary_create_copy_no_simplification(self): pybamm.Sign(b), ], ): - self.assertEqual( - symbol_a.create_copy(new_children=[b], perform_simplifications=False), - symbol_b, + assert ( + symbol_a.create_copy(new_children=[b], perform_simplifications=False) + == symbol_b ) v_n = pybamm.Variable("v", "negative electrode") w_n = pybamm.Variable("w", "negative electrode") - self.assertEqual( - pybamm.grad(v_n).create_copy( - new_children=[w_n], perform_simplifications=False - ), - pybamm.Gradient(w_n), - ) + assert pybamm.grad(v_n).create_copy( + new_children=[w_n], perform_simplifications=False + ) == pybamm.Gradient(w_n) - self.assertEqual( - pybamm.div(pybamm.grad(v_n)).create_copy( - new_children=[pybamm.grad(w_n)], perform_simplifications=False - ), - pybamm.Divergence(pybamm.grad(w_n)), - ) + assert pybamm.div(pybamm.grad(v_n)).create_copy( + new_children=[pybamm.grad(w_n)], perform_simplifications=False + ) == pybamm.Divergence(pybamm.grad(w_n)) var = pybamm.Variable("var", domain="test") ible = pybamm.Variable("ible", domain="test") left_extrap = pybamm.BoundaryValue(var, "left") - self.assertEqual( - left_extrap.create_copy(new_children=[ible], perform_simplifications=False), - pybamm.BoundaryValue(ible, "left"), - ) + assert left_extrap.create_copy( + new_children=[ible], perform_simplifications=False + ) == pybamm.BoundaryValue(ible, "left") def test_unary_create_copy_no_simplification_averages(self): a_v = pybamm.Variable("a", domain=["negative electrode"]) @@ -315,12 +297,9 @@ def test_unary_create_copy_no_simplification_averages(self): ], [a_v, a_v, c, c], ): - self.assertEqual( - average(var).create_copy( - new_children=[var], perform_simplifications=False - ), - average(var), - ) + assert average(var).create_copy( + new_children=[var], perform_simplifications=False + ) == average(var) d = pybamm.Symbol("d", domain=["negative particle size"]) R = pybamm.SpatialVariable("R", ["negative particle size"]) @@ -329,10 +308,9 @@ def test_unary_create_copy_no_simplification_averages(self): s_a = pybamm.SizeAverage(d, f_a_dist=f_a_dist) - self.assertEqual( - s_a.create_copy(new_children=[d], perform_simplifications=False), - pybamm.SizeAverage(d, f_a_dist=f_a_dist), - ) + assert s_a.create_copy( + new_children=[d], perform_simplifications=False + ) == pybamm.SizeAverage(d, f_a_dist=f_a_dist) def test_concatenation_create_copy_no_simplification(self): a = pybamm.Parameter("a") @@ -351,15 +329,16 @@ def test_concatenation_create_copy_no_simplification(self): pybamm.DomainConcatenation([v_s, v_n], mesh), ], ): - self.assertEqual( + assert ( symbol_n.create_copy( new_children=[v_s, v_n], perform_simplifications=False - ), - symbol_s, + ) + == symbol_s ) - with self.assertRaisesRegex( - NotImplementedError, "should always be copied using simplification checks" + with pytest.raises( + NotImplementedError, + match="should always be copied using simplification checks", ): pybamm.NumpyConcatenation(a, b, v_s).create_copy( new_children=[a, b], perform_simplifications=False @@ -369,15 +348,12 @@ def test_function_create_copy_no_simplification(self): a = pybamm.Parameter("a") b = pybamm.Parameter("b") - self.assertEqual( - pybamm.Function(np.sin, a).create_copy( - new_children=[b], perform_simplifications=False - ), - pybamm.Function(np.sin, b), - ) + assert pybamm.Function(np.sin, a).create_copy( + new_children=[b], perform_simplifications=False + ) == pybamm.Function(np.sin, b) def test_symbol_new_copy_warning(self): - with self.assertWarns(DeprecationWarning): + with pytest.warns(DeprecationWarning): pybamm.Symbol("a").new_copy() def test_symbol_copy_tree(self): @@ -395,13 +371,3 @@ def test_symbol_copy_tree(self): np.testing.assert_array_equal( model.concatenated_rhs.evaluate(None, y), copied_rhs.evaluate(None, y) ) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_expression_tree/test_operations/test_evaluate_python.py b/tests/unit/test_expression_tree/test_operations/test_evaluate_python.py index 667522c286..518d8f8231 100644 --- a/tests/unit/test_expression_tree/test_operations/test_evaluate_python.py +++ b/tests/unit/test_expression_tree/test_operations/test_evaluate_python.py @@ -2,6 +2,7 @@ # Test for the evaluate-to-python functions # +import pytest import pybamm from tests import get_discretisation_for_testing, get_1p1d_discretisation_for_testing @@ -9,6 +10,7 @@ import numpy as np import scipy.sparse from collections import OrderedDict +import re if pybamm.have_jax(): import jax @@ -18,7 +20,7 @@ ) -class TestEvaluate(unittest.TestCase): +class TestEvaluate: def test_find_symbols(self): a = pybamm.StateVector(slice(0, 1)) b = pybamm.StateVector(slice(1, 2)) @@ -28,84 +30,82 @@ def test_find_symbols(self): variable_symbols = OrderedDict() expr = a + b pybamm.find_symbols(expr, constant_symbols, variable_symbols) - self.assertEqual(len(constant_symbols), 0) + assert len(constant_symbols) == 0 # test keys of known_symbols - self.assertEqual(next(iter(variable_symbols.keys())), a.id) - self.assertEqual(list(variable_symbols.keys())[1], b.id) - self.assertEqual(list(variable_symbols.keys())[2], expr.id) + assert next(iter(variable_symbols.keys())) == a.id + assert list(variable_symbols.keys())[1] == b.id + assert list(variable_symbols.keys())[2] == expr.id # test values of variable_symbols - self.assertEqual(next(iter(variable_symbols.values())), "y[0:1]") - self.assertEqual(list(variable_symbols.values())[1], "y[1:2]") + assert next(iter(variable_symbols.values())) == "y[0:1]" + assert list(variable_symbols.values())[1] == "y[1:2]" var_a = pybamm.id_to_python_variable(a.id) var_b = pybamm.id_to_python_variable(b.id) - self.assertEqual(list(variable_symbols.values())[2], f"{var_a} + {var_b}") + assert list(variable_symbols.values())[2] == f"{var_a} + {var_b}" # test identical subtree constant_symbols = OrderedDict() variable_symbols = OrderedDict() expr = a + b + b pybamm.find_symbols(expr, constant_symbols, variable_symbols) - self.assertEqual(len(constant_symbols), 0) + assert len(constant_symbols) == 0 # test keys of variable_symbols - self.assertEqual(next(iter(variable_symbols.keys())), a.id) - self.assertEqual(list(variable_symbols.keys())[1], b.id) - self.assertEqual(list(variable_symbols.keys())[2], expr.children[0].id) - self.assertEqual(list(variable_symbols.keys())[3], expr.id) + assert next(iter(variable_symbols.keys())) == a.id + assert list(variable_symbols.keys())[1] == b.id + assert list(variable_symbols.keys())[2] == expr.children[0].id + assert list(variable_symbols.keys())[3] == expr.id # test values of variable_symbols - self.assertEqual(next(iter(variable_symbols.values())), "y[0:1]") - self.assertEqual(list(variable_symbols.values())[1], "y[1:2]") - self.assertEqual(list(variable_symbols.values())[2], f"{var_a} + {var_b}") + assert next(iter(variable_symbols.values())) == "y[0:1]" + assert list(variable_symbols.values())[1] == "y[1:2]" + assert list(variable_symbols.values())[2] == f"{var_a} + {var_b}" var_child = pybamm.id_to_python_variable(expr.children[0].id) - self.assertEqual(list(variable_symbols.values())[3], f"{var_child} + {var_b}") + assert list(variable_symbols.values())[3] == f"{var_child} + {var_b}" # test unary op constant_symbols = OrderedDict() variable_symbols = OrderedDict() expr = pybamm.maximum(a, -(b)) pybamm.find_symbols(expr, constant_symbols, variable_symbols) - self.assertEqual(len(constant_symbols), 0) + assert len(constant_symbols) == 0 # test keys of variable_symbols - self.assertEqual(next(iter(variable_symbols.keys())), a.id) - self.assertEqual(list(variable_symbols.keys())[1], b.id) - self.assertEqual(list(variable_symbols.keys())[2], expr.children[1].id) - self.assertEqual(list(variable_symbols.keys())[3], expr.id) + assert next(iter(variable_symbols.keys())) == a.id + assert list(variable_symbols.keys())[1] == b.id + assert list(variable_symbols.keys())[2] == expr.children[1].id + assert list(variable_symbols.keys())[3] == expr.id # test values of variable_symbols - self.assertEqual(next(iter(variable_symbols.values())), "y[0:1]") - self.assertEqual(list(variable_symbols.values())[1], "y[1:2]") - self.assertEqual(list(variable_symbols.values())[2], f"-({var_b})") + assert next(iter(variable_symbols.values())) == "y[0:1]" + assert list(variable_symbols.values())[1] == "y[1:2]" + assert list(variable_symbols.values())[2] == f"-({var_b})" var_child = pybamm.id_to_python_variable(expr.children[1].id) - self.assertEqual( - list(variable_symbols.values())[3], f"np.maximum({var_a},{var_child})" - ) + assert list(variable_symbols.values())[3] == f"np.maximum({var_a},{var_child})" # test function constant_symbols = OrderedDict() variable_symbols = OrderedDict() expr = pybamm.Function(function_test, a) pybamm.find_symbols(expr, constant_symbols, variable_symbols) - self.assertEqual(next(iter(constant_symbols.keys())), expr.id) - self.assertEqual(next(iter(constant_symbols.values())), function_test) - self.assertEqual(next(iter(variable_symbols.keys())), a.id) - self.assertEqual(list(variable_symbols.keys())[1], expr.id) - self.assertEqual(next(iter(variable_symbols.values())), "y[0:1]") + assert next(iter(constant_symbols.keys())) == expr.id + assert next(iter(constant_symbols.values())) == function_test + assert next(iter(variable_symbols.keys())) == a.id + assert list(variable_symbols.keys())[1] == expr.id + assert next(iter(variable_symbols.values())) == "y[0:1]" var_funct = pybamm.id_to_python_variable(expr.id, True) - self.assertEqual(list(variable_symbols.values())[1], f"{var_funct}({var_a})") + assert list(variable_symbols.values())[1] == f"{var_funct}({var_a})" # test matrix constant_symbols = OrderedDict() variable_symbols = OrderedDict() A = pybamm.Matrix([[1, 2], [3, 4]]) pybamm.find_symbols(A, constant_symbols, variable_symbols) - self.assertEqual(len(variable_symbols), 0) - self.assertEqual(next(iter(constant_symbols.keys())), A.id) + assert len(variable_symbols) == 0 + assert next(iter(constant_symbols.keys())) == A.id np.testing.assert_allclose( next(iter(constant_symbols.values())), np.array([[1, 2], [3, 4]]) ) @@ -115,8 +115,8 @@ def test_find_symbols(self): variable_symbols = OrderedDict() A = pybamm.Matrix(scipy.sparse.csr_matrix(np.array([[0, 2], [0, 4]]))) pybamm.find_symbols(A, constant_symbols, variable_symbols) - self.assertEqual(len(variable_symbols), 0) - self.assertEqual(next(iter(constant_symbols.keys())), A.id) + assert len(variable_symbols) == 0 + assert next(iter(constant_symbols.keys())) == A.id np.testing.assert_allclose( next(iter(constant_symbols.values())).toarray(), A.entries.toarray() ) @@ -126,13 +126,12 @@ def test_find_symbols(self): variable_symbols = OrderedDict() expr = pybamm.NumpyConcatenation(a, b) pybamm.find_symbols(expr, constant_symbols, variable_symbols) - self.assertEqual(len(constant_symbols), 0) - self.assertEqual(next(iter(variable_symbols.keys())), a.id) - self.assertEqual(list(variable_symbols.keys())[1], b.id) - self.assertEqual(list(variable_symbols.keys())[2], expr.id) - self.assertEqual( - list(variable_symbols.values())[2], - f"np.concatenate(({var_a},{var_b}))", + assert len(constant_symbols) == 0 + assert next(iter(variable_symbols.keys())) == a.id + assert list(variable_symbols.keys())[1] == b.id + assert list(variable_symbols.keys())[2] == expr.id + assert ( + list(variable_symbols.values())[2] == f"np.concatenate(({var_a},{var_b}))" ) # test domain concatentate @@ -140,13 +139,12 @@ def test_find_symbols(self): variable_symbols = OrderedDict() expr = pybamm.NumpyConcatenation(a, b) pybamm.find_symbols(expr, constant_symbols, variable_symbols) - self.assertEqual(len(constant_symbols), 0) - self.assertEqual(next(iter(variable_symbols.keys())), a.id) - self.assertEqual(list(variable_symbols.keys())[1], b.id) - self.assertEqual(list(variable_symbols.keys())[2], expr.id) - self.assertEqual( - list(variable_symbols.values())[2], - f"np.concatenate(({var_a},{var_b}))", + assert len(constant_symbols) == 0 + assert next(iter(variable_symbols.keys())) == a.id + assert list(variable_symbols.keys())[1] == b.id + assert list(variable_symbols.keys())[2] == expr.id + assert ( + list(variable_symbols.values())[2] == f"np.concatenate(({var_a},{var_b}))" ) # test that Concatentation throws @@ -154,12 +152,12 @@ def test_find_symbols(self): b = pybamm.StateVector(slice(1, 2), domain="test b") expr = pybamm.concatenation(a, b) - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): pybamm.find_symbols(expr, constant_symbols, variable_symbols) # test that these nodes throw for expr in (pybamm.Variable("a"), pybamm.Parameter("a")): - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): pybamm.find_symbols(expr, constant_symbols, variable_symbols) def test_domain_concatenation(self): @@ -182,16 +180,16 @@ def test_domain_concatenation(self): constant_symbols = OrderedDict() variable_symbols = OrderedDict() pybamm.find_symbols(expr, constant_symbols, variable_symbols) - self.assertEqual(next(iter(variable_symbols.keys())), a.id) - self.assertEqual(list(variable_symbols.keys())[1], b.id) - self.assertEqual(list(variable_symbols.keys())[2], expr.id) + assert next(iter(variable_symbols.keys())) == a.id + assert list(variable_symbols.keys())[1] == b.id + assert list(variable_symbols.keys())[2] == expr.id var_a = pybamm.id_to_python_variable(a.id) var_b = pybamm.id_to_python_variable(b.id) - self.assertEqual(len(constant_symbols), 0) - self.assertEqual( - list(variable_symbols.values())[2], - f"np.concatenate(({var_a}[0:{a_pts}],{var_b}[0:{b_pts}]))", + assert len(constant_symbols) == 0 + assert ( + list(variable_symbols.values())[2] + == f"np.concatenate(({var_a}[0:{a_pts}],{var_b}[0:{b_pts}]))" ) evaluator = pybamm.EvaluatorPython(expr) @@ -229,10 +227,10 @@ def test_domain_concatenation(self): a0_str = f"{var_a}[0:{a0_pts}]" b1_str = f"{var_b}[{b0_pts}:{b0_pts + b1_pts}]" - self.assertEqual(len(constant_symbols), 0) - self.assertEqual( - list(variable_symbols.values())[2], - f"np.concatenate(({a0_str},{b0_str},{b1_str}))", + assert len(constant_symbols) == 0 + assert ( + list(variable_symbols.values())[2] + == f"np.concatenate(({a0_str},{b0_str},{b1_str}))" ) evaluator = pybamm.EvaluatorPython(expr) @@ -249,7 +247,7 @@ def test_domain_concatenation_2D(self): conc = pybamm.concatenation(2 * a, 3 * b) disc.set_variable_slices([a, b]) expr = disc.process_symbol(conc) - self.assertIsInstance(expr, pybamm.DomainConcatenation) + assert isinstance(expr, pybamm.DomainConcatenation) y = np.empty((expr._size, 1)) for i in range(len(y)): @@ -259,7 +257,7 @@ def test_domain_concatenation_2D(self): variable_symbols = OrderedDict() pybamm.find_symbols(expr, constant_symbols, variable_symbols) - self.assertEqual(len(constant_symbols), 0) + assert len(constant_symbols) == 0 evaluator = pybamm.EvaluatorPython(expr) result = evaluator(y=y) @@ -284,7 +282,7 @@ def test_to_python(self): r"var_[0-9m]+ = var_[0-9m]+ \+ var_[0-9m]+" ) - self.assertRegex(variable_str, expected_str) + assert re.search(expected_str, variable_str) def test_evaluator_python(self): a = pybamm.StateVector(slice(0, 1)) @@ -297,40 +295,40 @@ def test_evaluator_python(self): expr = a * b evaluator = pybamm.EvaluatorPython(expr) result = evaluator(t=None, y=np.array([[2], [3]])) - self.assertEqual(result, 6) + assert result == 6 result = evaluator(t=None, y=np.array([[1], [3]])) - self.assertEqual(result, 3) + assert result == 3 # test function(a*b) expr = pybamm.Function(function_test, a * b) evaluator = pybamm.EvaluatorPython(expr) result = evaluator(t=None, y=np.array([[2], [3]])) - self.assertEqual(result, 12) + assert result == 12 expr = pybamm.Function(multi_var_function_test, a, b) evaluator = pybamm.EvaluatorPython(expr) result = evaluator(t=None, y=np.array([[2], [3]])) - self.assertEqual(result, 5) + assert result == 5 # test a constant expression expr = pybamm.Scalar(2) * pybamm.Scalar(3) evaluator = pybamm.EvaluatorPython(expr) result = evaluator() - self.assertEqual(result, 6) + assert result == 6 # test a larger expression expr = a * b + b + a**2 / b + 2 * a + b / 2 + 4 evaluator = pybamm.EvaluatorPython(expr) for y in y_tests: result = evaluator(t=None, y=y) - self.assertEqual(result, expr.evaluate(t=None, y=y)) + assert result == expr.evaluate(t=None, y=y) # test something with time expr = a * pybamm.t evaluator = pybamm.EvaluatorPython(expr) for t, y in zip(t_tests, y_tests): result = evaluator(t=t, y=y) - self.assertEqual(result, expr.evaluate(t=t, y=y)) + assert result == expr.evaluate(t=t, y=y) # test something with a matrix multiplication A = pybamm.Matrix([[1, 2], [3, 4]]) @@ -373,7 +371,7 @@ def test_evaluator_python(self): evaluator = pybamm.EvaluatorPython(expr) for t, y in zip(t_tests, y_tests): result = evaluator(t=t, y=y) - self.assertEqual(result, expr.evaluate(t=t, y=y)) + assert result == expr.evaluate(t=t, y=y) # test something with a sparse matrix multiplication A = pybamm.Matrix([[1, 2], [3, 4]]) @@ -448,20 +446,20 @@ def test_evaluator_python(self): result = evaluator(t=t, y=y) np.testing.assert_allclose(result, expr.evaluate(t=t, y=y)) - @unittest.skipIf(not pybamm.have_jax(), "jax or jaxlib is not installed") + @pytest.mark.skipif(not pybamm.have_jax(), reason="jax or jaxlib is not installed") def test_find_symbols_jax(self): # test sparse conversion constant_symbols = OrderedDict() variable_symbols = OrderedDict() A = pybamm.Matrix(scipy.sparse.csr_matrix(np.array([[0, 2], [0, 4]]))) pybamm.find_symbols(A, constant_symbols, variable_symbols, output_jax=True) - self.assertEqual(len(variable_symbols), 0) - self.assertEqual(next(iter(constant_symbols.keys())), A.id) + assert len(variable_symbols) == 0 + assert next(iter(constant_symbols.keys())) == A.id np.testing.assert_allclose( next(iter(constant_symbols.values())).toarray(), A.entries.toarray() ) - @unittest.skipIf(not pybamm.have_jax(), "jax or jaxlib is not installed") + @pytest.mark.skipif(not pybamm.have_jax(), reason="jax or jaxlib is not installed") def test_evaluator_jax(self): a = pybamm.StateVector(slice(0, 1)) b = pybamm.StateVector(slice(1, 2)) @@ -477,15 +475,15 @@ def test_evaluator_jax(self): expr = a * b evaluator = pybamm.EvaluatorJax(expr) result = evaluator(t=None, y=np.array([[2], [3]])) - self.assertEqual(result, 6) + assert result == 6 result = evaluator(t=None, y=np.array([[1], [3]])) - self.assertEqual(result, 3) + assert result == 3 # test function(a*b) expr = pybamm.Function(function_test, a * b) evaluator = pybamm.EvaluatorJax(expr) result = evaluator(t=None, y=np.array([[2], [3]])) - self.assertEqual(result, 12) + assert result == 12 # test exp expr = pybamm.exp(a * b) @@ -497,7 +495,7 @@ def test_evaluator_jax(self): expr = pybamm.Scalar(2) * pybamm.Scalar(3) evaluator = pybamm.EvaluatorJax(expr) result = evaluator() - self.assertEqual(result, 6) + assert result == 6 # test a larger expression expr = a * b + b + a**2 / b + 2 * a + b / 2 + 4 @@ -511,7 +509,7 @@ def test_evaluator_jax(self): evaluator = pybamm.EvaluatorJax(expr) for t, y in zip(t_tests, y_tests): result = evaluator(t=t, y=y) - self.assertEqual(result, expr.evaluate(t=t, y=y)) + assert result == expr.evaluate(t=t, y=y) # test something with a matrix multiplication A = pybamm.Matrix(np.array([[1, 2], [3, 4]])) @@ -554,7 +552,7 @@ def test_evaluator_jax(self): evaluator = pybamm.EvaluatorJax(expr) for t, y in zip(t_tests, y_tests): result = evaluator(t=t, y=y) - self.assertEqual(result, expr.evaluate(t=t, y=y)) + assert result == expr.evaluate(t=t, y=y) # test something with a sparse matrix-vector multiplication A = pybamm.Matrix(np.array([[1, 2], [3, 4]])) @@ -590,7 +588,7 @@ def test_evaluator_jax(self): B = pybamm.Matrix(scipy.sparse.csr_matrix(np.array([[2, 0], [5, 0]]))) a = pybamm.StateVector(slice(0, 1)) expr = pybamm.SparseStack(A, a * B) - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): evaluator = pybamm.EvaluatorJax(expr) # test sparse mat-mat mult @@ -598,7 +596,7 @@ def test_evaluator_jax(self): B = pybamm.Matrix(scipy.sparse.csr_matrix(np.array([[2, 0], [5, 0]]))) a = pybamm.StateVector(slice(0, 1)) expr = A @ (a * B) - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): evaluator = pybamm.EvaluatorJax(expr) # test numpy concatenation @@ -623,7 +621,7 @@ def test_evaluator_jax(self): result = evaluator(t=t, y=y) np.testing.assert_allclose(result, expr.evaluate(t=t, y=y)) - @unittest.skipIf(not pybamm.have_jax(), "jax or jaxlib is not installed") + @pytest.mark.skipif(not pybamm.have_jax(), reason="jax or jaxlib is not installed") def test_evaluator_jax_jacobian(self): a = pybamm.StateVector(slice(0, 1)) y_tests = [np.array([[2.0]]), np.array([[1.0]]), np.array([1.0])] @@ -638,7 +636,7 @@ def test_evaluator_jax_jacobian(self): result_true = evaluator_jac(t=None, y=y) np.testing.assert_allclose(result_test, result_true) - @unittest.skipIf(not pybamm.have_jax(), "jax or jaxlib is not installed") + @pytest.mark.skipif(not pybamm.have_jax(), reason="jax or jaxlib is not installed") def test_evaluator_jax_jvp(self): a = pybamm.StateVector(slice(0, 1)) y_tests = [np.array([[2.0]]), np.array([[1.0]]), np.array([1.0])] @@ -658,7 +656,7 @@ def test_evaluator_jax_jvp(self): np.testing.assert_allclose(result_test, result_true) np.testing.assert_allclose(result_test_times_v, result_true_times_v) - @unittest.skipIf(not pybamm.have_jax(), "jax or jaxlib is not installed") + @pytest.mark.skipif(not pybamm.have_jax(), reason="jax or jaxlib is not installed") def test_evaluator_jax_debug(self): a = pybamm.StateVector(slice(0, 1)) expr = a**2 @@ -666,15 +664,15 @@ def test_evaluator_jax_debug(self): evaluator = pybamm.EvaluatorJax(expr) evaluator.debug(y=y_test) - @unittest.skipIf(not pybamm.have_jax(), "jax or jaxlib is not installed") + @pytest.mark.skipif(not pybamm.have_jax(), reason="jax or jaxlib is not installed") def test_evaluator_jax_inputs(self): a = pybamm.InputParameter("a") expr = a**2 evaluator = pybamm.EvaluatorJax(expr) result = evaluator(inputs={"a": 2}) - self.assertEqual(result, 4) + assert result == 4 - @unittest.skipIf(not pybamm.have_jax(), "jax or jaxlib is not installed") + @pytest.mark.skipif(not pybamm.have_jax(), reason="jax or jaxlib is not installed") def test_evaluator_jax_demotion(self): for demote in [True, False]: pybamm.demote_expressions_to_32bit = demote # global flag @@ -685,9 +683,9 @@ def test_evaluator_jax_demotion(self): 1.0, 1, ]: - self.assertEqual( - str(pybamm.EvaluatorJax._demote_64_to_32(c).dtype)[-2:], - target_dtype, + assert ( + str(pybamm.EvaluatorJax._demote_64_to_32(c).dtype)[-2:] + == target_dtype ) for c in [ np.float64(1.0), @@ -697,36 +695,32 @@ def test_evaluator_jax_demotion(self): jax.numpy.array([1.0], dtype=np.float64), jax.numpy.array([1], dtype=np.int64), ]: - self.assertEqual( - str(pybamm.EvaluatorJax._demote_64_to_32(c).dtype)[-2:], - target_dtype, + assert ( + str(pybamm.EvaluatorJax._demote_64_to_32(c).dtype)[-2:] + == target_dtype ) for c in [ {key: np.float64(1.0) for key in ["a", "b"]}, ]: expr_demoted = pybamm.EvaluatorJax._demote_64_to_32(c) - self.assertTrue( - all( - str(c_v.dtype)[-2:] == target_dtype - for c_k, c_v in expr_demoted.items() - ) + assert all( + str(c_v.dtype)[-2:] == target_dtype + for c_k, c_v in expr_demoted.items() ) for c in [ (np.float64(1.0), np.float64(2.0)), [np.float64(1.0), np.float64(2.0)], ]: expr_demoted = pybamm.EvaluatorJax._demote_64_to_32(c) - self.assertTrue( - all(str(c_i.dtype)[-2:] == target_dtype for c_i in expr_demoted) - ) + assert all(str(c_i.dtype)[-2:] == target_dtype for c_i in expr_demoted) for dtype in [ np.float64, jax.numpy.float64, ]: c = pybamm.JaxCooMatrix([0, 1], [0, 1], dtype([1.0, 2.0]), (2, 2)) c_demoted = pybamm.EvaluatorJax._demote_64_to_32(c) - self.assertTrue( - all(str(c_i.dtype)[-2:] == target_dtype for c_i in c_demoted.data) + assert all( + str(c_i.dtype)[-2:] == target_dtype for c_i in c_demoted.data ) for dtype in [ np.int64, @@ -736,15 +730,11 @@ def test_evaluator_jax_demotion(self): dtype([0, 1]), dtype([0, 1]), [1.0, 2.0], (2, 2) ) c_demoted = pybamm.EvaluatorJax._demote_64_to_32(c) - self.assertTrue( - all(str(c_i.dtype)[-2:] == target_dtype for c_i in c_demoted.row) - ) - self.assertTrue( - all(str(c_i.dtype)[-2:] == target_dtype for c_i in c_demoted.col) - ) + assert all(str(c_i.dtype)[-2:] == target_dtype for c_i in c_demoted.row) + assert all(str(c_i.dtype)[-2:] == target_dtype for c_i in c_demoted.col) pybamm.demote_expressions_to_32bit = False - @unittest.skipIf(not pybamm.have_jax(), "jax or jaxlib is not installed") + @pytest.mark.skipif(not pybamm.have_jax(), reason="jax or jaxlib is not installed") def test_jax_coo_matrix(self): A = pybamm.JaxCooMatrix([0, 1], [0, 1], [1.0, 2.0], (2, 2)) Adense = jax.numpy.array([[1.0, 0], [0, 2.0]]) @@ -754,7 +744,7 @@ def test_jax_coo_matrix(self): np.testing.assert_allclose(A @ v, Adense @ v) np.testing.assert_allclose(A.scalar_multiply(3.0).toarray(), Adense * 3.0) - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): A.multiply(v) diff --git a/tests/unit/test_expression_tree/test_operations/test_jac.py b/tests/unit/test_expression_tree/test_operations/test_jac.py index 34a65f007b..0d6a2b0038 100644 --- a/tests/unit/test_expression_tree/test_operations/test_jac.py +++ b/tests/unit/test_expression_tree/test_operations/test_jac.py @@ -2,19 +2,20 @@ # Tests for the jacobian methods # +import pytest import pybamm import numpy as np -import unittest from scipy.sparse import eye from tests import get_mesh_for_testing -class TestJacobian(unittest.TestCase): +class TestJacobian: def test_variable_is_statevector(self): a = pybamm.Symbol("a") - with self.assertRaisesRegex( - TypeError, "Jacobian can only be taken with respect to a 'StateVector'" + with pytest.raises( + TypeError, + match="Jacobian can only be taken with respect to a 'StateVector'", ): a.jac(a) @@ -52,7 +53,7 @@ def test_linear(self): np.testing.assert_array_equal(jacobian, dfunc_dy.toarray()) func = u @ pybamm.StateVector(slice(0, 1)) - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): func.jac(y) # when differentiating by independent part of the state vector @@ -111,11 +112,11 @@ def test_multislice_raises(self): y1 = pybamm.StateVector(slice(0, 4), slice(7, 8)) y_dot1 = pybamm.StateVectorDot(slice(0, 4), slice(7, 8)) y2 = pybamm.StateVector(slice(4, 7)) - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): y1.jac(y1) - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): y2.jac(y1) - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): y_dot1.jac(y1) def test_linear_ydot(self): @@ -237,41 +238,41 @@ def test_jac_of_number(self): y = pybamm.StateVector(slice(0, 1)) - self.assertEqual(a.jac(y).evaluate(), 0) + assert a.jac(y).evaluate() == 0 add = a + b - self.assertEqual(add.jac(y).evaluate(), 0) + assert add.jac(y).evaluate() == 0 subtract = a - b - self.assertEqual(subtract.jac(y).evaluate(), 0) + assert subtract.jac(y).evaluate() == 0 multiply = a * b - self.assertEqual(multiply.jac(y).evaluate(), 0) + assert multiply.jac(y).evaluate() == 0 divide = a / b - self.assertEqual(divide.jac(y).evaluate(), 0) + assert divide.jac(y).evaluate() == 0 power = a**b - self.assertEqual(power.jac(y).evaluate(), 0) + assert power.jac(y).evaluate() == 0 def test_jac_of_symbol(self): a = pybamm.Symbol("a") y = pybamm.StateVector(slice(0, 1)) - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): a.jac(y) def test_spatial_operator(self): a = pybamm.Variable("a") b = pybamm.SpatialOperator("Operator", a) y = pybamm.StateVector(slice(0, 1)) - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): b.jac(y) def test_jac_of_unary_operator(self): a = pybamm.Scalar(1) b = pybamm.UnaryOperator("Operator", a) y = pybamm.StateVector(slice(0, 1)) - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): b.jac(y) def test_jac_of_binary_operator(self): @@ -284,26 +285,26 @@ def test_jac_of_binary_operator(self): i = pybamm.grad(phi_s) inner = pybamm.inner(2, i) - self.assertEqual(inner._binary_jac(a, b), 2 * b) + assert inner._binary_jac(a, b) == 2 * b inner = pybamm.inner(i, 2) - self.assertEqual(inner._binary_jac(a, b), 2 * a) + assert inner._binary_jac(a, b) == 2 * a inner = pybamm.inner(i, i) - self.assertEqual(inner._binary_jac(a, b), i * a + i * b) + assert inner._binary_jac(a, b) == i * a + i * b def test_jac_of_independent_variable(self): a = pybamm.IndependentVariable("Variable") y = pybamm.StateVector(slice(0, 1)) - self.assertEqual(a.jac(y).evaluate(), 0) + assert a.jac(y).evaluate() == 0 def test_jac_of_inner(self): a = pybamm.Scalar(1) b = pybamm.Scalar(2) y = pybamm.StateVector(slice(0, 1)) - self.assertEqual(pybamm.inner(a, b).jac(y).evaluate(), 0) - self.assertEqual(pybamm.inner(a, y).jac(y).evaluate(), 1) - self.assertEqual(pybamm.inner(y, b).jac(y).evaluate(), 2) + assert pybamm.inner(a, b).jac(y).evaluate() == 0 + assert pybamm.inner(a, y).jac(y).evaluate() == 1 + assert pybamm.inner(y, b).jac(y).evaluate() == 2 vec = pybamm.StateVector(slice(0, 2)) jac = pybamm.inner(a * vec, b * vec).jac(vec).evaluate(y=np.ones(2)).toarray() np.testing.assert_array_equal(jac, 4 * np.eye(2)) @@ -406,7 +407,7 @@ def test_jac_of_numpy_concatenation(self): np.testing.assert_array_equal(jacobian, dfunc_dy.toarray()) # One child - self.assertEqual(u.jac(u), pybamm.NumpyConcatenation(u).jac(u)) + assert u.jac(u) == pybamm.NumpyConcatenation(u).jac(u) def test_jac_of_domain_concatenation(self): # create mesh @@ -454,17 +455,8 @@ def test_jac_of_domain_concatenation(self): slice(a_npts, a_npts + b_npts + c_npts), domain=b_dom + c_dom ) conc = pybamm.DomainConcatenation([a, b], mesh) - with self.assertRaisesRegex( - NotImplementedError, "jacobian only implemented for when each child has" + with pytest.raises( + NotImplementedError, + match="jacobian only implemented for when each child has", ): conc.jac(y) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_expression_tree/test_operations/test_jac_2D.py b/tests/unit/test_expression_tree/test_operations/test_jac_2D.py index e7001b184b..273c84ced7 100644 --- a/tests/unit/test_expression_tree/test_operations/test_jac_2D.py +++ b/tests/unit/test_expression_tree/test_operations/test_jac_2D.py @@ -2,17 +2,17 @@ # Tests for the jacobian methods for two-dimensional objects # +import pytest import pybamm import numpy as np -import unittest from scipy.sparse import eye from tests import ( get_1p1d_discretisation_for_testing, ) -class TestJacobian(unittest.TestCase): +class TestJacobian: def test_linear(self): y = pybamm.StateVector(slice(0, 8)) u = pybamm.StateVector(slice(0, 2), slice(4, 6)) @@ -82,7 +82,7 @@ def test_linear(self): np.testing.assert_array_equal(jacobian, dfunc_dy.toarray()) # when differentiating by independent part of the state vector - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): u.jac(v) def test_nonlinear(self): @@ -243,13 +243,3 @@ def test_jac_of_domain_concatenation(self): y0 = np.ones(1500) jac = conc_disc.jac(y).evaluate(y=y0).toarray() np.testing.assert_array_equal(jac, np.eye(1500)) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_expression_tree/test_operations/test_latexify.py b/tests/unit/test_expression_tree/test_operations/test_latexify.py index 746efb4269..66cd30d399 100644 --- a/tests/unit/test_expression_tree/test_operations/test_latexify.py +++ b/tests/unit/test_expression_tree/test_operations/test_latexify.py @@ -2,15 +2,15 @@ Tests for the latexify.py """ +import pytest import os import platform -import unittest import uuid import pybamm -class TestLatexify(unittest.TestCase): +class TestLatexify: def test_latexify(self): model_dfn = pybamm.lithium_ion.DFN() func_dfn = str(model_dfn.latexify()) @@ -19,49 +19,49 @@ def test_latexify(self): func_spme = str(model_spme.latexify()) # Test model name - self.assertIn("Single Particle Model with electrolyte Equations", func_spme) + assert "Single Particle Model with electrolyte Equations" in func_spme # Test newline=False - self.assertIn(r"\\", str(model_spme.latexify(newline=False))) + assert r"\\" in str(model_spme.latexify(newline=False)) # Test voltage equation name - self.assertIn("Voltage [V]", func_spme) + assert "Voltage [V]" in func_spme # Test derivative in boundary conditions - self.assertIn(r"\nabla", func_spme) + assert r"\nabla" in func_spme # Test boundary conditions range - self.assertIn("r =", func_spme) + assert "r =" in func_spme # Test derivative in equations - self.assertIn("frac{d}{d t}", func_spme) + assert "frac{d}{d t}" in func_spme # Test rhs geometry ranges - self.assertIn("0 < r < ", func_spme) + assert "0 < r < " in func_spme # Test initial conditions - self.assertIn("; t=0", func_spme) + assert "; t=0" in func_spme # Test DFN algebraic lhs - self.assertIn("0 =", func_dfn) + assert "0 =" in func_dfn # Test concatenation cases try: - self.assertIn("begin{cases}", func_spme) - self.assertIn("end{cases}", func_spme) + assert "begin{cases}" in func_spme + assert "end{cases}" in func_spme except AssertionError: for eqn in model_spme.rhs.values(): concat_displays = model_spme._get_concat_displays(eqn) if concat_displays: - self.assertIn("begin{cases}", str(concat_displays)) - self.assertIn("end{cases}", str(concat_displays)) + assert "begin{cases}" in str(concat_displays) + assert "end{cases}" in str(concat_displays) break # Test parameters and variables - self.assertIn("Parameters and Variables", func_spme) - self.assertIn("coefficient", func_spme) - self.assertIn("diffusivity", func_spme) + assert "Parameters and Variables" in func_spme + assert "coefficient" in func_spme + assert "diffusivity" in func_spme def test_latexify_other_variables(self): model_spme = pybamm.lithium_ion.SPMe() @@ -70,7 +70,7 @@ def test_latexify_other_variables(self): output_variables=["Electrolyte concentration [mol.m-3]"] ) ) - self.assertIn("Electrolyte concentration [mol.m-3]", func_spme) + assert "Electrolyte concentration [mol.m-3]" in func_spme # Default behavior when voltage is not in the model variables model = pybamm.BaseModel() @@ -78,9 +78,11 @@ def test_latexify_other_variables(self): model.rhs = {var: 0} model.initial_conditions = {var: 0} func = str(model.latexify()) - self.assertNotIn("Voltage [V]", func) + assert "Voltage [V]" not in func - @unittest.skipIf(platform.system() in ["Windows", "Darwin"], "Only run for Linux") + @pytest.mark.skipif( + platform.system() in ["Windows", "Darwin"], reason="Only run for Linux" + ) def test_sympy_preview(self): # Test sympy preview model_spme = pybamm.lithium_ion.SPMe() @@ -89,13 +91,3 @@ def test_sympy_preview(self): filename = f"{uuid.uuid4()}.{ext}" model_spme.latexify(filename) os.remove(filename) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_expression_tree/test_parameter.py b/tests/unit/test_expression_tree/test_parameter.py index e2eadffba1..127ac4d814 100644 --- a/tests/unit/test_expression_tree/test_parameter.py +++ b/tests/unit/test_expression_tree/test_parameter.py @@ -2,22 +2,22 @@ # Tests for the Parameter class # +import pytest import numbers -import unittest import pybamm import sympy -class TestParameter(unittest.TestCase): +class TestParameter: def test_parameter_init(self): a = pybamm.Parameter("a") - self.assertEqual(a.name, "a") - self.assertEqual(a.domain, []) + assert a.name == "a" + assert a.domain == [] def test_evaluate_for_shape(self): a = pybamm.Parameter("a") - self.assertIsInstance(a.evaluate_for_shape(), numbers.Number) + assert isinstance(a.evaluate_for_shape(), numbers.Number) def test_to_equation(self): func = pybamm.Parameter("test_string") @@ -25,46 +25,46 @@ def test_to_equation(self): # Test print_name func.print_name = "test" - self.assertEqual(func.to_equation(), sympy.Symbol("test")) + assert func.to_equation() == sympy.Symbol("test") # Test name - self.assertEqual(func1.to_equation(), sympy.Symbol("test_name")) + assert func1.to_equation() == sympy.Symbol("test_name") def test_to_json_error(self): func = pybamm.Parameter("test_string") - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): func.to_json() - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): pybamm.Parameter._from_json({}) -class TestFunctionParameter(unittest.TestCase): +class TestFunctionParameter: def test_function_parameter_init(self): var = pybamm.Variable("var") func = pybamm.FunctionParameter("func", {"var": var}) - self.assertEqual(func.name, "func") - self.assertEqual(func.children[0], var) - self.assertEqual(func.domain, []) - self.assertEqual(func.diff_variable, None) + assert func.name == "func" + assert func.children[0] == var + assert func.domain == [] + assert func.diff_variable is None def test_function_parameter_diff(self): var = pybamm.Variable("var") func = pybamm.FunctionParameter("a", {"var": var}).diff(var) - self.assertEqual(func.diff_variable, var) + assert func.diff_variable == var def test_evaluate_for_shape(self): a = pybamm.Parameter("a") func = pybamm.FunctionParameter("func", {"2a": 2 * a}) - self.assertIsInstance(func.evaluate_for_shape(), numbers.Number) + assert isinstance(func.evaluate_for_shape(), numbers.Number) def test_copy(self): a = pybamm.Parameter("a") func = pybamm.FunctionParameter("func", {"2a": 2 * a}) new_func = func.create_copy() - self.assertEqual(func.input_names, new_func.input_names) + assert func.input_names == new_func.input_names def test_print_input_names(self): var = pybamm.Variable("var") @@ -74,7 +74,7 @@ def test_print_input_names(self): def test_get_children_domains(self): var = pybamm.Variable("var", domain=["negative electrode"]) var_2 = pybamm.Variable("var", domain=["positive electrode"]) - with self.assertRaises(pybamm.DomainError): + with pytest.raises(pybamm.DomainError): pybamm.FunctionParameter("a", {"var": var, "var 2": var_2}) def test_set_input_names(self): @@ -84,13 +84,13 @@ def test_set_input_names(self): new_input_names = ["first", "second"] func.input_names = new_input_names - self.assertEqual(func.input_names, new_input_names) + assert func.input_names == new_input_names - with self.assertRaises(TypeError): + with pytest.raises(TypeError): new_input_names = {"wrong": "input type"} func.input_names = new_input_names - with self.assertRaises(TypeError): + with pytest.raises(TypeError): new_input_names = [var] func.input_names = new_input_names @@ -102,8 +102,8 @@ def _myfun(x): return pybamm.FunctionParameter("my function", {"x": x}) x = pybamm.Scalar(1) - self.assertEqual(myfun(x).print_name, "myfun") - self.assertEqual(_myfun(x).print_name, None) + assert myfun(x).print_name == "myfun" + assert _myfun(x).print_name is None def test_function_parameter_to_equation(self): func = pybamm.FunctionParameter("test", {"x": pybamm.Scalar(1)}) @@ -111,27 +111,17 @@ def test_function_parameter_to_equation(self): # Test print_name func.print_name = "test" - self.assertEqual(func.to_equation(), sympy.Symbol("test")) + assert func.to_equation() == sympy.Symbol("test") # Test name func1.print_name = None - self.assertEqual(func1.to_equation(), sympy.Symbol("func")) + assert func1.to_equation() == sympy.Symbol("func") def test_to_json_error(self): func = pybamm.FunctionParameter("test", {"x": pybamm.Scalar(1)}) - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): func.to_json() - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): pybamm.FunctionParameter._from_json({}) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() diff --git a/tests/unit/test_expression_tree/test_state_vector.py b/tests/unit/test_expression_tree/test_state_vector.py index 0cbc6faa4d..9ed118c2d4 100644 --- a/tests/unit/test_expression_tree/test_state_vector.py +++ b/tests/unit/test_expression_tree/test_state_vector.py @@ -4,12 +4,10 @@ import pybamm import numpy as np +import pytest -import unittest -import unittest.mock as mock - -class TestStateVector(unittest.TestCase): +class TestStateVector: def test_evaluate(self): sv = pybamm.StateVector(slice(0, 10)) y = np.linspace(0, 2, 19) @@ -19,8 +17,9 @@ def test_evaluate(self): # Try evaluating with a y that is too short y2 = np.ones(5) - with self.assertRaisesRegex( - ValueError, "y is too short, so value with slice is smaller than expected" + with pytest.raises( + ValueError, + match="y is too short, so value with slice is smaller than expected", ): sv.evaluate(y=y2) @@ -39,13 +38,13 @@ def test_evaluate_list(self): def test_name(self): sv = pybamm.StateVector(slice(0, 10)) - self.assertEqual(sv.name, "y[0:10]") + assert sv.name == "y[0:10]" sv = pybamm.StateVector(slice(0, 10), slice(20, 30)) - self.assertEqual(sv.name, "y[0:10,20:30]") + assert sv.name == "y[0:10,20:30]" sv = pybamm.StateVector( slice(0, 10), slice(20, 30), slice(40, 50), slice(60, 70) ) - self.assertEqual(sv.name, "y[0:10,20:30,...,60:70]") + assert sv.name == "y[0:10,20:30,...,60:70]" def test_pass_evaluation_array(self): # Turn off debug mode for this test @@ -60,10 +59,10 @@ def test_pass_evaluation_array(self): pybamm.settings.debug_mode = original_debug_mode def test_failure(self): - with self.assertRaisesRegex(TypeError, "all y_slices must be slice objects"): + with pytest.raises(TypeError, match="all y_slices must be slice objects"): pybamm.StateVector(slice(0, 10), 1) - def test_to_from_json(self): + def test_to_from_json(self, mocker): original_debug_mode = pybamm.settings.debug_mode pybamm.settings.debug_mode = False @@ -72,7 +71,7 @@ def test_to_from_json(self): json_dict = { "name": "y[0:10]", - "id": mock.ANY, + "id": mocker.ANY, "domains": { "primary": [], "secondary": [], @@ -89,15 +88,15 @@ def test_to_from_json(self): "evaluation_array": [1, 2, 3, 4, 5], } - self.assertEqual(sv.to_json(), json_dict) + assert sv.to_json() == json_dict - self.assertEqual(pybamm.StateVector._from_json(json_dict), sv) + assert pybamm.StateVector._from_json(json_dict) == sv # Turn debug mode back to what is was before pybamm.settings.debug_mode = original_debug_mode -class TestStateVectorDot(unittest.TestCase): +class TestStateVectorDot: def test_evaluate(self): sv = pybamm.StateVectorDot(slice(0, 10)) y_dot = np.linspace(0, 2, 19) @@ -107,29 +106,19 @@ def test_evaluate(self): # Try evaluating with a y that is too short y_dot2 = np.ones(5) - with self.assertRaisesRegex( + with pytest.raises( ValueError, - "y_dot is too short, so value with slice is smaller than expected", + match="y_dot is too short, so value with slice is smaller than expected", ): sv.evaluate(y_dot=y_dot2) # Try evaluating with y_dot=None - with self.assertRaisesRegex( + with pytest.raises( TypeError, - "StateVectorDot cannot evaluate input 'y_dot=None'", + match="StateVectorDot cannot evaluate input 'y_dot=None'", ): sv.evaluate(y_dot=None) def test_name(self): sv = pybamm.StateVectorDot(slice(0, 10)) - self.assertEqual(sv.name, "y_dot[0:10]") - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() + assert sv.name == "y_dot[0:10]" diff --git a/tests/unit/test_expression_tree/test_symbol.py b/tests/unit/test_expression_tree/test_symbol.py index f1090fe7dd..a61d86cbe0 100644 --- a/tests/unit/test_expression_tree/test_symbol.py +++ b/tests/unit/test_expression_tree/test_symbol.py @@ -2,26 +2,25 @@ # Test for the Symbol class # +import pytest import os -import unittest -import unittest.mock as mock from tempfile import TemporaryDirectory import numpy as np from scipy.sparse import csr_matrix, coo_matrix - +import re import pybamm from pybamm.expression_tree.binary_operators import _Heaviside import sympy -class TestSymbol(unittest.TestCase): +class TestSymbol: def test_symbol_init(self): sym = pybamm.Symbol("a symbol") - with self.assertRaises(TypeError): + with pytest.raises(TypeError): sym.name = 1 - self.assertEqual(sym.name, "a symbol") - self.assertEqual(str(sym), "a symbol") + assert sym.name == "a symbol" + assert str(sym) == "a symbol" def test_children(self): symc1 = pybamm.Symbol("child1") @@ -30,44 +29,44 @@ def test_children(self): # test tuples of children for equality based on their name def check_are_equal(children1, children2): - self.assertEqual(len(children1), len(children2)) + assert len(children1) == len(children2) for i in range(len(children1)): - self.assertEqual(children1[i].name, children2[i].name) + assert children1[i].name == children2[i].name check_are_equal(symp.children, (symc1, symc2)) def test_symbol_domains(self): a = pybamm.Symbol("a", domain="test") - self.assertEqual(a.domain, ["test"]) + assert a.domain == ["test"] # test for updating domain with same as existing domain a.domains = {"primary": ["test"]} - self.assertEqual(a.domains["primary"], ["test"]) + assert a.domains["primary"] == ["test"] a = pybamm.Symbol("a", domain=["t", "e", "s"]) - self.assertEqual(a.domain, ["t", "e", "s"]) - with self.assertRaises(TypeError): + assert a.domain == ["t", "e", "s"] + with pytest.raises(TypeError): a = pybamm.Symbol("a", domain=1) - with self.assertRaisesRegex( + with pytest.raises( pybamm.DomainError, - "Domain levels must be filled in order", + match="Domain levels must be filled in order", ): b = pybamm.Symbol("b", auxiliary_domains={"secondary": ["test sec"]}) b = pybamm.Symbol( "b", domain="test", auxiliary_domains={"secondary": ["test sec"]} ) - with self.assertRaisesRegex(pybamm.DomainError, "keys must be one of"): + with pytest.raises(pybamm.DomainError, match="keys must be one of"): b.domains = {"test": "test"} - with self.assertRaisesRegex(ValueError, "Only one of 'domain' or 'domains'"): + with pytest.raises(ValueError, match="Only one of 'domain' or 'domains'"): pybamm.Symbol("b", domain="test", domains={"primary": "test"}) - with self.assertRaisesRegex( - ValueError, "Only one of 'auxiliary_domains' or 'domains'" + with pytest.raises( + ValueError, match="Only one of 'auxiliary_domains' or 'domains'" ): pybamm.Symbol( "b", auxiliary_domains={"secondary": "other test"}, domains={"test": "test"}, ) - with self.assertRaisesRegex(NotImplementedError, "Cannot set domain directly"): + with pytest.raises(NotImplementedError, match="Cannot set domain directly"): b.domain = "test" def test_symbol_auxiliary_domains(self): @@ -80,40 +79,33 @@ def test_symbol_auxiliary_domains(self): "quaternary": "quat", }, ) - self.assertEqual(a.domain, ["test"]) - self.assertEqual(a.secondary_domain, ["sec"]) - self.assertEqual(a.tertiary_domain, ["tert"]) - self.assertEqual(a.tertiary_domain, ["tert"]) - self.assertEqual(a.quaternary_domain, ["quat"]) - self.assertEqual( - a.domains, - { - "primary": ["test"], - "secondary": ["sec"], - "tertiary": ["tert"], - "quaternary": ["quat"], - }, - ) + assert a.domain == ["test"] + assert a.secondary_domain == ["sec"] + assert a.tertiary_domain == ["tert"] + assert a.tertiary_domain == ["tert"] + assert a.quaternary_domain == ["quat"] + assert a.domains == { + "primary": ["test"], + "secondary": ["sec"], + "tertiary": ["tert"], + "quaternary": ["quat"], + } a = pybamm.Symbol("a", domain=["t", "e", "s"]) - self.assertEqual(a.domain, ["t", "e", "s"]) - with self.assertRaises(TypeError): + assert a.domain == ["t", "e", "s"] + with pytest.raises(TypeError): a = pybamm.Symbol("a", domain=1) b = pybamm.Symbol("b", domain="test sec") - with self.assertRaisesRegex( - pybamm.DomainError, "All domains must be different" - ): + with pytest.raises(pybamm.DomainError, match="All domains must be different"): b.domains = {"primary": "test", "secondary": "test"} - with self.assertRaisesRegex( - pybamm.DomainError, "All domains must be different" - ): + with pytest.raises(pybamm.DomainError, match="All domains must be different"): b = pybamm.Symbol( "b", domain="test", auxiliary_domains={"secondary": ["test sec"], "tertiary": ["test sec"]}, ) - with self.assertRaisesRegex(NotImplementedError, "auxiliary_domains"): + with pytest.raises(NotImplementedError, match="auxiliary_domains"): a.auxiliary_domains def test_symbol_methods(self): @@ -121,62 +113,63 @@ def test_symbol_methods(self): b = pybamm.Symbol("b") # unary - self.assertIsInstance(-a, pybamm.Negate) - self.assertIsInstance(abs(a), pybamm.AbsoluteValue) + assert isinstance(-a, pybamm.Negate) + assert isinstance(abs(a), pybamm.AbsoluteValue) # special cases - self.assertEqual(-(-a), a) # noqa: B002 - self.assertEqual(-(a - b), b - a) - self.assertEqual(abs(abs(a)), abs(a)) + assert -(-a) == a # noqa: B002 + assert -(a - b) == b - a + assert abs(abs(a)) == abs(a) # binary - two symbols - self.assertIsInstance(a + b, pybamm.Addition) - self.assertIsInstance(a - b, pybamm.Subtraction) - self.assertIsInstance(a * b, pybamm.Multiplication) - self.assertIsInstance(a @ b, pybamm.MatrixMultiplication) - self.assertIsInstance(a / b, pybamm.Division) - self.assertIsInstance(a**b, pybamm.Power) - self.assertIsInstance(a < b, _Heaviside) - self.assertIsInstance(a <= b, _Heaviside) - self.assertIsInstance(a > b, _Heaviside) - self.assertIsInstance(a >= b, _Heaviside) - self.assertIsInstance(a % b, pybamm.Modulo) + assert isinstance(a + b, pybamm.Addition) + assert isinstance(a - b, pybamm.Subtraction) + assert isinstance(a * b, pybamm.Multiplication) + assert isinstance(a @ b, pybamm.MatrixMultiplication) + assert isinstance(a / b, pybamm.Division) + assert isinstance(a**b, pybamm.Power) + assert isinstance(a < b, _Heaviside) + assert isinstance(a <= b, _Heaviside) + assert isinstance(a > b, _Heaviside) + assert isinstance(a >= b, _Heaviside) + assert isinstance(a % b, pybamm.Modulo) # binary - symbol and number - self.assertIsInstance(a + 2, pybamm.Addition) - self.assertIsInstance(2 - a, pybamm.Subtraction) - self.assertIsInstance(a * 2, pybamm.Multiplication) - self.assertIsInstance(a @ 2, pybamm.MatrixMultiplication) - self.assertIsInstance(2 / a, pybamm.Division) - self.assertIsInstance(a**2, pybamm.Power) + assert isinstance(a + 2, pybamm.Addition) + assert isinstance(2 - a, pybamm.Subtraction) + assert isinstance(a * 2, pybamm.Multiplication) + assert isinstance(a @ 2, pybamm.MatrixMultiplication) + assert isinstance(2 / a, pybamm.Division) + assert isinstance(a**2, pybamm.Power) # binary - number and symbol - self.assertIsInstance(3 + b, pybamm.Addition) - self.assertEqual((3 + b).children[1], b) - self.assertIsInstance(3 - b, pybamm.Subtraction) - self.assertEqual((3 - b).children[1], b) - self.assertIsInstance(3 * b, pybamm.Multiplication) - self.assertEqual((3 * b).children[1], b) - self.assertIsInstance(3 @ b, pybamm.MatrixMultiplication) - self.assertEqual((3 @ b).children[1], b) - self.assertIsInstance(3 / b, pybamm.Division) - self.assertEqual((3 / b).children[1], b) - self.assertIsInstance(3**b, pybamm.Power) - self.assertEqual((3**b).children[1], b) + assert isinstance(3 + b, pybamm.Addition) + assert (3 + b).children[1] == b + assert isinstance(3 - b, pybamm.Subtraction) + assert (3 - b).children[1] == b + assert isinstance(3 * b, pybamm.Multiplication) + assert (3 * b).children[1] == b + assert isinstance(3 @ b, pybamm.MatrixMultiplication) + assert (3 @ b).children[1] == b + assert isinstance(3 / b, pybamm.Division) + assert (3 / b).children[1] == b + assert isinstance(3**b, pybamm.Power) + assert (3**b).children[1] == b # error raising - with self.assertRaisesRegex( - NotImplementedError, "BinaryOperator not implemented for symbols of type" + with pytest.raises( + NotImplementedError, + match="BinaryOperator not implemented for symbols of type", ): a + "two" def test_symbol_create_copy(self): a = pybamm.Symbol("a") new_a = a.create_copy() - self.assertEqual(new_a, a) + assert new_a == a b = pybamm.Symbol("b") new_b = b.create_copy(new_children=[a]) - self.assertEqual(new_b, pybamm.Symbol("b", children=[a])) + assert new_b == pybamm.Symbol("b", children=[a]) def test_sigmoid(self): # Test that smooth heaviside is used when the setting is changed @@ -185,18 +178,18 @@ def test_sigmoid(self): pybamm.settings.heaviside_smoothing = 10 - self.assertEqual(str(a < b), str(pybamm.sigmoid(a, b, 10))) - self.assertEqual(str(a <= b), str(pybamm.sigmoid(a, b, 10))) - self.assertEqual(str(a > b), str(pybamm.sigmoid(b, a, 10))) - self.assertEqual(str(a >= b), str(pybamm.sigmoid(b, a, 10))) + assert str(a < b) == str(pybamm.sigmoid(a, b, 10)) + assert str(a <= b) == str(pybamm.sigmoid(a, b, 10)) + assert str(a > b) == str(pybamm.sigmoid(b, a, 10)) + assert str(a >= b) == str(pybamm.sigmoid(b, a, 10)) # But exact heavisides should still be used if both variables are constant a = pybamm.Scalar(1) b = pybamm.Scalar(2) - self.assertEqual(str(a < b), str(pybamm.Scalar(1))) - self.assertEqual(str(a <= b), str(pybamm.Scalar(1))) - self.assertEqual(str(a > b), str(pybamm.Scalar(0))) - self.assertEqual(str(a >= b), str(pybamm.Scalar(0))) + assert str(a < b) == str(pybamm.Scalar(1)) + assert str(a <= b) == str(pybamm.Scalar(1)) + assert str(a > b) == str(pybamm.Scalar(0)) + assert str(a >= b) == str(pybamm.Scalar(0)) # Change setting back for other tests pybamm.settings.heaviside_smoothing = "exact" @@ -205,11 +198,11 @@ def test_smooth_absolute_value(self): # Test that smooth absolute value is used when the setting is changed a = pybamm.Symbol("a") pybamm.settings.abs_smoothing = 10 - self.assertEqual(str(abs(a)), str(pybamm.smooth_absolute_value(a, 10))) + assert str(abs(a)) == str(pybamm.smooth_absolute_value(a, 10)) # But exact absolute value should still be used for constants a = pybamm.Scalar(-5) - self.assertEqual(str(abs(a)), str(pybamm.Scalar(5))) + assert str(abs(a)) == str(pybamm.Scalar(5)) # Change setting back for other tests pybamm.settings.abs_smoothing = "exact" @@ -237,27 +230,27 @@ def test_multiple_symbols(self): "a", ] for node, expect in zip(exp.pre_order(), expected_preorder): - self.assertEqual(node.name, expect) + assert node.name == expect def test_symbol_diff(self): a = pybamm.Symbol("a") b = pybamm.Symbol("b") - self.assertIsInstance(a.diff(a), pybamm.Scalar) - self.assertEqual(a.diff(a).evaluate(), 1) - self.assertIsInstance(a.diff(b), pybamm.Scalar) - self.assertEqual(a.diff(b).evaluate(), 0) + assert isinstance(a.diff(a), pybamm.Scalar) + assert a.diff(a).evaluate() == 1 + assert isinstance(a.diff(b), pybamm.Scalar) + assert a.diff(b).evaluate() == 0 def test_symbol_evaluation(self): a = pybamm.Symbol("a") - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): a.evaluate() def test_evaluate_ignoring_errors(self): - self.assertIsNone(pybamm.t.evaluate_ignoring_errors(t=None)) - self.assertEqual(pybamm.t.evaluate_ignoring_errors(t=0), 0) - self.assertIsNone(pybamm.Parameter("a").evaluate_ignoring_errors()) - self.assertIsNone(pybamm.StateVector(slice(0, 1)).evaluate_ignoring_errors()) - self.assertIsNone(pybamm.StateVectorDot(slice(0, 1)).evaluate_ignoring_errors()) + assert pybamm.t.evaluate_ignoring_errors(t=None) is None + assert pybamm.t.evaluate_ignoring_errors(t=0) == 0 + assert pybamm.Parameter("a").evaluate_ignoring_errors() is None + assert pybamm.StateVector(slice(0, 1)).evaluate_ignoring_errors() is None + assert pybamm.StateVectorDot(slice(0, 1)).evaluate_ignoring_errors() is None np.testing.assert_array_equal( pybamm.InputParameter("a").evaluate_ignoring_errors(), np.nan @@ -265,82 +258,82 @@ def test_evaluate_ignoring_errors(self): def test_symbol_is_constant(self): a = pybamm.Variable("a") - self.assertFalse(a.is_constant()) + assert not a.is_constant() a = pybamm.Parameter("a") - self.assertFalse(a.is_constant()) + assert not a.is_constant() a = pybamm.Scalar(1) * pybamm.Variable("a") - self.assertFalse(a.is_constant()) + assert not a.is_constant() a = pybamm.Scalar(1) * pybamm.StateVector(slice(10)) - self.assertFalse(a.is_constant()) + assert not a.is_constant() a = pybamm.Scalar(1) * pybamm.Vector(np.zeros(10)) - self.assertTrue(a.is_constant()) + assert a.is_constant() def test_symbol_evaluates_to_number(self): a = pybamm.Scalar(3) - self.assertTrue(a.evaluates_to_number()) + assert a.evaluates_to_number() a = pybamm.Parameter("a") - self.assertTrue(a.evaluates_to_number()) + assert a.evaluates_to_number() a = pybamm.Scalar(3) * pybamm.Time() - self.assertTrue(a.evaluates_to_number()) + assert a.evaluates_to_number() # highlight difference between this function and isinstance(a, Scalar) - self.assertNotIsInstance(a, pybamm.Scalar) + assert not isinstance(a, pybamm.Scalar) a = pybamm.Variable("a") - self.assertFalse(a.evaluates_to_number()) + assert not a.evaluates_to_number() a = pybamm.Scalar(3) - 2 - self.assertTrue(a.evaluates_to_number()) + assert a.evaluates_to_number() a = pybamm.Vector(np.ones(5)) - self.assertFalse(a.evaluates_to_number()) + assert not a.evaluates_to_number() a = pybamm.Matrix(np.ones((4, 6))) - self.assertFalse(a.evaluates_to_number()) + assert not a.evaluates_to_number() a = pybamm.StateVector(slice(0, 10)) - self.assertFalse(a.evaluates_to_number()) + assert not a.evaluates_to_number() # Time variable returns false a = 3 * pybamm.t + 2 - self.assertTrue(a.evaluates_to_number()) + assert a.evaluates_to_number() def test_symbol_evaluates_to_constant_number(self): a = pybamm.Scalar(3) - self.assertTrue(a.evaluates_to_constant_number()) + assert a.evaluates_to_constant_number() a = pybamm.Parameter("a") - self.assertFalse(a.evaluates_to_constant_number()) + assert not a.evaluates_to_constant_number() a = pybamm.Variable("a") - self.assertFalse(a.evaluates_to_constant_number()) + assert not a.evaluates_to_constant_number() a = pybamm.Scalar(3) - 2 - self.assertTrue(a.evaluates_to_constant_number()) + assert a.evaluates_to_constant_number() a = pybamm.Vector(np.ones(5)) - self.assertFalse(a.evaluates_to_constant_number()) + assert not a.evaluates_to_constant_number() a = pybamm.Matrix(np.ones((4, 6))) - self.assertFalse(a.evaluates_to_constant_number()) + assert not a.evaluates_to_constant_number() a = pybamm.StateVector(slice(0, 10)) - self.assertFalse(a.evaluates_to_constant_number()) + assert not a.evaluates_to_constant_number() # Time variable returns true a = 3 * pybamm.t + 2 - self.assertFalse(a.evaluates_to_constant_number()) + assert not a.evaluates_to_constant_number() def test_simplify_if_constant(self): m = pybamm.Matrix(np.zeros((10, 10))) m_simp = pybamm.simplify_if_constant(m) - self.assertIsInstance(m_simp, pybamm.Matrix) - self.assertIsInstance(m_simp.entries, csr_matrix) + assert isinstance(m_simp, pybamm.Matrix) + assert isinstance(m_simp.entries, csr_matrix) def test_symbol_repr(self): """ @@ -354,43 +347,48 @@ def test_symbol_repr(self): "d", domain=["test"], auxiliary_domains={"secondary": "other test"} ) hex_regex = r"\-?0x[0-9,a-f]+" - self.assertRegex( + assert re.search( + r"Symbol\(" + hex_regex + r", a, children=\[\], domains=\{\}\)", a.__repr__(), - r"Symbol\(" + hex_regex + r", a, children\=\[\], domains\=\{\}\)", ) - self.assertRegex( + assert re.search( + r"Symbol\(" + hex_regex + r", b, children=\[\], domains=\{\}\)", b.__repr__(), - r"Symbol\(" + hex_regex + r", b, children\=\[\], domains\=\{\}\)", ) - self.assertRegex( - c.__repr__(), + + assert re.search( r"Symbol\(" + hex_regex - + r", c, children\=\[\], domains\=\{'primary': \['test'\]\}\)", + + r", c, children=\[\], domains=\{'primary': \['test'\]\}\)", + c.__repr__(), ) - self.assertRegex( - d.__repr__(), + + assert re.search( r"Symbol\(" + hex_regex - + r", d, children\=\[\], domains\=\{'primary': \['test'\], " + + r", d, children=\[\], domains=\{'primary': \['test'\], " + r"'secondary': \['other test'\]\}\)", + d.__repr__(), ) - self.assertRegex( + + assert re.search( + r"Addition\(" + hex_regex + r", \+, children=\['a', 'b'\], domains=\{\}\)", (a + b).__repr__(), - r"Addition\(" + hex_regex + r", \+, children\=\['a', 'b'\], domains=\{\}", ) - self.assertRegex( - (a * d).__repr__(), + + assert re.search( r"Multiplication\(" + hex_regex - + r", \*, children\=\['a', 'd'\], domains\=\{'primary': \['test'\], " + + r", \*, children=\['a', 'd'\], domains=\{'primary': \['test'\], " + r"'secondary': \['other test'\]\}\)", + (a * d).__repr__(), ) - self.assertRegex( - pybamm.grad(c).__repr__(), + + assert re.search( r"Gradient\(" + hex_regex - + r", grad, children\=\['c'\], domains\=\{'primary': \['test'\]}", + + r", grad, children=\['c'\], domains=\{'primary': \['test'\]\}\)", + pybamm.grad(c).__repr__(), ) def test_symbol_visualise(self): @@ -401,8 +399,8 @@ def test_symbol_visualise(self): d = pybamm.Variable("d", "negative electrode") sym = pybamm.div(c * pybamm.grad(c)) + (c / d + c - d) ** 5 sym.visualise(test_name) - self.assertTrue(os.path.exists(test_name)) - with self.assertRaises(ValueError): + assert os.path.exists(test_name) + with pytest.raises(ValueError): sym.visualise(test_stub) def test_has_spatial_derivatives(self): @@ -411,14 +409,14 @@ def test_has_spatial_derivatives(self): div_eqn = pybamm.div(pybamm.standard_spatial_vars.x_edge) grad_div_eqn = pybamm.div(grad_eqn) algebraic_eqn = 2 * var + 3 - self.assertTrue(grad_eqn.has_symbol_of_classes(pybamm.Gradient)) - self.assertFalse(grad_eqn.has_symbol_of_classes(pybamm.Divergence)) - self.assertFalse(div_eqn.has_symbol_of_classes(pybamm.Gradient)) - self.assertTrue(div_eqn.has_symbol_of_classes(pybamm.Divergence)) - self.assertTrue(grad_div_eqn.has_symbol_of_classes(pybamm.Gradient)) - self.assertTrue(grad_div_eqn.has_symbol_of_classes(pybamm.Divergence)) - self.assertFalse(algebraic_eqn.has_symbol_of_classes(pybamm.Gradient)) - self.assertFalse(algebraic_eqn.has_symbol_of_classes(pybamm.Divergence)) + assert grad_eqn.has_symbol_of_classes(pybamm.Gradient) + assert not grad_eqn.has_symbol_of_classes(pybamm.Divergence) + assert not div_eqn.has_symbol_of_classes(pybamm.Gradient) + assert div_eqn.has_symbol_of_classes(pybamm.Divergence) + assert grad_div_eqn.has_symbol_of_classes(pybamm.Gradient) + assert grad_div_eqn.has_symbol_of_classes(pybamm.Divergence) + assert not algebraic_eqn.has_symbol_of_classes(pybamm.Gradient) + assert not algebraic_eqn.has_symbol_of_classes(pybamm.Divergence) def test_orphans(self): a = pybamm.Scalar(1) @@ -426,59 +424,55 @@ def test_orphans(self): summ = a + b a_orp, b_orp = summ.orphans - self.assertEqual(a, a_orp) - self.assertEqual(b, b_orp) + assert a == a_orp + assert b == b_orp def test_shape(self): scal = pybamm.Scalar(1) - self.assertEqual(scal.shape, ()) - self.assertEqual(scal.size, 1) + assert scal.shape == () + assert scal.size == 1 state = pybamm.StateVector(slice(10)) - self.assertEqual(state.shape, (10, 1)) - self.assertEqual(state.size, 10) + assert state.shape == (10, 1) + assert state.size == 10 state = pybamm.StateVector(slice(10, 25)) - self.assertEqual(state.shape, (15, 1)) + assert state.shape == (15, 1) # test with big object state = 2 * pybamm.StateVector(slice(100000)) - self.assertEqual(state.shape, (100000, 1)) + assert state.shape == (100000, 1) def test_shape_and_size_for_testing(self): scal = pybamm.Scalar(1) - self.assertEqual(scal.shape_for_testing, scal.shape) - self.assertEqual(scal.size_for_testing, scal.size) + assert scal.shape_for_testing == scal.shape + assert scal.size_for_testing == scal.size state = pybamm.StateVector(slice(10, 25), domain="test") state2 = pybamm.StateVector(slice(10, 25), domain="test 2") - self.assertEqual(state.shape_for_testing, state.shape) + assert state.shape_for_testing == state.shape param = pybamm.Parameter("a") - self.assertEqual(param.shape_for_testing, ()) + assert param.shape_for_testing == () func = pybamm.FunctionParameter("func", {"state": state}) - self.assertEqual(func.shape_for_testing, state.shape_for_testing) + assert func.shape_for_testing == state.shape_for_testing concat = pybamm.concatenation(state, state2) - self.assertEqual(concat.shape_for_testing, (30, 1)) - self.assertEqual(concat.size_for_testing, 30) + assert concat.shape_for_testing == (30, 1) + assert concat.size_for_testing == 30 var = pybamm.Variable("var", domain="negative electrode") broadcast = pybamm.PrimaryBroadcast(0, "negative electrode") - self.assertEqual(var.shape_for_testing, broadcast.shape_for_testing) - self.assertEqual( - (var + broadcast).shape_for_testing, broadcast.shape_for_testing - ) + assert var.shape_for_testing == broadcast.shape_for_testing + assert (var + broadcast).shape_for_testing == broadcast.shape_for_testing var = pybamm.Variable("var", domain=["random domain", "other domain"]) broadcast = pybamm.PrimaryBroadcast(0, ["random domain", "other domain"]) - self.assertEqual(var.shape_for_testing, broadcast.shape_for_testing) - self.assertEqual( - (var + broadcast).shape_for_testing, broadcast.shape_for_testing - ) + assert var.shape_for_testing == broadcast.shape_for_testing + assert (var + broadcast).shape_for_testing == broadcast.shape_for_testing sym = pybamm.Symbol("sym") - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): sym.shape_for_testing def test_test_shape(self): @@ -487,24 +481,24 @@ def test_test_shape(self): y1.test_shape() # bad shape, fails y2 = pybamm.StateVector(slice(0, 5)) - with self.assertRaises(pybamm.ShapeError): + with pytest.raises(pybamm.ShapeError): (y1 + y2).test_shape() def test_to_equation(self): - self.assertEqual(pybamm.Symbol("test").to_equation(), sympy.Symbol("test")) + assert pybamm.Symbol("test").to_equation() == sympy.Symbol("test") def test_numpy_array_ufunc(self): x = pybamm.Symbol("x") - self.assertEqual(np.exp(x), pybamm.exp(x)) + assert np.exp(x) == pybamm.exp(x) - def test_to_from_json(self): + def test_to_from_json(self, mocker): symc1 = pybamm.Symbol("child1", domain=["domain_1"]) symc2 = pybamm.Symbol("child2", domain=["domain_2"]) symp = pybamm.Symbol("parent", domain=["domain_3"], children=[symc1, symc2]) json_dict = { "name": "parent", - "id": mock.ANY, + "id": mocker.ANY, "domains": { "primary": ["domain_3"], "secondary": [], @@ -513,50 +507,40 @@ def test_to_from_json(self): }, } - self.assertEqual(symp.to_json(), json_dict) + assert symp.to_json() == json_dict json_dict["children"] = [symc1, symc2] - self.assertEqual(pybamm.Symbol._from_json(json_dict), symp) + assert pybamm.Symbol._from_json(json_dict) == symp -class TestIsZero(unittest.TestCase): +class TestIsZero: def test_is_scalar_zero(self): a = pybamm.Scalar(0) b = pybamm.Scalar(2) - self.assertTrue(pybamm.is_scalar_zero(a)) - self.assertFalse(pybamm.is_scalar_zero(b)) + assert pybamm.is_scalar_zero(a) + assert not pybamm.is_scalar_zero(b) def test_is_matrix_zero(self): a = pybamm.Matrix(coo_matrix(np.zeros((10, 10)))) b = pybamm.Matrix(coo_matrix(np.ones((10, 10)))) c = pybamm.Matrix(coo_matrix(([1], ([0], [0])), shape=(5, 5))) - self.assertTrue(pybamm.is_matrix_zero(a)) - self.assertFalse(pybamm.is_matrix_zero(b)) - self.assertFalse(pybamm.is_matrix_zero(c)) + assert pybamm.is_matrix_zero(a) + assert not pybamm.is_matrix_zero(b) + assert not pybamm.is_matrix_zero(c) a = pybamm.Matrix(np.zeros((10, 10))) b = pybamm.Matrix(np.ones((10, 10))) c = pybamm.Matrix([1, 0, 0]) - self.assertTrue(pybamm.is_matrix_zero(a)) - self.assertFalse(pybamm.is_matrix_zero(b)) - self.assertFalse(pybamm.is_matrix_zero(c)) + assert pybamm.is_matrix_zero(a) + assert not pybamm.is_matrix_zero(b) + assert not pybamm.is_matrix_zero(c) def test_bool(self): a = pybamm.Symbol("a") - with self.assertRaisesRegex(NotImplementedError, "Boolean"): + with pytest.raises(NotImplementedError, match="Boolean"): bool(a) # if statement calls Boolean - with self.assertRaisesRegex(NotImplementedError, "Boolean"): + with pytest.raises(NotImplementedError, match="Boolean"): if a > 1: print("a is greater than 1") - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - pybamm.settings.debug_mode = True - unittest.main() From 34d7f0e28090767385c7750150c286db84094046 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 12 Aug 2024 17:28:05 -0400 Subject: [PATCH 56/82] chore: update pre-commit hooks (#4336) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.5.6 → v0.5.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.6...v0.5.7) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 24fec627f4..4f1b0162a4 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.6" + rev: "v0.5.7" hooks: - id: ruff args: [--fix, --show-fixes] From fdab3efdff675ca51051ba3b7719407cc6d1930f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Aug 2024 21:10:45 -0400 Subject: [PATCH 57/82] Build(deps): bump the actions group with 2 updates (#4335) Bumps the actions group with 2 updates: [actions/upload-artifact](https://github.com/actions/upload-artifact) and [github/codeql-action](https://github.com/github/codeql-action). Updates `actions/upload-artifact` from 4.3.5 to 4.3.6 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4.3.5...v4.3.6) Updates `github/codeql-action` from 3.25.15 to 3.26.0 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/afb54ba388a7dca6ecae48f608c4ff05ff4cc77a...eb055d739abdc2e8de2e5f4ba1a8b246daa779aa) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/periodic_benchmarks.yml | 2 +- .github/workflows/publish_pypi.yml | 8 ++++---- .github/workflows/run_benchmarks_over_history.yml | 2 +- .github/workflows/scorecard.yml | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/periodic_benchmarks.yml b/.github/workflows/periodic_benchmarks.yml index 69921f210e..faa008ff05 100644 --- a/.github/workflows/periodic_benchmarks.yml +++ b/.github/workflows/periodic_benchmarks.yml @@ -48,7 +48,7 @@ jobs: LD_LIBRARY_PATH: $HOME/.local/lib - name: Upload results as artifact - uses: actions/upload-artifact@v4.3.5 + uses: actions/upload-artifact@v4.3.6 with: name: asv_periodic_results path: results diff --git a/.github/workflows/publish_pypi.yml b/.github/workflows/publish_pypi.yml index 740b4580b9..0e11e61c48 100644 --- a/.github/workflows/publish_pypi.yml +++ b/.github/workflows/publish_pypi.yml @@ -88,7 +88,7 @@ jobs: CIBW_TEST_COMMAND: python -c "import pybamm; print(pybamm.IDAKLUSolver())" - name: Upload Windows wheels - uses: actions/upload-artifact@v4.3.5 + uses: actions/upload-artifact@v4.3.6 with: name: wheels_windows path: ./wheelhouse/*.whl @@ -123,7 +123,7 @@ jobs: python -c "import pybamm; print(pybamm.IDAKLUSolver())" - name: Upload wheels for Linux - uses: actions/upload-artifact@v4.3.5 + uses: actions/upload-artifact@v4.3.6 with: name: wheels_manylinux path: ./wheelhouse/*.whl @@ -254,7 +254,7 @@ jobs: python -c "import pybamm; print(pybamm.IDAKLUSolver())" - name: Upload wheels for macOS (amd64, arm64) - uses: actions/upload-artifact@v4.3.5 + uses: actions/upload-artifact@v4.3.6 with: name: wheels_${{ matrix.os }} path: ./wheelhouse/*.whl @@ -274,7 +274,7 @@ jobs: run: pipx run build --sdist - name: Upload SDist - uses: actions/upload-artifact@v4.3.5 + uses: actions/upload-artifact@v4.3.6 with: name: sdist path: ./dist/*.tar.gz diff --git a/.github/workflows/run_benchmarks_over_history.yml b/.github/workflows/run_benchmarks_over_history.yml index 2b0d338c41..d01564b210 100644 --- a/.github/workflows/run_benchmarks_over_history.yml +++ b/.github/workflows/run_benchmarks_over_history.yml @@ -46,7 +46,7 @@ jobs: ${{ github.event.inputs.commit_start }}..${{ github.event.inputs.commit_end }} - name: Upload results as artifact - uses: actions/upload-artifact@v4.3.5 + uses: actions/upload-artifact@v4.3.6 with: name: asv_over_history_results path: results diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index d2b2178a9c..2f4bcbc33a 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -59,7 +59,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: SARIF file path: results.sarif @@ -68,6 +68,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15 + uses: github/codeql-action/upload-sarif@eb055d739abdc2e8de2e5f4ba1a8b246daa779aa # v3.26.0 with: sarif_file: results.sarif From 9691d09ab37ffe42d75e19b9282110b1d98294a6 Mon Sep 17 00:00:00 2001 From: Pradyot Ranjan <99216956+prady0t@users.noreply.github.com> Date: Wed, 14 Aug 2024 00:01:28 +0530 Subject: [PATCH 58/82] Using posargs to pass tests as arguments to `tests` nox session (#4334) * Using posargs to pass tests as arguments to nox session Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * style: pre-commit fixes * Adding info to docs Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * style: pre-commit fixes --------- Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> Co-authored-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Arjun Verma --- CONTRIBUTING.md | 5 +++++ noxfile.py | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 73977370b4..3724922fbc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -183,6 +183,11 @@ If you want to check integration tests as well as unit tests, type ```bash nox -s tests ``` +or, alternatively, you can use posargs to pass the path to the test to `nox`. For example: + +```bash +nox -s tests -- tests/unit/test_plotting/test_quick_plot.py::TestQuickPlot::test_simple_ode_model +``` When you commit anything to PyBaMM, these checks will also be run automatically (see [infrastructure](#infrastructure)). diff --git a/noxfile.py b/noxfile.py index 0e9ebc1776..a34d6e81f4 100644 --- a/noxfile.py +++ b/noxfile.py @@ -266,7 +266,10 @@ def run_tests(session): set_environment_variables(PYBAMM_ENV, session=session) session.install("setuptools", silent=False) session.install("-e", ".[all,dev,jax]", silent=False) - session.run("python", "-m", "pytest", "-m", "unit or integration") + specific_test_files = session.posargs if session.posargs else [] + session.run( + "python", "-m", "pytest", *specific_test_files, "-m", "unit or integration" + ) @nox.session(name="docs") From 1e3f13952982503bbd2f5e081c02d1ef22701402 Mon Sep 17 00:00:00 2001 From: Santhosh <52504160+santacodes@users.noreply.github.com> Date: Thu, 15 Aug 2024 21:00:57 +0530 Subject: [PATCH 59/82] changed ruff link in CONTRIBUTING.md (#4346) --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3724922fbc..556a732518 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -68,7 +68,7 @@ PyBaMM follows the [PEP8 recommendations](https://www.python.org/dev/peps/pep-00 ### Ruff -We use [ruff](https://github.com/charliermarsh/ruff) to check our PEP8 adherence. To try this on your system, navigate to the PyBaMM directory in a console and type +We use [ruff](https://github.com/astral-sh/ruff) to check our PEP8 adherence. To try this on your system, navigate to the PyBaMM directory in a console and type ```bash python -m pip install pre-commit From 3da425ac42e368562f2e634832fbe8302795dcd0 Mon Sep 17 00:00:00 2001 From: "Eric G. Kratz" Date: Mon, 19 Aug 2024 11:54:01 -0400 Subject: [PATCH 60/82] Fix CODEOWNERS after src change (#4361) --- .github/CODEOWNERS | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 789a05a9b8..0f503f09a7 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,18 +1,18 @@ # Automatically request reviews from maintainers # Package -/pybamm/discretisations/ @martinjrobins @rtimms @valentinsulzer -/pybamm/experiment/ @brosaplanella @martinjrobins @rtimms @valentinsulzer @TomTranter -/pybamm/expression_tree/ @martinjrobins @rtimms @valentinsulzer -/pybamm/geometry/ @martinjrobins @rtimms @valentinsulzer -/pybamm/input/ @brosaplanella @DrSOKane @rtimms @valentinsulzer @TomTranter @kratman -/pybamm/meshes/ @martinjrobins @rtimms @valentinsulzer @rtimms -/pybamm/models/ @brosaplanella @DrSOKane @rtimms @valentinsulzer @TomTranter @rtimms -/pybamm/parameters/ @brosaplanella @DrSOKane @rtimms @valentinsulzer @TomTranter @rtimms @kratman -/pybamm/plotting/ @martinjrobins @rtimms @Saransh-cpp @valentinsulzer @rtimms @kratman @agriyakhetarpal -/pybamm/solvers/ @martinjrobins @rtimms @valentinsulzer @TomTranter @rtimms -/pybamm/spatial_methods/ @martinjrobins @rtimms @valentinsulzer @rtimms -/pybamm/* @pybamm-team/maintainers # the files directly under /pybamm/, will not recurse +src/pybamm/discretisations/ @martinjrobins @rtimms @valentinsulzer +src/pybamm/experiment/ @brosaplanella @martinjrobins @rtimms @valentinsulzer @TomTranter +src/pybamm/expression_tree/ @martinjrobins @rtimms @valentinsulzer +src/pybamm/geometry/ @martinjrobins @rtimms @valentinsulzer +src/pybamm/input/ @brosaplanella @DrSOKane @rtimms @valentinsulzer @TomTranter @kratman +src/pybamm/meshes/ @martinjrobins @rtimms @valentinsulzer @rtimms +src/pybamm/models/ @brosaplanella @DrSOKane @rtimms @valentinsulzer @TomTranter @rtimms +src/pybamm/parameters/ @brosaplanella @DrSOKane @rtimms @valentinsulzer @TomTranter @rtimms @kratman +src/pybamm/plotting/ @martinjrobins @rtimms @Saransh-cpp @valentinsulzer @rtimms @kratman @agriyakhetarpal +src/pybamm/solvers/ @martinjrobins @rtimms @valentinsulzer @TomTranter @rtimms +src/pybamm/spatial_methods/ @martinjrobins @rtimms @valentinsulzer @rtimms +src/pybamm/* @pybamm-team/maintainers # the files directly under /pybamm/, will not recurse # CI/CD workflows /.github/ @martinjrobins @Saransh-cpp @agriyakhetarpal @kratman @arjxn-py From fb661d1f049811b30cd2a1b1ed946b917f8739b5 Mon Sep 17 00:00:00 2001 From: "Eric G. Kratz" Date: Mon, 19 Aug 2024 14:20:56 -0400 Subject: [PATCH 61/82] Remove "check_already_exists" from BPX (#4360) * Remove BPX key * Fix windows tests * Update tests/unit/test_parameters/test_bpx.py Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> --------- Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> --- src/pybamm/parameters/bpx.py | 4 +--- tests/unit/test_parameters/test_bpx.py | 16 ++++++++++------ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/pybamm/parameters/bpx.py b/src/pybamm/parameters/bpx.py index 2f259a8f52..7485e805b9 100644 --- a/src/pybamm/parameters/bpx.py +++ b/src/pybamm/parameters/bpx.py @@ -214,9 +214,7 @@ def _bpx_to_param_dict(bpx: BPX) -> dict: pybamm_dict[domain.pre_name + "conductivity [S.m-1]"] = 4e7 # add a default heat transfer coefficient - pybamm_dict.update( - {"Total heat transfer coefficient [W.m-2.K-1]": 0}, check_already_exists=False - ) + pybamm_dict.update({"Total heat transfer coefficient [W.m-2.K-1]": 0}) # transport efficiency for domain in [negative_electrode, separator, positive_electrode]: diff --git a/tests/unit/test_parameters/test_bpx.py b/tests/unit/test_parameters/test_bpx.py index f57fe8f7fa..ab4c25f97a 100644 --- a/tests/unit/test_parameters/test_bpx.py +++ b/tests/unit/test_parameters/test_bpx.py @@ -1,8 +1,3 @@ -# -# Tests for the create_from_bpx function -# - - import tempfile import unittest import json @@ -155,6 +150,15 @@ def test_bpx(self): sols[0]["Voltage [V]"].data, sols[1]["Voltage [V]"].data, atol=1e-7 ) + def test_no_already_exists_in_BPX(self): + with tempfile.NamedTemporaryFile( + suffix="test.json", delete=False, mode="w" + ) as test_file: + json.dump(copy.copy(self.base), test_file) + test_file.flush() + params = pybamm.ParameterValues.create_from_bpx(test_file.name) + assert "check_already_exists" not in params.keys() + def test_constant_functions(self): bpx_obj = copy.copy(self.base) bpx_obj["Parameterisation"]["Electrolyte"].update( @@ -234,7 +238,7 @@ def test_table_data(self): with tempfile.NamedTemporaryFile( suffix=filename, delete=False, mode="w" ) as tmp: - # write to a tempory file so we can + # write to a temporary file so we can # get the source later on using inspect.getsource # (as long as the file still exists) json.dump(bpx_obj, tmp) From 902fa1176093571f77fe1a7f5c112e8892c43dd4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Aug 2024 16:16:15 -0400 Subject: [PATCH 62/82] Build(deps): bump github/codeql-action in the actions group (#4363) Bumps the actions group with 1 update: [github/codeql-action](https://github.com/github/codeql-action). Updates `github/codeql-action` from 3.26.0 to 3.26.3 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/eb055d739abdc2e8de2e5f4ba1a8b246daa779aa...883d8588e56d1753a8a58c1c86e88976f0c23449) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/scorecard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 2f4bcbc33a..a3537cc010 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -68,6 +68,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@eb055d739abdc2e8de2e5f4ba1a8b246daa779aa # v3.26.0 + uses: github/codeql-action/upload-sarif@883d8588e56d1753a8a58c1c86e88976f0c23449 # v3.26.3 with: sarif_file: results.sarif From 977dcf9cdfc2fc978da3b1b56b7423454a2a2929 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 Aug 2024 17:14:18 -0400 Subject: [PATCH 63/82] chore: update pre-commit hooks (#4364) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.5.7 → v0.6.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.7...v0.6.1) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4f1b0162a4..ddbdb6350e 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.7" + rev: "v0.6.1" hooks: - id: ruff args: [--fix, --show-fixes] From 25a9936f14ea32c70da1656275d34bfe5d1c079f Mon Sep 17 00:00:00 2001 From: Pezhman Zarabadi-Poor Date: Wed, 21 Aug 2024 21:34:07 +0100 Subject: [PATCH 64/82] changed default value to float (#4366) * changed default value to float * added type hint of float to target_soc --- src/pybamm/parameters/parameter_values.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pybamm/parameters/parameter_values.py b/src/pybamm/parameters/parameter_values.py index 815dbedcc0..43c1ea17ce 100644 --- a/src/pybamm/parameters/parameter_values.py +++ b/src/pybamm/parameters/parameter_values.py @@ -79,7 +79,7 @@ def __init__(self, values, chemistry=None): pybamm.citations.register(citation) @staticmethod - def create_from_bpx(filename, target_soc=1): + def create_from_bpx(filename, target_soc: float = 1): """ Parameters ---------- From fcab586dcb986d74c27061d2abc51afbd14e88a2 Mon Sep 17 00:00:00 2001 From: Pradyot Ranjan <99216956+prady0t@users.noreply.github.com> Date: Fri, 23 Aug 2024 04:15:23 +0530 Subject: [PATCH 65/82] Testing the built wheels with `cibuildwheel` (#4341) * Using cibuildwheel to run tests Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * using cibw marker Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * removing trailing whitespace Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> --------- Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> Co-authored-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> --- .github/workflows/publish_pypi.yml | 10 ++++++++-- conftest.py | 7 +++++++ pyproject.toml | 1 + tests/unit/test_solvers/test_idaklu_solver.py | 3 ++- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish_pypi.yml b/.github/workflows/publish_pypi.yml index 0e11e61c48..7482f03003 100644 --- a/.github/workflows/publish_pypi.yml +++ b/.github/workflows/publish_pypi.yml @@ -85,8 +85,10 @@ jobs: CIBW_ARCHS: AMD64 CIBW_BEFORE_BUILD: python -m pip install setuptools wheel delvewheel # skip CasADi and CMake CIBW_REPAIR_WHEEL_COMMAND: delvewheel repair -w {dest_dir} {wheel} - CIBW_TEST_COMMAND: python -c "import pybamm; print(pybamm.IDAKLUSolver())" - + CIBW_TEST_EXTRAS: "all,dev,jax" + CIBW_TEST_COMMAND: | + python -c "import pybamm; print(pybamm.IDAKLUSolver())" + python -m pytest -m cibw {project}/tests/unit - name: Upload Windows wheels uses: actions/upload-artifact@v4.3.6 with: @@ -118,9 +120,11 @@ jobs: bash scripts/install_sundials.sh 6.0.3 6.5.0 CIBW_BEFORE_BUILD_LINUX: python -m pip install cmake casadi setuptools wheel CIBW_REPAIR_WHEEL_COMMAND_LINUX: auditwheel repair -w {dest_dir} {wheel} + CIBW_TEST_EXTRAS: "all,dev,jax" CIBW_TEST_COMMAND: | set -e -x python -c "import pybamm; print(pybamm.IDAKLUSolver())" + python -m pytest -m cibw {project}/tests/unit - name: Upload wheels for Linux uses: actions/upload-artifact@v4.3.6 @@ -249,9 +253,11 @@ jobs: delocate-listdeps {wheel} && delocate-wheel -v -w {dest_dir} {wheel} --require-target-macos-version 11.1 for file in {dest_dir}/*.whl; do mv "$file" "${file//macosx_11_1/macosx_11_0}"; done fi + CIBW_TEST_EXTRAS: "all,dev,jax" CIBW_TEST_COMMAND: | set -e -x python -c "import pybamm; print(pybamm.IDAKLUSolver())" + python -m pytest -m cibw {project}/tests/unit - name: Upload wheels for macOS (amd64, arm64) uses: actions/upload-artifact@v4.3.6 diff --git a/conftest.py b/conftest.py index 90439f910b..7ac6cf3c74 100644 --- a/conftest.py +++ b/conftest.py @@ -19,12 +19,19 @@ def pytest_addoption(parser): default=False, help="run integration tests", ) + parser.addoption( + "--cibw", + action="store_true", + default=False, + help="test build wheels", + ) def pytest_configure(config): config.addinivalue_line("markers", "scripts: mark test as an example script") config.addinivalue_line("markers", "unit: mark test as a unit test") config.addinivalue_line("markers", "integration: mark test as an integration test") + config.addinivalue_line("markers", "cibw: mark test as build wheel test") def pytest_collection_modifyitems(items): diff --git a/pyproject.toml b/pyproject.toml index 5e9c891877..7ab3d5f573 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -234,6 +234,7 @@ required_plugins = [ "pytest-xdist", "pytest-mock", ] +norecursedirs = 'pybind11*' addopts = [ "-nauto", "-vra", diff --git a/tests/unit/test_solvers/test_idaklu_solver.py b/tests/unit/test_solvers/test_idaklu_solver.py index c14c29fbb6..33e50eaa7d 100644 --- a/tests/unit/test_solvers/test_idaklu_solver.py +++ b/tests/unit/test_solvers/test_idaklu_solver.py @@ -5,13 +5,14 @@ from contextlib import redirect_stdout import io import unittest - +import pytest import numpy as np import pybamm from tests import get_discretisation_for_testing +@pytest.mark.cibw @unittest.skipIf(not pybamm.have_idaklu(), "idaklu solver is not installed") class TestIDAKLUSolver(unittest.TestCase): def test_ida_roberts_klu(self): From 4d391fa695578200edebe7a102fcbde50dff868b Mon Sep 17 00:00:00 2001 From: Marc Berliner <34451391+MarcBerliner@users.noreply.github.com> Date: Fri, 23 Aug 2024 11:36:18 -0700 Subject: [PATCH 66/82] IDA adaptive time stepping (#4351) * IDA adaptive time stepping * codecov and ubuntu runtime * update `IDAKLU` comments * better memory useage, codecov * codecov * fix ubuntu test * address comments * fix codecov, add tests * fix windows test * fix codecov * move integration test to unit fixes codecov * updates changes --------- Co-authored-by: Eric G. Kratz --- CHANGELOG.md | 1 + CMakeLists.txt | 1 + setup.py | 1 + src/pybamm/batch_study.py | 1 + src/pybamm/experiment/experiment.py | 2 +- src/pybamm/experiment/step/base_step.py | 78 ++ src/pybamm/simulation.py | 27 +- src/pybamm/solvers/algebraic_solver.py | 4 +- src/pybamm/solvers/base_solver.py | 72 +- src/pybamm/solvers/c_solvers/idaklu.cpp | 3 +- .../solvers/c_solvers/idaklu/IDAKLUSolver.hpp | 3 +- .../c_solvers/idaklu/IDAKLUSolverOpenMP.hpp | 145 +++- .../c_solvers/idaklu/IDAKLUSolverOpenMP.inl | 763 ++++++++++++------ .../solvers/c_solvers/idaklu/Solution.hpp | 2 +- .../solvers/c_solvers/idaklu/common.cpp | 31 + .../solvers/c_solvers/idaklu/common.hpp | 18 + src/pybamm/solvers/casadi_algebraic_solver.py | 4 +- src/pybamm/solvers/casadi_solver.py | 2 +- src/pybamm/solvers/dummy_solver.py | 2 +- src/pybamm/solvers/idaklu_jax.py | 22 +- src/pybamm/solvers/idaklu_solver.py | 64 +- src/pybamm/solvers/jax_solver.py | 6 +- src/pybamm/solvers/processed_variable.py | 57 +- src/pybamm/solvers/scipy_solver.py | 4 +- .../test_models/standard_model_tests.py | 21 +- tests/integration/test_solvers/test_idaklu.py | 71 +- .../unit/test_experiments/test_experiment.py | 8 +- .../test_simulation_with_experiment.py | 4 +- tests/unit/test_solvers/test_base_solver.py | 2 +- tests/unit/test_solvers/test_casadi_solver.py | 24 + tests/unit/test_solvers/test_idaklu_jax.py | 8 +- tests/unit/test_solvers/test_idaklu_solver.py | 253 ++++-- 32 files changed, 1251 insertions(+), 453 deletions(-) create mode 100644 src/pybamm/solvers/c_solvers/idaklu/common.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 57e0aad007..48d0d7ba32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ ## Optimizations +- Improved adaptive time-stepping performance of the (`IDAKLUSolver`). ([#4351](https://github.com/pybamm-team/PyBaMM/pull/4351)) - Improved performance and reliability of DAE consistent initialization. ([#4301](https://github.com/pybamm-team/PyBaMM/pull/4301)) - Replaced rounded Faraday constant with its exact value in `bpx.py` for better comparison between different tools. ([#4290](https://github.com/pybamm-team/PyBaMM/pull/4290)) diff --git a/CMakeLists.txt b/CMakeLists.txt index 42ab10ee69..8b3a2adfe5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,6 +91,7 @@ pybind11_add_module(idaklu src/pybamm/solvers/c_solvers/idaklu/IdakluJax.cpp src/pybamm/solvers/c_solvers/idaklu/IdakluJax.hpp src/pybamm/solvers/c_solvers/idaklu/common.hpp + src/pybamm/solvers/c_solvers/idaklu/common.cpp src/pybamm/solvers/c_solvers/idaklu/python.hpp src/pybamm/solvers/c_solvers/idaklu/python.cpp src/pybamm/solvers/c_solvers/idaklu/Solution.cpp diff --git a/setup.py b/setup.py index 95108454ed..6ceb049b31 100644 --- a/setup.py +++ b/setup.py @@ -322,6 +322,7 @@ def compile_KLU(): "src/pybamm/solvers/c_solvers/idaklu/IdakluJax.cpp", "src/pybamm/solvers/c_solvers/idaklu/IdakluJax.hpp", "src/pybamm/solvers/c_solvers/idaklu/common.hpp", + "src/pybamm/solvers/c_solvers/idaklu/common.cpp", "src/pybamm/solvers/c_solvers/idaklu/python.hpp", "src/pybamm/solvers/c_solvers/idaklu/python.cpp", "src/pybamm/solvers/c_solvers/idaklu/Solution.cpp", diff --git a/src/pybamm/batch_study.py b/src/pybamm/batch_study.py index e854c94e00..ffa5a83530 100644 --- a/src/pybamm/batch_study.py +++ b/src/pybamm/batch_study.py @@ -106,6 +106,7 @@ def solve( calc_esoh=True, starting_solution=None, initial_soc=None, + t_interp=None, **kwargs, ): """ diff --git a/src/pybamm/experiment/experiment.py b/src/pybamm/experiment/experiment.py index 39c49780e4..fb20a0180e 100644 --- a/src/pybamm/experiment/experiment.py +++ b/src/pybamm/experiment/experiment.py @@ -40,7 +40,7 @@ class Experiment: def __init__( self, operating_conditions: list[str | tuple[str]], - period: str = "1 minute", + period: str | None = None, temperature: float | None = None, termination: list[str] | None = None, ): diff --git a/src/pybamm/experiment/step/base_step.py b/src/pybamm/experiment/step/base_step.py index 6b77bed2cf..a7dfa9c9ba 100644 --- a/src/pybamm/experiment/step/base_step.py +++ b/src/pybamm/experiment/step/base_step.py @@ -266,6 +266,84 @@ def default_duration(self, value): else: return 24 * 3600 # one day in seconds + @staticmethod + def default_period(): + return 60.0 # seconds + + def default_time_vector(self, tf, t0=0): + if self.period is None: + period = self.default_period() + else: + period = self.period + npts = max(int(round(np.abs(tf - t0) / period)) + 1, 2) + + return np.linspace(t0, tf, npts) + + def setup_timestepping(self, solver, tf, t_interp=None): + """ + Setup timestepping for the model. + + Parameters + ---------- + solver: :class`pybamm.BaseSolver` + The solver + tf: float + The final time + t_interp: np.array | None + The time points at which to interpolate the solution + """ + if solver.supports_interp: + return self._setup_timestepping(solver, tf, t_interp) + else: + return self._setup_timestepping_dense_t_eval(solver, tf, t_interp) + + def _setup_timestepping(self, solver, tf, t_interp): + """ + Setup timestepping for the model. This returns a t_eval vector that stops + only at the first and last time points. If t_interp and the period are + unspecified, then the solver will use adaptive time-stepping. For a given + period, t_interp will be set to return the solution at the end of each period + and at the final time. + + Parameters + ---------- + solver: :class`pybamm.BaseSolver` + The solver + tf: float + The final time + t_interp: np.array | None + The time points at which to interpolate the solution + """ + t_eval = np.array([0, tf]) + if t_interp is None: + if self.period is not None: + t_interp = self.default_time_vector(tf) + else: + t_interp = solver.process_t_interp(t_interp) + + return t_eval, t_interp + + def _setup_timestepping_dense_t_eval(self, solver, tf, t_interp): + """ + Setup timestepping for the model. By default, this returns a dense t_eval which + stops the solver at each point in the t_eval vector. This method is for solvers + that do not support intra-solve interpolation for the solution. + + Parameters + ---------- + solver: :class`pybamm.BaseSolver` + The solver + tf: float + The final time + t_interp: np.array | None + The time points at which to interpolate the solution + """ + t_eval = self.default_time_vector(tf) + + t_interp = solver.process_t_interp(t_interp) + + return t_eval, t_interp + def process_model(self, model, parameter_values): new_model = model.new_copy() new_parameter_values = parameter_values.copy() diff --git a/src/pybamm/simulation.py b/src/pybamm/simulation.py index a54c76ec7d..5b999d6c83 100644 --- a/src/pybamm/simulation.py +++ b/src/pybamm/simulation.py @@ -352,6 +352,7 @@ def solve( callbacks=None, showprogress=False, inputs=None, + t_interp=None, **kwargs, ): """ @@ -361,11 +362,14 @@ def solve( Parameters ---------- t_eval : numeric type, optional - The times (in seconds) at which to compute the solution. Can be - provided as an array of times at which to return the solution, or as a - list `[t0, tf]` where `t0` is the initial time and `tf` is the final time. - If provided as a list the solution is returned at 100 points within the - interval `[t0, tf]`. + The times at which to stop the integration due to a discontinuity in time. + Can be provided as an array of times at which to return the solution, or as + a list `[t0, tf]` where `t0` is the initial time and `tf` is the final + time. If the solver does not support intra-solve interpolation, providing + `t_eval` as a list returns the solution at 100 points within the interval + `[t0, tf]`. Otherwise, the solution is returned at the times specified in + `t_interp` or as a result of the adaptive time-stepping solution. See the + `t_interp` argument for more details. If not using an experiment or running a drive cycle simulation (current provided as data) `t_eval` *must* be provided. @@ -400,6 +404,9 @@ def solve( Whether to show a progress bar for cycling. If true, shows a progress bar for cycles. Has no effect when not used with an experiment. Default is False. + t_interp : None, list or ndarray, optional + The times (in seconds) at which to interpolate the solution. Defaults to None. + Only valid for solvers that support intra-solve interpolation (`IDAKLUSolver`). **kwargs Additional key-word arguments passed to `solver.solve`. See :meth:`pybamm.BaseSolver.solve`. @@ -486,7 +493,7 @@ def solve( ) self._solution = solver.solve( - self._built_model, t_eval, inputs=inputs, **kwargs + self._built_model, t_eval, inputs=inputs, t_interp=t_interp, **kwargs ) elif self.operating_mode == "with experiment": @@ -687,13 +694,17 @@ def solve( "start time": start_time, } # Make sure we take at least 2 timesteps - npts = max(int(round(dt / step.period)) + 1, 2) + t_eval, t_interp_processed = step.setup_timestepping( + solver, dt, t_interp + ) + try: step_solution = solver.step( current_solution, model, dt, - t_eval=np.linspace(0, dt, npts), + t_eval, + t_interp=t_interp_processed, save=False, inputs=inputs, **kwargs, diff --git a/src/pybamm/solvers/algebraic_solver.py b/src/pybamm/solvers/algebraic_solver.py index 5811e3b16d..9b6663d007 100644 --- a/src/pybamm/solvers/algebraic_solver.py +++ b/src/pybamm/solvers/algebraic_solver.py @@ -36,7 +36,7 @@ def __init__(self, method="lm", tol=1e-6, extra_options=None): self.tol = tol self.extra_options = extra_options or {} self.name = f"Algebraic solver ({method})" - self.algebraic_solver = True + self._algebraic_solver = True pybamm.citations.register("Virtanen2020") @property @@ -47,7 +47,7 @@ def tol(self): def tol(self, value): self._tol = value - def _integrate(self, model, t_eval, inputs_dict=None): + def _integrate(self, model, t_eval, inputs_dict=None, t_interp=None): """ Calculate the solution of the algebraic equations through root-finding diff --git a/src/pybamm/solvers/base_solver.py b/src/pybamm/solvers/base_solver.py index b4ff3a5774..9027bd51c4 100644 --- a/src/pybamm/solvers/base_solver.py +++ b/src/pybamm/solvers/base_solver.py @@ -63,12 +63,25 @@ def __init__( # Defaults, can be overwritten by specific solver self.name = "Base solver" - self.ode_solver = False - self.algebraic_solver = False + self._ode_solver = False + self._algebraic_solver = False + self._supports_interp = False self._on_extrapolation = "warn" self.computed_var_fcns = {} self._mp_context = self.get_platform_context(platform.system()) + @property + def ode_solver(self): + return self._ode_solver + + @property + def algebraic_solver(self): + return self._algebraic_solver + + @property + def supports_interp(self): + return self._supports_interp + @property def root_method(self): return self._root_method @@ -107,7 +120,7 @@ def set_up(self, model, inputs=None, t_eval=None, ics_only=False): inputs : dict, optional Any input parameters to pass to the model when solving t_eval : numeric type, optional - The times (in seconds) at which to compute the solution + The times at which to stop the integration due to a discontinuity in time. """ inputs = inputs or {} @@ -321,7 +334,7 @@ def _check_and_prepare_model_inplace(self, model, inputs, ics_only): f"Cannot use ODE solver '{self.name}' to solve DAE model" ) # Check model.rhs for algebraic solvers - if self.algebraic_solver is True and len(model.rhs) > 0: + if self._algebraic_solver is True and len(model.rhs) > 0: raise pybamm.SolverError( """Cannot use algebraic solver to solve model with time derivatives""" ) @@ -614,7 +627,7 @@ def _set_consistent_initialization(self, model, time, inputs_dict): """ - if self.algebraic_solver or model.len_alg == 0: + if self._algebraic_solver or model.len_alg == 0: # Don't update model.y0 return @@ -664,6 +677,7 @@ def solve( inputs=None, nproc=None, calculate_sensitivities=False, + t_interp=None, ): """ Execute the solver setup and calculate the solution of the model at @@ -687,6 +701,9 @@ def solve( Whether the solver calculates sensitivities of all input parameters. Defaults to False. If only a subset of sensitivities are required, can also pass a list of input parameter names + t_interp : None, list or ndarray, optional + The times (in seconds) at which to interpolate the solution. Defaults to None. + Only valid for solvers that support intra-solve interpolation (`IDAKLUSolver`). Returns ------- @@ -720,13 +737,13 @@ def solve( # t_eval can only be None if the solver is an algebraic solver. In that case # set it to 0 if t_eval is None: - if self.algebraic_solver is False: + if self._algebraic_solver is False: raise ValueError("t_eval cannot be None") t_eval = np.array([0]) # If t_eval is provided as [t0, tf] return the solution at 100 points elif isinstance(t_eval, list): - if len(t_eval) == 1 and self.algebraic_solver is True: + if len(t_eval) == 1 and self._algebraic_solver is True: t_eval = np.array(t_eval) elif len(t_eval) != 2: raise pybamm.SolverError( @@ -735,13 +752,15 @@ def solve( "initial time and tf is the final time, but has been provided " f"as a list of length {len(t_eval)}." ) - else: + elif not self.supports_interp: t_eval = np.linspace(t_eval[0], t_eval[-1], 100) # Make sure t_eval is monotonic if (np.diff(t_eval) < 0).any(): raise pybamm.SolverError("t_eval must increase monotonically") + t_interp = self.process_t_interp(t_interp) + # Set up inputs # # Argument "inputs" can be either a list of input dicts or @@ -813,7 +832,7 @@ def solve( self._model_set_up[model]["initial conditions"] != model.concatenated_initial_conditions ): - if self.algebraic_solver: + if self._algebraic_solver: # For an algebraic solver, we don't need to set up the initial # conditions function and we can just evaluate # model.concatenated_initial_conditions @@ -860,6 +879,7 @@ def solve( model, t_eval[start_index:end_index], model_inputs_list[0], + t_interp=t_interp, ) new_solutions = [new_solution] elif model.convert_to_format == "jax": @@ -868,6 +888,7 @@ def solve( model, t_eval[start_index:end_index], model_inputs_list, + t_interp, ) else: with mp.get_context(self._mp_context).Pool(processes=nproc) as p: @@ -877,6 +898,7 @@ def solve( [model] * ninputs, [t_eval[start_index:end_index]] * ninputs, model_inputs_list, + [t_interp] * ninputs, ), ) p.close() @@ -940,7 +962,7 @@ def solve( # Raise error if solutions[0] only contains one timestep (except for algebraic # solvers, where we may only expect one time in the solution) if ( - self.algebraic_solver is False + self._algebraic_solver is False and len(solutions[0].all_ts) == 1 and len(solutions[0].all_ts[0]) == 1 ): @@ -1044,6 +1066,23 @@ def _check_events_with_initialization(t_eval, model, inputs_dict): f"Events {event_names} are non-positive at initial conditions" ) + def process_t_interp(self, t_interp): + # set a variable for this + no_interp = (not self.supports_interp) and ( + t_interp is not None and len(t_interp) != 0 + ) + if no_interp: + warnings.warn( + f"Explicit interpolation times not implemented for {self.name}", + pybamm.SolverWarning, + stacklevel=2, + ) + + if no_interp or t_interp is None: + t_interp = np.empty(0) + + return t_interp + def step( self, old_solution, @@ -1053,6 +1092,7 @@ def step( npts=None, inputs=None, save=True, + t_interp=None, ): """ Step the solution of the model forward by a given time increment. The @@ -1069,7 +1109,7 @@ def step( dt : numeric type The timestep (in seconds) over which to step the solution t_eval : list or numpy.ndarray, optional - An array of times at which to return the solution during the step + An array of times at which to stop the simulation and return the solution during the step (Note: t_eval is the time measured from the start of the step, so should start at 0 and end at dt). By default, the solution is returned at t0 and t0 + dt. npts : deprecated @@ -1077,6 +1117,9 @@ def step( Any input parameters to pass to the model when solving save : bool, optional Save solution with all previous timesteps. Defaults to True. + t_interp : None, list or ndarray, optional + The times (in seconds) at which to interpolate the solution. Defaults to None. + Only valid for solvers that support intra-solve interpolation (`IDAKLUSolver`). Raises ------ :class:`pybamm.ModelError` @@ -1123,8 +1166,11 @@ def step( else: pass + t_interp = self.process_t_interp(t_interp) + t_start = old_solution.t[-1] t_eval = t_start + t_eval + t_interp = t_start + t_interp t_end = t_start + dt if t_start == 0: @@ -1136,6 +1182,8 @@ def step( # the start of the next step t_start_shifted = t_start + step_start_offset t_eval[0] = t_start_shifted + if t_interp.size > 0 and t_interp[0] == t_start: + t_interp[0] = t_start_shifted # Set timer timer = pybamm.Timer() @@ -1187,7 +1235,7 @@ def step( # Step pybamm.logger.verbose(f"Stepping for {t_start_shifted:.0f} < t < {t_end:.0f}") timer.reset() - solution = self._integrate(model, t_eval, model_inputs) + solution = self._integrate(model, t_eval, model_inputs, t_interp) solution.solve_time = timer.time() # Check if extrapolation occurred diff --git a/src/pybamm/solvers/c_solvers/idaklu.cpp b/src/pybamm/solvers/c_solvers/idaklu.cpp index 3427c01853..bb9466d40b 100644 --- a/src/pybamm/solvers/c_solvers/idaklu.cpp +++ b/src/pybamm/solvers/c_solvers/idaklu.cpp @@ -59,7 +59,8 @@ PYBIND11_MODULE(idaklu, m) py::class_(m, "IDAKLUSolver") .def("solve", &IDAKLUSolver::solve, "perform a solve", - py::arg("t"), + py::arg("t_eval"), + py::arg("t_interp"), py::arg("y0"), py::arg("yp0"), py::arg("inputs"), diff --git a/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolver.hpp b/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolver.hpp index 26e587e424..29b451e6d3 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolver.hpp +++ b/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolver.hpp @@ -27,7 +27,8 @@ class IDAKLUSolver * @brief Abstract solver method that returns a Solution class */ virtual Solution solve( - np_array t_np, + np_array t_eval_np, + np_array t_interp_np, np_array y0_np, np_array yp0_np, np_array_dense inputs) = 0; diff --git a/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.hpp b/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.hpp index 98148a3c9f..ca710fbff6 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.hpp +++ b/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.hpp @@ -3,6 +3,9 @@ #include "IDAKLUSolver.hpp" #include "common.hpp" +#include +using std::vector; + #include "Options.hpp" #include "Solution.hpp" #include "sundials_legacy_wrapper.hpp" @@ -46,26 +49,32 @@ class IDAKLUSolverOpenMP : public IDAKLUSolver void *ida_mem = nullptr; np_array atol_np; np_array rhs_alg_id; - int number_of_states; // cppcheck-suppress unusedStructMember - int number_of_parameters; // cppcheck-suppress unusedStructMember - int number_of_events; // cppcheck-suppress unusedStructMember + int const number_of_states; // cppcheck-suppress unusedStructMember + int const number_of_parameters; // cppcheck-suppress unusedStructMember + int const number_of_events; // cppcheck-suppress unusedStructMember int precon_type; // cppcheck-suppress unusedStructMember N_Vector yy, yp, avtol; // y, y', and absolute tolerance N_Vector *yyS; // cppcheck-suppress unusedStructMember N_Vector *ypS; // cppcheck-suppress unusedStructMember N_Vector id; // rhs_alg_id realtype rtol; - const int jac_times_cjmass_nnz; // cppcheck-suppress unusedStructMember - int jac_bandwidth_lower; // cppcheck-suppress unusedStructMember - int jac_bandwidth_upper; // cppcheck-suppress unusedStructMember + int const jac_times_cjmass_nnz; // cppcheck-suppress unusedStructMember + int const jac_bandwidth_lower; // cppcheck-suppress unusedStructMember + int const jac_bandwidth_upper; // cppcheck-suppress unusedStructMember SUNMatrix J; SUNLinearSolver LS = nullptr; std::unique_ptr functions; - std::vector res; - std::vector res_dvar_dy; - std::vector res_dvar_dp; - SetupOptions setup_opts; - SolverOptions solver_opts; + vector res; + vector res_dvar_dy; + vector res_dvar_dp; + bool const sensitivity; // cppcheck-suppress unusedStructMember + bool const save_outputs_only; // cppcheck-suppress unusedStructMember + int length_of_return_vector; // cppcheck-suppress unusedStructMember + vector t; // cppcheck-suppress unusedStructMember + vector> y; // cppcheck-suppress unusedStructMember + vector>> yS; // cppcheck-suppress unusedStructMember + SetupOptions const setup_opts; + SolverOptions const solver_opts; #if SUNDIALS_VERSION_MAJOR >= 6 SUNContext sunctx; @@ -94,36 +103,12 @@ class IDAKLUSolverOpenMP : public IDAKLUSolver */ ~IDAKLUSolverOpenMP(); - /** - * Evaluate functions (including sensitivies) for each requested - * variable and store - * @brief Evaluate functions - */ - void CalcVars( - realtype *y_return, - size_t length_of_return_vector, - size_t t_i, - realtype *tret, - realtype *yval, - const std::vector& ySval, - realtype *yS_return, - size_t *ySk); - - /** - * @brief Evaluate functions for sensitivities - */ - void CalcVarsSensitivities( - realtype *tret, - realtype *yval, - const std::vector& ySval, - realtype *yS_return, - size_t *ySk); - /** * @brief The main solve method that solves for each variable and time step */ Solution solve( - np_array t_np, + np_array t_eval_np, + np_array t_interp_np, np_array y0_np, np_array yp0_np, np_array_dense inputs) override; @@ -143,6 +128,16 @@ class IDAKLUSolverOpenMP : public IDAKLUSolver */ void SetMatrix(); + /** + * @brief Get the length of the return vector + */ + int ReturnVectorLength(); + + /** + * @brief Initialize the storage for the solution + */ + void InitializeStorage(int const N); + /** * @brief Apply user-configurable IDA options */ @@ -152,6 +147,82 @@ class IDAKLUSolverOpenMP : public IDAKLUSolver * @brief Check the return flag for errors */ void CheckErrors(int const & flag); + + /** + * @brief Print the solver statistics + */ + void PrintStats(); + + /** + * @brief Extend the adaptive arrays by 1 + */ + void ExtendAdaptiveArrays(); + + /** + * @brief Set the step values + */ + void SetStep( + realtype &t_val, + realtype *y_val, + vector const &yS_val, + int &i_save + ); + + /** + * @brief Save the interpolated step values + */ + void SetStepInterp( + int &i_interp, + realtype &t_interp_next, + vector const &t_interp, + realtype &t_val, + realtype &t_prev, + realtype const &t_next, + realtype *y_val, + vector const &yS_val, + int &i_save + ); + + /** + * @brief Save y and yS at the current time + */ + void SetStepFull( + realtype &t_val, + realtype *y_val, + vector const &yS_val, + int &i_save + ); + + /** + * @brief Save yS at the current time + */ + void SetStepFullSensitivities( + realtype &t_val, + realtype *y_val, + vector const &yS_val, + int &i_save + ); + + /** + * @brief Save the output function results at the requested time + */ + void SetStepOutput( + realtype &t_val, + realtype *y_val, + const vector &yS_val, + int &i_save + ); + + /** + * @brief Save the output function sensitivities at the requested time + */ + void SetStepOutputSensitivities( + realtype &t_val, + realtype *y_val, + const vector &yS_val, + int &i_save + ); + }; #include "IDAKLUSolverOpenMP.inl" diff --git a/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl b/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl index b309fd6028..de6c43466a 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl +++ b/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl @@ -1,5 +1,8 @@ #include "Expressions/Expressions.hpp" #include "sundials_functions.hpp" +#include + +#include "common.hpp" template IDAKLUSolverOpenMP::IDAKLUSolverOpenMP( @@ -24,6 +27,8 @@ IDAKLUSolverOpenMP::IDAKLUSolverOpenMP( jac_bandwidth_lower(jac_bandwidth_lower_input), jac_bandwidth_upper(jac_bandwidth_upper_input), functions(std::move(functions_arg)), + sensitivity(number_of_parameters > 0), + save_outputs_only(functions->var_fcns.size() > 0), setup_opts(setup_input), solver_opts(solver_input) { @@ -40,7 +45,7 @@ IDAKLUSolverOpenMP::IDAKLUSolverOpenMP( // create the vector of initial values AllocateVectors(); - if (number_of_parameters > 0) { + if (sensitivity) { yyS = N_VCloneVectorArray(number_of_parameters, yy); ypS = N_VCloneVectorArray(number_of_parameters, yp); } @@ -88,6 +93,55 @@ void IDAKLUSolverOpenMP::AllocateVectors() { id = N_VNew_OpenMP(number_of_states, setup_opts.num_threads, sunctx); } +template +void IDAKLUSolverOpenMP::InitializeStorage(int const N) { + length_of_return_vector = ReturnVectorLength(); + + t = vector(N, 0.0); + + y = vector>( + N, + vector(length_of_return_vector, 0.0) + ); + + yS = vector>>( + N, + vector>( + number_of_parameters, + vector(length_of_return_vector, 0.0) + ) + ); +} + +template +int IDAKLUSolverOpenMP::ReturnVectorLength() { + if (!save_outputs_only) { + return number_of_states; + } + + // set return vectors + int length_of_return_vector = 0; + size_t max_res_size = 0; // maximum result size (for common result buffer) + size_t max_res_dvar_dy = 0, max_res_dvar_dp = 0; + // return only the requested variables list after computation + for (auto& var_fcn : functions->var_fcns) { + max_res_size = std::max(max_res_size, size_t(var_fcn->out_shape(0))); + length_of_return_vector += var_fcn->nnz_out(); + for (auto& dvar_fcn : functions->dvar_dy_fcns) { + max_res_dvar_dy = std::max(max_res_dvar_dy, size_t(dvar_fcn->out_shape(0))); + } + for (auto& dvar_fcn : functions->dvar_dp_fcns) { + max_res_dvar_dp = std::max(max_res_dvar_dp, size_t(dvar_fcn->out_shape(0))); + } + + res.resize(max_res_size); + res_dvar_dy.resize(max_res_dvar_dy); + res_dvar_dp.resize(max_res_dvar_dp); + } + + return length_of_return_vector; +} + template void IDAKLUSolverOpenMP::SetSolverOptions() { // Maximum order of the linear multistep method @@ -153,8 +207,6 @@ void IDAKLUSolverOpenMP::SetSolverOptions() { } } - - template void IDAKLUSolverOpenMP::SetMatrix() { // Create Matrix object @@ -215,7 +267,7 @@ void IDAKLUSolverOpenMP::Initialize() { CheckErrors(IDASetJacFn(ida_mem, jacobian_eval)); } - if (number_of_parameters > 0) { + if (sensitivity) { CheckErrors(IDASensInit(ida_mem, number_of_parameters, IDA_SIMULTANEOUS, sensitivities_eval, yyS, ypS)); CheckErrors(IDASensEEtolerances(ida_mem)); @@ -238,7 +290,6 @@ void IDAKLUSolverOpenMP::Initialize() { template IDAKLUSolverOpenMP::~IDAKLUSolverOpenMP() { - bool sensitivity = number_of_parameters > 0; // Free memory if (sensitivity) { IDASensFree(ida_mem); @@ -261,70 +312,10 @@ IDAKLUSolverOpenMP::~IDAKLUSolverOpenMP() { SUNContext_Free(&sunctx); } -template -void IDAKLUSolverOpenMP::CalcVars( - realtype *y_return, - size_t length_of_return_vector, - size_t t_i, - realtype *tret, - realtype *yval, - const std::vector& ySval, - realtype *yS_return, - size_t *ySk -) { - DEBUG("IDAKLUSolver::CalcVars"); - // Evaluate functions for each requested variable and store - size_t j = 0; - for (auto& var_fcn : functions->var_fcns) { - (*var_fcn)({tret, yval, functions->inputs.data()}, {&res[0]}); - // store in return vector - for (size_t jj=0; jjnnz_out(); jj++) { - y_return[t_i*length_of_return_vector + j++] = res[jj]; - } - } - // calculate sensitivities - CalcVarsSensitivities(tret, yval, ySval, yS_return, ySk); -} - -template -void IDAKLUSolverOpenMP::CalcVarsSensitivities( - realtype *tret, - realtype *yval, - const std::vector& ySval, - realtype *yS_return, - size_t *ySk -) { - DEBUG("IDAKLUSolver::CalcVarsSensitivities"); - // Calculate sensitivities - std::vector dens_dvar_dp = std::vector(number_of_parameters, 0); - for (size_t dvar_k = 0; dvar_k < functions->dvar_dy_fcns.size(); dvar_k++) { - // Isolate functions - Expression* dvar_dy = functions->dvar_dy_fcns[dvar_k]; - Expression* dvar_dp = functions->dvar_dp_fcns[dvar_k]; - // Calculate dvar/dy - (*dvar_dy)({tret, yval, functions->inputs.data()}, {&res_dvar_dy[0]}); - // Calculate dvar/dp and convert to dense array for indexing - (*dvar_dp)({tret, yval, functions->inputs.data()}, {&res_dvar_dp[0]}); - for (int k=0; knnz_out(); k++) { - dens_dvar_dp[dvar_dp->get_row()[k]] = res_dvar_dp[k]; - } - // Calculate sensitivities - for (int paramk = 0; paramk < number_of_parameters; paramk++) { - yS_return[*ySk] = dens_dvar_dp[paramk]; - for (int spk = 0; spk < dvar_dy->nnz_out(); spk++) { - yS_return[*ySk] += res_dvar_dy[spk] * ySval[paramk][dvar_dy->get_col()[spk]]; - } - (*ySk)++; - } - } -} - template Solution IDAKLUSolverOpenMP::solve( - np_array t_np, + np_array t_eval_np, + np_array t_interp_np, np_array y0_np, np_array yp0_np, np_array_dense inputs @@ -332,21 +323,78 @@ Solution IDAKLUSolverOpenMP::solve( { DEBUG("IDAKLUSolver::solve"); - int number_of_timesteps = t_np.request().size; - auto t = t_np.unchecked<1>(); - realtype t0 = RCONST(t(0)); + // If t_interp is empty, save all adaptive steps + bool save_adaptive_steps = t_interp_np.unchecked<1>().size() == 0; + + // Process the time inputs + // 1. Get the sorted and unique t_eval vector + auto const t_eval = makeSortedUnique(t_eval_np); + + // 2.1. Get the sorted and unique t_interp vector + auto const t_interp_unique_sorted = makeSortedUnique(t_interp_np); + + // 2.2 Remove the t_eval values from t_interp + auto const t_interp_setdiff = setDiff(t_interp_unique_sorted, t_eval); + + // 2.3 Finally, get the sorted and unique t_interp vector with t_eval values removed + auto const t_interp = makeSortedUnique(t_interp_setdiff); + + int const number_of_evals = t_eval.size(); + int const number_of_interps = t_interp.size(); + + // setDiff removes entries of t_interp that overlap with + // t_eval, so we need to check if we need to interpolate any unique points. + // This is not the same as save_adaptive_steps since some entries of t_interp + // may be removed by setDiff + bool save_interp_steps = number_of_interps > 0; + + // 3. Check if the timestepping entries are valid + if (number_of_evals < 2) { + throw std::invalid_argument( + "t_eval must have at least 2 entries" + ); + } else if (save_interp_steps) { + if (t_interp.front() < t_eval.front()) { + throw std::invalid_argument( + "t_interp values must be greater than the smallest t_eval value: " + + std::to_string(t_eval.front()) + ); + } else if (t_interp.back() > t_eval.back()) { + throw std::invalid_argument( + "t_interp values must be less than the greatest t_eval value: " + + std::to_string(t_eval.back()) + ); + } + } + + // Initialize length_of_return_vector, t, y, and yS + InitializeStorage(number_of_evals + number_of_interps); + + int i_save = 0; + + realtype t0 = t_eval.front(); + realtype tf = t_eval.back(); + + realtype t_val = t0; + realtype t_prev = t0; + int i_eval = 0; + + realtype t_interp_next; + int i_interp = 0; + // If t_interp is empty, save all adaptive steps + if (save_interp_steps) { + t_interp_next = t_interp[0]; + } + auto y0 = y0_np.unchecked<1>(); auto yp0 = yp0_np.unchecked<1>(); auto n_coeffs = number_of_states + number_of_parameters * number_of_states; - bool const sensitivity = number_of_parameters > 0; if (y0.size() != n_coeffs) { throw std::domain_error( "y0 has wrong size. Expected " + std::to_string(n_coeffs) + " but got " + std::to_string(y0.size())); - } - - if (yp0.size() != n_coeffs) { + } else if (yp0.size() != n_coeffs) { throw std::domain_error( "yp0 has wrong size. Expected " + std::to_string(n_coeffs) + " but got " + std::to_string(yp0.size())); @@ -358,23 +406,23 @@ Solution IDAKLUSolverOpenMP::solve( functions->inputs[i] = p_inputs(i, 0); } - // set initial conditions - realtype *yval = N_VGetArrayPointer(yy); - realtype *ypval = N_VGetArrayPointer(yp); - std::vector ySval(number_of_parameters); - std::vector ypSval(number_of_parameters); + // Setup consistent initialization + realtype *y_val = N_VGetArrayPointer(yy); + realtype *yp_val = N_VGetArrayPointer(yp); + vector yS_val(number_of_parameters); + vector ypS_val(number_of_parameters); for (int p = 0 ; p < number_of_parameters; p++) { - ySval[p] = N_VGetArrayPointer(yyS[p]); - ypSval[p] = N_VGetArrayPointer(ypS[p]); + yS_val[p] = N_VGetArrayPointer(yyS[p]); + ypS_val[p] = N_VGetArrayPointer(ypS[p]); for (int i = 0; i < number_of_states; i++) { - ySval[p][i] = y0[i + (p + 1) * number_of_states]; - ypSval[p][i] = yp0[i + (p + 1) * number_of_states]; + yS_val[p][i] = y0[i + (p + 1) * number_of_states]; + ypS_val[p][i] = yp0[i + (p + 1) * number_of_states]; } } for (int i = 0; i < number_of_states; i++) { - yval[i] = y0[i]; - ypval[i] = yp0[i]; + y_val[i] = y0[i]; + yp_val[i] = yp0[i]; } SetSolverOptions(); @@ -384,54 +432,127 @@ Solution IDAKLUSolverOpenMP::solve( CheckErrors(IDASensReInit(ida_mem, IDA_SIMULTANEOUS, yyS, ypS)); } - // correct initial values + // Prepare first time step + i_eval = 1; + realtype t_eval_next = t_eval[i_eval]; + + // Consistent initialization int const init_type = solver_opts.init_all_y_ic ? IDA_Y_INIT : IDA_YA_YDP_INIT; if (solver_opts.calc_ic) { DEBUG("IDACalcIC"); // IDACalcIC will throw a warning if it fails to find initial conditions - IDACalcIC(ida_mem, init_type, t(1)); + IDACalcIC(ida_mem, init_type, t_eval_next); } if (sensitivity) { - CheckErrors(IDAGetSens(ida_mem, &t0, yyS)); + CheckErrors(IDAGetSens(ida_mem, &t_val, yyS)); } - realtype tret; - realtype t_final = t(number_of_timesteps - 1); + // Store Consistent initialization + SetStep(t0, y_val, yS_val, i_save); - // set return vectors - int length_of_return_vector = 0; - int length_of_final_sv_slice = 0; - size_t max_res_size = 0; // maximum result size (for common result buffer) - size_t max_res_dvar_dy = 0, max_res_dvar_dp = 0; - if (functions->var_fcns.size() > 0) { - // return only the requested variables list after computation - for (auto& var_fcn : functions->var_fcns) { - max_res_size = std::max(max_res_size, size_t(var_fcn->out_shape(0))); - length_of_return_vector += var_fcn->nnz_out(); - for (auto& dvar_fcn : functions->dvar_dy_fcns) { - max_res_dvar_dy = std::max(max_res_dvar_dy, size_t(dvar_fcn->out_shape(0))); + // Set the initial stop time + IDASetStopTime(ida_mem, t_eval_next); + + // Solve the system + int retval; + DEBUG("IDASolve"); + while (true) { + // Progress one step + retval = IDASolve(ida_mem, tf, &t_val, yy, yp, IDA_ONE_STEP); + + if (retval < 0) { + // failed + break; + } else if (t_prev == t_val) { + // IDA sometimes returns an identical time point twice + // instead of erroring. Assign a retval and break + retval = IDA_ERR_FAIL; + break; + } + + bool hit_tinterp = save_interp_steps && t_interp_next >= t_prev; + bool hit_teval = retval == IDA_TSTOP_RETURN; + bool hit_final_time = t_val >= tf || (hit_teval && i_eval == number_of_evals); + bool hit_event = retval == IDA_ROOT_RETURN; + bool hit_adaptive = save_adaptive_steps && retval == IDA_SUCCESS; + + if (sensitivity) { + CheckErrors(IDAGetSens(ida_mem, &t_val, yyS)); + } + + if (hit_tinterp) { + // Save the interpolated state at t_prev < t < t_val, for all t in t_interp + SetStepInterp(i_interp, + t_interp_next, + t_interp, + t_val, + t_prev, + t_eval_next, + y_val, + yS_val, + i_save); + } + + if (hit_adaptive || hit_teval || hit_event) { + if (hit_tinterp) { + // Reset the states and sensitivities at t = t_val + CheckErrors(IDAGetDky(ida_mem, t_val, 0, yy)); + if (sensitivity) { + CheckErrors(IDAGetSens(ida_mem, &t_val, yyS)); + } } - for (auto& dvar_fcn : functions->dvar_dp_fcns) { - max_res_dvar_dp = std::max(max_res_dvar_dp, size_t(dvar_fcn->out_shape(0))); + + // Save the current state at t_val + if (hit_adaptive) { + // Dynamically allocate memory for the adaptive step + ExtendAdaptiveArrays(); } - length_of_final_sv_slice = number_of_states; + SetStep(t_val, y_val, yS_val, i_save); } - } else { - // Return full y state-vector - length_of_return_vector = number_of_states; + + if (hit_final_time || hit_event) { + // Successful simulation. Exit the while loop + break; + } else if (hit_teval) { + // Set the next stop time + i_eval += 1; + t_eval_next = t_eval[i_eval]; + CheckErrors(IDASetStopTime(ida_mem, t_eval_next)); + + // Reinitialize the solver to deal with the discontinuity at t = t_val. + // We must reinitialize the algebraic terms, so do not use init_type. + IDACalcIC(ida_mem, IDA_YA_YDP_INIT, t_eval_next); + CheckErrors(IDAReInit(ida_mem, t_val, yy, yp)); + if (sensitivity) { + CheckErrors(IDASensReInit(ida_mem, IDA_SIMULTANEOUS, yyS, ypS)); + } + } + + t_prev = t_val; } - realtype *t_return = new realtype[number_of_timesteps]; - realtype *y_return = new realtype[number_of_timesteps * - length_of_return_vector]; - realtype *yS_return = new realtype[number_of_parameters * - number_of_timesteps * - length_of_return_vector]; + + int const length_of_final_sv_slice = save_outputs_only ? number_of_states : 0; realtype *yterm_return = new realtype[length_of_final_sv_slice]; + if (save_outputs_only) { + // store final state slice if outout variables are specified + yterm_return = y_val; + } + + if (solver_opts.print_stats) { + PrintStats(); + } + + int const number_of_timesteps = i_save; + int count; - res.resize(max_res_size); - res_dvar_dy.resize(max_res_dvar_dy); - res_dvar_dp.resize(max_res_dvar_dp); + // Copy the data to return as numpy arrays + + // Time, t + realtype *t_return = new realtype[number_of_timesteps]; + for (size_t i = 0; i < number_of_timesteps; i++) { + t_return[i] = t[i]; + } py::capsule free_t_when_done( t_return, @@ -440,6 +561,23 @@ Solution IDAKLUSolverOpenMP::solve( delete[] vect; } ); + + np_array t_ret = np_array( + number_of_timesteps, + &t_return[0], + free_t_when_done + ); + + // States, y + realtype *y_return = new realtype[number_of_timesteps * length_of_return_vector]; + count = 0; + for (size_t i = 0; i < number_of_timesteps; i++) { + for (size_t j = 0; j < length_of_return_vector; j++) { + y_return[count] = y[i][j]; + count++; + } + } + py::capsule free_y_when_done( y_return, [](void *f) { @@ -447,6 +585,35 @@ Solution IDAKLUSolverOpenMP::solve( delete[] vect; } ); + + np_array y_ret = np_array( + number_of_timesteps * length_of_return_vector, + &y_return[0], + free_y_when_done + ); + + // Sensitivity states, yS + // Note: Ordering of vector is different if computing outputs vs returning + // the complete state vector + auto const arg_sens0 = (save_outputs_only ? number_of_timesteps : number_of_parameters); + auto const arg_sens1 = (save_outputs_only ? length_of_return_vector : number_of_timesteps); + auto const arg_sens2 = (save_outputs_only ? number_of_parameters : length_of_return_vector); + + realtype *yS_return = new realtype[arg_sens0 * arg_sens1 * arg_sens2]; + count = 0; + for (size_t idx0 = 0; idx0 < arg_sens0; idx0++) { + for (size_t idx1 = 0; idx1 < arg_sens1; idx1++) { + for (size_t idx2 = 0; idx2 < arg_sens2; idx2++) { + auto i = (save_outputs_only ? idx0 : idx1); + auto j = (save_outputs_only ? idx1 : idx2); + auto k = (save_outputs_only ? idx2 : idx0); + + yS_return[count] = yS[i][k][j]; + count++; + } + } + } + py::capsule free_yS_when_done( yS_return, [](void *f) { @@ -454,6 +621,18 @@ Solution IDAKLUSolverOpenMP::solve( delete[] vect; } ); + + np_array yS_ret = np_array( + vector { + arg_sens0, + arg_sens1, + arg_sens2 + }, + &yS_return[0], + free_yS_when_done + ); + + // Final state slice, yterm py::capsule free_yterm_when_done( yterm_return, [](void *f) { @@ -462,167 +641,188 @@ Solution IDAKLUSolverOpenMP::solve( } ); - // Initial state (t_i=0) - int t_i = 0; - size_t ySk = 0; - t_return[t_i] = t(t_i); - if (functions->var_fcns.size() > 0) { - // Evaluate functions for each requested variable and store - CalcVars(y_return, length_of_return_vector, t_i, - &tret, yval, ySval, yS_return, &ySk); + np_array y_term = np_array( + length_of_final_sv_slice, + &yterm_return[0], + free_yterm_when_done + ); + + // Store the solution + Solution sol(retval, t_ret, y_ret, yS_ret, y_term); + + return sol; +} + +template +void IDAKLUSolverOpenMP::ExtendAdaptiveArrays() { + DEBUG("IDAKLUSolver::ExtendAdaptiveArrays"); + // Time + t.emplace_back(0.0); + + // States + y.emplace_back(length_of_return_vector, 0.0); + + // Sensitivity + if (sensitivity) { + yS.emplace_back(number_of_parameters, vector(length_of_return_vector, 0.0)); + } +} + +template +void IDAKLUSolverOpenMP::SetStep( + realtype &tval, + realtype *y_val, + vector const &yS_val, + int &i_save +) { + // Set adaptive step results for y and yS + DEBUG("IDAKLUSolver::SetStep"); + + // Time + t[i_save] = tval; + + if (save_outputs_only) { + SetStepOutput(tval, y_val, yS_val, i_save); } else { - // Retain complete copy of the state vector - for (int j = 0; j < number_of_states; j++) { - y_return[j] = yval[j]; - } - for (int j = 0; j < number_of_parameters; j++) { - const int base_index = j * number_of_timesteps * number_of_states; - for (int k = 0; k < number_of_states; k++) { - yS_return[base_index + k] = ySval[j][k]; - } - } + SetStepFull(tval, y_val, yS_val, i_save); } - // Subsequent states (t_i>0) - int retval; - t_i = 1; - while (true) { - realtype t_next = t(t_i); - IDASetStopTime(ida_mem, t_next); - DEBUG("IDASolve"); - retval = IDASolve(ida_mem, t_final, &tret, yy, yp, IDA_NORMAL); - - if (!(retval == IDA_TSTOP_RETURN || - retval == IDA_SUCCESS || - retval == IDA_ROOT_RETURN)) { - // failed - break; - } + i_save++; +} + +template +void IDAKLUSolverOpenMP::SetStepInterp( + int &i_interp, + realtype &t_interp_next, + vector const &t_interp, + realtype &t_val, + realtype &t_prev, + realtype const &t_eval_next, + realtype *y_val, + vector const &yS_val, + int &i_save + ) { + // Save the state at the requested time + DEBUG("IDAKLUSolver::SetStepInterp"); + + while (i_interp <= (t_interp.size()-1) && t_interp_next <= t_val) { + CheckErrors(IDAGetDky(ida_mem, t_interp_next, 0, yy)); if (sensitivity) { - CheckErrors(IDAGetSens(ida_mem, &tret, yyS)); + CheckErrors(IDAGetSensDky(ida_mem, t_interp_next, 0, yyS)); } - // Evaluate and store results for the time step - t_return[t_i] = tret; - if (functions->var_fcns.size() > 0) { - // Evaluate functions for each requested variable and store - // NOTE: Indexing of yS_return is (time:var:param) - CalcVars(y_return, length_of_return_vector, t_i, - &tret, yval, ySval, yS_return, &ySk); - } else { - // Retain complete copy of the state vector - for (int j = 0; j < number_of_states; j++) { - y_return[t_i * number_of_states + j] = yval[j]; - } - for (int j = 0; j < number_of_parameters; j++) { - const int base_index = - j * number_of_timesteps * number_of_states + - t_i * number_of_states; - for (int k = 0; k < number_of_states; k++) { - // NOTE: Indexing of yS_return is (time:param:yvec) - yS_return[base_index + k] = ySval[j][k]; - } - } - } - t_i += 1; + // Memory is already allocated for the interpolated values + SetStep(t_interp_next, y_val, yS_val, i_save); - if (retval == IDA_SUCCESS || retval == IDA_ROOT_RETURN) { - if (functions->var_fcns.size() > 0) { - // store final state slice if outout variables are specified - yterm_return = yval; - } + i_interp++; + if (i_interp == (t_interp.size())) { + // Reached the final t_interp value break; } + t_interp_next = t_interp[i_interp]; } +} - np_array t_ret = np_array( - t_i, - &t_return[0], - free_t_when_done - ); - np_array y_ret = np_array( - t_i * length_of_return_vector, - &y_return[0], - free_y_when_done - ); - // Note: Ordering of vector is different if computing variables vs returning - // the complete state vector - np_array yS_ret; - if (functions->var_fcns.size() > 0) { - yS_ret = np_array( - std::vector { - number_of_timesteps, - length_of_return_vector, - number_of_parameters - }, - &yS_return[0], - free_yS_when_done - ); - } else { - yS_ret = np_array( - std::vector { - number_of_parameters, - number_of_timesteps, - length_of_return_vector - }, - &yS_return[0], - free_yS_when_done - ); +template +void IDAKLUSolverOpenMP::SetStepFull( + realtype &tval, + realtype *y_val, + vector const &yS_val, + int &i_save +) { + // Set adaptive step results for y and yS + DEBUG("IDAKLUSolver::SetStepFull"); + + // States + auto &y_back = y[i_save]; + for (size_t j = 0; j < number_of_states; ++j) { + y_back[j] = y_val[j]; } - np_array y_term = np_array( - length_of_final_sv_slice, - &yterm_return[0], - free_yterm_when_done - ); - Solution sol(retval, t_ret, y_ret, yS_ret, y_term); + // Sensitivity + if (sensitivity) { + SetStepFullSensitivities(tval, y_val, yS_val, i_save); + } +} - if (solver_opts.print_stats) { - long nsteps, nrevals, nlinsetups, netfails; - int klast, kcur; - realtype hinused, hlast, hcur, tcur; - - CheckErrors(IDAGetIntegratorStats( - ida_mem, - &nsteps, - &nrevals, - &nlinsetups, - &netfails, - &klast, - &kcur, - &hinused, - &hlast, - &hcur, - &tcur - )); - - long nniters, nncfails; - CheckErrors(IDAGetNonlinSolvStats(ida_mem, &nniters, &nncfails)); - - long int ngevalsBBDP = 0; - if (setup_opts.using_iterative_solver) { - CheckErrors(IDABBDPrecGetNumGfnEvals(ida_mem, &ngevalsBBDP)); +template +void IDAKLUSolverOpenMP::SetStepFullSensitivities( + realtype &tval, + realtype *y_val, + vector const &yS_val, + int &i_save +) { + DEBUG("IDAKLUSolver::SetStepFullSensitivities"); + + // Calculate sensitivities for the full yS array + for (size_t j = 0; j < number_of_parameters; ++j) { + auto &yS_back_j = yS[i_save][j]; + auto &ySval_j = yS_val[j]; + for (size_t k = 0; k < number_of_states; ++k) { + yS_back_j[k] = ySval_j[k]; } + } +} - py::print("Solver Stats:"); - py::print("\tNumber of steps =", nsteps); - py::print("\tNumber of calls to residual function =", nrevals); - py::print("\tNumber of calls to residual function in preconditioner =", - ngevalsBBDP); - py::print("\tNumber of linear solver setup calls =", nlinsetups); - py::print("\tNumber of error test failures =", netfails); - py::print("\tMethod order used on last step =", klast); - py::print("\tMethod order used on next step =", kcur); - py::print("\tInitial step size =", hinused); - py::print("\tStep size on last step =", hlast); - py::print("\tStep size on next step =", hcur); - py::print("\tCurrent internal time reached =", tcur); - py::print("\tNumber of nonlinear iterations performed =", nniters); - py::print("\tNumber of nonlinear convergence failures =", nncfails); +template +void IDAKLUSolverOpenMP::SetStepOutput( + realtype &tval, + realtype *y_val, + const vector& yS_val, + int &i_save +) { + DEBUG("IDAKLUSolver::SetStepOutput"); + // Evaluate functions for each requested variable and store + + size_t j = 0; + for (auto& var_fcn : functions->var_fcns) { + (*var_fcn)({&tval, y_val, functions->inputs.data()}, {&res[0]}); + // store in return vector + for (size_t jj=0; jjnnz_out(); jj++) { + y[i_save][j++] = res[jj]; + } + } + // calculate sensitivities + if (sensitivity) { + SetStepOutputSensitivities(tval, y_val, yS_val, i_save); } +} - return sol; +template +void IDAKLUSolverOpenMP::SetStepOutputSensitivities( + realtype &tval, + realtype *y_val, + const vector& yS_val, + int &i_save + ) { + DEBUG("IDAKLUSolver::SetStepOutputSensitivities"); + // Calculate sensitivities + vector dens_dvar_dp = vector(number_of_parameters, 0); + for (size_t dvar_k=0; dvar_kdvar_dy_fcns.size(); dvar_k++) { + // Isolate functions + Expression* dvar_dy = functions->dvar_dy_fcns[dvar_k]; + Expression* dvar_dp = functions->dvar_dp_fcns[dvar_k]; + // Calculate dvar/dy + (*dvar_dy)({&tval, y_val, functions->inputs.data()}, {&res_dvar_dy[0]}); + // Calculate dvar/dp and convert to dense array for indexing + (*dvar_dp)({&tval, y_val, functions->inputs.data()}, {&res_dvar_dp[0]}); + for (int k=0; knnz_out(); k++) { + dens_dvar_dp[dvar_dp->get_row()[k]] = res_dvar_dp[k]; + } + // Calculate sensitivities + for (int paramk=0; paramknnz_out(); spk++) { + yS_back_paramk[dvar_k] += res_dvar_dy[spk] * yS_val[paramk][dvar_dy->get_col()[spk]]; + } + } + } } template @@ -633,3 +833,48 @@ void IDAKLUSolverOpenMP::CheckErrors(int const & flag) { throw py::error_already_set(); } } + +template +void IDAKLUSolverOpenMP::PrintStats() { + long nsteps, nrevals, nlinsetups, netfails; + int klast, kcur; + realtype hinused, hlast, hcur, tcur; + + CheckErrors(IDAGetIntegratorStats( + ida_mem, + &nsteps, + &nrevals, + &nlinsetups, + &netfails, + &klast, + &kcur, + &hinused, + &hlast, + &hcur, + &tcur + )); + + long nniters, nncfails; + CheckErrors(IDAGetNonlinSolvStats(ida_mem, &nniters, &nncfails)); + + long int ngevalsBBDP = 0; + if (setup_opts.using_iterative_solver) { + CheckErrors(IDABBDPrecGetNumGfnEvals(ida_mem, &ngevalsBBDP)); + } + + py::print("Solver Stats:"); + py::print("\tNumber of steps =", nsteps); + py::print("\tNumber of calls to residual function =", nrevals); + py::print("\tNumber of calls to residual function in preconditioner =", + ngevalsBBDP); + py::print("\tNumber of linear solver setup calls =", nlinsetups); + py::print("\tNumber of error test failures =", netfails); + py::print("\tMethod order used on last step =", klast); + py::print("\tMethod order used on next step =", kcur); + py::print("\tInitial step size =", hinused); + py::print("\tStep size on last step =", hlast); + py::print("\tStep size on next step =", hcur); + py::print("\tCurrent internal time reached =", tcur); + py::print("\tNumber of nonlinear iterations performed =", nniters); + py::print("\tNumber of nonlinear convergence failures =", nncfails); +} diff --git a/src/pybamm/solvers/c_solvers/idaklu/Solution.hpp b/src/pybamm/solvers/c_solvers/idaklu/Solution.hpp index ad0ea06762..72d48fa644 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/Solution.hpp +++ b/src/pybamm/solvers/c_solvers/idaklu/Solution.hpp @@ -12,7 +12,7 @@ class Solution /** * @brief Constructor */ - Solution(int retval, np_array t_np, np_array y_np, np_array yS_np, np_array y_term_np) + Solution(int &retval, np_array &t_np, np_array &y_np, np_array &yS_np, np_array &y_term_np) : flag(retval), t(t_np), y(y_np), yS(yS_np), y_term(y_term_np) { } diff --git a/src/pybamm/solvers/c_solvers/idaklu/common.cpp b/src/pybamm/solvers/c_solvers/idaklu/common.cpp new file mode 100644 index 0000000000..bf38acc56a --- /dev/null +++ b/src/pybamm/solvers/c_solvers/idaklu/common.cpp @@ -0,0 +1,31 @@ +#include "common.hpp" + +std::vector numpy2realtype(const np_array& input_np) { + std::vector output(input_np.request().size); + + auto const inputData = input_np.unchecked<1>(); + for (int i = 0; i < output.size(); i++) { + output[i] = inputData[i]; + } + + return output; +} + +std::vector setDiff(const std::vector& A, const std::vector& B) { + std::vector result; + if (!(A.empty())) { + std::set_difference(A.begin(), A.end(), B.begin(), B.end(), std::back_inserter(result)); + } + return result; +} + +std::vector makeSortedUnique(const std::vector& input) { + std::unordered_set uniqueSet(input.begin(), input.end()); // Remove duplicates + std::vector uniqueVector(uniqueSet.begin(), uniqueSet.end()); // Convert to vector + std::sort(uniqueVector.begin(), uniqueVector.end()); // Sort the vector + return uniqueVector; +} + +std::vector makeSortedUnique(const np_array& input_np) { + return makeSortedUnique(numpy2realtype(input_np)); +} diff --git a/src/pybamm/solvers/c_solvers/idaklu/common.hpp b/src/pybamm/solvers/c_solvers/idaklu/common.hpp index 0ef7ee60a0..3289326541 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/common.hpp +++ b/src/pybamm/solvers/c_solvers/idaklu/common.hpp @@ -74,6 +74,24 @@ void csc_csr(const realtype f[], const T1 c[], const T1 r[], realtype nf[], T2 n } } + +/** + * @brief Utility function to convert numpy array to std::vector + */ +std::vector numpy2realtype(const np_array& input_np); + +/** + * @brief Utility function to compute the set difference of two vectors + */ +std::vector setDiff(const std::vector& A, const std::vector& B); + +/** + * @brief Utility function to make a sorted and unique vector + */ +std::vector makeSortedUnique(const std::vector& input); + +std::vector makeSortedUnique(const np_array& input_np); + #ifdef NDEBUG #define DEBUG_VECTOR(vector) #define DEBUG_VECTORn(vector, N) diff --git a/src/pybamm/solvers/casadi_algebraic_solver.py b/src/pybamm/solvers/casadi_algebraic_solver.py index 635adb5d34..cf44912952 100644 --- a/src/pybamm/solvers/casadi_algebraic_solver.py +++ b/src/pybamm/solvers/casadi_algebraic_solver.py @@ -25,7 +25,7 @@ def __init__(self, tol=1e-6, extra_options=None): super().__init__() self.tol = tol self.name = "CasADi algebraic solver" - self.algebraic_solver = True + self._algebraic_solver = True self.extra_options = extra_options or {} pybamm.citations.register("Andersson2019") @@ -37,7 +37,7 @@ def tol(self): def tol(self, value): self._tol = value - def _integrate(self, model, t_eval, inputs_dict=None): + def _integrate(self, model, t_eval, inputs_dict=None, t_interp=None): """ Calculate the solution of the algebraic equations through root-finding diff --git a/src/pybamm/solvers/casadi_solver.py b/src/pybamm/solvers/casadi_solver.py index f67a2decb4..b4ac9d1561 100644 --- a/src/pybamm/solvers/casadi_solver.py +++ b/src/pybamm/solvers/casadi_solver.py @@ -133,7 +133,7 @@ def __init__( pybamm.citations.register("Andersson2019") - def _integrate(self, model, t_eval, inputs_dict=None): + def _integrate(self, model, t_eval, inputs_dict=None, t_interp=None): """ Solve a DAE model defined by residuals with initial conditions y0. diff --git a/src/pybamm/solvers/dummy_solver.py b/src/pybamm/solvers/dummy_solver.py index c98667293d..45a6a332b1 100644 --- a/src/pybamm/solvers/dummy_solver.py +++ b/src/pybamm/solvers/dummy_solver.py @@ -12,7 +12,7 @@ def __init__(self): super().__init__() self.name = "Dummy solver" - def _integrate(self, model, t_eval, inputs_dict=None): + def _integrate(self, model, t_eval, inputs_dict=None, t_interp=None): """ Solve an empty model. diff --git a/src/pybamm/solvers/idaklu_jax.py b/src/pybamm/solvers/idaklu_jax.py index ba1c80c1c4..5a73d42c6e 100644 --- a/src/pybamm/solvers/idaklu_jax.py +++ b/src/pybamm/solvers/idaklu_jax.py @@ -55,6 +55,7 @@ def __init__( t_eval, output_variables=None, calculate_sensitivities=True, + t_interp=None, ): if not pybamm.have_jax(): raise ModuleNotFoundError( @@ -77,6 +78,7 @@ def __init__( t_eval, output_variables=output_variables, calculate_sensitivities=calculate_sensitivities, + t_interp=t_interp, ) def get_jaxpr(self): @@ -355,16 +357,15 @@ def _jaxify_solve(self, t, invar, *inputs_values): fo reuse. """ # Reconstruct dictionary of inputs - if self.jax_inputs is None: - d = self._hashabledict() - else: + d = self._hashabledict() + if self.jax_inputs is not None: # Use hashable dictionaries for caching the solve - d = self._hashabledict() for key, value in zip(self.jax_inputs.keys(), inputs_values): d[key] = value # Solver logger.debug("_jaxify_solve:") logger.debug(f" t_eval: {self.jax_t_eval}") + logger.debug(f" t_interp: {self.jax_t_interp}") logger.debug(f" t: {t}") logger.debug(f" invar: {invar}") logger.debug(f" inputs: {dict(d)}") @@ -375,6 +376,7 @@ def _jaxify_solve(self, t, invar, *inputs_values): tuple(self.jax_t_eval), inputs=self._hashabledict(d), calculate_sensitivities=self.jax_calculate_sensitivities, + t_interp=tuple(self.jax_t_interp), ) if invar is not None: if isinstance(invar, numbers.Number): @@ -549,6 +551,7 @@ def jaxify( *, output_variables=None, calculate_sensitivities=True, + t_interp=None, ): """JAXify the model and solver @@ -560,12 +563,14 @@ def jaxify( model : :class:`pybamm.BaseModel` The model to be solved t_eval : numeric type, optional - The times at which to compute the solution. If None, the times in the model - are used. + The times at which to stop the integration due to a discontinuity in time. output_variables : list of str, optional The variables to be returned. If None, the variables in the model are used. calculate_sensitivities : bool, optional Whether to calculate sensitivities. Default is True. + t_interp : None, list or ndarray, optional + The times (in seconds) at which to interpolate the solution. Defaults to None. + Only valid for solvers that support intra-solve interpolation (`IDAKLUSolver`). """ if self.jaxpr is not None: warnings.warn( @@ -579,6 +584,7 @@ def jaxify( t_eval, output_variables=output_variables, calculate_sensitivities=calculate_sensitivities, + t_interp=t_interp, ) return self.jaxpr @@ -589,11 +595,15 @@ def _jaxify( *, output_variables=None, calculate_sensitivities=True, + t_interp=None, ): """JAXify the model and solver""" self.jax_model = model self.jax_t_eval = t_eval + if t_interp is None: + t_interp = np.empty(0) + self.jax_t_interp = t_interp self.jax_output_variables = ( output_variables if output_variables else self.solver.output_variables ) diff --git a/src/pybamm/solvers/idaklu_solver.py b/src/pybamm/solvers/idaklu_solver.py index 34f7c1abaa..85731f4e12 100644 --- a/src/pybamm/solvers/idaklu_solver.py +++ b/src/pybamm/solvers/idaklu_solver.py @@ -13,6 +13,7 @@ import importlib import warnings + if pybamm.have_jax(): import jax from jax import numpy as jnp @@ -232,6 +233,7 @@ def __init__( output_variables, ) self.name = "IDA KLU solver" + self._supports_interp = True pybamm.citations.register("Hindmarsh2000") pybamm.citations.register("Hindmarsh2005") @@ -828,7 +830,7 @@ def _check_mlir_conversion(self, name, mlir: str): def _demote_64_to_32(self, x: pybamm.EvaluatorJax): return pybamm.EvaluatorJax._demote_64_to_32(x) - def _integrate(self, model, t_eval, inputs_dict=None): + def _integrate(self, model, t_eval, inputs_dict=None, t_interp=None): """ Solve a DAE model defined by residuals with initial conditions y0. @@ -837,9 +839,12 @@ def _integrate(self, model, t_eval, inputs_dict=None): model : :class:`pybamm.BaseModel` The model whose solution to calculate. t_eval : numeric type - The times at which to compute the solution + The times at which to stop the integration due to a discontinuity in time. inputs_dict : dict, optional Any input parameters to pass to the model when solving. + t_interp : None, list or ndarray, optional + The times (in seconds) at which to interpolate the solution. Defaults to `None`, + which returns the adaptive time-stepping times. """ inputs_dict = inputs_dict or {} # stack inputs @@ -863,6 +868,7 @@ def _integrate(self, model, t_eval, inputs_dict=None): ): sol = self._setup["solver"].solve( t_eval, + t_interp, y0full, ydot0full, inputs, @@ -919,13 +925,13 @@ def _integrate(self, model, t_eval, inputs_dict=None): yS_out = False # 0 = solved for all t_eval - if sol.flag == 0: - termination = "final time" # 2 = found root(s) - elif sol.flag == 2: + if sol.flag == 2: termination = "event" + elif sol.flag >= 0: + termination = "final time" else: - raise pybamm.SolverError("idaklu solver failed") + raise pybamm.SolverError(f"FAILURE {self._solver_flag(sol.flag)}") newsol = pybamm.Solution( sol.t, @@ -939,6 +945,7 @@ def _integrate(self, model, t_eval, inputs_dict=None): ) newsol.integration_time = integration_time if not self.output_variables: + # print((newsol.y).shape) return newsol # Populate variables and sensititivies dictionaries directly @@ -1138,6 +1145,7 @@ def jaxify( *, output_variables=None, calculate_sensitivities=True, + t_interp=None, ): """JAXify the solver object @@ -1149,12 +1157,14 @@ def jaxify( model : :class:`pybamm.BaseModel` The model to be solved t_eval : numeric type, optional - The times at which to compute the solution. If None, the times in the model - are used. + The times at which to stop the integration due to a discontinuity in time. output_variables : list of str, optional The variables to be returned. If None, all variables in the model are used. calculate_sensitivities : bool, optional Whether to calculate sensitivities. Default is True. + t_interp : None, list or ndarray, optional + The times (in seconds) at which to interpolate the solution. Defaults to `None`, + which returns the adaptive time-stepping times. """ obj = pybamm.IDAKLUJax( self, # IDAKLU solver instance @@ -1162,5 +1172,43 @@ def jaxify( t_eval, output_variables=output_variables, calculate_sensitivities=calculate_sensitivities, + t_interp=t_interp, ) return obj + + @staticmethod + def _solver_flag(flag): + flags = { + 99: "IDA_WARNING: IDASolve succeeded but an unusual situation occurred.", + 2: "IDA_ROOT_RETURN: IDASolve succeeded and found one or more roots.", + 1: "IDA_TSTOP_RETURN: IDASolve succeeded by reaching the specified stopping point.", + 0: "IDA_SUCCESS: Successful function return.", + -1: "IDA_TOO_MUCH_WORK: The solver took mxstep internal steps but could not reach tout.", + -2: "IDA_TOO_MUCH_ACC: The solver could not satisfy the accuracy demanded by the user for some internal step.", + -3: "IDA_ERR_FAIL: Error test failures occurred too many times during one internal time step or minimum step size was reached.", + -4: "IDA_CONV_FAIL: Convergence test failures occurred too many times during one internal time step or minimum step size was reached.", + -5: "IDA_LINIT_FAIL: The linear solver's initialization function failed.", + -6: "IDA_LSETUP_FAIL: The linear solver's setup function failed in an unrecoverable manner.", + -7: "IDA_LSOLVE_FAIL: The linear solver's solve function failed in an unrecoverable manner.", + -8: "IDA_RES_FAIL: The user-provided residual function failed in an unrecoverable manner.", + -9: "IDA_REP_RES_FAIL: The user-provided residual function repeatedly returned a recoverable error flag, but the solver was unable to recover.", + -10: "IDA_RTFUNC_FAIL: The rootfinding function failed in an unrecoverable manner.", + -11: "IDA_CONSTR_FAIL: The inequality constraints were violated and the solver was unable to recover.", + -12: "IDA_FIRST_RES_FAIL: The user-provided residual function failed recoverably on the first call.", + -13: "IDA_LINESEARCH_FAIL: The line search failed.", + -14: "IDA_NO_RECOVERY: The residual function, linear solver setup function, or linear solver solve function had a recoverable failure, but IDACalcIC could not recover.", + -15: "IDA_NLS_INIT_FAIL: The nonlinear solver's init routine failed.", + -16: "IDA_NLS_SETUP_FAIL: The nonlinear solver's setup routine failed.", + -20: "IDA_MEM_NULL: The ida mem argument was NULL.", + -21: "IDA_MEM_FAIL: A memory allocation failed.", + -22: "IDA_ILL_INPUT: One of the function inputs is illegal.", + -23: "IDA_NO_MALLOC: The ida memory was not allocated by a call to IDAInit.", + -24: "IDA_BAD_EWT: Zero value of some error weight component.", + -25: "IDA_BAD_K: The k-th derivative is not available.", + -26: "IDA_BAD_T: The time t is outside the last step taken.", + -27: "IDA_BAD_DKY: The vector argument where derivative should be stored is NULL.", + } + + flag_unknown = "Unknown IDA flag." + + return flags.get(flag, flag_unknown) diff --git a/src/pybamm/solvers/jax_solver.py b/src/pybamm/solvers/jax_solver.py index fbe047b3cc..26a069e0fe 100644 --- a/src/pybamm/solvers/jax_solver.py +++ b/src/pybamm/solvers/jax_solver.py @@ -72,9 +72,7 @@ def __init__( method_options = ["RK45", "BDF"] if method not in method_options: raise ValueError(f"method must be one of {method_options}") - self.ode_solver = False - if method == "RK45": - self.ode_solver = True + self._ode_solver = method == "RK45" self.extra_options = extra_options or {} self.name = f"JAX solver ({method})" self._cached_solves = dict() @@ -187,7 +185,7 @@ def solve_model_bdf(inputs): else: return jax.jit(solve_model_bdf) - def _integrate(self, model, t_eval, inputs=None): + def _integrate(self, model, t_eval, inputs=None, t_interp=None): """ Solve a model defined by dydt with initial conditions y0. diff --git a/src/pybamm/solvers/processed_variable.py b/src/pybamm/solvers/processed_variable.py index 38314ca5c2..8c1190c2f4 100644 --- a/src/pybamm/solvers/processed_variable.py +++ b/src/pybamm/solvers/processed_variable.py @@ -75,43 +75,42 @@ def __init__( self.base_eval_shape = self.base_variables[0].shape self.base_eval_size = self.base_variables[0].size + # xr_data_array is initialized + self._xr_data_array = None + # handle 2D (in space) finite element variables differently if ( self.mesh and "current collector" in self.domain and isinstance(self.mesh, pybamm.ScikitSubMesh2D) ): - self.initialise_2D_scikit_fem() + return self.initialise_2D_scikit_fem() # check variable shape - else: - if len(self.base_eval_shape) == 0 or self.base_eval_shape[0] == 1: - self.initialise_0D() - else: - n = self.mesh.npts - base_shape = self.base_eval_shape[0] - # Try some shapes that could make the variable a 1D variable - if base_shape in [n, n + 1]: - self.initialise_1D() - else: - # Try some shapes that could make the variable a 2D variable - first_dim_nodes = self.mesh.nodes - first_dim_edges = self.mesh.edges - second_dim_pts = self.base_variables[0].secondary_mesh.nodes - if self.base_eval_size // len(second_dim_pts) in [ - len(first_dim_nodes), - len(first_dim_edges), - ]: - self.initialise_2D() - else: - # Raise error for 3D variable - raise NotImplementedError( - f"Shape not recognized for {base_variables[0]}" - + "(note processing of 3D variables is not yet implemented)" - ) - - # xr_data_array is initialized when needed - self._xr_data_array = None + if len(self.base_eval_shape) == 0 or self.base_eval_shape[0] == 1: + return self.initialise_0D() + + n = self.mesh.npts + base_shape = self.base_eval_shape[0] + # Try some shapes that could make the variable a 1D variable + if base_shape in [n, n + 1]: + return self.initialise_1D() + + # Try some shapes that could make the variable a 2D variable + first_dim_nodes = self.mesh.nodes + first_dim_edges = self.mesh.edges + second_dim_pts = self.base_variables[0].secondary_mesh.nodes + if self.base_eval_size // len(second_dim_pts) in [ + len(first_dim_nodes), + len(first_dim_edges), + ]: + return self.initialise_2D() + + # Raise error for 3D variable + raise NotImplementedError( + f"Shape not recognized for {base_variables[0]}" + + "(note processing of 3D variables is not yet implemented)" + ) def initialise_0D(self): # initialise empty array of the correct size diff --git a/src/pybamm/solvers/scipy_solver.py b/src/pybamm/solvers/scipy_solver.py index 9a66f5bc01..226b096887 100644 --- a/src/pybamm/solvers/scipy_solver.py +++ b/src/pybamm/solvers/scipy_solver.py @@ -42,12 +42,12 @@ def __init__( atol=atol, extrap_tol=extrap_tol, ) - self.ode_solver = True + self._ode_solver = True self.extra_options = extra_options or {} self.name = f"Scipy solver ({method})" pybamm.citations.register("Virtanen2020") - def _integrate(self, model, t_eval, inputs_dict=None): + def _integrate(self, model, t_eval, inputs_dict=None, t_interp=None): """ Solve a model defined by dydt with initial conditions y0. diff --git a/tests/integration/test_models/standard_model_tests.py b/tests/integration/test_models/standard_model_tests.py index 3f9cb56354..a962894c44 100644 --- a/tests/integration/test_models/standard_model_tests.py +++ b/tests/integration/test_models/standard_model_tests.py @@ -104,7 +104,8 @@ def test_sensitivities(self, param_name, param_value, output_name="Voltage [V]") self.parameter_values["Current function [A]"] / self.parameter_values["Nominal cell capacity [A.h]"] ) - t_eval = np.linspace(0, 3600 / Crate, 100) + t_interp = np.linspace(0, 3600 / Crate, 100) + t_eval = np.array([t_interp[0], t_interp[-1]]) # make param_name an input self.parameter_values.update({param_name: "[input]"}) @@ -118,7 +119,11 @@ def test_sensitivities(self, param_name, param_value, output_name="Voltage [V]") self.solver.atol = 1e-8 self.solution = self.solver.solve( - self.model, t_eval, inputs=inputs, calculate_sensitivities=True + self.model, + t_eval, + inputs=inputs, + calculate_sensitivities=True, + t_interp=t_interp, ) output_sens = self.solution[output_name].sensitivities[param_name] @@ -126,10 +131,14 @@ def test_sensitivities(self, param_name, param_value, output_name="Voltage [V]") h = 1e-2 * param_value inputs_plus = {param_name: (param_value + 0.5 * h)} inputs_neg = {param_name: (param_value - 0.5 * h)} - sol_plus = self.solver.solve(self.model, t_eval, inputs=inputs_plus) - output_plus = sol_plus[output_name](t=t_eval) - sol_neg = self.solver.solve(self.model, t_eval, inputs=inputs_neg) - output_neg = sol_neg[output_name](t=t_eval) + sol_plus = self.solver.solve( + self.model, t_eval, inputs=inputs_plus, t_interp=t_interp + ) + output_plus = sol_plus[output_name].data + sol_neg = self.solver.solve( + self.model, t_eval, inputs=inputs_neg, t_interp=t_interp + ) + output_neg = sol_neg[output_name].data fd = (np.array(output_plus) - np.array(output_neg)) / h fd = fd.transpose().reshape(-1, 1) np.testing.assert_allclose( diff --git a/tests/integration/test_solvers/test_idaklu.py b/tests/integration/test_solvers/test_idaklu.py index 31083319cf..abc7741c0c 100644 --- a/tests/integration/test_solvers/test_idaklu.py +++ b/tests/integration/test_solvers/test_idaklu.py @@ -31,13 +31,15 @@ def test_on_spme_sensitivities(self): mesh = pybamm.Mesh(geometry, model.default_submesh_types, model.default_var_pts) disc = pybamm.Discretisation(mesh, model.default_spatial_methods) disc.process_model(model) - t_eval = np.linspace(0, 3500, 100) + t_interp = np.linspace(0, 3500, 100) + t_eval = np.array([t_interp[0], t_interp[-1]]) solver = pybamm.IDAKLUSolver(rtol=1e-10, atol=1e-10) solution = solver.solve( model, t_eval, inputs=inputs, calculate_sensitivities=True, + t_interp=t_interp, ) np.testing.assert_array_less(1, solution.t.size) @@ -47,10 +49,10 @@ def test_on_spme_sensitivities(self): # evaluate the sensitivities using finite difference h = 1e-5 sol_plus = solver.solve( - model, t_eval, inputs={param_name: param_value + 0.5 * h} + model, t_eval, inputs={param_name: param_value + 0.5 * h}, t_interp=t_interp ) sol_neg = solver.solve( - model, t_eval, inputs={param_name: param_value - 0.5 * h} + model, t_eval, inputs={param_name: param_value - 0.5 * h}, t_interp=t_interp ) dyda_fd = (sol_plus.y - sol_neg.y) / h dyda_fd = dyda_fd.transpose().reshape(-1, 1) @@ -87,3 +89,66 @@ def test_changing_grid(self): # solve solver.solve(model_disc, t_eval) + + def test_interpolation(self): + model = pybamm.BaseModel() + u1 = pybamm.Variable("u1") + u2 = pybamm.Variable("u2") + u3 = pybamm.Variable("u3") + v = pybamm.Variable("v") + a = pybamm.InputParameter("a") + b = pybamm.InputParameter("b", expected_size=2) + model.rhs = {u1: a * v, u2: pybamm.Index(b, 0), u3: pybamm.Index(b, 1)} + model.algebraic = {v: 1 - v} + model.initial_conditions = {u1: 0, u2: 0, u3: 0, v: 1} + + disc = pybamm.Discretisation() + model_disc = disc.process_model(model, inplace=False) + + a_value = 0.1 + b_value = np.array([[0.2], [0.3]]) + inputs = {"a": a_value, "b": b_value} + + # Calculate time for each solver and each number of grid points + t0 = 0 + tf = 3600 + t_eval_dense = np.linspace(t0, tf, 1000) + t_eval_sparse = [t0, tf] + + t_interp_dense = np.linspace(t0, tf, 800) + t_interp_sparse = [t0, tf] + solver = pybamm.IDAKLUSolver() + + # solve + # 1. dense t_eval + adaptive time stepping + sol1 = solver.solve(model_disc, t_eval_dense, inputs=inputs) + np.testing.assert_array_less(len(t_eval_dense), len(sol1.t)) + + # 2. sparse t_eval + adaptive time stepping + sol2 = solver.solve(model_disc, t_eval_sparse, inputs=inputs) + np.testing.assert_array_less(len(sol2.t), len(sol1.t)) + + # 3. dense t_eval + dense t_interp + sol3 = solver.solve( + model_disc, t_eval_dense, t_interp=t_interp_dense, inputs=inputs + ) + t_combined = np.concatenate((sol3.t, t_interp_dense)) + t_combined = np.unique(t_combined) + t_combined.sort() + np.testing.assert_array_almost_equal(sol3.t, t_combined) + + # 4. sparse t_eval + sparse t_interp + sol4 = solver.solve( + model_disc, t_eval_sparse, t_interp=t_interp_sparse, inputs=inputs + ) + np.testing.assert_array_almost_equal(sol4.t, np.array([t0, tf])) + + sols = [sol1, sol2, sol3, sol4] + for sol in sols: + # test that y[0] = to true solution + true_solution = a_value * sol.t + np.testing.assert_array_almost_equal(sol.y[0], true_solution) + + # test that y[1:3] = to true solution + true_solution = b_value * sol.t + np.testing.assert_array_almost_equal(sol.y[1:3], true_solution) diff --git a/tests/unit/test_experiments/test_experiment.py b/tests/unit/test_experiments/test_experiment.py index e445b05e31..2eda78e1e0 100644 --- a/tests/unit/test_experiments/test_experiment.py +++ b/tests/unit/test_experiments/test_experiment.py @@ -21,7 +21,7 @@ def test_cycle_unpacking(self): "value": 0.05, "type": "CRate", "duration": 1800.0, - "period": 60.0, + "period": None, "temperature": None, "description": "Discharge at C/20 for 0.5 hours", "termination": [], @@ -32,7 +32,7 @@ def test_cycle_unpacking(self): "value": -0.2, "type": "CRate", "duration": 2700.0, - "period": 60.0, + "period": None, "temperature": None, "description": "Charge at C/5 for 45 minutes", "termination": [], @@ -43,7 +43,7 @@ def test_cycle_unpacking(self): "value": 0.05, "type": "CRate", "duration": 1800.0, - "period": 60.0, + "period": None, "temperature": None, "description": "Discharge at C/20 for 0.5 hours", "termination": [], @@ -54,7 +54,7 @@ def test_cycle_unpacking(self): "value": -0.2, "type": "CRate", "duration": 2700.0, - "period": 60.0, + "period": None, "temperature": None, "description": "Charge at C/5 for 45 minutes", "termination": [], diff --git a/tests/unit/test_experiments/test_simulation_with_experiment.py b/tests/unit/test_experiments/test_simulation_with_experiment.py index defca33b00..c4f55889a1 100644 --- a/tests/unit/test_experiments/test_simulation_with_experiment.py +++ b/tests/unit/test_experiments/test_simulation_with_experiment.py @@ -191,12 +191,12 @@ def test_run_experiment_cccv_solvers(self): np.testing.assert_array_almost_equal( solutions[0]["Voltage [V]"].data, - solutions[1]["Voltage [V]"].data, + solutions[1]["Voltage [V]"](solutions[0].t), decimal=1, ) np.testing.assert_array_almost_equal( solutions[0]["Current [A]"].data, - solutions[1]["Current [A]"].data, + solutions[1]["Current [A]"](solutions[0].t), decimal=0, ) self.assertEqual(solutions[1].termination, "final time") diff --git a/tests/unit/test_solvers/test_base_solver.py b/tests/unit/test_solvers/test_base_solver.py index 884c85f87f..a35b864a64 100644 --- a/tests/unit/test_solvers/test_base_solver.py +++ b/tests/unit/test_solvers/test_base_solver.py @@ -114,7 +114,7 @@ def test_block_symbolic_inputs(self): ): solver.solve(model, np.array([1, 2, 3])) - def test_ode_solver_fail_with_dae(self): + def testode_solver_fail_with_dae(self): model = pybamm.BaseModel() a = pybamm.Scalar(1) model.algebraic = {a: a} diff --git a/tests/unit/test_solvers/test_casadi_solver.py b/tests/unit/test_solvers/test_casadi_solver.py index 5ce29a365d..eaaeedf0d0 100644 --- a/tests/unit/test_solvers/test_casadi_solver.py +++ b/tests/unit/test_solvers/test_casadi_solver.py @@ -1078,6 +1078,30 @@ def test_solve_sensitivity_subset(self): ), ) + def test_solver_interpolation_warning(self): + # Create model + model = pybamm.BaseModel() + domain = ["negative electrode", "separator", "positive electrode"] + var = pybamm.Variable("var", domain=domain) + model.rhs = {var: 0.1 * var} + model.initial_conditions = {var: 1} + # create discretisation + mesh = get_mesh_for_testing() + spatial_methods = {"macroscale": pybamm.FiniteVolume()} + disc = pybamm.Discretisation(mesh, spatial_methods) + disc.process_model(model) + + solver = pybamm.CasadiSolver() + + # Check for warning with t_interp + t_eval = np.linspace(0, 1, 10) + t_interp = t_eval + with self.assertWarns( + pybamm.SolverWarning, + msg=f"Explicit interpolation times not implemented for {solver.name}", + ): + solver.solve(model, t_eval, t_interp=t_interp) + if __name__ == "__main__": print("Add -v for more debug output") diff --git a/tests/unit/test_solvers/test_idaklu_jax.py b/tests/unit/test_solvers/test_idaklu_jax.py index 7bae5d74e9..d985991929 100644 --- a/tests/unit/test_solvers/test_idaklu_jax.py +++ b/tests/unit/test_solvers/test_idaklu_jax.py @@ -40,6 +40,7 @@ t_eval, inputs=inputs, calculate_sensitivities=True, + t_interp=t_eval, ) # Get jax expressions for IDAKLU solver @@ -54,6 +55,7 @@ t_eval, output_variables=output_variables[:1], calculate_sensitivities=True, + t_interp=t_eval, ) f1 = idaklu_jax_solver1.get_jaxpr() # Multiple output variables @@ -62,6 +64,7 @@ t_eval, output_variables=output_variables, calculate_sensitivities=True, + t_interp=t_eval, ) f3 = idaklu_jax_solver3.get_jaxpr() @@ -151,11 +154,12 @@ def test_no_inputs(self): t_eval = np.linspace(0, 1, 100) idaklu_solver = pybamm.IDAKLUSolver(rtol=1e-6, atol=1e-6) # Regenerate surrogate data - sim = idaklu_solver.solve(model, t_eval) + sim = idaklu_solver.solve(model, t_eval, t_interp=t_eval) idaklu_jax_solver = idaklu_solver.jaxify( model, t_eval, output_variables=output_variables, + t_interp=t_eval, ) f = idaklu_jax_solver.get_jaxpr() # Check that evaluation can occur (and is correct) with no inputs @@ -423,7 +427,6 @@ def test_jacrev_scalar(self, output_variables, idaklu_jax_solver, f, wrapper): @parameterized.expand(testcase, skip_on_empty=True) def test_jacrev_vector(self, output_variables, idaklu_jax_solver, f, wrapper): - out = wrapper(jax.jacrev(f, argnums=1))(t_eval[k], inputs) out = wrapper(jax.jacrev(f, argnums=1))(t_eval, inputs) flat_out, _ = tree_flatten(out) flat_out = np.concatenate(np.array([f for f in flat_out]), 1).T.flatten() @@ -847,6 +850,7 @@ def sse(t, inputs): t_eval, inputs=inputs_pred, calculate_sensitivities=True, + t_interp=t_eval, ) pred = sim_pred["v"] diff --git a/tests/unit/test_solvers/test_idaklu_solver.py b/tests/unit/test_solvers/test_idaklu_solver.py index 33e50eaa7d..5bc845d66c 100644 --- a/tests/unit/test_solvers/test_idaklu_solver.py +++ b/tests/unit/test_solvers/test_idaklu_solver.py @@ -91,11 +91,26 @@ def test_model_events(self): root_method=root_method, options={"jax_evaluator": "iree"} if form == "iree" else {}, ) - t_eval = np.linspace(0, 1, 100) - solution = solver.solve(model_disc, t_eval) - np.testing.assert_array_equal(solution.t, t_eval) + + if model.convert_to_format == "casadi" or ( + model.convert_to_format == "jax" + and solver._options["jax_evaluator"] == "iree" + ): + t_interp = np.linspace(0, 1, 100) + t_eval = np.array([t_interp[0], t_interp[-1]]) + else: + t_eval = np.linspace(0, 1, 100) + t_interp = t_eval + + solution = solver.solve(model_disc, t_eval, t_interp=t_interp) + np.testing.assert_array_equal( + solution.t, t_interp, err_msg=f"Failed for form {form}" + ) np.testing.assert_array_almost_equal( - solution.y[0], np.exp(0.1 * solution.t), decimal=5 + solution.y[0], + np.exp(0.1 * solution.t), + decimal=5, + err_msg=f"Failed for form {form}", ) # Check invalid atol type raises an error @@ -111,10 +126,13 @@ def test_model_events(self): root_method=root_method, options={"jax_evaluator": "iree"} if form == "iree" else {}, ) - solution = solver.solve(model_disc, t_eval) - np.testing.assert_array_equal(solution.t, t_eval) + solution = solver.solve(model_disc, t_eval, t_interp=t_interp) + np.testing.assert_array_equal(solution.t, t_interp) np.testing.assert_array_almost_equal( - solution.y[0], np.exp(0.1 * solution.t), decimal=5 + solution.y[0], + np.exp(0.1 * solution.t), + decimal=5, + err_msg=f"Failed for form {form}", ) # enforce events that will be triggered @@ -126,10 +144,13 @@ def test_model_events(self): root_method=root_method, options={"jax_evaluator": "iree"} if form == "iree" else {}, ) - solution = solver.solve(model_disc, t_eval) - self.assertLess(len(solution.t), len(t_eval)) + solution = solver.solve(model_disc, t_eval, t_interp=t_interp) + self.assertLess(len(solution.t), len(t_interp)) np.testing.assert_array_almost_equal( - solution.y[0], np.exp(0.1 * solution.t), decimal=5 + solution.y[0], + np.exp(0.1 * solution.t), + decimal=5, + err_msg=f"Failed for form {form}", ) # bigger dae model with multiple events @@ -153,17 +174,23 @@ def test_model_events(self): root_method=root_method, options={"jax_evaluator": "iree"} if form == "iree" else {}, ) - t_eval = np.linspace(0, 5, 100) + t_eval = np.array([0, 5]) solution = solver.solve(model, t_eval) np.testing.assert_array_less(solution.y[0, :-1], 1.5) np.testing.assert_array_less(solution.y[-1, :-1], 2.5) np.testing.assert_equal(solution.t_event[0], solution.t[-1]) np.testing.assert_array_equal(solution.y_event[:, 0], solution.y[:, -1]) np.testing.assert_array_almost_equal( - solution.y[0], np.exp(0.1 * solution.t), decimal=5 + solution.y[0], + np.exp(0.1 * solution.t), + decimal=5, + err_msg=f"Failed for form {form}", ) np.testing.assert_array_almost_equal( - solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5 + solution.y[-1], + 2 * np.exp(0.1 * solution.t), + decimal=5, + err_msg=f"Failed for form {form}", ) def test_input_params(self): @@ -208,15 +235,21 @@ def test_input_params(self): ) # test that y[3] remains constant - np.testing.assert_array_almost_equal(sol.y[3], np.ones(sol.t.shape)) + np.testing.assert_array_almost_equal( + sol.y[3], np.ones(sol.t.shape), err_msg=f"Failed for form {form}" + ) # test that y[0] = to true solution true_solution = a_value * sol.t - np.testing.assert_array_almost_equal(sol.y[0], true_solution) + np.testing.assert_array_almost_equal( + sol.y[0], true_solution, err_msg=f"Failed for form {form}" + ) # test that y[1:3] = to true solution true_solution = b_value * sol.t - np.testing.assert_array_almost_equal(sol.y[1:3], true_solution) + np.testing.assert_array_almost_equal( + sol.y[1:3], true_solution, err_msg=f"Failed for form {form}" + ) def test_sensitivities_initial_condition(self): for form in ["casadi", "iree"]: @@ -249,17 +282,24 @@ def test_sensitivities_initial_condition(self): options={"jax_evaluator": "iree"} if form == "iree" else {}, ) - t_eval = np.linspace(0, 3, 100) + t_interp = np.linspace(0, 3, 100) + t_eval = np.array([t_interp[0], t_interp[-1]]) + a_value = 0.1 sol = solver.solve( - model, t_eval, inputs={"a": a_value}, calculate_sensitivities=True + model, + t_eval, + inputs={"a": a_value}, + calculate_sensitivities=True, + t_interp=t_interp, ) np.testing.assert_array_almost_equal( sol["2v"].sensitivities["a"].full().flatten(), np.exp(-sol.t) * 2, decimal=4, + err_msg=f"Failed for form {form}", ) def test_ida_roberts_klu_sensitivities(self): @@ -293,7 +333,8 @@ def test_ida_roberts_klu_sensitivities(self): options={"jax_evaluator": "iree"} if form == "iree" else {}, ) - t_eval = np.linspace(0, 3, 100) + t_interp = np.linspace(0, 3, 100) + t_eval = np.array([t_interp[0], t_interp[-1]]) a_value = 0.1 # solve first without sensitivities @@ -301,14 +342,19 @@ def test_ida_roberts_klu_sensitivities(self): model, t_eval, inputs={"a": a_value}, + t_interp=t_interp, ) # test that y[1] remains constant - np.testing.assert_array_almost_equal(sol.y[1, :], np.ones(sol.t.shape)) + np.testing.assert_array_almost_equal( + sol.y[1, :], np.ones(sol.t.shape), err_msg=f"Failed for form {form}" + ) # test that y[0] = to true solution true_solution = a_value * sol.t - np.testing.assert_array_almost_equal(sol.y[0, :], true_solution) + np.testing.assert_array_almost_equal( + sol.y[0, :], true_solution, err_msg=f"Failed for form {form}" + ) # should be no sensitivities calculated with self.assertRaises(KeyError): @@ -316,35 +362,52 @@ def test_ida_roberts_klu_sensitivities(self): # now solve with sensitivities (this should cause set_up to be run again) sol = solver.solve( - model, t_eval, inputs={"a": a_value}, calculate_sensitivities=True + model, + t_eval, + inputs={"a": a_value}, + calculate_sensitivities=True, + t_interp=t_interp, ) # test that y[1] remains constant - np.testing.assert_array_almost_equal(sol.y[1, :], np.ones(sol.t.shape)) + np.testing.assert_array_almost_equal( + sol.y[1, :], np.ones(sol.t.shape), err_msg=f"Failed for form {form}" + ) # test that y[0] = to true solution true_solution = a_value * sol.t - np.testing.assert_array_almost_equal(sol.y[0, :], true_solution) + np.testing.assert_array_almost_equal( + sol.y[0, :], true_solution, err_msg=f"Failed for form {form}" + ) # evaluate the sensitivities using idas dyda_ida = sol.sensitivities["a"] # evaluate the sensitivities using finite difference h = 1e-6 - sol_plus = solver.solve(model, t_eval, inputs={"a": a_value + 0.5 * h}) - sol_neg = solver.solve(model, t_eval, inputs={"a": a_value - 0.5 * h}) + sol_plus = solver.solve( + model, t_eval, inputs={"a": a_value + 0.5 * h}, t_interp=t_interp + ) + sol_neg = solver.solve( + model, t_eval, inputs={"a": a_value - 0.5 * h}, t_interp=t_interp + ) dyda_fd = (sol_plus.y - sol_neg.y) / h dyda_fd = dyda_fd.transpose().reshape(-1, 1) decimal = ( 2 if form == "iree" else 6 ) # iree currently operates with single precision - np.testing.assert_array_almost_equal(dyda_ida, dyda_fd, decimal=decimal) + np.testing.assert_array_almost_equal( + dyda_ida, dyda_fd, decimal=decimal, err_msg=f"Failed for form {form}" + ) # get the sensitivities for the variable d2uda = sol["2u"].sensitivities["a"] np.testing.assert_array_almost_equal( - 2 * dyda_ida[0:200:2], d2uda, decimal=decimal + 2 * dyda_ida[0:200:2], + d2uda, + decimal=decimal, + err_msg=f"Failed for form {form}", ) def test_ida_roberts_consistent_initialization(self): @@ -382,10 +445,14 @@ def test_ida_roberts_consistent_initialization(self): solver._set_consistent_initialization(model, t0, inputs_dict={}) # u(t0) = 0, v(t0) = 1 - np.testing.assert_array_almost_equal(model.y0full, [0, 1]) + np.testing.assert_array_almost_equal( + model.y0full, [0, 1], err_msg=f"Failed for form {form}" + ) # u'(t0) = 0.1 * v(t0) = 0.1 # Since v is algebraic, the initial derivative is set to 0 - np.testing.assert_array_almost_equal(model.ydot0full, [0.1, 0]) + np.testing.assert_array_almost_equal( + model.ydot0full, [0.1, 0], err_msg=f"Failed for form {form}" + ) def test_sensitivities_with_events(self): # this test implements a python version of the ida Roberts @@ -419,7 +486,9 @@ def test_sensitivities_with_events(self): options={"jax_evaluator": "iree"} if form == "iree" else {}, ) - t_eval = np.linspace(0, 3, 100) + t_interp = np.linspace(0, 3, 100) + t_eval = np.array([t_interp[0], t_interp[-1]]) + a_value = 0.1 b_value = 0.0 @@ -429,14 +498,19 @@ def test_sensitivities_with_events(self): t_eval, inputs={"a": a_value, "b": b_value}, calculate_sensitivities=True, + t_interp=t_interp, ) # test that y[1] remains constant - np.testing.assert_array_almost_equal(sol.y[1, :], np.ones(sol.t.shape)) + np.testing.assert_array_almost_equal( + sol.y[1, :], np.ones(sol.t.shape), err_msg=f"Failed for form {form}" + ) # test that y[0] = to true solution true_solution = a_value * sol.t - np.testing.assert_array_almost_equal(sol.y[0, :], true_solution) + np.testing.assert_array_almost_equal( + sol.y[0, :], true_solution, err_msg=f"Failed for form {form}" + ) # evaluate the sensitivities using idas dyda_ida = sol.sensitivities["a"] @@ -445,10 +519,16 @@ def test_sensitivities_with_events(self): # evaluate the sensitivities using finite difference h = 1e-6 sol_plus = solver.solve( - model, t_eval, inputs={"a": a_value + 0.5 * h, "b": b_value} + model, + t_eval, + inputs={"a": a_value + 0.5 * h, "b": b_value}, + t_interp=t_interp, ) sol_neg = solver.solve( - model, t_eval, inputs={"a": a_value - 0.5 * h, "b": b_value} + model, + t_eval, + inputs={"a": a_value - 0.5 * h, "b": b_value}, + t_interp=t_interp, ) max_index = min(sol_plus.y.shape[1], sol_neg.y.shape[1]) - 1 dyda_fd = (sol_plus.y[:, :max_index] - sol_neg.y[:, :max_index]) / h @@ -458,21 +538,33 @@ def test_sensitivities_with_events(self): 2 if form == "iree" else 6 ) # iree currently operates with single precision np.testing.assert_array_almost_equal( - dyda_ida[: (2 * max_index), :], dyda_fd, decimal=decimal + dyda_ida[: (2 * max_index), :], + dyda_fd, + decimal=decimal, + err_msg=f"Failed for form {form}", ) sol_plus = solver.solve( - model, t_eval, inputs={"a": a_value, "b": b_value + 0.5 * h} + model, + t_eval, + inputs={"a": a_value, "b": b_value + 0.5 * h}, + t_interp=t_interp, ) sol_neg = solver.solve( - model, t_eval, inputs={"a": a_value, "b": b_value - 0.5 * h} + model, + t_eval, + inputs={"a": a_value, "b": b_value - 0.5 * h}, + t_interp=t_interp, ) max_index = min(sol_plus.y.shape[1], sol_neg.y.shape[1]) - 1 dydb_fd = (sol_plus.y[:, :max_index] - sol_neg.y[:, :max_index]) / h dydb_fd = dydb_fd.transpose().reshape(-1, 1) np.testing.assert_array_almost_equal( - dydb_ida[: (2 * max_index), :], dydb_fd, decimal=decimal + dydb_ida[: (2 * max_index), :], + dydb_fd, + decimal=decimal, + err_msg=f"Failed for form {form}", ) def test_failures(self): @@ -523,7 +615,7 @@ def test_failures(self): solver = pybamm.IDAKLUSolver() t_eval = np.linspace(0, 3, 100) - with self.assertRaisesRegex(pybamm.SolverError, "idaklu solver failed"): + with self.assertRaisesRegex(pybamm.SolverError, "FAILURE IDA"): solver.solve(model, t_eval) def test_dae_solver_algebraic_model(self): @@ -592,22 +684,23 @@ def test_setup_options(self): disc = pybamm.Discretisation() disc.process_model(model) - t_eval = np.linspace(0, 1) + t_interp = np.linspace(0, 1) + t_eval = np.array([t_interp[0], t_interp[-1]]) solver = pybamm.IDAKLUSolver() - soln_base = solver.solve(model, t_eval) + soln_base = solver.solve(model, t_eval, t_interp=t_interp) # test print_stats solver = pybamm.IDAKLUSolver(options={"print_stats": True}) f = io.StringIO() with redirect_stdout(f): - solver.solve(model, t_eval) + solver.solve(model, t_eval, t_interp=t_interp) s = f.getvalue() self.assertIn("Solver Stats", s) solver = pybamm.IDAKLUSolver(options={"print_stats": False}) f = io.StringIO() with redirect_stdout(f): - solver.solve(model, t_eval) + solver.solve(model, t_eval, t_interp=t_interp) s = f.getvalue() self.assertEqual(len(s), 0) @@ -634,7 +727,7 @@ def test_setup_options(self): rtol=1e-8, options=options, ) - if ( + works = ( jacobian == "none" and (linear_solver == "SUNLinSol_Dense") or jacobian == "dense" @@ -650,14 +743,11 @@ def test_setup_options(self): and linear_solver != "SUNLinSol_Dense" and linear_solver != "garbage" ) - ): - works = True - else: - works = False + ) if works: - soln = solver.solve(model, t_eval) - np.testing.assert_array_almost_equal(soln.y, soln_base.y, 5) + soln = solver.solve(model, t_eval, t_interp=t_interp) + np.testing.assert_array_almost_equal(soln.y, soln_base.y, 4) else: with self.assertRaises(ValueError): soln = solver.solve(model, t_eval) @@ -672,9 +762,10 @@ def test_solver_options(self): disc = pybamm.Discretisation() disc.process_model(model) - t_eval = np.linspace(0, 1) + t_interp = np.linspace(0, 1) + t_eval = np.array([t_interp[0], t_interp[-1]]) solver = pybamm.IDAKLUSolver() - soln_base = solver.solve(model, t_eval) + soln_base = solver.solve(model, t_eval, t_interp=t_interp) options_success = { "max_order_bdf": 4, @@ -704,9 +795,9 @@ def test_solver_options(self): for option in options_success: options = {option: options_success[option]} solver = pybamm.IDAKLUSolver(rtol=1e-6, atol=1e-6, options=options) - soln = solver.solve(model, t_eval) + soln = solver.solve(model, t_eval, t_interp=t_interp) - np.testing.assert_array_almost_equal(soln.y, soln_base.y, 5) + np.testing.assert_array_almost_equal(soln.y, soln_base.y, 4) options_fail = { "max_order_bdf": -1, @@ -817,7 +908,9 @@ def construct_model(): # Compare output to sol_all for varname in [*output_variables, *model_vars]: - self.assertTrue(np.allclose(sol[varname].data, sol_all[varname].data)) + np.testing.assert_array_almost_equal( + sol[varname](t_eval), sol_all[varname](t_eval), 3 + ) # Check that the missing variables are not available in the solution for varname in inaccessible_vars: @@ -860,12 +953,13 @@ def test_with_output_variables_and_sensitivities(self): disc = pybamm.Discretisation(mesh, model.default_spatial_methods) disc.process_model(model) - t_eval = np.linspace(0, 100, 100) + t_eval = np.linspace(0, 100, 5) options = { "linear_solver": "SUNLinSol_KLU", "jacobian": "sparse", "num_threads": 4, + "max_num_steps": 1000, } if form == "iree": options["jax_evaluator"] = "iree" @@ -912,7 +1006,10 @@ def test_with_output_variables_and_sensitivities(self): tol = 1e-5 if form != "iree" else 1e-2 # iree has reduced precision for varname in output_variables: np.testing.assert_array_almost_equal( - sol[varname].data, sol_all[varname].data, tol + sol[varname](t_eval), + sol_all[varname](t_eval), + tol, + err_msg=f"Failed for {varname} with form {form}", ) # Mock a 1D current collector and initialise (none in the model) @@ -943,7 +1040,7 @@ def test_with_output_variables_and_event_termination(self): parameter_values=parameter_values, solver=pybamm.IDAKLUSolver(output_variables=["Terminal voltage [V]"]), ) - sol = sim.solve(np.linspace(0, 3600, 1000)) + sol = sim.solve(np.linspace(0, 3600, 2)) self.assertEqual(sol.termination, "event: Minimum voltage [V]") # create an event that doesn't require the state vector @@ -961,9 +1058,45 @@ def test_with_output_variables_and_event_termination(self): parameter_values=parameter_values, solver=pybamm.IDAKLUSolver(output_variables=["Terminal voltage [V]"]), ) - sol3 = sim3.solve(np.linspace(0, 3600, 1000)) + sol3 = sim3.solve(np.linspace(0, 3600, 2)) self.assertEqual(sol3.termination, "event: Minimum voltage [V]") + def test_simulation_period(self): + model = pybamm.lithium_ion.DFN() + parameter_values = pybamm.ParameterValues("Chen2020") + solver = pybamm.IDAKLUSolver() + + experiment = pybamm.Experiment( + ["Charge at C/10 for 10 seconds"], period="0.1 seconds" + ) + + sim = pybamm.Simulation( + model, + parameter_values=parameter_values, + experiment=experiment, + solver=solver, + ) + sol = sim.solve() + + np.testing.assert_array_almost_equal(sol.t, np.arange(0, 10.1, 0.1), decimal=4) + + def test_interpolate_time_step_start_offset(self): + model = pybamm.lithium_ion.SPM() + experiment = pybamm.Experiment( + [ + "Discharge at C/10 for 10 seconds", + "Charge at C/10 for 10 seconds", + ], + period="1 seconds", + ) + solver = pybamm.IDAKLUSolver() + sim = pybamm.Simulation(model, experiment=experiment, solver=solver) + sol = sim.solve() + np.testing.assert_equal( + sol.sub_solutions[0].t[-1] + pybamm.settings.step_start_offset, + sol.sub_solutions[1].t[0], + ) + if __name__ == "__main__": print("Add -v for more debug output") From 7537784ff9e7484751cd4f8c6d858eaa016f66fd Mon Sep 17 00:00:00 2001 From: Brady Planden <55357039+BradyPlanden@users.noreply.github.com> Date: Mon, 26 Aug 2024 15:11:07 +0100 Subject: [PATCH 67/82] Adds additional tests for Casadi modes (#4028) * Add casadi solver mode tests * add experiment definition to test * tests: relocate to integration, update for pytest. --------- Co-authored-by: Martin Robinson --- .../test_solvers/test_casadi_mode.py | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 tests/integration/test_solvers/test_casadi_mode.py diff --git a/tests/integration/test_solvers/test_casadi_mode.py b/tests/integration/test_solvers/test_casadi_mode.py new file mode 100644 index 0000000000..bbde0f04e2 --- /dev/null +++ b/tests/integration/test_solvers/test_casadi_mode.py @@ -0,0 +1,56 @@ +# +# Tests for the Casadi Solver Modes +# + +import pybamm +import numpy as np + + +class TestCasadiModes: + def test_casadi_solver_mode(self): + solvers = [ + pybamm.CasadiSolver(mode="safe", atol=1e-6, rtol=1e-6), + pybamm.CasadiSolver(mode="fast", atol=1e-6, rtol=1e-6), + pybamm.CasadiSolver(mode="fast with events", atol=1e-6, rtol=1e-6), + ] + + # define experiment + experiment = pybamm.Experiment( + [ + ("Discharge at 1C until 3.0 V (10 seconds period)",), + ] + ) + + solutions = [] + for solver in solvers: + # define model + model = pybamm.lithium_ion.SPM() + + # solve simulation + sim = pybamm.Simulation( + model, + experiment=experiment, + solver=solver, + ) + + # append to solutions + solutions.append(sim.solve()) + + # define variables to compare + output_vars = [ + "Terminal voltage [V]", + "Positive particle surface concentration [mol.m-3]", + "Electrolyte concentration [mol.m-3]", + ] + + # assert solutions + for var in output_vars: + np.testing.assert_allclose( + solutions[0][var].data, solutions[1][var].data, rtol=1e-6, atol=1e-6 + ) + np.testing.assert_allclose( + solutions[0][var].data, solutions[2][var].data, rtol=1e-6, atol=1e-6 + ) + np.testing.assert_allclose( + solutions[1][var].data, solutions[2][var].data, rtol=1e-6, atol=1e-6 + ) From e78f95a6eee8225c871ae348d2b7754ef691a3ff Mon Sep 17 00:00:00 2001 From: Marc Berliner <34451391+MarcBerliner@users.noreply.github.com> Date: Mon, 26 Aug 2024 13:08:22 -0700 Subject: [PATCH 68/82] fix segfaults (#4379) --- CHANGELOG.md | 1 + src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48d0d7ba32..ee21541239 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ ## Bug Fixes +- Fixed memory issue that caused failure when `output variables` were specified with (`IDAKLUSolver`). ([#4379](https://github.com/pybamm-team/PyBaMM/issues/4379)) - Fixed bug where IDAKLU solver failed when `output variables` were specified and an event triggered. ([#4300](https://github.com/pybamm-team/PyBaMM/pull/4300)) # [v24.5](https://github.com/pybamm-team/PyBaMM/tree/v24.5) - 2024-07-26 diff --git a/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl b/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl index de6c43466a..7ed4dcfad8 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl +++ b/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl @@ -536,7 +536,7 @@ Solution IDAKLUSolverOpenMP::solve( realtype *yterm_return = new realtype[length_of_final_sv_slice]; if (save_outputs_only) { // store final state slice if outout variables are specified - yterm_return = y_val; + std::memcpy(yterm_return, y_val, length_of_final_sv_slice * sizeof(realtype*)); } if (solver_opts.print_stats) { From 232e32c36f676c4d957b034c70e614f4233e70a3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Aug 2024 22:31:31 +0200 Subject: [PATCH 69/82] Build(deps): bump github/codeql-action in the actions group (#4381) Bumps the actions group with 1 update: [github/codeql-action](https://github.com/github/codeql-action). Updates `github/codeql-action` from 3.26.3 to 3.26.5 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/883d8588e56d1753a8a58c1c86e88976f0c23449...2c779ab0d087cd7fe7b826087247c2c81f27bfa6) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/scorecard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index a3537cc010..8b33553737 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -68,6 +68,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@883d8588e56d1753a8a58c1c86e88976f0c23449 # v3.26.3 + uses: github/codeql-action/upload-sarif@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5 with: sarif_file: results.sarif From 8057c160b6eebf7ef2999d0ea7fa7541c2b4ac01 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 26 Aug 2024 23:17:08 +0200 Subject: [PATCH 70/82] chore: update pre-commit hooks (#4382) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.6.1 → v0.6.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.1...v0.6.2) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ddbdb6350e..835678e034 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.6.1" + rev: "v0.6.2" hooks: - id: ruff args: [--fix, --show-fixes] From 8084c906adeefb95834c4e8eb43c9afb64d984b4 Mon Sep 17 00:00:00 2001 From: Pip Liggins Date: Tue, 27 Aug 2024 19:18:06 +0100 Subject: [PATCH 71/82] Remove python-idaklu (#4326) * Remove references in python to python-idaklu, edit tests * Remove relevant C++ files * style: pre-commit fixes * Fix coverage * remove unused function * Add warning for sensitivities with python evaluator style fixes from review * Add to changelog * fix test name --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- CHANGELOG.md | 4 + CMakeLists.txt | 2 - setup.py | 2 - src/pybamm/models/base_model.py | 1 + src/pybamm/solvers/base_solver.py | 23 +- src/pybamm/solvers/c_solvers/idaklu.cpp | 23 - .../solvers/c_solvers/idaklu/python.cpp | 486 --------------- .../solvers/c_solvers/idaklu/python.hpp | 37 -- src/pybamm/solvers/idaklu_solver.py | 559 +++++++----------- tests/unit/test_solvers/test_base_solver.py | 80 ++- tests/unit/test_solvers/test_idaklu_solver.py | 54 +- 11 files changed, 311 insertions(+), 960 deletions(-) delete mode 100644 src/pybamm/solvers/c_solvers/idaklu/python.cpp delete mode 100644 src/pybamm/solvers/c_solvers/idaklu/python.hpp diff --git a/CHANGELOG.md b/CHANGELOG.md index ee21541239..82be87f95f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ - Fixed memory issue that caused failure when `output variables` were specified with (`IDAKLUSolver`). ([#4379](https://github.com/pybamm-team/PyBaMM/issues/4379)) - Fixed bug where IDAKLU solver failed when `output variables` were specified and an event triggered. ([#4300](https://github.com/pybamm-team/PyBaMM/pull/4300)) +## Breaking changes + +- Removed legacy python-IDAKLU solver. ([#4326](https://github.com/pybamm-team/PyBaMM/pull/4326)) + # [v24.5](https://github.com/pybamm-team/PyBaMM/tree/v24.5) - 2024-07-26 ## Features diff --git a/CMakeLists.txt b/CMakeLists.txt index 8b3a2adfe5..ad56ac34ca 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -92,8 +92,6 @@ pybind11_add_module(idaklu src/pybamm/solvers/c_solvers/idaklu/IdakluJax.hpp src/pybamm/solvers/c_solvers/idaklu/common.hpp src/pybamm/solvers/c_solvers/idaklu/common.cpp - src/pybamm/solvers/c_solvers/idaklu/python.hpp - src/pybamm/solvers/c_solvers/idaklu/python.cpp src/pybamm/solvers/c_solvers/idaklu/Solution.cpp src/pybamm/solvers/c_solvers/idaklu/Solution.hpp src/pybamm/solvers/c_solvers/idaklu/Options.hpp diff --git a/setup.py b/setup.py index 6ceb049b31..74de1baca4 100644 --- a/setup.py +++ b/setup.py @@ -323,8 +323,6 @@ def compile_KLU(): "src/pybamm/solvers/c_solvers/idaklu/IdakluJax.hpp", "src/pybamm/solvers/c_solvers/idaklu/common.hpp", "src/pybamm/solvers/c_solvers/idaklu/common.cpp", - "src/pybamm/solvers/c_solvers/idaklu/python.hpp", - "src/pybamm/solvers/c_solvers/idaklu/python.cpp", "src/pybamm/solvers/c_solvers/idaklu/Solution.cpp", "src/pybamm/solvers/c_solvers/idaklu/Solution.hpp", "src/pybamm/solvers/c_solvers/idaklu/Options.hpp", diff --git a/src/pybamm/models/base_model.py b/src/pybamm/models/base_model.py index e90f974676..0d4638e178 100644 --- a/src/pybamm/models/base_model.py +++ b/src/pybamm/models/base_model.py @@ -39,6 +39,7 @@ class BaseModel: calling `evaluate(t, y)` on the given expression treeself. - "casadi": convert into CasADi expression tree, which then uses CasADi's \ algorithm to calculate the Jacobian. + - "jax": convert into JAX expression tree Default is "casadi". """ diff --git a/src/pybamm/solvers/base_solver.py b/src/pybamm/solvers/base_solver.py index 9027bd51c4..efef7e9357 100644 --- a/src/pybamm/solvers/base_solver.py +++ b/src/pybamm/solvers/base_solver.py @@ -1514,26 +1514,13 @@ def report(string): elif model.convert_to_format != "casadi": y = vars_for_processing["y"] jacobian = vars_for_processing["jacobian"] - # Process with pybamm functions, converting - # to python evaluator + if model.calculate_sensitivities: - report( - f"Calculating sensitivities for {name} with respect " - f"to parameters {model.calculate_sensitivities}" + raise pybamm.SolverError( # pragma: no cover + "Sensitivies are no longer supported for the python " + "evaluator. Please use `convert_to_format = 'casadi'`, or `jax` " + "to calculate sensitivities." ) - jacp_dict = { - p: symbol.diff(pybamm.InputParameter(p)) - for p in model.calculate_sensitivities - } - - report(f"Converting sensitivities for {name} to python") - jacp_dict = { - p: pybamm.EvaluatorPython(jacp) for p, jacp in jacp_dict.items() - } - - # jacp should be a function that returns a dict of sensitivities - def jacp(*args, **kwargs): - return {k: v(*args, **kwargs) for k, v in jacp_dict.items()} else: jacp = None diff --git a/src/pybamm/solvers/c_solvers/idaklu.cpp b/src/pybamm/solvers/c_solvers/idaklu.cpp index bb9466d40b..3ef0194403 100644 --- a/src/pybamm/solvers/c_solvers/idaklu.cpp +++ b/src/pybamm/solvers/c_solvers/idaklu.cpp @@ -11,7 +11,6 @@ #include "idaklu/idaklu_solver.hpp" #include "idaklu/IdakluJax.hpp" #include "idaklu/common.hpp" -#include "idaklu/python.hpp" #include "idaklu/Expressions/Casadi/CasadiFunctions.hpp" #ifdef IREE_ENABLE @@ -34,28 +33,6 @@ PYBIND11_MODULE(idaklu, m) py::bind_vector>(m, "VectorNdArray"); - m.def("solve_python", &solve_python, - "The solve function for python evaluators", - py::arg("t"), - py::arg("y0"), - py::arg("yp0"), - py::arg("res"), - py::arg("jac"), - py::arg("sens"), - py::arg("get_jac_data"), - py::arg("get_jac_row_vals"), - py::arg("get_jac_col_ptr"), - py::arg("nnz"), - py::arg("events"), - py::arg("number_of_events"), - py::arg("use_jacobian"), - py::arg("rhs_alg_id"), - py::arg("atol"), - py::arg("rtol"), - py::arg("inputs"), - py::arg("number_of_sensitivity_parameters"), - py::return_value_policy::take_ownership); - py::class_(m, "IDAKLUSolver") .def("solve", &IDAKLUSolver::solve, "perform a solve", diff --git a/src/pybamm/solvers/c_solvers/idaklu/python.cpp b/src/pybamm/solvers/c_solvers/idaklu/python.cpp deleted file mode 100644 index 015f504086..0000000000 --- a/src/pybamm/solvers/c_solvers/idaklu/python.cpp +++ /dev/null @@ -1,486 +0,0 @@ -#include "common.hpp" -#include "python.hpp" -#include - -class PybammFunctions -{ -public: - int number_of_states; - int number_of_parameters; - int number_of_events; - - PybammFunctions(const residual_type &res, const jacobian_type &jac, - const sensitivities_type &sens, - const jac_get_type &get_jac_data_in, - const jac_get_type &get_jac_row_vals_in, - const jac_get_type &get_jac_col_ptrs_in, - const event_type &event, - const int n_s, int n_e, const int n_p, - const np_array &inputs) - : number_of_states(n_s), number_of_events(n_e), - number_of_parameters(n_p), - py_res(res), py_jac(jac), - py_sens(sens), - py_event(event), py_get_jac_data(get_jac_data_in), - py_get_jac_row_vals(get_jac_row_vals_in), - py_get_jac_col_ptrs(get_jac_col_ptrs_in), - inputs(inputs) - { - } - - np_array operator()(double t, np_array y, np_array yp) - { - return py_res(t, y, inputs, yp); - } - - np_array res(double t, np_array y, np_array yp) - { - return py_res(t, y, inputs, yp); - } - - void jac(double t, np_array y, double cj) - { - // this function evaluates the jacobian and sets it to be the attribute - // of a python class which can then be called by get_jac_data, - // get_jac_col_ptr, etc - py_jac(t, y, inputs, cj); - } - - void sensitivities( - std::vector& resvalS, - const double t, const np_array& y, const np_array& yp, - const std::vector& yS, const std::vector& ypS) - { - // this function evaluates the sensitivity equations required by IDAS, - // returning them in resvalS, which is preallocated as a numpy array - // of size (np, n), where n is the number of states and np is the number - // of parameters - // - // yS and ypS are also shape (np, n), y and yp are shape (n) - // - // dF/dy * s_i + dF/dyd * sd + dFdp_i for i in range(np) - py_sens(resvalS, t, y, inputs, yp, yS, ypS); - } - - np_array get_jac_data() { - return py_get_jac_data(); - } - - np_array get_jac_row_vals() { - return py_get_jac_row_vals(); - } - - np_array get_jac_col_ptrs() { - return py_get_jac_col_ptrs(); - } - - np_array events(double t, np_array y) { - return py_event(t, y, inputs); - } - -private: - residual_type py_res; - sensitivities_type py_sens; - jacobian_type py_jac; - event_type py_event; - jac_get_type py_get_jac_data; - jac_get_type py_get_jac_row_vals; - jac_get_type py_get_jac_col_ptrs; - const np_array &inputs; -}; - -int residual(realtype tres, N_Vector yy, N_Vector yp, N_Vector rr, - void *user_data) -{ - PybammFunctions *python_functions_ptr = - static_cast(user_data); - PybammFunctions python_functions = *python_functions_ptr; - - realtype *yval, *ypval, *rval; - yval = N_VGetArrayPointer(yy); - ypval = N_VGetArrayPointer(yp); - rval = N_VGetArrayPointer(rr); - - int n = python_functions.number_of_states; - py::array_t y_np = py::array_t(n, yval); - py::array_t yp_np = py::array_t(n, ypval); - - py::array_t r_np; - - r_np = python_functions.res(tres, y_np, yp_np); - - auto r_np_ptr = r_np.unchecked<1>(); - - // just copying data - int i; - for (i = 0; i < n; i++) - { - rval[i] = r_np_ptr[i]; - } - return 0; -} - -int jacobian(realtype tt, realtype cj, N_Vector yy, N_Vector yp, - N_Vector resvec, SUNMatrix JJ, void *user_data, N_Vector tempv1, - N_Vector tempv2, N_Vector tempv3) -{ - realtype *yval; - yval = N_VGetArrayPointer(yy); - - PybammFunctions *python_functions_ptr = - static_cast(user_data); - PybammFunctions python_functions = *python_functions_ptr; - - int n = python_functions.number_of_states; - py::array_t y_np = py::array_t(n, yval); - - // create pointer to jac data, column pointers, and row values - sunindextype *jac_colptrs = SUNSparseMatrix_IndexPointers(JJ); - sunindextype *jac_rowvals = SUNSparseMatrix_IndexValues(JJ); - realtype *jac_data = SUNSparseMatrix_Data(JJ); - - py::array_t jac_np_array; - - python_functions.jac(tt, y_np, cj); - - np_array jac_np_data = python_functions.get_jac_data(); - int n_data = jac_np_data.request().size; - auto jac_np_data_ptr = jac_np_data.unchecked<1>(); - - // just copy across data - int i; - for (i = 0; i < n_data; i++) - { - jac_data[i] = jac_np_data_ptr[i]; - } - - np_array jac_np_row_vals = python_functions.get_jac_row_vals(); - int n_row_vals = jac_np_row_vals.request().size; - - auto jac_np_row_vals_ptr = jac_np_row_vals.unchecked<1>(); - // just copy across row vals (this might be unneeded) - for (i = 0; i < n_row_vals; i++) - { - jac_rowvals[i] = jac_np_row_vals_ptr[i]; - } - - np_array jac_np_col_ptrs = python_functions.get_jac_col_ptrs(); - int n_col_ptrs = jac_np_col_ptrs.request().size; - auto jac_np_col_ptrs_ptr = jac_np_col_ptrs.unchecked<1>(); - - // just copy across col ptrs (this might be unneeded) - for (i = 0; i < n_col_ptrs; i++) - { - jac_colptrs[i] = jac_np_col_ptrs_ptr[i]; - } - - return (0); -} - -int events(realtype t, N_Vector yy, N_Vector yp, realtype *events_ptr, - void *user_data) -{ - realtype *yval; - yval = N_VGetArrayPointer(yy); - - PybammFunctions *python_functions_ptr = - static_cast(user_data); - PybammFunctions python_functions = *python_functions_ptr; - - int number_of_events = python_functions.number_of_events; - int number_of_states = python_functions.number_of_states; - py::array_t y_np = py::array_t(number_of_states, yval); - - py::array_t events_np_array; - - events_np_array = python_functions.events(t, y_np); - - auto events_np_data_ptr = events_np_array.unchecked<1>(); - - // just copying data (figure out how to pass pointers later) - int i; - for (i = 0; i < number_of_events; i++) - { - events_ptr[i] = events_np_data_ptr[i]; - } - - return (0); -} - -int sensitivities(int Ns, realtype t, N_Vector yy, N_Vector yp, - N_Vector resval, N_Vector *yS, N_Vector *ypS, N_Vector *resvalS, - void *user_data, N_Vector tmp1, N_Vector tmp2, N_Vector tmp3) { -// This function computes the sensitivity residual for all sensitivity -// equations. It must compute the vectors -// (∂F/∂y)s i (t)+(∂F/∂ ẏ) ṡ i (t)+(∂F/∂p i ) and store them in resvalS[i]. -// Ns is the number of sensitivities. -// t is the current value of the independent variable. -// yy is the current value of the state vector, y(t). -// yp is the current value of ẏ(t). -// resval contains the current value F of the original DAE residual. -// yS contains the current values of the sensitivities s i . -// ypS contains the current values of the sensitivity derivatives ṡ i . -// resvalS contains the output sensitivity residual vectors. -// Memory allocation for resvalS is handled within idas. -// user data is a pointer to user data. -// tmp1, tmp2, tmp3 are N Vectors of length N which can be used as -// temporary storage. -// -// Return value An IDASensResFn should return 0 if successful, -// a positive value if a recoverable error -// occurred (in which case idas will attempt to correct), -// or a negative value if it failed unrecoverably (in which case the integration is halted and IDA SRES FAIL is returned) -// - PybammFunctions *python_functions_ptr = - static_cast(user_data); - PybammFunctions python_functions = *python_functions_ptr; - - int n = python_functions.number_of_states; - int np = python_functions.number_of_parameters; - - // memory managed by sundials, so pass a destructor that does nothing - auto state_vector_shape = std::vector {n, 1}; - np_array y_np = np_array(state_vector_shape, N_VGetArrayPointer(yy), - py::capsule(&yy, [](void* p) {})); - np_array yp_np = np_array(state_vector_shape, N_VGetArrayPointer(yp), - py::capsule(&yp, [](void* p) {})); - - std::vector yS_np(np); - for (int i = 0; i < np; i++) { - auto capsule = py::capsule(yS + i, [](void* p) {}); - yS_np[i] = np_array(state_vector_shape, N_VGetArrayPointer(yS[i]), capsule); - } - - std::vector ypS_np(np); - for (int i = 0; i < np; i++) { - auto capsule = py::capsule(ypS + i, [](void* p) {}); - ypS_np[i] = np_array(state_vector_shape, N_VGetArrayPointer(ypS[i]), capsule); - } - - std::vector resvalS_np(np); - for (int i = 0; i < np; i++) { - auto capsule = py::capsule(resvalS + i, [](void* p) {}); - resvalS_np[i] = np_array(state_vector_shape, - N_VGetArrayPointer(resvalS[i]), capsule); - } - - realtype *ptr1 = static_cast(resvalS_np[0].request().ptr); - const realtype* resvalSval = N_VGetArrayPointer(resvalS[0]); - - python_functions.sensitivities(resvalS_np, t, y_np, yp_np, yS_np, ypS_np); - - return 0; -} - -/* main program */ -Solution solve_python(np_array t_np, np_array y0_np, np_array yp0_np, - residual_type res, jacobian_type jac, - sensitivities_type sens, - jac_get_type gjd, jac_get_type gjrv, jac_get_type gjcp, - int nnz, event_type event, - int number_of_events, int use_jacobian, np_array rhs_alg_id, - np_array atol_np, double rel_tol, np_array inputs, - int number_of_parameters) -{ - auto t = t_np.unchecked<1>(); - auto y0 = y0_np.unchecked<1>(); - auto yp0 = yp0_np.unchecked<1>(); - auto atol = atol_np.unchecked<1>(); - - int number_of_states = y0_np.request().size; - int number_of_timesteps = t_np.request().size; - void *ida_mem; // pointer to memory - N_Vector yy, yp, avtol; // y, y', and absolute tolerance - N_Vector *yyS, *ypS; // y, y' for sensitivities - N_Vector id; - realtype rtol, *yval, *ypval, *atval; - std::vector ySval(number_of_parameters); - int retval; - SUNMatrix J; - SUNLinearSolver LS; - -#if SUNDIALS_VERSION_MAJOR >= 6 - SUNContext sunctx; - SUNContext_Create(NULL, &sunctx); - - // allocate memory for solver - ida_mem = IDACreate(sunctx); - - // allocate vectors - yy = N_VNew_Serial(number_of_states, sunctx); - yp = N_VNew_Serial(number_of_states, sunctx); - avtol = N_VNew_Serial(number_of_states, sunctx); - id = N_VNew_Serial(number_of_states, sunctx); -#else - // allocate memory for solver - ida_mem = IDACreate(); - - // allocate vectors - yy = N_VNew_Serial(number_of_states); - yp = N_VNew_Serial(number_of_states); - avtol = N_VNew_Serial(number_of_states); - id = N_VNew_Serial(number_of_states); -#endif - - if (number_of_parameters > 0) { - yyS = N_VCloneVectorArray(number_of_parameters, yy); - ypS = N_VCloneVectorArray(number_of_parameters, yp); - } - - // set initial value - yval = N_VGetArrayPointer(yy); - ypval = N_VGetArrayPointer(yp); - atval = N_VGetArrayPointer(avtol); - int i; - for (i = 0; i < number_of_states; i++) - { - yval[i] = y0[i]; - ypval[i] = yp0[i]; - atval[i] = atol[i]; - } - - for (int is = 0 ; is < number_of_parameters; is++) { - ySval[is] = N_VGetArrayPointer(yyS[is]); - N_VConst(RCONST(0.0), yyS[is]); - N_VConst(RCONST(0.0), ypS[is]); - } - - // initialise solver - realtype t0 = RCONST(t(0)); - IDAInit(ida_mem, residual, t0, yy, yp); - - // set tolerances - rtol = RCONST(rel_tol); - - IDASVtolerances(ida_mem, rtol, avtol); - - // set events - IDARootInit(ida_mem, number_of_events, events); - - // set pybamm functions by passing pointer to it - PybammFunctions pybamm_functions(res, jac, sens, gjd, gjrv, gjcp, event, - number_of_states, number_of_events, - number_of_parameters, inputs); - void *user_data = &pybamm_functions; - IDASetUserData(ida_mem, user_data); - - // set linear solver -#if SUNDIALS_VERSION_MAJOR >= 6 - J = SUNSparseMatrix(number_of_states, number_of_states, nnz, CSR_MAT, sunctx); - LS = SUNLinSol_KLU(yy, J, sunctx); -#else - J = SUNSparseMatrix(number_of_states, number_of_states, nnz, CSR_MAT); - LS = SUNLinSol_KLU(yy, J); -#endif - - IDASetLinearSolver(ida_mem, LS, J); - - if (use_jacobian == 1) - { - IDASetJacFn(ida_mem, jacobian); - } - - if (number_of_parameters > 0) - { - IDASensInit(ida_mem, number_of_parameters, - IDA_SIMULTANEOUS, sensitivities, yyS, ypS); - IDASensEEtolerances(ida_mem); - } - - int t_i = 1; - realtype tret; - realtype t_next; - realtype t_final = t(number_of_timesteps - 1); - - // set return vectors - std::vector t_return(number_of_timesteps); - std::vector y_return(number_of_timesteps * number_of_states); - std::vector yS_return(number_of_parameters * number_of_timesteps * number_of_states); - - t_return[0] = t(0); - for (int j = 0; j < number_of_states; j++) - { - y_return[j] = yval[j]; - } - for (int j = 0; j < number_of_parameters; j++) { - const int base_index = j * number_of_timesteps * number_of_states; - for (int k = 0; k < number_of_states; k++) { - yS_return[base_index + k] = ySval[j][k]; - } - } - - // calculate consistent initial conditions - auto id_np_val = rhs_alg_id.unchecked<1>(); - realtype *id_val; - id_val = N_VGetArrayPointer(id); - - int ii; - for (ii = 0; ii < number_of_states; ii++) - { - id_val[ii] = id_np_val[ii]; - } - - IDASetId(ida_mem, id); - IDACalcIC(ida_mem, IDA_YA_YDP_INIT, t(1)); - - while (true) - { - t_next = t(t_i); - IDASetStopTime(ida_mem, t_next); - retval = IDASolve(ida_mem, t_final, &tret, yy, yp, IDA_NORMAL); - - if (retval == IDA_TSTOP_RETURN || retval == IDA_SUCCESS || retval == IDA_ROOT_RETURN) - { - if (number_of_parameters > 0) { - IDAGetSens(ida_mem, &tret, yyS); - } - - t_return[t_i] = tret; - for (int j = 0; j < number_of_states; j++) - { - y_return[t_i * number_of_states + j] = yval[j]; - } - for (int j = 0; j < number_of_parameters; j++) { - const int base_index = j * number_of_timesteps * number_of_states - + t_i * number_of_states; - for (int k = 0; k < number_of_states; k++) { - yS_return[base_index + k] = ySval[j][k]; - } - } - t_i += 1; - if (retval == IDA_SUCCESS || retval == IDA_ROOT_RETURN) { - break; - } - - } - } - - /* Free memory */ - if (number_of_parameters > 0) { - IDASensFree(ida_mem); - } - IDAFree(&ida_mem); - SUNLinSolFree(LS); - SUNMatDestroy(J); - N_VDestroy(avtol); - N_VDestroy(yp); - if (number_of_parameters > 0) { - N_VDestroyVectorArray(yyS, number_of_parameters); - N_VDestroyVectorArray(ypS, number_of_parameters); - } -#if SUNDIALS_VERSION_MAJOR >= 6 - SUNContext_Free(&sunctx); -#endif - - np_array t_ret = np_array(t_i, &t_return[0]); - np_array y_ret = np_array(t_i * number_of_states, &y_return[0]); - np_array yS_ret = np_array( - std::vector {number_of_parameters, number_of_timesteps, number_of_states}, - &yS_return[0] - ); - np_array yterm_ret = np_array(0); - - Solution sol(retval, t_ret, y_ret, yS_ret, yterm_ret); - - return sol; -} diff --git a/src/pybamm/solvers/c_solvers/idaklu/python.hpp b/src/pybamm/solvers/c_solvers/idaklu/python.hpp deleted file mode 100644 index 6231d13eb6..0000000000 --- a/src/pybamm/solvers/c_solvers/idaklu/python.hpp +++ /dev/null @@ -1,37 +0,0 @@ -#ifndef PYBAMM_IDAKLU_HPP -#define PYBAMM_IDAKLU_HPP - -#include "common.hpp" -#include "Solution.hpp" -#include - -using residual_type = std::function< - np_array(realtype, np_array, np_array, np_array) - >; -using sensitivities_type = std::function&, realtype, const np_array&, - const np_array&, - const np_array&, const std::vector&, - const std::vector& - )>; -using jacobian_type = std::function; - -using event_type = - std::function; - -using jac_get_type = std::function; - - -/** - * @brief Interface to the python solver - */ -Solution solve_python(np_array t_np, np_array y0_np, np_array yp0_np, - residual_type res, jacobian_type jac, - sensitivities_type sens, - jac_get_type gjd, jac_get_type gjrv, jac_get_type gjcp, - int nnz, event_type event, - int number_of_events, int use_jacobian, np_array rhs_alg_id, - np_array atol_np, double rel_tol, np_array inputs, - int number_of_parameters); - -#endif // PYBAMM_IDAKLU_HPP diff --git a/src/pybamm/solvers/idaklu_solver.py b/src/pybamm/solvers/idaklu_solver.py index 85731f4e12..b92006d12d 100644 --- a/src/pybamm/solvers/idaklu_solver.py +++ b/src/pybamm/solvers/idaklu_solver.py @@ -289,7 +289,21 @@ def inputs_to_dict(inputs): if ics_only: return base_set_up_return + if model.convert_to_format not in ["casadi", "jax"]: + msg = ( + "The python-idaklu solver has been deprecated. " + "To use the IDAKLU solver set `convert_to_format = 'casadi'`, or `jax`" + " if using IREE." + ) + warnings.warn(msg, DeprecationWarning, stacklevel=2) + if model.convert_to_format == "jax": + if self._options["jax_evaluator"] != "iree": + raise pybamm.SolverError( + "Unsupported evaluation engine for convert_to_format=" + f"{model.convert_to_format} " + f"(jax_evaluator={self._options['jax_evaluator']})" + ) mass_matrix = model.mass_matrix.entries.toarray() elif model.convert_to_format == "casadi": if self._options["jacobian"] == "dense": @@ -297,19 +311,15 @@ def inputs_to_dict(inputs): else: mass_matrix = casadi.DM(model.mass_matrix.entries) else: - mass_matrix = model.mass_matrix.entries + raise pybamm.SolverError( + "Unsupported option for convert_to_format=" + f"{model.convert_to_format} " + ) # construct residuals function by binding inputs if model.convert_to_format == "casadi": # TODO: do we need densify here? rhs_algebraic = model.rhs_algebraic_eval - else: - - def resfn(t, y, inputs, ydot): - return ( - model.rhs_algebraic_eval(t, y, inputs_to_dict(inputs)).flatten() - - mass_matrix @ ydot - ) if not model.use_jacobian: raise pybamm.SolverError("KLU requires the Jacobian") @@ -384,52 +394,6 @@ def resfn(t, y, inputs, ydot): ) ) - elif self._options["jax_evaluator"] == "jax": - t0 = 0 if t_eval is None else t_eval[0] - jac_y0_t0 = model.jac_rhs_algebraic_eval(t0, y0, inputs_dict) - if sparse.issparse(jac_y0_t0): - - def jacfn(t, y, inputs, cj): - j = ( - model.jac_rhs_algebraic_eval(t, y, inputs_to_dict(inputs)) - - cj * mass_matrix - ) - return j - - else: - - def jacfn(t, y, inputs, cj): - jac_eval = ( - model.jac_rhs_algebraic_eval(t, y, inputs_to_dict(inputs)) - - cj * mass_matrix - ) - return sparse.csr_matrix(jac_eval) - - class SundialsJacobian: - def __init__(self): - self.J = None - - random = np.random.random(size=y0.size) - J = jacfn(10, random, inputs, 20) - self.nnz = J.nnz # hoping nnz remains constant... - - def jac_res(self, t, y, inputs, cj): - # must be of form j_res = (dr/dy) - (cj) (dr/dy') - # cj is just the input parameter - # see p68 of the ida_guide.pdf for more details - self.J = jacfn(t, y, inputs, cj) - - def get_jac_data(self): - return self.J.data - - def get_jac_row_vals(self): - return self.J.indices - - def get_jac_col_ptrs(self): - return self.J.indptr - - jac_class = SundialsJacobian() - num_of_events = len(model.terminate_events_eval) # rootfn needs to return an array of length num_of_events @@ -446,15 +410,6 @@ def get_jac_col_ptrs(self): ) ], ) - elif self._options["jax_evaluator"] == "jax": - - def rootfn(t, y, inputs): - new_inputs = inputs_to_dict(inputs) - return_root = np.array( - [event(t, y, new_inputs) for event in model.terminate_events_eval] - ).reshape(-1) - - return return_root # get ids of rhs and algebraic variables if model.convert_to_format == "casadi": @@ -481,301 +436,242 @@ def rootfn(t, y, inputs): else: sensfn = model.jacp_rhs_algebraic_eval - else: - # for the python solver we give it the full sensitivity equations - # required by IDAS - def sensfn(resvalS, t, y, inputs, yp, yS, ypS): - """ - this function evaluates the sensitivity equations required by IDAS, - returning them in resvalS, which is preallocated as a numpy array of - size (np, n), where n is the number of states and np is the number of - parameters - - The equations returned are: - - dF/dy * s_i + dF/dyd * sd_i + dFdp_i for i in range(np) - - Parameters - ---------- - resvalS: ndarray of shape (np, n) - returns the sensitivity equations in this preallocated array - t: number - time value - y: ndarray of shape (n) - current state vector - yp: list (np) of ndarray of shape (n) - current time derivative of state vector - yS: list (np) of ndarray of shape (n) - current state vector of sensitivity equations - ypS: list (np) of ndarray of shape (n) - current time derivative of state vector of sensitivity equations - - """ - - new_inputs = inputs_to_dict(inputs) - dFdy = model.jac_rhs_algebraic_eval(t, y, new_inputs) - dFdyd = mass_matrix - dFdp = model.jacp_rhs_algebraic_eval(t, y, new_inputs) - - for i, dFdp_i in enumerate(dFdp.values()): - resvalS[i][:] = dFdy @ yS[i] - dFdyd @ ypS[i] + dFdp_i - atol = getattr(model, "atol", self.atol) atol = self._check_atol_type(atol, y0.size) rtol = self.rtol - if model.convert_to_format == "casadi" or ( + if model.convert_to_format == "casadi": + # Serialize casadi functions + idaklu_solver_fcn = idaklu.create_casadi_solver + rhs_algebraic = idaklu.generate_function(rhs_algebraic.serialize()) + jac_times_cjmass = idaklu.generate_function(jac_times_cjmass.serialize()) + jac_rhs_algebraic_action = idaklu.generate_function( + jac_rhs_algebraic_action.serialize() + ) + rootfn = idaklu.generate_function(rootfn.serialize()) + mass_action = idaklu.generate_function(mass_action.serialize()) + sensfn = idaklu.generate_function(sensfn.serialize()) + elif ( model.convert_to_format == "jax" and self._options["jax_evaluator"] == "iree" ): - if model.convert_to_format == "casadi": - # Serialize casadi functions - idaklu_solver_fcn = idaklu.create_casadi_solver - rhs_algebraic = idaklu.generate_function(rhs_algebraic.serialize()) - jac_times_cjmass = idaklu.generate_function( - jac_times_cjmass.serialize() + # Convert Jax functions to MLIR (also, demote to single precision) + idaklu_solver_fcn = idaklu.create_iree_solver + pybamm.demote_expressions_to_32bit = True + if pybamm.demote_expressions_to_32bit: + warnings.warn( + "Demoting expressions to 32-bit for MLIR conversion", + stacklevel=2, ) - jac_rhs_algebraic_action = idaklu.generate_function( - jac_rhs_algebraic_action.serialize() + jnpfloat = jnp.float32 + else: # pragma: no cover + jnpfloat = jnp.float64 + raise pybamm.SolverError( + "Demoting expressions to 32-bit is required for MLIR conversion" + " at this time" ) - rootfn = idaklu.generate_function(rootfn.serialize()) - mass_action = idaklu.generate_function(mass_action.serialize()) - sensfn = idaklu.generate_function(sensfn.serialize()) - elif ( - model.convert_to_format == "jax" - and self._options["jax_evaluator"] == "iree" - ): - # Convert Jax functions to MLIR (also, demote to single precision) - idaklu_solver_fcn = idaklu.create_iree_solver - pybamm.demote_expressions_to_32bit = True - if pybamm.demote_expressions_to_32bit: - warnings.warn( - "Demoting expressions to 32-bit for MLIR conversion", - stacklevel=2, - ) - jnpfloat = jnp.float32 - else: # pragma: no cover - jnpfloat = jnp.float64 - raise pybamm.SolverError( - "Demoting expressions to 32-bit is required for MLIR conversion" - " at this time" - ) - # input arguments (used for lowering) - t_eval = self._demote_64_to_32(jnp.array([0.0], dtype=jnpfloat)) - y0 = self._demote_64_to_32(model.y0) - inputs0 = self._demote_64_to_32(inputs_to_dict(inputs)) - cj = self._demote_64_to_32(jnp.array([1.0], dtype=jnpfloat)) # array - v0 = jnp.zeros(model.len_rhs_and_alg, jnpfloat) - mass_matrix = model.mass_matrix.entries.toarray() - mass_matrix_demoted = self._demote_64_to_32(mass_matrix) - - # rhs_algebraic - rhs_algebraic_demoted = model.rhs_algebraic_eval - rhs_algebraic_demoted._demote_constants() - - def fcn_rhs_algebraic(t, y, inputs): - # function wraps an expression tree (and names MLIR module) - return rhs_algebraic_demoted(t, y, inputs) - - rhs_algebraic = self._make_iree_function( - fcn_rhs_algebraic, t_eval, y0, inputs0 - ) + # input arguments (used for lowering) + t_eval = self._demote_64_to_32(jnp.array([0.0], dtype=jnpfloat)) + y0 = self._demote_64_to_32(model.y0) + inputs0 = self._demote_64_to_32(inputs_to_dict(inputs)) + cj = self._demote_64_to_32(jnp.array([1.0], dtype=jnpfloat)) # array + v0 = jnp.zeros(model.len_rhs_and_alg, jnpfloat) + mass_matrix = model.mass_matrix.entries.toarray() + mass_matrix_demoted = self._demote_64_to_32(mass_matrix) - # jac_times_cjmass - jac_rhs_algebraic_demoted = rhs_algebraic_demoted.get_jacobian() + # rhs_algebraic + rhs_algebraic_demoted = model.rhs_algebraic_eval + rhs_algebraic_demoted._demote_constants() - def fcn_jac_times_cjmass(t, y, p, cj): - return jac_rhs_algebraic_demoted(t, y, p) - cj * mass_matrix_demoted + def fcn_rhs_algebraic(t, y, inputs): + # function wraps an expression tree (and names MLIR module) + return rhs_algebraic_demoted(t, y, inputs) - sparse_eval = sparse.csc_matrix( - fcn_jac_times_cjmass(t_eval, y0, inputs0, cj) - ) - jac_times_cjmass_nnz = sparse_eval.nnz - jac_times_cjmass_colptrs = sparse_eval.indptr - jac_times_cjmass_rowvals = sparse_eval.indices - jac_bw_lower, jac_bw_upper = bandwidth( - sparse_eval.todense() - ) # potentially slow - if jac_bw_upper <= 1: - jac_bw_upper = jac_bw_lower - 1 - if jac_bw_lower <= 1: - jac_bw_lower = jac_bw_upper + 1 - coo = sparse_eval.tocoo() # convert to COOrdinate format for indexing - - def fcn_jac_times_cjmass_sparse(t, y, p, cj): - return fcn_jac_times_cjmass(t, y, p, cj)[coo.row, coo.col] - - jac_times_cjmass = self._make_iree_function( - fcn_jac_times_cjmass_sparse, t_eval, y0, inputs0, cj - ) + rhs_algebraic = self._make_iree_function( + fcn_rhs_algebraic, t_eval, y0, inputs0 + ) - # Mass action - def fcn_mass_action(v): - return mass_matrix_demoted @ v + # jac_times_cjmass + jac_rhs_algebraic_demoted = rhs_algebraic_demoted.get_jacobian() - mass_action_demoted = self._demote_64_to_32(fcn_mass_action) - mass_action = self._make_iree_function(mass_action_demoted, v0) + def fcn_jac_times_cjmass(t, y, p, cj): + return jac_rhs_algebraic_demoted(t, y, p) - cj * mass_matrix_demoted - # rootfn - for ix, _ in enumerate(model.terminate_events_eval): - model.terminate_events_eval[ix]._demote_constants() + sparse_eval = sparse.csc_matrix( + fcn_jac_times_cjmass(t_eval, y0, inputs0, cj) + ) + jac_times_cjmass_nnz = sparse_eval.nnz + jac_times_cjmass_colptrs = sparse_eval.indptr + jac_times_cjmass_rowvals = sparse_eval.indices + jac_bw_lower, jac_bw_upper = bandwidth( + sparse_eval.todense() + ) # potentially slow + if jac_bw_upper <= 1: + jac_bw_upper = jac_bw_lower - 1 + if jac_bw_lower <= 1: + jac_bw_lower = jac_bw_upper + 1 + coo = sparse_eval.tocoo() # convert to COOrdinate format for indexing + + def fcn_jac_times_cjmass_sparse(t, y, p, cj): + return fcn_jac_times_cjmass(t, y, p, cj)[coo.row, coo.col] + + jac_times_cjmass = self._make_iree_function( + fcn_jac_times_cjmass_sparse, t_eval, y0, inputs0, cj + ) - def fcn_rootfn(t, y, inputs): - return jnp.array( - [event(t, y, inputs) for event in model.terminate_events_eval], - dtype=jnpfloat, - ).reshape(-1) + # Mass action + def fcn_mass_action(v): + return mass_matrix_demoted @ v - def fcn_rootfn_demoted(t, y, inputs): - return self._demote_64_to_32(fcn_rootfn)(t, y, inputs) + mass_action_demoted = self._demote_64_to_32(fcn_mass_action) + mass_action = self._make_iree_function(mass_action_demoted, v0) - rootfn = self._make_iree_function( - fcn_rootfn_demoted, t_eval, y0, inputs0 - ) + # rootfn + for ix, _ in enumerate(model.terminate_events_eval): + model.terminate_events_eval[ix]._demote_constants() - # jac_rhs_algebraic_action - jac_rhs_algebraic_action_demoted = ( - rhs_algebraic_demoted.get_jacobian_action() - ) + def fcn_rootfn(t, y, inputs): + return jnp.array( + [event(t, y, inputs) for event in model.terminate_events_eval], + dtype=jnpfloat, + ).reshape(-1) - def fcn_jac_rhs_algebraic_action( - t, y, p, v - ): # sundials calls (t, y, inputs, v) - return jac_rhs_algebraic_action_demoted( - t, y, v, p - ) # jvp calls (t, y, v, inputs) + def fcn_rootfn_demoted(t, y, inputs): + return self._demote_64_to_32(fcn_rootfn)(t, y, inputs) - jac_rhs_algebraic_action = self._make_iree_function( - fcn_jac_rhs_algebraic_action, t_eval, y0, inputs0, v0 - ) + rootfn = self._make_iree_function(fcn_rootfn_demoted, t_eval, y0, inputs0) - # sensfn - if model.jacp_rhs_algebraic_eval is None: - sensfn = idaklu.IREEBaseFunctionType() # empty equation - else: - sensfn_demoted = rhs_algebraic_demoted.get_sensitivities() + # jac_rhs_algebraic_action + jac_rhs_algebraic_action_demoted = ( + rhs_algebraic_demoted.get_jacobian_action() + ) - def fcn_sensfn(t, y, p): - return sensfn_demoted(t, y, p) + def fcn_jac_rhs_algebraic_action( + t, y, p, v + ): # sundials calls (t, y, inputs, v) + return jac_rhs_algebraic_action_demoted( + t, y, v, p + ) # jvp calls (t, y, v, inputs) - sensfn = self._make_iree_function( - fcn_sensfn, t_eval, jnp.zeros_like(y0), inputs0 - ) + jac_rhs_algebraic_action = self._make_iree_function( + fcn_jac_rhs_algebraic_action, t_eval, y0, inputs0, v0 + ) - # output_variables - self.var_idaklu_fcns = [] - self.dvar_dy_idaklu_fcns = [] - self.dvar_dp_idaklu_fcns = [] - for key in self.output_variables: - fcn = self.computed_var_fcns[key] - fcn._demote_constants() - self.var_idaklu_fcns.append( + # sensfn + if model.jacp_rhs_algebraic_eval is None: + sensfn = idaklu.IREEBaseFunctionType() # empty equation + else: + sensfn_demoted = rhs_algebraic_demoted.get_sensitivities() + + def fcn_sensfn(t, y, p): + return sensfn_demoted(t, y, p) + + sensfn = self._make_iree_function( + fcn_sensfn, t_eval, jnp.zeros_like(y0), inputs0 + ) + + # output_variables + self.var_idaklu_fcns = [] + self.dvar_dy_idaklu_fcns = [] + self.dvar_dp_idaklu_fcns = [] + for key in self.output_variables: + fcn = self.computed_var_fcns[key] + fcn._demote_constants() + self.var_idaklu_fcns.append( + self._make_iree_function( + lambda t, y, p: fcn(t, y, p), # noqa: B023 + t_eval, + y0, + inputs0, + ) + ) + # Convert derivative functions for sensitivities + if (len(inputs) > 0) and (model.calculate_sensitivities): + dvar_dy = fcn.get_jacobian() + self.dvar_dy_idaklu_fcns.append( self._make_iree_function( - lambda t, y, p: fcn(t, y, p), # noqa: B023 + lambda t, y, p: dvar_dy(t, y, p), # noqa: B023 t_eval, y0, inputs0, + sparse_index=True, ) ) - # Convert derivative functions for sensitivities - if (len(inputs) > 0) and (model.calculate_sensitivities): - dvar_dy = fcn.get_jacobian() - self.dvar_dy_idaklu_fcns.append( - self._make_iree_function( - lambda t, y, p: dvar_dy(t, y, p), # noqa: B023 - t_eval, - y0, - inputs0, - sparse_index=True, - ) - ) - dvar_dp = fcn.get_sensitivities() - self.dvar_dp_idaklu_fcns.append( - self._make_iree_function( - lambda t, y, p: dvar_dp(t, y, p), # noqa: B023 - t_eval, - y0, - inputs0, - ) + dvar_dp = fcn.get_sensitivities() + self.dvar_dp_idaklu_fcns.append( + self._make_iree_function( + lambda t, y, p: dvar_dp(t, y, p), # noqa: B023 + t_eval, + y0, + inputs0, ) + ) - # Identify IREE library - iree_lib_path = os.path.join(iree.compiler.__path__[0], "_mlir_libs") - os.environ["IREE_COMPILER_LIB"] = os.path.join( - iree_lib_path, - next(f for f in os.listdir(iree_lib_path) if "IREECompiler" in f), - ) + # Identify IREE library + iree_lib_path = os.path.join(iree.compiler.__path__[0], "_mlir_libs") + os.environ["IREE_COMPILER_LIB"] = os.path.join( + iree_lib_path, + next(f for f in os.listdir(iree_lib_path) if "IREECompiler" in f), + ) - pybamm.demote_expressions_to_32bit = False - else: # pragma: no cover - raise pybamm.SolverError( - "Unsupported evaluation engine for convert_to_format='jax'" - ) + pybamm.demote_expressions_to_32bit = False + else: # pragma: no cover + raise pybamm.SolverError( + "Unsupported evaluation engine for convert_to_format='jax'" + ) - self._setup = { - "solver_function": idaklu_solver_fcn, # callable - "jac_bandwidth_upper": jac_bw_upper, # int - "jac_bandwidth_lower": jac_bw_lower, # int - "rhs_algebraic": rhs_algebraic, # function - "jac_times_cjmass": jac_times_cjmass, # function - "jac_times_cjmass_colptrs": jac_times_cjmass_colptrs, # array - "jac_times_cjmass_rowvals": jac_times_cjmass_rowvals, # array - "jac_times_cjmass_nnz": jac_times_cjmass_nnz, # int - "jac_rhs_algebraic_action": jac_rhs_algebraic_action, # function - "mass_action": mass_action, # function - "sensfn": sensfn, # function - "rootfn": rootfn, # function - "num_of_events": num_of_events, # int - "ids": ids, # array - "sensitivity_names": sensitivity_names, - "number_of_sensitivity_parameters": number_of_sensitivity_parameters, - "output_variables": self.output_variables, - "var_fcns": self.computed_var_fcns, - "var_idaklu_fcns": self.var_idaklu_fcns, - "dvar_dy_idaklu_fcns": self.dvar_dy_idaklu_fcns, - "dvar_dp_idaklu_fcns": self.dvar_dp_idaklu_fcns, - } + self._setup = { + "solver_function": idaklu_solver_fcn, # callable + "jac_bandwidth_upper": jac_bw_upper, # int + "jac_bandwidth_lower": jac_bw_lower, # int + "rhs_algebraic": rhs_algebraic, # function + "jac_times_cjmass": jac_times_cjmass, # function + "jac_times_cjmass_colptrs": jac_times_cjmass_colptrs, # array + "jac_times_cjmass_rowvals": jac_times_cjmass_rowvals, # array + "jac_times_cjmass_nnz": jac_times_cjmass_nnz, # int + "jac_rhs_algebraic_action": jac_rhs_algebraic_action, # function + "mass_action": mass_action, # function + "sensfn": sensfn, # function + "rootfn": rootfn, # function + "num_of_events": num_of_events, # int + "ids": ids, # array + "sensitivity_names": sensitivity_names, + "number_of_sensitivity_parameters": number_of_sensitivity_parameters, + "output_variables": self.output_variables, + "var_fcns": self.computed_var_fcns, + "var_idaklu_fcns": self.var_idaklu_fcns, + "dvar_dy_idaklu_fcns": self.dvar_dy_idaklu_fcns, + "dvar_dp_idaklu_fcns": self.dvar_dp_idaklu_fcns, + } - solver = self._setup["solver_function"]( - number_of_states=len(y0), - number_of_parameters=self._setup["number_of_sensitivity_parameters"], - rhs_alg=self._setup["rhs_algebraic"], - jac_times_cjmass=self._setup["jac_times_cjmass"], - jac_times_cjmass_colptrs=self._setup["jac_times_cjmass_colptrs"], - jac_times_cjmass_rowvals=self._setup["jac_times_cjmass_rowvals"], - jac_times_cjmass_nnz=self._setup["jac_times_cjmass_nnz"], - jac_bandwidth_lower=jac_bw_lower, - jac_bandwidth_upper=jac_bw_upper, - jac_action=self._setup["jac_rhs_algebraic_action"], - mass_action=self._setup["mass_action"], - sens=self._setup["sensfn"], - events=self._setup["rootfn"], - number_of_events=self._setup["num_of_events"], - rhs_alg_id=self._setup["ids"], - atol=atol, - rtol=rtol, - inputs=len(inputs), - var_fcns=self._setup["var_idaklu_fcns"], - dvar_dy_fcns=self._setup["dvar_dy_idaklu_fcns"], - dvar_dp_fcns=self._setup["dvar_dp_idaklu_fcns"], - options=self._options, - ) + solver = self._setup["solver_function"]( + number_of_states=len(y0), + number_of_parameters=self._setup["number_of_sensitivity_parameters"], + rhs_alg=self._setup["rhs_algebraic"], + jac_times_cjmass=self._setup["jac_times_cjmass"], + jac_times_cjmass_colptrs=self._setup["jac_times_cjmass_colptrs"], + jac_times_cjmass_rowvals=self._setup["jac_times_cjmass_rowvals"], + jac_times_cjmass_nnz=self._setup["jac_times_cjmass_nnz"], + jac_bandwidth_lower=jac_bw_lower, + jac_bandwidth_upper=jac_bw_upper, + jac_action=self._setup["jac_rhs_algebraic_action"], + mass_action=self._setup["mass_action"], + sens=self._setup["sensfn"], + events=self._setup["rootfn"], + number_of_events=self._setup["num_of_events"], + rhs_alg_id=self._setup["ids"], + atol=atol, + rtol=rtol, + inputs=len(inputs), + var_fcns=self._setup["var_idaklu_fcns"], + dvar_dy_fcns=self._setup["dvar_dy_idaklu_fcns"], + dvar_dp_fcns=self._setup["dvar_dp_idaklu_fcns"], + options=self._options, + ) - self._setup["solver"] = solver - else: - self._setup = { - "resfn": resfn, - "jac_class": jac_class, - "sensfn": sensfn, - "rootfn": rootfn, - "num_of_events": num_of_events, - "use_jac": 1, - "ids": ids, - "sensitivity_names": sensitivity_names, - "number_of_sensitivity_parameters": number_of_sensitivity_parameters, - } + self._setup["solver"] = solver return base_set_up_return @@ -859,7 +755,6 @@ def _integrate(self, model, t_eval, inputs_dict=None, t_interp=None): atol = getattr(model, "atol", self.atol) atol = self._check_atol_type(atol, y0full.size) - rtol = self.rtol timer = pybamm.Timer() if model.convert_to_format == "casadi" or ( @@ -873,27 +768,9 @@ def _integrate(self, model, t_eval, inputs_dict=None, t_interp=None): ydot0full, inputs, ) - else: - sol = idaklu.solve_python( - t_eval, - y0full, - ydot0full, - self._setup["resfn"], - self._setup["jac_class"].jac_res, - self._setup["sensfn"], - self._setup["jac_class"].get_jac_data, - self._setup["jac_class"].get_jac_row_vals, - self._setup["jac_class"].get_jac_col_ptrs, - self._setup["jac_class"].nnz, - self._setup["rootfn"], - self._setup["num_of_events"], - self._setup["use_jac"], - self._setup["ids"], - atol, - rtol, - inputs, - self._setup["number_of_sensitivity_parameters"], - ) + else: # pragma: no cover + # Shouldn't ever reach this point + raise pybamm.SolverError("Unsupported IDAKLU solver configuration.") integration_time = timer.time() number_of_sensitivity_parameters = self._setup[ diff --git a/tests/unit/test_solvers/test_base_solver.py b/tests/unit/test_solvers/test_base_solver.py index a35b864a64..e86b0f702e 100644 --- a/tests/unit/test_solvers/test_base_solver.py +++ b/tests/unit/test_solvers/test_base_solver.py @@ -114,7 +114,7 @@ def test_block_symbolic_inputs(self): ): solver.solve(model, np.array([1, 2, 3])) - def testode_solver_fail_with_dae(self): + def test_ode_solver_fail_with_dae(self): model = pybamm.BaseModel() a = pybamm.Scalar(1) model.algebraic = {a: a} @@ -364,49 +364,41 @@ def exact_diff_a(y, a, b): def exact_diff_b(y, a, b): return np.array([[y[0]], [0]]) - for convert_to_format in ["", "python", "casadi", "jax"]: - model = pybamm.BaseModel() - v = pybamm.Variable("v") - u = pybamm.Variable("u") - a = pybamm.InputParameter("a") - b = pybamm.InputParameter("b") - model.rhs = {v: a * v**2 + b * v + a**2} - model.algebraic = {u: a * v - u} - model.initial_conditions = {v: 1, u: a * 1} - model.convert_to_format = convert_to_format - solver = pybamm.IDAKLUSolver(root_method="lm") - model.calculate_sensitivities = ["a", "b"] - solver.set_up(model, inputs={"a": 0, "b": 0}) - all_inputs = [] - for v_value in [0.1, -0.2, 1.5, 8.4]: - for u_value in [0.13, -0.23, 1.3, 13.4]: - for a_value in [0.12, 1.5]: - for b_value in [0.82, 1.9]: - y = np.array([v_value, u_value]) - t = 0 - inputs = {"a": a_value, "b": b_value} - all_inputs.append((t, y, inputs)) - for t, y, inputs in all_inputs: - if model.convert_to_format == "casadi": - use_inputs = casadi.vertcat(*[x for x in inputs.values()]) - else: - use_inputs = inputs - - sens = model.jacp_rhs_algebraic_eval(t, y, use_inputs) - - if convert_to_format == "casadi": - sens_a = sens[0] - sens_b = sens[1] - else: - sens_a = sens["a"] - sens_b = sens["b"] - - np.testing.assert_allclose( - sens_a, exact_diff_a(y, inputs["a"], inputs["b"]) - ) - np.testing.assert_allclose( - sens_b, exact_diff_b(y, inputs["a"], inputs["b"]) - ) + model = pybamm.BaseModel() + v = pybamm.Variable("v") + u = pybamm.Variable("u") + a = pybamm.InputParameter("a") + b = pybamm.InputParameter("b") + model.rhs = {v: a * v**2 + b * v + a**2} + model.algebraic = {u: a * v - u} + model.initial_conditions = {v: 1, u: a * 1} + model.convert_to_format = "casadi" + solver = pybamm.IDAKLUSolver(root_method="lm") + model.calculate_sensitivities = ["a", "b"] + solver.set_up(model, inputs={"a": 0, "b": 0}) + all_inputs = [] + for v_value in [0.1, -0.2, 1.5, 8.4]: + for u_value in [0.13, -0.23, 1.3, 13.4]: + for a_value in [0.12, 1.5]: + for b_value in [0.82, 1.9]: + y = np.array([v_value, u_value]) + t = 0 + inputs = {"a": a_value, "b": b_value} + all_inputs.append((t, y, inputs)) + for t, y, inputs in all_inputs: + use_inputs = casadi.vertcat(*[x for x in inputs.values()]) + + sens = model.jacp_rhs_algebraic_eval(t, y, use_inputs) + + sens_a = sens[0] + sens_b = sens[1] + + np.testing.assert_allclose( + sens_a, exact_diff_a(y, inputs["a"], inputs["b"]) + ) + np.testing.assert_allclose( + sens_b, exact_diff_b(y, inputs["a"], inputs["b"]) + ) if __name__ == "__main__": diff --git a/tests/unit/test_solvers/test_idaklu_solver.py b/tests/unit/test_solvers/test_idaklu_solver.py index 5bc845d66c..67e68e9c6a 100644 --- a/tests/unit/test_solvers/test_idaklu_solver.py +++ b/tests/unit/test_solvers/test_idaklu_solver.py @@ -19,7 +19,7 @@ def test_ida_roberts_klu(self): # this test implements a python version of the ida Roberts # example provided in sundials # see sundials ida examples pdf - for form in ["python", "casadi", "jax", "iree"]: + for form in ["casadi", "iree"]: if (form == "jax" or form == "iree") and not pybamm.have_jax(): continue if (form == "iree") and not pybamm.have_iree(): @@ -65,7 +65,7 @@ def test_ida_roberts_klu(self): np.testing.assert_array_almost_equal(solution.y[0, :], true_solution) def test_model_events(self): - for form in ["python", "casadi", "jax", "iree"]: + for form in ["casadi", "iree"]: if (form == "jax" or form == "iree") and not pybamm.have_jax(): continue if (form == "iree") and not pybamm.have_iree(): @@ -195,7 +195,7 @@ def test_model_events(self): def test_input_params(self): # test a mix of scalar and vector input params - for form in ["python", "casadi", "jax", "iree"]: + for form in ["casadi", "iree"]: if (form == "jax" or form == "iree") and not pybamm.have_jax(): continue if (form == "iree") and not pybamm.have_iree(): @@ -306,7 +306,7 @@ def test_ida_roberts_klu_sensitivities(self): # this test implements a python version of the ida Roberts # example provided in sundials # see sundials ida examples pdf - for form in ["python", "casadi", "jax", "iree"]: + for form in ["casadi", "iree"]: if (form == "jax" or form == "iree") and not pybamm.have_jax(): continue if (form == "iree") and not pybamm.have_iree(): @@ -414,7 +414,7 @@ def test_ida_roberts_consistent_initialization(self): # this test implements a python version of the ida Roberts # example provided in sundials # see sundials ida examples pdf - for form in ["python", "casadi", "jax", "iree"]: + for form in ["casadi", "iree"]: if (form == "jax" or form == "iree") and not pybamm.have_jax(): continue if (form == "iree") and not pybamm.have_iree(): @@ -458,7 +458,7 @@ def test_sensitivities_with_events(self): # this test implements a python version of the ida Roberts # example provided in sundials # see sundials ida examples pdf - for form in ["casadi", "python", "jax", "iree"]: + for form in ["casadi", "iree"]: if (form == "jax" or form == "iree") and not pybamm.have_jax(): continue if (form == "iree") and not pybamm.have_iree(): @@ -619,7 +619,7 @@ def test_failures(self): solver.solve(model, t_eval) def test_dae_solver_algebraic_model(self): - for form in ["python", "casadi", "jax", "iree"]: + for form in ["casadi", "iree"]: if (form == "jax" or form == "iree") and not pybamm.have_jax(): continue if (form == "iree") and not pybamm.have_iree(): @@ -1097,6 +1097,46 @@ def test_interpolate_time_step_start_offset(self): sol.sub_solutions[1].t[0], ) + def test_python_idaklu_deprecation_errors(self): + for form in ["python", "", "jax"]: + if form == "jax" and not pybamm.have_jax(): + continue + + model = pybamm.BaseModel() + model.convert_to_format = form + u = pybamm.Variable("u") + v = pybamm.Variable("v") + model.rhs = {u: 0.1 * v} + model.algebraic = {v: 1 - v} + model.initial_conditions = {u: 0, v: 1} + model.events = [pybamm.Event("1", 0.2 - u), pybamm.Event("2", v)] + + disc = pybamm.Discretisation() + disc.process_model(model) + + t_eval = np.linspace(0, 3, 100) + + solver = pybamm.IDAKLUSolver( + root_method="lm", + ) + + if form == "python": + with self.assertRaisesRegex( + pybamm.SolverError, + "Unsupported option for convert_to_format=python", + ): + with self.assertWarnsRegex( + DeprecationWarning, + "The python-idaklu solver has been deprecated.", + ): + _ = solver.solve(model, t_eval) + elif form == "jax": + with self.assertRaisesRegex( + pybamm.SolverError, + "Unsupported evaluation engine for convert_to_format=jax", + ): + _ = solver.solve(model, t_eval) + if __name__ == "__main__": print("Add -v for more debug output") From 265dcd649fad4f6d8fbfddcfbcd374b974b74b85 Mon Sep 17 00:00:00 2001 From: "Eric G. Kratz" Date: Tue, 27 Aug 2024 16:38:25 -0400 Subject: [PATCH 72/82] Remove install_jax and rename have_jax (#4362) * Rename have_jax to has_jax * Remove some extra stuff * Update changelog * style: pre-commit fixes * Update changelog * Remove has_jax * Fix typo --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- CHANGELOG.md | 1 + docs/source/api/util.rst | 2 - .../user_guide/installation/gnu-linux-mac.rst | 2 +- .../user_guide/installation/windows.rst | 2 +- pyproject.toml | 3 - src/pybamm/__init__.py | 1 - src/pybamm/util.py | 57 ------------------- 7 files changed, 3 insertions(+), 65 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82be87f95f..c1e8a720e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ ## Breaking changes +- Remove deprecated function `pybamm_install_jax` ([#4362](https://github.com/pybamm-team/PyBaMM/pull/4362)) - Removed legacy python-IDAKLU solver. ([#4326](https://github.com/pybamm-team/PyBaMM/pull/4326)) # [v24.5](https://github.com/pybamm-team/PyBaMM/tree/v24.5) - 2024-07-26 diff --git a/docs/source/api/util.rst b/docs/source/api/util.rst index f187cfbabb..7496b59554 100644 --- a/docs/source/api/util.rst +++ b/docs/source/api/util.rst @@ -16,8 +16,6 @@ Utility functions .. autofunction:: pybamm.load -.. autofunction:: pybamm.install_jax - .. autofunction:: pybamm.have_jax .. autofunction:: pybamm.is_jax_compatible diff --git a/docs/source/user_guide/installation/gnu-linux-mac.rst b/docs/source/user_guide/installation/gnu-linux-mac.rst index 7e69afa839..97171b53b7 100644 --- a/docs/source/user_guide/installation/gnu-linux-mac.rst +++ b/docs/source/user_guide/installation/gnu-linux-mac.rst @@ -99,7 +99,7 @@ Users can install ``jax`` and ``jaxlib`` to use the Jax solver. pip install "pybamm[jax]" -The ``pip install "pybamm[jax]"`` command automatically downloads and installs ``pybamm`` and the compatible versions of ``jax`` and ``jaxlib`` on your system. (``pybamm_install_jax`` is deprecated.) +The ``pip install "pybamm[jax]"`` command automatically downloads and installs ``pybamm`` and the compatible versions of ``jax`` and ``jaxlib`` on your system. .. _optional-iree-mlir-support: diff --git a/docs/source/user_guide/installation/windows.rst b/docs/source/user_guide/installation/windows.rst index 02d9f8dd29..44dc79a7d3 100644 --- a/docs/source/user_guide/installation/windows.rst +++ b/docs/source/user_guide/installation/windows.rst @@ -75,7 +75,7 @@ Users can install ``jax`` and ``jaxlib`` to use the Jax solver. pip install "pybamm[jax]" -The ``pip install "pybamm[jax]"`` command automatically downloads and installs ``pybamm`` and the compatible versions of ``jax`` and ``jaxlib`` on your system. (``pybamm_install_jax`` is deprecated.) +The ``pip install "pybamm[jax]"`` command automatically downloads and installs ``pybamm`` and the compatible versions of ``jax`` and ``jaxlib`` on your system. Uninstall PyBaMM ---------------- diff --git a/pyproject.toml b/pyproject.toml index 7ab3d5f573..e4cce3eccd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -134,9 +134,6 @@ all = [ "pybamm[examples,plot,cite,bpx,tqdm]", ] -[project.scripts] -pybamm_install_jax = "pybamm.util:install_jax" - [project.entry-points."pybamm_parameter_sets"] Sulzer2019 = "pybamm.input.parameters.lead_acid.Sulzer2019:get_parameter_values" Ai2020 = "pybamm.input.parameters.lithium_ion.Ai2020:get_parameter_values" diff --git a/src/pybamm/__init__.py b/src/pybamm/__init__.py index a371fdbc03..75f5f4f160 100644 --- a/src/pybamm/__init__.py +++ b/src/pybamm/__init__.py @@ -16,7 +16,6 @@ from .util import ( get_parameters_filepath, have_jax, - install_jax, import_optional_dependency, is_jax_compatible, get_git_commit_info, diff --git a/src/pybamm/util.py b/src/pybamm/util.py index ee1431ecb1..fd94eb88f4 100644 --- a/src/pybamm/util.py +++ b/src/pybamm/util.py @@ -4,7 +4,6 @@ # The code in this file is adapted from Pints # (see https://github.com/pints-team/pints) # -import argparse import importlib.util import importlib.metadata import numbers @@ -12,9 +11,7 @@ import pathlib import pickle import subprocess -import sys import timeit -from platform import system import difflib from warnings import warn @@ -314,60 +311,6 @@ def is_constant_and_can_evaluate(symbol): return False -def install_jax(arguments=None): # pragma: no cover - """ - Install compatible versions of jax, jaxlib. - - Command Line Interface:: - - $ pybamm_install_jax - - | optional arguments: - | -h, --help show help message - | -f, --force force install compatible versions of jax and jaxlib - """ - parser = argparse.ArgumentParser(description="Install jax and jaxlib") - parser.add_argument( - "-f", - "--force", - action="store_true", - help="force install compatible versions of" - f" jax ({JAX_VERSION}) and jaxlib ({JAXLIB_VERSION})", - ) - - args = parser.parse_args(arguments) - - if system() == "Windows": - raise NotImplementedError("Jax is not available on Windows") - - # Raise an error if jax and jaxlib are already installed, but incompatible - # and --force is not set - elif importlib.util.find_spec("jax") is not None: - if not args.force and not is_jax_compatible(): - raise ValueError( - "Jax is already installed but the installed version of jax or jaxlib is" - " not supported by PyBaMM. \nYou can force install compatible versions" - f" of jax ({JAX_VERSION}) and jaxlib ({JAXLIB_VERSION}) using the" - " following command: \npybamm_install_jax --force" - ) - - msg = ( - "pybamm_install_jax is deprecated," - " use 'pip install pybamm[jax]' to install jax & jaxlib" - ) - warn(msg, DeprecationWarning, stacklevel=2) - subprocess.check_call( - [ - sys.executable, - "-m", - "pip", - "install", - f"jax>={JAX_VERSION}", - f"jaxlib>={JAXLIB_VERSION}", - ] - ) - - # https://docs.pybamm.org/en/latest/source/user_guide/contributing.html#managing-optional-dependencies-and-their-imports def import_optional_dependency(module_name, attribute=None): err_msg = f"Optional dependency {module_name} is not available. See https://docs.pybamm.org/en/latest/source/user_guide/installation/index.html#optional-dependencies for more details." From 358637745d39e95b583fec84a9b653241c0f1c99 Mon Sep 17 00:00:00 2001 From: "Eric G. Kratz" Date: Tue, 27 Aug 2024 16:53:48 -0400 Subject: [PATCH 73/82] Remove pybtex warning when importing PyBaMM (#4383) * Update error handling * Minor updates * Update src/pybamm/citations.py * Update citations.py * Update tests --- src/pybamm/citations.py | 125 +++++++++++++++-------------------- tests/unit/test_callbacks.py | 3 - tests/unit/test_citations.py | 12 +++- 3 files changed, 64 insertions(+), 76 deletions(-) diff --git a/src/pybamm/citations.py b/src/pybamm/citations.py index 9f036b9bfd..80261550a6 100644 --- a/src/pybamm/citations.py +++ b/src/pybamm/citations.py @@ -1,8 +1,3 @@ -# -# Bibliographical information for PyBaMM -# Inspired by firedrake/PETSc citation workflow -# https://firedrakeproject.org/citing.html -# import pybamm import os import warnings @@ -25,29 +20,32 @@ class Citations: >>> pybamm.print_citations("citations.txt") """ - def __init__(self): - # Set of citation keys that have been registered - self._papers_to_cite = set() + _module_import_error = False + # Set of citation keys that have been registered + _papers_to_cite: set + # Set of unknown citations to parse with pybtex + _unknown_citations: set + # Dict mapping citation tags for use when registering citations + _citation_tags: dict + def __init__(self): + self._check_for_bibtex() # Dict mapping citations keys to BibTex entries self._all_citations: dict[str, str] = dict() - # Set of unknown citations to parse with pybtex - self._unknown_citations = set() - - # Dict mapping citation tags for use when registering citations - self._citation_tags = dict() - self.read_citations() self._reset() + def _check_for_bibtex(self): + try: + import_optional_dependency("pybtex") + except ModuleNotFoundError: + self._module_import_error = True + def _reset(self): """Reset citations to default only (only for testing purposes)""" - # Initialize empty papers to cite self._papers_to_cite = set() - # Initialize empty set of unknown citations self._unknown_citations = set() - # Initialize empty citation tags self._citation_tags = dict() # Register the PyBaMM paper and the NumPy paper self.register("Sulzer2021") @@ -66,24 +64,18 @@ def read_citations(self): """Reads the citations in `pybamm.CITATIONS.bib`. Other works can be cited by passing a BibTeX citation to :meth:`register`. """ - try: + if not self._module_import_error: parse_file = import_optional_dependency("pybtex.database", "parse_file") citations_file = os.path.join(pybamm.__path__[0], "CITATIONS.bib") bib_data = parse_file(citations_file, bib_format="bibtex") for key, entry in bib_data.entries.items(): self._add_citation(key, entry) - except ModuleNotFoundError: # pragma: no cover - pybamm.logger.warning( - "Citations could not be read because the 'pybtex' library is not installed. " - "Install 'pybamm[cite]' to enable citation reading." - ) def _add_citation(self, key, entry): """Adds `entry` to `self._all_citations` under `key`, warning the user if a previous entry is overwritten """ - - try: + if not self._module_import_error: Entry = import_optional_dependency("pybtex.database", "Entry") # Check input types are correct if not isinstance(key, str) or not isinstance(entry, Entry): @@ -96,11 +88,6 @@ def _add_citation(self, key, entry): # Add to database self._all_citations[key] = new_citation - except ModuleNotFoundError: # pragma: no cover - pybamm.logger.warning( - f"Could not add citation for '{key}' because the 'pybtex' library is not installed. " - "Install 'pybamm[cite]' to enable adding citations." - ) def _add_citation_tag(self, key, entry): """Adds a tag for a citation key in the dict, which represents the name of the @@ -154,7 +141,7 @@ def _parse_citation(self, key): key: str A BibTeX formatted citation """ - try: + if not self._module_import_error: PybtexError = import_optional_dependency("pybtex.scanner", "PybtexError") parse_string = import_optional_dependency("pybtex.database", "parse_string") try: @@ -165,21 +152,13 @@ def _parse_citation(self, key): # Add and register all citations for key, entry in bib_data.entries.items(): - # Add to _all_citations dictionary self._add_citation(key, entry) - # Add to _papers_to_cite set self._papers_to_cite.add(key) - return + return except PybtexError as error: - # Unable to parse / unknown key raise KeyError( f"Not a bibtex citation or known citation: {key}" ) from error - except ModuleNotFoundError: # pragma: no cover - pybamm.logger.warning( - f"Could not parse citation for '{key}' because the 'pybtex' library is not installed. " - "Install 'pybamm[cite]' to enable citation parsing." - ) def _tag_citations(self): """Prints the citation tags for the citations that have been registered @@ -193,7 +172,7 @@ def _tag_citations(self): def print(self, filename=None, output_format="text", verbose=False): """Print all citations that were used for running simulations. The verbose option is provided to print tags for citations in the output such that it can - can be seen where the citations were registered due to the use of PyBaMM models + be seen where the citations were registered due to the use of PyBaMM models and solvers in the code. .. note:: @@ -230,7 +209,7 @@ def print(self, filename=None, output_format="text", verbose=False): """ # Parse citations that were not known keys at registration, but do not # fail if they cannot be parsed - try: + if not self._module_import_error: pybtex = import_optional_dependency("pybtex") try: for key in self._unknown_citations: @@ -244,26 +223,36 @@ def print(self, filename=None, output_format="text", verbose=False): # delete the invalid citation from the set self._unknown_citations.remove(key) - if output_format == "text": - citations = pybtex.format_from_strings( - self._cited, style="plain", output_backend="plaintext" - ) - elif output_format == "bibtex": - citations = "\n".join(self._cited) - else: - raise pybamm.OptionError( - f"Output format {output_format} not recognised." - "It should be 'text' or 'bibtex'." - ) + cite_list = self.format_citations(output_format, pybtex) + self.write_citations(cite_list, filename, verbose) + else: + self.print_import_warning() - if filename is None: - print(citations) - if verbose: - self._tag_citations() # pragma: no cover - else: - with open(filename, "w") as f: - f.write(citations) - except ModuleNotFoundError: # pragma: no cover + def write_citations(self, cite_list, filename, verbose): + if filename is None: + print(cite_list) + if verbose: + self._tag_citations() # pragma: no cover + else: + with open(filename, "w") as f: + f.write(cite_list) + + def format_citations(self, output_format, pybtex): + if output_format == "text": + cite_list = pybtex.format_from_strings( + self._cited, style="plain", output_backend="plaintext" + ) + elif output_format == "bibtex": + cite_list = "\n".join(self._cited) + else: + raise pybamm.OptionError( + f"Output format {output_format} not recognised." + "It should be 'text' or 'bibtex'." + ) + return cite_list + + def print_import_warning(self): + if self._module_import_error: pybamm.logger.warning( "Could not print citations because the 'pybtex' library is not installed. " "Please, install 'pybamm[cite]' to print citations." @@ -272,15 +261,11 @@ def print(self, filename=None, output_format="text", verbose=False): def print_citations(filename=None, output_format="text", verbose=False): """See :meth:`Citations.print`""" - if verbose: # pragma: no cover - if filename is not None: # pragma: no cover - raise Exception( - "Verbose output is available only for the terminal and not for printing to files", - ) - else: - citations.print(filename, output_format, verbose=True) - else: - pybamm.citations.print(filename, output_format) + if verbose and filename is not None: # pragma: no cover + raise Exception( + "Verbose output is available only for the terminal and not for printing to files", + ) + pybamm.citations.print(filename, output_format, verbose) citations = Citations() diff --git a/tests/unit/test_callbacks.py b/tests/unit/test_callbacks.py index 414b2ecebe..4a7558c842 100644 --- a/tests/unit/test_callbacks.py +++ b/tests/unit/test_callbacks.py @@ -1,6 +1,3 @@ -# -# Tests the citations class. -# import pytest import pybamm diff --git a/tests/unit/test_citations.py b/tests/unit/test_citations.py index 442bb7d50e..0928cc993c 100644 --- a/tests/unit/test_citations.py +++ b/tests/unit/test_citations.py @@ -1,6 +1,3 @@ -# -# Tests the citations class. -# import pytest import pybamm import os @@ -111,6 +108,15 @@ def test_input_validation(self): with pytest.raises(TypeError): pybamm.citations._add_citation(1001, Entry("misc")) + def test_pybtex_warning(self, caplog): + class CiteWithWarning(pybamm.Citations): + def __init__(self): + super().__init__() + self._module_import_error = True + + CiteWithWarning().print_import_warning() + assert "Could not print citations" in caplog.text + def test_andersson_2019(self): citations = pybamm.citations citations._reset() From ac6c45021a9560ac5b5b4053fb497ba7e476987d Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Tue, 27 Aug 2024 16:37:28 -0700 Subject: [PATCH 74/82] Surface temperature model (#4203) * add surface temperature equal to ambient temperature * #4022 surface model works * update temperature comparison script * #4022 update tests and example, rename parameters * fix plot_thermal_components * fix unit tests * #4022 fix lead acid thermal tests * #4022 fix example * coverage * fix test * style * Moving missed files * Rob's suggestion --------- Co-authored-by: Eric G. Kratz Co-authored-by: kratman --- .../scripts/compare_surface_temperature.py | 61 +++++++++++++++++++ src/pybamm/CITATIONS.bib | 10 +++ .../full_battery_models/base_battery_model.py | 30 +++++---- .../full_battery_models/lead_acid/full.py | 1 + .../full_battery_models/lead_acid/loqs.py | 1 + .../lithium_ion/base_lithium_ion_model.py | 1 + .../models/submodels/thermal/__init__.py | 3 +- .../models/submodels/thermal/isothermal.py | 4 +- src/pybamm/models/submodels/thermal/lumped.py | 10 +-- .../pouch_cell_1D_current_collectors.py | 16 ++--- .../pouch_cell_2D_current_collectors.py | 14 ++--- .../submodels/thermal/pouch_cell/x_full.py | 12 ++-- .../submodels/thermal/surface/__init__.py | 2 + .../submodels/thermal/surface/ambient.py | 33 ++++++++++ .../submodels/thermal/surface/lumped.py | 51 ++++++++++++++++ .../plotting/plot_thermal_components.py | 6 +- .../test_lithium_ion/test_thermal_models.py | 29 +++++++++ .../test_base_battery_model.py | 15 ++--- .../base_lithium_ion_tests.py | 4 ++ 19 files changed, 253 insertions(+), 50 deletions(-) create mode 100644 examples/scripts/compare_surface_temperature.py create mode 100644 src/pybamm/models/submodels/thermal/surface/__init__.py create mode 100644 src/pybamm/models/submodels/thermal/surface/ambient.py create mode 100644 src/pybamm/models/submodels/thermal/surface/lumped.py diff --git a/examples/scripts/compare_surface_temperature.py b/examples/scripts/compare_surface_temperature.py new file mode 100644 index 0000000000..24ccfc5dfc --- /dev/null +++ b/examples/scripts/compare_surface_temperature.py @@ -0,0 +1,61 @@ +# +# Compare lithium-ion battery models +# +import pybamm + +pybamm.set_logging_level("INFO") + +# load models +models = [ + pybamm.lithium_ion.SPMe( + {"thermal": "lumped", "surface temperature": "ambient"}, + name="ambient surface temperature", + ), + pybamm.lithium_ion.SPMe( + {"thermal": "lumped", "surface temperature": "lumped"}, + name="lumped surface temperature", + ), +] + +experiment = pybamm.Experiment( + [ + "Discharge at 1C until 2.5V", + "Rest for 1 hour", + ] +) + +parameter_values = pybamm.ParameterValues("Chen2020") +parameter_values.update( + { + "Casing heat capacity [J.K-1]": 30, + "Environment thermal resistance [K.W-1]": 10, + }, + check_already_exists=False, +) + +# create and run simulations +sols = [] +for model in models: + model.variables["Bulk temperature [°C]"] = ( + model.variables["Volume-averaged cell temperature [K]"] - 273.15 + ) + model.variables["Surface temperature [°C]"] = ( + model.variables["Surface temperature [K]"] - 273.15 + ) + sim = pybamm.Simulation( + model, parameter_values=parameter_values, experiment=experiment + ) + sol = sim.solve([0, 3600]) + sols.append(sol) + +# plot +pybamm.dynamic_plot( + sols, + [ + "Voltage [V]", + "Bulk temperature [°C]", + "Surface temperature [°C]", + "Surface total cooling [W]", + "Environment total cooling [W]", + ], +) diff --git a/src/pybamm/CITATIONS.bib b/src/pybamm/CITATIONS.bib index 8fb9c6dc98..3d853738b4 100644 --- a/src/pybamm/CITATIONS.bib +++ b/src/pybamm/CITATIONS.bib @@ -252,6 +252,16 @@ @article{Lain2019 doi = {10.3390/batteries5040064}, } +@article{lin2014lumped, + title={A lumped-parameter electro-thermal model for cylindrical batteries}, + author={Lin, Xinfan and Perez, Hector E and Mohan, Shankar and Siegel, Jason B and Stefanopoulou, Anna G and Ding, Yi and Castanier, Matthew P}, + journal={Journal of Power Sources}, + volume={257}, + pages={1--11}, + year={2014}, + publisher={Elsevier} +} + @article{Marquis2019, title = {{An asymptotic derivation of a single particle model with electrolyte}}, author = {Marquis, Scott G. and Sulzer, Valentin and Timms, Robert and Please, Colin P. and Chapman, S. Jon}, diff --git a/src/pybamm/models/full_battery_models/base_battery_model.py b/src/pybamm/models/full_battery_models/base_battery_model.py index 8a2e443338..ccda594b14 100644 --- a/src/pybamm/models/full_battery_models/base_battery_model.py +++ b/src/pybamm/models/full_battery_models/base_battery_model.py @@ -194,6 +194,11 @@ class BatteryModelOptions(pybamm.FuzzyDict): * "surface form" : str Whether to use the surface formulation of the problem. Can be "false" (default), "differential" or "algebraic". + * "surface temperature" : str + Sets the surface temperature model to use. Can be "ambient" (default), + which sets the surface temperature equal to the ambient temperature, or + "lumped", which adds an ODE for the surface temperature (e.g. to model + internal heating of a thermal chamber). * "thermal" : str Sets the thermal model to use. Can be "isothermal" (default), "lumped", "x-lumped", or "x-full". The 'cell geometry' option must be set to @@ -278,7 +283,6 @@ def __init__(self, extra_options): ], "particle": [ "Fickian diffusion", - "fast diffusion", "uniform profile", "quadratic profile", "quartic profile", @@ -304,6 +308,7 @@ def __init__(self, extra_options): "SEI porosity change": ["false", "true"], "stress-induced diffusion": ["false", "true"], "surface form": ["false", "differential", "algebraic"], + "surface temperature": ["ambient", "lumped"], "thermal": ["isothermal", "lumped", "x-lumped", "x-full"], "total interfacial current density as a state": ["false", "true"], "transport efficiency": [ @@ -554,16 +559,6 @@ def __init__(self, extra_options): ) # Renamed options - if options["particle"] == "fast diffusion": - raise pybamm.OptionError( - "The 'fast diffusion' option has been renamed. " - "Use 'uniform profile' instead." - ) - if options["SEI porosity change"] in [True, False]: - raise pybamm.OptionError( - "SEI porosity change must now be given in string format " - "('true' or 'false')" - ) if options["working electrode"] == "negative": raise pybamm.OptionError( "The 'negative' working electrode option has been removed because " @@ -633,6 +628,12 @@ def __init__(self, extra_options): "be 'none': 'particle mechanics', 'loss of active material'" ) + if options["surface temperature"] == "lumped": + if options["thermal"] not in ["isothermal", "lumped"]: + raise pybamm.OptionError( + "lumped surface temperature model only compatible with isothermal " + "or lumped thermal model" + ) if "true" in options["SEI on cracks"]: sei_on_cr = options["SEI on cracks"] p_mechanics = options["particle mechanics"] @@ -1265,6 +1266,13 @@ def set_thermal_submodel(self): self.param, self.options, x_average ) + def set_surface_temperature_submodel(self): + if self.options["surface temperature"] == "ambient": + submodel = pybamm.thermal.surface.Ambient + elif self.options["surface temperature"] == "lumped": + submodel = pybamm.thermal.surface.Lumped + self.submodels["surface temperature"] = submodel(self.param, self.options) + def set_current_collector_submodel(self): if self.options["current collector"] in ["uniform"]: submodel = pybamm.current_collector.Uniform(self.param) diff --git a/src/pybamm/models/full_battery_models/lead_acid/full.py b/src/pybamm/models/full_battery_models/lead_acid/full.py index b5b561e6dd..f873c779b6 100644 --- a/src/pybamm/models/full_battery_models/lead_acid/full.py +++ b/src/pybamm/models/full_battery_models/lead_acid/full.py @@ -27,6 +27,7 @@ def __init__(self, options=None, name="Full model", build=True): self.set_electrolyte_submodel() self.set_solid_submodel() self.set_thermal_submodel() + self.set_surface_temperature_submodel() self.set_side_reaction_submodels() self.set_current_collector_submodel() self.set_sei_submodel() diff --git a/src/pybamm/models/full_battery_models/lead_acid/loqs.py b/src/pybamm/models/full_battery_models/lead_acid/loqs.py index c63c9cd11b..76a4142a08 100644 --- a/src/pybamm/models/full_battery_models/lead_acid/loqs.py +++ b/src/pybamm/models/full_battery_models/lead_acid/loqs.py @@ -27,6 +27,7 @@ def __init__(self, options=None, name="LOQS model", build=True): self.set_electrolyte_submodel() self.set_electrode_submodels() self.set_thermal_submodel() + self.set_surface_temperature_submodel() self.set_side_reaction_submodels() self.set_current_collector_submodel() self.set_sei_submodel() diff --git a/src/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py b/src/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py index 6db56b74c4..dfe2512f6e 100644 --- a/src/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py +++ b/src/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py @@ -48,6 +48,7 @@ def set_submodels(self, build): self.set_electrolyte_concentration_submodel() self.set_electrolyte_potential_submodel() self.set_thermal_submodel() + self.set_surface_temperature_submodel() self.set_current_collector_submodel() self.set_sei_submodel() self.set_sei_on_cracks_submodel() diff --git a/src/pybamm/models/submodels/thermal/__init__.py b/src/pybamm/models/submodels/thermal/__init__.py index cc8f769f36..90bd14bc39 100644 --- a/src/pybamm/models/submodels/thermal/__init__.py +++ b/src/pybamm/models/submodels/thermal/__init__.py @@ -2,5 +2,6 @@ from .isothermal import Isothermal from .lumped import Lumped from . import pouch_cell +from . import surface -__all__ = ['base_thermal', 'isothermal', 'lumped', 'pouch_cell'] +__all__ = ["base_thermal", "isothermal", "lumped", "pouch_cell"] diff --git a/src/pybamm/models/submodels/thermal/isothermal.py b/src/pybamm/models/submodels/thermal/isothermal.py index edcd47bbdf..4b729f1294 100644 --- a/src/pybamm/models/submodels/thermal/isothermal.py +++ b/src/pybamm/models/submodels/thermal/isothermal.py @@ -73,8 +73,8 @@ def get_coupled_variables(self, variables): "Total heating [W]", "Negative current collector Ohmic heating [W.m-3]", "Positive current collector Ohmic heating [W.m-3]", - "Lumped total cooling [W.m-3]", - "Lumped total cooling [W]", + "Surface total cooling [W.m-3]", + "Surface total cooling [W]", ]: # All variables are zero variables.update({var: zero}) diff --git a/src/pybamm/models/submodels/thermal/lumped.py b/src/pybamm/models/submodels/thermal/lumped.py index 76af2904bc..6915a3b180 100644 --- a/src/pybamm/models/submodels/thermal/lumped.py +++ b/src/pybamm/models/submodels/thermal/lumped.py @@ -49,9 +49,9 @@ def get_coupled_variables(self, variables): # Newton cooling, accounting for surface area to volume ratio T_vol_av = variables["Volume-averaged cell temperature [K]"] - T_amb = variables["Volume-averaged ambient temperature [K]"] + T_surf = variables["Surface temperature [K]"] V = variables["Cell thermal volume [m3]"] - Q_cool_W = -self.param.h_total * (T_vol_av - T_amb) * self.param.A_cooling + Q_cool_W = -self.param.h_total * (T_vol_av - T_surf) * self.param.A_cooling Q_cool_vol_av = Q_cool_W / V # Contact resistance heating Q_cr @@ -67,8 +67,8 @@ def get_coupled_variables(self, variables): variables.update( { # Lumped cooling - "Lumped total cooling [W.m-3]": Q_cool_vol_av, - "Lumped total cooling [W]": Q_cool_W, + "Surface total cooling [W.m-3]": Q_cool_vol_av, + "Surface total cooling [W]": Q_cool_W, # Contact resistance "Lumped contact resistance heating [W.m-3]": Q_cr_vol_av, "Lumped contact resistance heating [W]": Q_cr_W, @@ -79,7 +79,7 @@ def get_coupled_variables(self, variables): def set_rhs(self, variables): T_vol_av = variables["Volume-averaged cell temperature [K]"] Q_vol_av = variables["Volume-averaged total heating [W.m-3]"] - Q_cool_vol_av = variables["Lumped total cooling [W.m-3]"] + Q_cool_vol_av = variables["Surface total cooling [W.m-3]"] Q_cr_vol_av = variables["Lumped contact resistance heating [W.m-3]"] rho_c_p_eff_av = variables[ "Volume-averaged effective heat capacity [J.K-1.m-3]" diff --git a/src/pybamm/models/submodels/thermal/pouch_cell/pouch_cell_1D_current_collectors.py b/src/pybamm/models/submodels/thermal/pouch_cell/pouch_cell_1D_current_collectors.py index a4908c6f5d..fb026a9a0a 100644 --- a/src/pybamm/models/submodels/thermal/pouch_cell/pouch_cell_1D_current_collectors.py +++ b/src/pybamm/models/submodels/thermal/pouch_cell/pouch_cell_1D_current_collectors.py @@ -54,7 +54,7 @@ def get_coupled_variables(self, variables): def set_rhs(self, variables): T_av = variables["X-averaged cell temperature [K]"] Q_av = variables["X-averaged total heating [W.m-3]"] - T_amb = variables["Ambient temperature [K]"] + T_surf = variables["Surface temperature [K]"] y = pybamm.standard_spatial_vars.y z = pybamm.standard_spatial_vars.z @@ -64,13 +64,13 @@ def set_rhs(self, variables): cell_volume = self.param.L * self.param.L_y * self.param.L_z Q_yz_surface = ( -(self.param.n.h_cc(y, z) + self.param.p.h_cc(y, z)) - * (T_av - T_amb) + * (T_av - T_surf) * yz_surface_area / cell_volume ) Q_edge = ( -(self.param.h_edge(0, z) + self.param.h_edge(self.param.L_y, z)) - * (T_av - T_amb) + * (T_av - T_surf) * edge_area / cell_volume ) @@ -87,7 +87,7 @@ def set_rhs(self, variables): def set_boundary_conditions(self, variables): param = self.param - T_amb = variables["Ambient temperature [K]"] + T_surf = variables["Surface temperature [K]"] T_av = variables["X-averaged cell temperature [K]"] # Find tab locations (top vs bottom) @@ -118,10 +118,10 @@ def set_boundary_conditions(self, variables): # Calculate heat fluxes weighted by area # Note: can't do y-average of h_edge here since y isn't meshed. Evaluate at # midpoint. - q_tab_n = -param.n.h_tab * (T_av - T_amb) - q_tab_p = -param.p.h_tab * (T_av - T_amb) - q_edge_top = -param.h_edge(L_y / 2, L_z) * (T_av - T_amb) - q_edge_bottom = -param.h_edge(L_y / 2, 0) * (T_av - T_amb) + q_tab_n = -param.n.h_tab * (T_av - T_surf) + q_tab_p = -param.p.h_tab * (T_av - T_surf) + q_edge_top = -param.h_edge(L_y / 2, L_z) * (T_av - T_surf) + q_edge_bottom = -param.h_edge(L_y / 2, 0) * (T_av - T_surf) q_top = ( q_tab_n * neg_tab_area * neg_tab_top_bool + q_tab_p * pos_tab_area * pos_tab_top_bool diff --git a/src/pybamm/models/submodels/thermal/pouch_cell/pouch_cell_2D_current_collectors.py b/src/pybamm/models/submodels/thermal/pouch_cell/pouch_cell_2D_current_collectors.py index 7955ee4c38..b2d69ff1bb 100644 --- a/src/pybamm/models/submodels/thermal/pouch_cell/pouch_cell_2D_current_collectors.py +++ b/src/pybamm/models/submodels/thermal/pouch_cell/pouch_cell_2D_current_collectors.py @@ -54,15 +54,15 @@ def get_coupled_variables(self, variables): def set_rhs(self, variables): T_av = variables["X-averaged cell temperature [K]"] Q_av = variables["X-averaged total heating [W.m-3]"] - T_amb = variables["Ambient temperature [K]"] + T_surf = variables["Surface temperature [K]"] y = pybamm.standard_spatial_vars.y z = pybamm.standard_spatial_vars.z # Calculate cooling Q_yz_surface_W_per_m2 = -(self.param.n.h_cc(y, z) + self.param.p.h_cc(y, z)) * ( - T_av - T_amb + T_av - T_surf ) - Q_edge_W_per_m2 = -self.param.h_edge(y, z) * (T_av - T_amb) + Q_edge_W_per_m2 = -self.param.h_edge(y, z) * (T_av - T_surf) # Account for surface area to volume ratio of pouch cell in surface cooling # term @@ -98,14 +98,14 @@ def set_rhs(self, variables): def set_boundary_conditions(self, variables): T_av = variables["X-averaged cell temperature [K]"] - T_amb = variables["Ambient temperature [K]"] + T_surf = variables["Surface temperature [K]"] y = pybamm.standard_spatial_vars.y z = pybamm.standard_spatial_vars.z # Calculate heat fluxes - q_tab_n = -self.param.n.h_tab * (T_av - T_amb) - q_tab_p = -self.param.p.h_tab * (T_av - T_amb) - q_edge = -self.param.h_edge(y, z) * (T_av - T_amb) + q_tab_n = -self.param.n.h_tab * (T_av - T_surf) + q_tab_p = -self.param.p.h_tab * (T_av - T_surf) + q_edge = -self.param.h_edge(y, z) * (T_av - T_surf) # Subtract the edge cooling from the tab portion so as to not double count # Note: tab cooling is also only applied on the current collector hence diff --git a/src/pybamm/models/submodels/thermal/pouch_cell/x_full.py b/src/pybamm/models/submodels/thermal/pouch_cell/x_full.py index f4aa07c563..630ce94b01 100644 --- a/src/pybamm/models/submodels/thermal/pouch_cell/x_full.py +++ b/src/pybamm/models/submodels/thermal/pouch_cell/x_full.py @@ -79,7 +79,7 @@ def set_rhs(self, variables): Q = variables["Total heating [W.m-3]"] Q_cn = variables["Negative current collector Ohmic heating [W.m-3]"] Q_cp = variables["Positive current collector Ohmic heating [W.m-3]"] - T_amb = variables["Ambient temperature [K]"] + T_surf = variables["Surface temperature [K]"] y = pybamm.standard_spatial_vars.y z = pybamm.standard_spatial_vars.z @@ -140,28 +140,28 @@ def set_rhs(self, variables): ( pybamm.boundary_value(lambda_n, "left") * pybamm.boundary_gradient(T_n, "left") - - h_cn * (T_cn - T_amb) + - h_cn * (T_cn - T_surf) ) / L_cn + Q_cn - + cooling_coefficient_cn * (T_cn - T_amb) + + cooling_coefficient_cn * (T_cn - T_surf) ) / self.param.n.rho_c_p_cc(T_cn), T: ( pybamm.div(lambda_ * pybamm.grad(T)) + Q - + cooling_coefficient * (T - T_amb) + + cooling_coefficient * (T - T_surf) ) / rho_c_p, T_cp: ( ( -pybamm.boundary_value(lambda_p, "right") * pybamm.boundary_gradient(T_p, "right") - - h_cp * (T_cp - T_amb) + - h_cp * (T_cp - T_surf) ) / L_cp + Q_cp - + cooling_coefficient_cp * (T_cp - T_amb) + + cooling_coefficient_cp * (T_cp - T_surf) ) / self.param.p.rho_c_p_cc(T_cp), } diff --git a/src/pybamm/models/submodels/thermal/surface/__init__.py b/src/pybamm/models/submodels/thermal/surface/__init__.py new file mode 100644 index 0000000000..dece4b44ae --- /dev/null +++ b/src/pybamm/models/submodels/thermal/surface/__init__.py @@ -0,0 +1,2 @@ +from .ambient import Ambient +from .lumped import Lumped diff --git a/src/pybamm/models/submodels/thermal/surface/ambient.py b/src/pybamm/models/submodels/thermal/surface/ambient.py new file mode 100644 index 0000000000..a28591af30 --- /dev/null +++ b/src/pybamm/models/submodels/thermal/surface/ambient.py @@ -0,0 +1,33 @@ +# +# Class for ambient surface temperature submodel +import pybamm + + +class Ambient(pybamm.BaseSubModel): + """ + Class for setting surface temperature equal to ambient temperature. + + Parameters + ---------- + param : parameter class + The parameters to use for this submodel + options : dict, optional + A dictionary of options to be passed to the model. + + """ + + def __init__(self, param, options=None): + super().__init__(param, options=options) + + def get_coupled_variables(self, variables): + T_amb = variables["Ambient temperature [K]"] + T_amb_av = variables["Volume-averaged ambient temperature [K]"] + + variables.update( + { + "Surface temperature [K]": T_amb, + "Volume-averaged surface temperature [K]": T_amb_av, + "Environment total cooling [W]": pybamm.Scalar(0), + } + ) + return variables diff --git a/src/pybamm/models/submodels/thermal/surface/lumped.py b/src/pybamm/models/submodels/thermal/surface/lumped.py new file mode 100644 index 0000000000..dc481947e8 --- /dev/null +++ b/src/pybamm/models/submodels/thermal/surface/lumped.py @@ -0,0 +1,51 @@ +# +# Class for ambient surface temperature submodel +import pybamm + + +class Lumped(pybamm.BaseSubModel): + """ + Class for the lumped surface temperature submodel, which adds an ODE for the + surface temperature. + + Parameters + ---------- + param : parameter class + The parameters to use for this submodel + options : dict, optional + A dictionary of options to be passed to the model. + + """ + + def __init__(self, param, options=None): + super().__init__(param, options=options) + pybamm.citations.register("lin2014lumped") + + def get_fundamental_variables(self): + T_surf = pybamm.Variable("Surface temperature [K]") + variables = {"Surface temperature [K]": T_surf} + + return variables + + def get_coupled_variables(self, variables): + T_surf = variables["Surface temperature [K]"] + T_amb = variables["Ambient temperature [K]"] + R_env = pybamm.Parameter("Environment thermal resistance [K.W-1]") + Q_cool_env = -(T_surf - T_amb) / R_env + variables["Environment total cooling [W]"] = Q_cool_env + return variables + + def set_rhs(self, variables): + T_surf = variables["Surface temperature [K]"] + + Q_cool_bulk = variables["Surface total cooling [W]"] + Q_heat_bulk = -Q_cool_bulk + + Q_cool_env = variables["Environment total cooling [W]"] + rho_c_p_case = pybamm.Parameter("Casing heat capacity [J.K-1]") + + self.rhs[T_surf] = (Q_heat_bulk + Q_cool_env) / rho_c_p_case + + def set_initial_conditions(self, variables): + T_surf = variables["Surface temperature [K]"] + self.initial_conditions = {T_surf: self.param.T_init} diff --git a/src/pybamm/plotting/plot_thermal_components.py b/src/pybamm/plotting/plot_thermal_components.py index 8cb99a454d..e45a08112c 100644 --- a/src/pybamm/plotting/plot_thermal_components.py +++ b/src/pybamm/plotting/plot_thermal_components.py @@ -55,7 +55,7 @@ def plot_thermal_components( volume = solution["Cell thermal volume [m3]"].entries heating_sources = [ - "Lumped total cooling", + "Surface total cooling", "Ohmic heating", "Irreversible electrochemical heating", "Reversible heating", @@ -77,9 +77,9 @@ def plot_thermal_components( # Plot # Initialise total_heat = 0 - bottom_heat = heats["Lumped total cooling"] + bottom_heat = heats["Surface total cooling"] total_cumul_heat = 0 - bottom_cumul_heat = cumul_heats["Lumped total cooling"] + bottom_cumul_heat = cumul_heats["Surface total cooling"] # Plot components for name in heating_sources: top_heat = bottom_heat + abs(heats[name]) diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_thermal_models.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_thermal_models.py index 603f64d716..ac58f40bb4 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_thermal_models.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_thermal_models.py @@ -101,6 +101,35 @@ def err(a, b): assert 1e-5 > err(solutions["SPMe 1+1D"], solutions["SPMe 2+1D"]) + def test_surface_temperature_models(self): + models = { + option: pybamm.lithium_ion.SPM( + {"thermal": "lumped", "surface temperature": option} + ) + for option in ["lumped", "ambient"] + } + + parameter_values = pybamm.ParameterValues("Chen2020") + parameter_values.update( + { + "Casing heat capacity [J.K-1]": 30, + "Environment thermal resistance [K.W-1]": 10, + }, + check_already_exists=False, + ) + + sols = {} + for name, model in models.items(): + sim = pybamm.Simulation(model, parameter_values=parameter_values) + sol = sim.solve([0, 3600]) + sols[name] = sol + + for var in ["Volume-averaged cell temperature [K]", "Surface temperature [K]"]: + # ignore first entry as it is the initial condition + T_ambient_model = sols["ambient"][var].entries[1:] + T_lumped_model = sols["lumped"][var].entries[1:] + np.testing.assert_array_less(T_ambient_model, T_lumped_model) + def test_lumped_contact_resistance(self): # Test that the heating with contact resistance is greater than without diff --git a/tests/unit/test_models/test_full_battery_models/test_base_battery_model.py b/tests/unit/test_models/test_full_battery_models/test_base_battery_model.py index 6ee38faf9a..033dcf5345 100644 --- a/tests/unit/test_models/test_full_battery_models/test_base_battery_model.py +++ b/tests/unit/test_models/test_full_battery_models/test_base_battery_model.py @@ -36,7 +36,7 @@ 'number of MSMR reactions': 'none' (possible: ['none']) 'open-circuit potential': 'single' (possible: ['single', 'current sigmoid', 'MSMR', 'Wycisk']) 'operating mode': 'current' (possible: ['current', 'voltage', 'power', 'differential power', 'explicit power', 'resistance', 'differential resistance', 'explicit resistance', 'CCCV']) -'particle': 'Fickian diffusion' (possible: ['Fickian diffusion', 'fast diffusion', 'uniform profile', 'quadratic profile', 'quartic profile', 'MSMR']) +'particle': 'Fickian diffusion' (possible: ['Fickian diffusion', 'uniform profile', 'quadratic profile', 'quartic profile', 'MSMR']) 'particle mechanics': 'swelling only' (possible: ['none', 'swelling only', 'swelling and cracking']) 'particle phases': '1' (possible: ['1', '2']) 'particle shape': 'spherical' (possible: ['spherical', 'no particles']) @@ -47,6 +47,7 @@ 'SEI porosity change': 'false' (possible: ['false', 'true']) 'stress-induced diffusion': 'true' (possible: ['false', 'true']) 'surface form': 'differential' (possible: ['false', 'differential', 'algebraic']) +'surface temperature': 'ambient' (possible: ['ambient', 'lumped']) 'thermal': 'x-full' (possible: ['isothermal', 'lumped', 'x-lumped', 'x-full']) 'total interfacial current density as a state': 'false' (possible: ['false', 'true']) 'transport efficiency': 'Bruggeman' (possible: ['Bruggeman', 'ordered packing', 'hyperbola of revolution', 'overlapping spheres', 'tortuosity factor', 'random overlapping cylinders', 'heterogeneous catalyst', 'cation-exchange membrane']) @@ -211,8 +212,6 @@ def test_options(self): pybamm.BaseBatteryModel({"convection": "full transverse"}) with self.assertRaisesRegex(pybamm.OptionError, "particle"): pybamm.BaseBatteryModel({"particle": "bad particle"}) - with self.assertRaisesRegex(pybamm.OptionError, "The 'fast diffusion'"): - pybamm.BaseBatteryModel({"particle": "fast diffusion"}) with self.assertRaisesRegex(pybamm.OptionError, "working electrode"): pybamm.BaseBatteryModel({"working electrode": "bad working electrode"}) with self.assertRaisesRegex(pybamm.OptionError, "The 'negative' working"): @@ -233,10 +232,6 @@ def test_options(self): pybamm.BaseBatteryModel({"SEI film resistance": "bad SEI film resistance"}) with self.assertRaisesRegex(pybamm.OptionError, "SEI porosity change"): pybamm.BaseBatteryModel({"SEI porosity change": "bad SEI porosity change"}) - with self.assertRaisesRegex( - pybamm.OptionError, "SEI porosity change must now be given in string format" - ): - pybamm.BaseBatteryModel({"SEI porosity change": True}) # changing defaults based on other options model = pybamm.BaseBatteryModel() self.assertEqual(model.options["SEI film resistance"], "none") @@ -387,6 +382,12 @@ def test_options(self): } ) + # surface thermal model + with self.assertRaisesRegex(pybamm.OptionError, "surface temperature"): + pybamm.BaseBatteryModel( + {"surface temperature": "lumped", "thermal": "x-full"} + ) + # phases with self.assertRaisesRegex(pybamm.OptionError, "multiple particle phases"): pybamm.BaseBatteryModel({"particle phases": "2", "surface form": "false"}) diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_tests.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_tests.py index c8a3f6b509..9c093c0c65 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_tests.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_tests.py @@ -31,6 +31,10 @@ def test_well_posed_lumped_thermal_model_1D(self): options = {"thermal": "lumped"} self.check_well_posedness(options) + def test_well_posed_lumped_thermal_model_surface_temperature(self): + options = {"thermal": "lumped", "surface temperature": "lumped"} + self.check_well_posedness(options) + def test_well_posed_x_full_thermal_model(self): options = {"thermal": "x-full"} self.check_well_posedness(options) From 5fdee8a7cb1a026dcc803575a026bed3c318ef17 Mon Sep 17 00:00:00 2001 From: Marc Berliner <34451391+MarcBerliner@users.noreply.github.com> Date: Wed, 28 Aug 2024 16:23:02 -0400 Subject: [PATCH 75/82] Update IDAKLU time stepping in tests and benchmarks (#4390) * time stepping updates * Update test_idaklu_solver.py * Update CHANGELOG.md --- CHANGELOG.md | 3 +- benchmarks/different_model_options.py | 24 +++-- benchmarks/time_solve_models.py | 45 +++++--- .../work_precision_sets/time_vs_abstols.py | 13 ++- .../work_precision_sets/time_vs_mesh_size.py | 1 + .../time_vs_no_of_states.py | 1 + .../work_precision_sets/time_vs_reltols.py | 13 ++- tests/integration/test_solvers/test_idaklu.py | 2 +- tests/unit/test_solvers/test_idaklu_solver.py | 100 ++++++++---------- 9 files changed, 113 insertions(+), 89 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1e8a720e7..38747ea7fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,13 +7,14 @@ ## Optimizations +- Update `IDAKLU` tests and benchmarks to use adaptive time stepping. ([#4390](https://github.com/pybamm-team/PyBaMM/pull/4390)) - Improved adaptive time-stepping performance of the (`IDAKLUSolver`). ([#4351](https://github.com/pybamm-team/PyBaMM/pull/4351)) - Improved performance and reliability of DAE consistent initialization. ([#4301](https://github.com/pybamm-team/PyBaMM/pull/4301)) - Replaced rounded Faraday constant with its exact value in `bpx.py` for better comparison between different tools. ([#4290](https://github.com/pybamm-team/PyBaMM/pull/4290)) ## Bug Fixes -- Fixed memory issue that caused failure when `output variables` were specified with (`IDAKLUSolver`). ([#4379](https://github.com/pybamm-team/PyBaMM/issues/4379)) +- Fixed memory issue that caused failure when `output variables` were specified with (`IDAKLUSolver`). ([#4379](https://github.com/pybamm-team/PyBaMM/pull/4379)) - Fixed bug where IDAKLU solver failed when `output variables` were specified and an event triggered. ([#4300](https://github.com/pybamm-team/PyBaMM/pull/4300)) ## Breaking changes diff --git a/benchmarks/different_model_options.py b/benchmarks/different_model_options.py index 72767e9f65..91c362756f 100644 --- a/benchmarks/different_model_options.py +++ b/benchmarks/different_model_options.py @@ -34,6 +34,7 @@ class SolveModel: solver: pybamm.BaseSolver model: pybamm.BaseModel t_eval: np.ndarray + t_interp: np.ndarray | None def solve_setup(self, parameter, model_, option, value, solver_class): import importlib @@ -51,8 +52,13 @@ def solve_setup(self, parameter, model_, option, value, solver_class): self.model = model_({option: value}) c_rate = 1 tmax = 4000 / c_rate - nb_points = 500 - self.t_eval = np.linspace(0, tmax, nb_points) + if self.solver.supports_interp: + self.t_eval = np.array([0, tmax]) + self.t_interp = None + else: + nb_points = 500 + self.t_eval = np.linspace(0, tmax, nb_points) + self.t_interp = None geometry = self.model.default_geometry # load parameter values and process model and geometry @@ -77,7 +83,7 @@ def solve_setup(self, parameter, model_, option, value, solver_class): disc.process_model(self.model) def solve_model(self, _model, _params): - self.solver.solve(self.model, t_eval=self.t_eval) + self.solver.solve(self.model, t_eval=self.t_eval, t_interp=self.t_interp) class TimeBuildModelLossActiveMaterial: @@ -109,7 +115,7 @@ def setup(self, model, params, solver_class): ) def time_solve_model(self, _model, _params, _solver_class): - self.solver.solve(self.model, t_eval=self.t_eval) + self.solver.solve(self.model, t_eval=self.t_eval, t_interp=self.t_interp) class TimeBuildModelLithiumPlating: @@ -141,7 +147,7 @@ def setup(self, model, params, solver_class): ) def time_solve_model(self, _model, _params, _solver_class): - self.solver.solve(self.model, t_eval=self.t_eval) + self.solver.solve(self.model, t_eval=self.t_eval, t_interp=self.t_interp) class TimeBuildModelSEI: @@ -187,7 +193,7 @@ def setup(self, model, params, solver_class): SolveModel.solve_setup(self, "Marquis2019", model, "SEI", params, solver_class) def time_solve_model(self, _model, _params, _solver_class): - self.solver.solve(self.model, t_eval=self.t_eval) + self.solver.solve(self.model, t_eval=self.t_eval, t_interp=self.t_interp) class TimeBuildModelParticle: @@ -229,7 +235,7 @@ def setup(self, model, params, solver_class): ) def time_solve_model(self, _model, _params, _solver_class): - self.solver.solve(self.model, t_eval=self.t_eval) + self.solver.solve(self.model, t_eval=self.t_eval, t_interp=self.t_interp) class TimeBuildModelThermal: @@ -261,7 +267,7 @@ def setup(self, model, params, solver_class): ) def time_solve_model(self, _model, _params, _solver_class): - self.solver.solve(self.model, t_eval=self.t_eval) + self.solver.solve(self.model, t_eval=self.t_eval, t_interp=self.t_interp) class TimeBuildModelSurfaceForm: @@ -299,4 +305,4 @@ def setup(self, model, params, solver_class): ) def time_solve_model(self, _model, _params, _solver_class): - self.solver.solve(self.model, t_eval=self.t_eval) + self.solver.solve(self.model, t_eval=self.t_eval, t_interp=self.t_interp) diff --git a/benchmarks/time_solve_models.py b/benchmarks/time_solve_models.py index e41a7ccd16..fbd663f540 100644 --- a/benchmarks/time_solve_models.py +++ b/benchmarks/time_solve_models.py @@ -6,8 +6,8 @@ import numpy as np -def solve_model_once(model, solver, t_eval): - solver.solve(model, t_eval=t_eval) +def solve_model_once(model, solver, t_eval, t_interp): + solver.solve(model, t_eval=t_eval, t_interp=t_interp) class TimeSolveSPM: @@ -31,6 +31,7 @@ class TimeSolveSPM: model: pybamm.BaseModel solver: pybamm.BaseSolver t_eval: np.ndarray + t_interp: np.ndarray | None def setup(self, solve_first, parameters, solver_class): set_random_seed() @@ -38,8 +39,14 @@ def setup(self, solve_first, parameters, solver_class): self.model = pybamm.lithium_ion.SPM() c_rate = 1 tmax = 4000 / c_rate - nb_points = 500 - self.t_eval = np.linspace(0, tmax, nb_points) + if self.solver.supports_interp: + self.t_eval = np.array([0, tmax]) + self.t_interp = None + else: + nb_points = 500 + self.t_eval = np.linspace(0, tmax, nb_points) + self.t_interp = None + geometry = self.model.default_geometry # load parameter values and process model and geometry @@ -63,10 +70,10 @@ def setup(self, solve_first, parameters, solver_class): disc = pybamm.Discretisation(mesh, self.model.default_spatial_methods) disc.process_model(self.model) if solve_first: - solve_model_once(self.model, self.solver, self.t_eval) + solve_model_once(self.model, self.solver, self.t_eval, self.t_interp) def time_solve_model(self, _solve_first, _parameters, _solver_class): - self.solver.solve(self.model, t_eval=self.t_eval) + self.solver.solve(self.model, t_eval=self.t_eval, t_interp=self.t_interp) class TimeSolveSPMe: @@ -97,8 +104,13 @@ def setup(self, solve_first, parameters, solver_class): self.model = pybamm.lithium_ion.SPMe() c_rate = 1 tmax = 4000 / c_rate - nb_points = 500 - self.t_eval = np.linspace(0, tmax, nb_points) + if self.solver.supports_interp: + self.t_eval = np.array([0, tmax]) + self.t_interp = None + else: + nb_points = 500 + self.t_eval = np.linspace(0, tmax, nb_points) + self.t_interp = None geometry = self.model.default_geometry # load parameter values and process model and geometry @@ -122,10 +134,10 @@ def setup(self, solve_first, parameters, solver_class): disc = pybamm.Discretisation(mesh, self.model.default_spatial_methods) disc.process_model(self.model) if solve_first: - solve_model_once(self.model, self.solver, self.t_eval) + solve_model_once(self.model, self.solver, self.t_eval, self.t_interp) def time_solve_model(self, _solve_first, _parameters, _solver_class): - self.solver.solve(self.model, t_eval=self.t_eval) + self.solver.solve(self.model, t_eval=self.t_eval, t_interp=self.t_interp) class TimeSolveDFN: @@ -161,8 +173,13 @@ def setup(self, solve_first, parameters, solver_class): self.model = pybamm.lithium_ion.DFN() c_rate = 1 tmax = 4000 / c_rate - nb_points = 500 - self.t_eval = np.linspace(0, tmax, nb_points) + if self.solver.supports_interp: + self.t_eval = np.array([0, tmax]) + self.t_interp = None + else: + nb_points = 500 + self.t_eval = np.linspace(0, tmax, nb_points) + self.t_interp = None geometry = self.model.default_geometry # load parameter values and process model and geometry @@ -186,7 +203,7 @@ def setup(self, solve_first, parameters, solver_class): disc = pybamm.Discretisation(mesh, self.model.default_spatial_methods) disc.process_model(self.model) if solve_first: - solve_model_once(self.model, self.solver, self.t_eval) + solve_model_once(self.model, self.solver, self.t_eval, self.t_interp) def time_solve_model(self, _solve_first, _parameters, _solver_class): - self.solver.solve(self.model, t_eval=self.t_eval) + self.solver.solve(self.model, t_eval=self.t_eval, t_interp=self.t_interp) diff --git a/benchmarks/work_precision_sets/time_vs_abstols.py b/benchmarks/work_precision_sets/time_vs_abstols.py index af76493abc..c5ca7b0c01 100644 --- a/benchmarks/work_precision_sets/time_vs_abstols.py +++ b/benchmarks/work_precision_sets/time_vs_abstols.py @@ -42,8 +42,13 @@ model = i[1].new_copy() c_rate = 1 tmax = 3500 / c_rate - nb_points = 500 - t_eval = np.linspace(0, tmax, nb_points) + if solver.supports_interp: + t_eval = np.array([0, tmax]) + t_interp = None + else: + nb_points = 500 + t_eval = np.linspace(0, tmax, nb_points) + t_interp = None geometry = model.default_geometry # load parameter values and process model and geometry @@ -69,11 +74,11 @@ for tol in abstols: solver.atol = tol - solver.solve(model, t_eval=t_eval) + solver.solve(model, t_eval=t_eval, t_interp=t_interp) time = 0 runs = 20 for _ in range(0, runs): - solution = solver.solve(model, t_eval=t_eval) + solution = solver.solve(model, t_eval=t_eval, t_interp=t_interp) time += solution.solve_time.value time = time / runs diff --git a/benchmarks/work_precision_sets/time_vs_mesh_size.py b/benchmarks/work_precision_sets/time_vs_mesh_size.py index 7b8ad525df..866541378a 100644 --- a/benchmarks/work_precision_sets/time_vs_mesh_size.py +++ b/benchmarks/work_precision_sets/time_vs_mesh_size.py @@ -17,6 +17,7 @@ npts = [4, 8, 16, 32, 64] solvers = { + "IDAKLUSolver": pybamm.IDAKLUSolver(), "Casadi - safe": pybamm.CasadiSolver(), "Casadi - fast": pybamm.CasadiSolver(mode="fast"), } diff --git a/benchmarks/work_precision_sets/time_vs_no_of_states.py b/benchmarks/work_precision_sets/time_vs_no_of_states.py index fdc039587f..cf0e34e1ea 100644 --- a/benchmarks/work_precision_sets/time_vs_no_of_states.py +++ b/benchmarks/work_precision_sets/time_vs_no_of_states.py @@ -16,6 +16,7 @@ npts = [4, 8, 16, 32, 64] solvers = { + "IDAKLUSolver": pybamm.IDAKLUSolver(), "Casadi - safe": pybamm.CasadiSolver(), "Casadi - fast": pybamm.CasadiSolver(mode="fast"), } diff --git a/benchmarks/work_precision_sets/time_vs_reltols.py b/benchmarks/work_precision_sets/time_vs_reltols.py index 4afcddf94d..f97c72ba4d 100644 --- a/benchmarks/work_precision_sets/time_vs_reltols.py +++ b/benchmarks/work_precision_sets/time_vs_reltols.py @@ -48,8 +48,13 @@ model = i[1].new_copy() c_rate = 1 tmax = 3500 / c_rate - nb_points = 500 - t_eval = np.linspace(0, tmax, nb_points) + if solver.supports_interp: + t_eval = np.array([0, tmax]) + t_interp = None + else: + nb_points = 500 + t_eval = np.linspace(0, tmax, nb_points) + t_interp = None geometry = model.default_geometry # load parameter values and process model and geometry @@ -75,11 +80,11 @@ for tol in reltols: solver.rtol = tol - solver.solve(model, t_eval=t_eval) + solver.solve(model, t_eval=t_eval, t_interp=t_interp) time = 0 runs = 20 for _ in range(0, runs): - solution = solver.solve(model, t_eval=t_eval) + solution = solver.solve(model, t_eval=t_eval, t_interp=t_interp) time += solution.solve_time.value time = time / runs diff --git a/tests/integration/test_solvers/test_idaklu.py b/tests/integration/test_solvers/test_idaklu.py index abc7741c0c..d70b64c783 100644 --- a/tests/integration/test_solvers/test_idaklu.py +++ b/tests/integration/test_solvers/test_idaklu.py @@ -32,7 +32,7 @@ def test_on_spme_sensitivities(self): disc = pybamm.Discretisation(mesh, model.default_spatial_methods) disc.process_model(model) t_interp = np.linspace(0, 3500, 100) - t_eval = np.array([t_interp[0], t_interp[-1]]) + t_eval = [t_interp[0], t_interp[-1]] solver = pybamm.IDAKLUSolver(rtol=1e-10, atol=1e-10) solution = solver.solve( model, diff --git a/tests/unit/test_solvers/test_idaklu_solver.py b/tests/unit/test_solvers/test_idaklu_solver.py index 67e68e9c6a..5da4e7e628 100644 --- a/tests/unit/test_solvers/test_idaklu_solver.py +++ b/tests/unit/test_solvers/test_idaklu_solver.py @@ -20,9 +20,7 @@ def test_ida_roberts_klu(self): # example provided in sundials # see sundials ida examples pdf for form in ["casadi", "iree"]: - if (form == "jax" or form == "iree") and not pybamm.have_jax(): - continue - if (form == "iree") and not pybamm.have_iree(): + if (form == "iree") and (not pybamm.have_jax() or not pybamm.have_iree()): continue if form == "casadi": root_method = "casadi" @@ -45,8 +43,10 @@ def test_ida_roberts_klu(self): options={"jax_evaluator": "iree"} if form == "iree" else {}, ) + # Test t_eval = np.linspace(0, 3, 100) - solution = solver.solve(model, t_eval) + t_interp = t_eval + solution = solver.solve(model, t_eval, t_interp=t_interp) # test that final time is time of event # y = 0.1 t + y0 so y=0.2 when t=2 @@ -66,9 +66,7 @@ def test_ida_roberts_klu(self): def test_model_events(self): for form in ["casadi", "iree"]: - if (form == "jax" or form == "iree") and not pybamm.have_jax(): - continue - if (form == "iree") and not pybamm.have_iree(): + if (form == "iree") and (not pybamm.have_jax() or not pybamm.have_iree()): continue if form == "casadi": root_method = "casadi" @@ -92,15 +90,8 @@ def test_model_events(self): options={"jax_evaluator": "iree"} if form == "iree" else {}, ) - if model.convert_to_format == "casadi" or ( - model.convert_to_format == "jax" - and solver._options["jax_evaluator"] == "iree" - ): - t_interp = np.linspace(0, 1, 100) - t_eval = np.array([t_interp[0], t_interp[-1]]) - else: - t_eval = np.linspace(0, 1, 100) - t_interp = t_eval + t_interp = np.linspace(0, 1, 100) + t_eval = [t_interp[0], t_interp[-1]] solution = solver.solve(model_disc, t_eval, t_interp=t_interp) np.testing.assert_array_equal( @@ -196,9 +187,7 @@ def test_model_events(self): def test_input_params(self): # test a mix of scalar and vector input params for form in ["casadi", "iree"]: - if (form == "jax" or form == "iree") and not pybamm.have_jax(): - continue - if (form == "iree") and not pybamm.have_iree(): + if (form == "iree") and (not pybamm.have_jax() or not pybamm.have_iree()): continue if form == "casadi": root_method = "casadi" @@ -224,7 +213,8 @@ def test_input_params(self): options={"jax_evaluator": "iree"} if form == "iree" else {}, ) - t_eval = np.linspace(0, 3, 100) + t_interp = np.linspace(0, 3, 100) + t_eval = [t_interp[0], t_interp[-1]] a_value = 0.1 b_value = np.array([[0.2], [0.3]]) @@ -232,6 +222,7 @@ def test_input_params(self): model, t_eval, inputs={"a": a_value, "b": b_value}, + t_interp=t_interp, ) # test that y[3] remains constant @@ -254,9 +245,9 @@ def test_input_params(self): def test_sensitivities_initial_condition(self): for form in ["casadi", "iree"]: for output_variables in [[], ["2v"]]: - if (form == "jax" or form == "iree") and not pybamm.have_jax(): - continue - if (form == "iree") and not pybamm.have_iree(): + if (form == "iree") and ( + not pybamm.have_jax() or not pybamm.have_iree() + ): continue if form == "casadi": root_method = "casadi" @@ -283,7 +274,7 @@ def test_sensitivities_initial_condition(self): ) t_interp = np.linspace(0, 3, 100) - t_eval = np.array([t_interp[0], t_interp[-1]]) + t_eval = [t_interp[0], t_interp[-1]] a_value = 0.1 @@ -307,9 +298,7 @@ def test_ida_roberts_klu_sensitivities(self): # example provided in sundials # see sundials ida examples pdf for form in ["casadi", "iree"]: - if (form == "jax" or form == "iree") and not pybamm.have_jax(): - continue - if (form == "iree") and not pybamm.have_iree(): + if (form == "iree") and (not pybamm.have_jax() or not pybamm.have_iree()): continue if form == "casadi": root_method = "casadi" @@ -334,7 +323,7 @@ def test_ida_roberts_klu_sensitivities(self): ) t_interp = np.linspace(0, 3, 100) - t_eval = np.array([t_interp[0], t_interp[-1]]) + t_eval = [t_interp[0], t_interp[-1]] a_value = 0.1 # solve first without sensitivities @@ -415,9 +404,7 @@ def test_ida_roberts_consistent_initialization(self): # example provided in sundials # see sundials ida examples pdf for form in ["casadi", "iree"]: - if (form == "jax" or form == "iree") and not pybamm.have_jax(): - continue - if (form == "iree") and not pybamm.have_iree(): + if (form == "iree") and (not pybamm.have_jax() or not pybamm.have_iree()): continue if form == "casadi": root_method = "casadi" @@ -459,9 +446,7 @@ def test_sensitivities_with_events(self): # example provided in sundials # see sundials ida examples pdf for form in ["casadi", "iree"]: - if (form == "jax" or form == "iree") and not pybamm.have_jax(): - continue - if (form == "iree") and not pybamm.have_iree(): + if (form == "iree") and (not pybamm.have_jax() or not pybamm.have_iree()): continue if form == "casadi": root_method = "casadi" @@ -487,7 +472,7 @@ def test_sensitivities_with_events(self): ) t_interp = np.linspace(0, 3, 100) - t_eval = np.array([t_interp[0], t_interp[-1]]) + t_eval = [t_interp[0], t_interp[-1]] a_value = 0.1 b_value = 0.0 @@ -582,7 +567,7 @@ def test_failures(self): solver = pybamm.IDAKLUSolver() - t_eval = np.linspace(0, 3, 100) + t_eval = [0, 3] with self.assertRaisesRegex(pybamm.SolverError, "KLU requires the Jacobian"): solver.solve(model, t_eval) @@ -597,7 +582,7 @@ def test_failures(self): solver = pybamm.IDAKLUSolver() # will give solver error - t_eval = np.linspace(0, -3, 100) + t_eval = [0, -3] with self.assertRaisesRegex( pybamm.SolverError, "t_eval must increase monotonically" ): @@ -614,15 +599,13 @@ def test_failures(self): solver = pybamm.IDAKLUSolver() - t_eval = np.linspace(0, 3, 100) + t_eval = [0, 3] with self.assertRaisesRegex(pybamm.SolverError, "FAILURE IDA"): solver.solve(model, t_eval) def test_dae_solver_algebraic_model(self): for form in ["casadi", "iree"]: - if (form == "jax" or form == "iree") and not pybamm.have_jax(): - continue - if (form == "iree") and not pybamm.have_iree(): + if (form == "iree") and (not pybamm.have_jax() or not pybamm.have_iree()): continue if form == "casadi": root_method = "casadi" @@ -641,7 +624,7 @@ def test_dae_solver_algebraic_model(self): root_method=root_method, options={"jax_evaluator": "iree"} if form == "iree" else {}, ) - t_eval = np.linspace(0, 1) + t_eval = [0, 1] solution = solver.solve(model, t_eval) np.testing.assert_array_equal(solution.y, -1) @@ -661,16 +644,17 @@ def test_banded(self): disc = pybamm.Discretisation(mesh, model.default_spatial_methods) disc.process_model(model) - t_eval = np.linspace(0, 3600, 100) + t_interp = np.linspace(0, 3600, 100) + t_eval = [t_interp[0], t_interp[-1]] solver = pybamm.IDAKLUSolver() - soln = solver.solve(model, t_eval) + soln = solver.solve(model, t_eval, t_interp=t_interp) options = { "jacobian": "banded", "linear_solver": "SUNLinSol_Band", } solver_banded = pybamm.IDAKLUSolver(options=options) - soln_banded = solver_banded.solve(model, t_eval) + soln_banded = solver_banded.solve(model, t_eval, t_interp=t_interp) np.testing.assert_array_almost_equal(soln.y, soln_banded.y, 5) @@ -685,7 +669,7 @@ def test_setup_options(self): disc.process_model(model) t_interp = np.linspace(0, 1) - t_eval = np.array([t_interp[0], t_interp[-1]]) + t_eval = [t_interp[0], t_interp[-1]] solver = pybamm.IDAKLUSolver() soln_base = solver.solve(model, t_eval, t_interp=t_interp) @@ -750,7 +734,7 @@ def test_setup_options(self): np.testing.assert_array_almost_equal(soln.y, soln_base.y, 4) else: with self.assertRaises(ValueError): - soln = solver.solve(model, t_eval) + soln = solver.solve(model, t_eval, t_interp=t_interp) def test_solver_options(self): model = pybamm.BaseModel() @@ -763,7 +747,7 @@ def test_solver_options(self): disc.process_model(model) t_interp = np.linspace(0, 1) - t_eval = np.array([t_interp[0], t_interp[-1]]) + t_eval = [t_interp[0], t_interp[-1]] solver = pybamm.IDAKLUSolver() soln_base = solver.solve(model, t_eval, t_interp=t_interp) @@ -822,7 +806,8 @@ def test_with_output_variables(self): # the 'output_variables' option for each variable in turn, confirming # equivalence input_parameters = {} # Sensitivities dictionary - t_eval = np.linspace(0, 3600, 100) + t_interp = np.linspace(0, 3600, 100) + t_eval = [t_interp[0], t_interp[-1]] # construct model def construct_model(): @@ -891,6 +876,7 @@ def construct_model(): t_eval, inputs=input_parameters, calculate_sensitivities=True, + t_interp=t_interp, ) # Solve for a subset of variables and compare results @@ -904,6 +890,7 @@ def construct_model(): construct_model(), t_eval, inputs=input_parameters, + t_interp=t_interp, ) # Compare output to sol_all @@ -927,9 +914,7 @@ def test_with_output_variables_and_sensitivities(self): # equivalence for form in ["casadi", "iree"]: - if (form == "jax" or form == "iree") and not pybamm.have_jax(): - continue - if (form == "iree") and not pybamm.have_iree(): + if (form == "iree") and (not pybamm.have_jax() or not pybamm.have_iree()): continue if form == "casadi": root_method = "casadi" @@ -953,7 +938,8 @@ def test_with_output_variables_and_sensitivities(self): disc = pybamm.Discretisation(mesh, model.default_spatial_methods) disc.process_model(model) - t_eval = np.linspace(0, 100, 5) + t_interp = np.linspace(0, 100, 5) + t_eval = [t_interp[0], t_interp[-1]] options = { "linear_solver": "SUNLinSol_KLU", @@ -985,6 +971,7 @@ def test_with_output_variables_and_sensitivities(self): t_eval, inputs=input_parameters, calculate_sensitivities=True, + t_interp=t_interp, ) # Solve for a subset of variables and compare results @@ -1000,14 +987,15 @@ def test_with_output_variables_and_sensitivities(self): t_eval, inputs=input_parameters, calculate_sensitivities=True, + t_interp=t_interp, ) # Compare output to sol_all tol = 1e-5 if form != "iree" else 1e-2 # iree has reduced precision for varname in output_variables: np.testing.assert_array_almost_equal( - sol[varname](t_eval), - sol_all[varname](t_eval), + sol[varname](t_interp), + sol_all[varname](t_interp), tol, err_msg=f"Failed for {varname} with form {form}", ) @@ -1114,7 +1102,7 @@ def test_python_idaklu_deprecation_errors(self): disc = pybamm.Discretisation() disc.process_model(model) - t_eval = np.linspace(0, 3, 100) + t_eval = [0, 3] solver = pybamm.IDAKLUSolver( root_method="lm", From ad41dbe9c483ae8cf74a05f37bf16c42769a54e2 Mon Sep 17 00:00:00 2001 From: "Eric G. Kratz" Date: Fri, 30 Aug 2024 15:39:28 -0400 Subject: [PATCH 76/82] Rename have_x to has_x to improve how logic reads (#4398) * Rename functions * style: pre-commit fixes * Update CHANGELOG.md --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- CHANGELOG.md | 2 ++ docs/source/api/util.rst | 2 +- examples/scripts/compare_dae_solver.py | 2 +- src/pybamm/__init__.py | 4 ++-- .../operations/evaluate_python.py | 6 ++--- src/pybamm/solvers/idaklu_jax.py | 6 ++--- src/pybamm/solvers/idaklu_solver.py | 6 ++--- src/pybamm/solvers/jax_bdf_solver.py | 4 ++-- src/pybamm/solvers/jax_solver.py | 4 ++-- src/pybamm/util.py | 2 +- .../base_lithium_ion_tests.py | 4 ++-- .../test_lithium_ion/test_mpm.py | 2 +- tests/integration/test_solvers/test_idaklu.py | 2 +- tests/unit/test_citations.py | 4 ++-- .../test_simulation_with_experiment.py | 2 +- .../test_operations/test_evaluate_python.py | 18 +++++++------- tests/unit/test_solvers/test_base_solver.py | 4 ++-- tests/unit/test_solvers/test_idaklu_jax.py | 6 ++--- tests/unit/test_solvers/test_idaklu_solver.py | 24 +++++++++---------- .../unit/test_solvers/test_jax_bdf_solver.py | 4 ++-- tests/unit/test_solvers/test_jax_solver.py | 4 ++-- tests/unit/test_solvers/test_scipy_solver.py | 4 ++-- tests/unit/test_util.py | 2 +- 23 files changed, 59 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38747ea7fb..6c532e839c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ ## Breaking changes +- Replaced `have_jax` with `has_jax`, `have_idaklu` with `has_idaklu`, and + `have_iree` with `has_iree` ([#4398](https://github.com/pybamm-team/PyBaMM/pull/4398)) - Remove deprecated function `pybamm_install_jax` ([#4362](https://github.com/pybamm-team/PyBaMM/pull/4362)) - Removed legacy python-IDAKLU solver. ([#4326](https://github.com/pybamm-team/PyBaMM/pull/4326)) diff --git a/docs/source/api/util.rst b/docs/source/api/util.rst index 7496b59554..824ec6126d 100644 --- a/docs/source/api/util.rst +++ b/docs/source/api/util.rst @@ -16,6 +16,6 @@ Utility functions .. autofunction:: pybamm.load -.. autofunction:: pybamm.have_jax +.. autofunction:: pybamm.has_jax .. autofunction:: pybamm.is_jax_compatible diff --git a/examples/scripts/compare_dae_solver.py b/examples/scripts/compare_dae_solver.py index 815b458f1a..52ead1a242 100644 --- a/examples/scripts/compare_dae_solver.py +++ b/examples/scripts/compare_dae_solver.py @@ -28,7 +28,7 @@ casadi_sol = pybamm.CasadiSolver(atol=1e-8, rtol=1e-8).solve(model, t_eval) solutions = [casadi_sol] -if pybamm.have_idaklu(): +if pybamm.has_idaklu(): klu_sol = pybamm.IDAKLUSolver(atol=1e-8, rtol=1e-8).solve(model, t_eval) solutions.append(klu_sol) else: diff --git a/src/pybamm/__init__.py b/src/pybamm/__init__.py index 75f5f4f160..36ad0b137a 100644 --- a/src/pybamm/__init__.py +++ b/src/pybamm/__init__.py @@ -15,7 +15,7 @@ ) from .util import ( get_parameters_filepath, - have_jax, + has_jax, import_optional_dependency, is_jax_compatible, get_git_commit_info, @@ -170,7 +170,7 @@ from .solvers.jax_bdf_solver import jax_bdf_integrate from .solvers.idaklu_jax import IDAKLUJax -from .solvers.idaklu_solver import IDAKLUSolver, have_idaklu, have_iree +from .solvers.idaklu_solver import IDAKLUSolver, has_idaklu, has_iree # Experiments from .experiment.experiment import Experiment diff --git a/src/pybamm/expression_tree/operations/evaluate_python.py b/src/pybamm/expression_tree/operations/evaluate_python.py index 20a6d4b4a2..a8a37ea7b2 100644 --- a/src/pybamm/expression_tree/operations/evaluate_python.py +++ b/src/pybamm/expression_tree/operations/evaluate_python.py @@ -11,7 +11,7 @@ import pybamm -if pybamm.have_jax(): +if pybamm.has_jax(): import jax platform = jax.lib.xla_bridge.get_backend().platform.casefold() @@ -43,7 +43,7 @@ class JaxCooMatrix: def __init__( self, row: ArrayLike, col: ArrayLike, data: ArrayLike, shape: tuple[int, int] ): - if not pybamm.have_jax(): # pragma: no cover + if not pybamm.has_jax(): # pragma: no cover raise ModuleNotFoundError( "Jax or jaxlib is not installed, please see https://docs.pybamm.org/en/latest/source/user_guide/installation/gnu-linux-mac.html#optional-jaxsolver" ) @@ -527,7 +527,7 @@ class EvaluatorJax: """ def __init__(self, symbol: pybamm.Symbol): - if not pybamm.have_jax(): # pragma: no cover + if not pybamm.has_jax(): # pragma: no cover raise ModuleNotFoundError( "Jax or jaxlib is not installed, please see https://docs.pybamm.org/en/latest/source/user_guide/installation/gnu-linux-mac.html#optional-jaxsolver" ) diff --git a/src/pybamm/solvers/idaklu_jax.py b/src/pybamm/solvers/idaklu_jax.py index 5a73d42c6e..991c16775e 100644 --- a/src/pybamm/solvers/idaklu_jax.py +++ b/src/pybamm/solvers/idaklu_jax.py @@ -22,7 +22,7 @@ except ImportError: # pragma: no cover idaklu_spec = None -if pybamm.have_jax(): +if pybamm.has_jax(): import jax from jax import lax from jax import numpy as jnp @@ -57,11 +57,11 @@ def __init__( calculate_sensitivities=True, t_interp=None, ): - if not pybamm.have_jax(): + if not pybamm.has_jax(): raise ModuleNotFoundError( "Jax or jaxlib is not installed, please see https://docs.pybamm.org/en/latest/source/user_guide/installation/gnu-linux-mac.html#optional-jaxsolver" ) # pragma: no cover - if not pybamm.have_idaklu(): + if not pybamm.has_idaklu(): raise ModuleNotFoundError( "IDAKLU is not installed, please see https://docs.pybamm.org/en/latest/source/user_guide/installation/index.html" ) # pragma: no cover diff --git a/src/pybamm/solvers/idaklu_solver.py b/src/pybamm/solvers/idaklu_solver.py index b92006d12d..41e0c8855f 100644 --- a/src/pybamm/solvers/idaklu_solver.py +++ b/src/pybamm/solvers/idaklu_solver.py @@ -14,7 +14,7 @@ import warnings -if pybamm.have_jax(): +if pybamm.has_jax(): import jax from jax import numpy as jnp @@ -33,11 +33,11 @@ idaklu_spec = None -def have_idaklu(): +def has_idaklu(): return idaklu_spec is not None -def have_iree(): +def has_iree(): try: import iree.compiler # noqa: F401 diff --git a/src/pybamm/solvers/jax_bdf_solver.py b/src/pybamm/solvers/jax_bdf_solver.py index 2c7bdc6d17..6f0c62b9a8 100644 --- a/src/pybamm/solvers/jax_bdf_solver.py +++ b/src/pybamm/solvers/jax_bdf_solver.py @@ -7,7 +7,7 @@ import pybamm -if pybamm.have_jax(): +if pybamm.has_jax(): import jax import jax.numpy as jnp from jax import core, dtypes @@ -1007,7 +1007,7 @@ def jax_bdf_integrate(func, y0, t_eval, *args, rtol=1e-6, atol=1e-6, mass=None): calculated state vector at each of the m time points """ - if not pybamm.have_jax(): + if not pybamm.has_jax(): raise ModuleNotFoundError( "Jax or jaxlib is not installed, please see https://docs.pybamm.org/en/latest/source/user_guide/installation/gnu-linux-mac.html#optional-jaxsolver" ) diff --git a/src/pybamm/solvers/jax_solver.py b/src/pybamm/solvers/jax_solver.py index 26a069e0fe..da5fd4983a 100644 --- a/src/pybamm/solvers/jax_solver.py +++ b/src/pybamm/solvers/jax_solver.py @@ -6,7 +6,7 @@ import pybamm -if pybamm.have_jax(): +if pybamm.has_jax(): import jax import jax.numpy as jnp from jax.experimental.ode import odeint @@ -59,7 +59,7 @@ def __init__( extrap_tol=None, extra_options=None, ): - if not pybamm.have_jax(): + if not pybamm.has_jax(): raise ModuleNotFoundError( "Jax or jaxlib is not installed, please see https://docs.pybamm.org/en/latest/source/user_guide/installation/gnu-linux-mac.html#optional-jaxsolver" ) diff --git a/src/pybamm/util.py b/src/pybamm/util.py index fd94eb88f4..527c55f526 100644 --- a/src/pybamm/util.py +++ b/src/pybamm/util.py @@ -264,7 +264,7 @@ def get_parameters_filepath(path): return os.path.join(pybamm.__path__[0], path) -def have_jax(): +def has_jax(): """ Check if jax and jaxlib are installed with the correct versions diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_tests.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_tests.py index eddf2aa1e4..60e8dfb819 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_tests.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/base_lithium_ion_tests.py @@ -22,7 +22,7 @@ def test_sensitivities(self): param = pybamm.ParameterValues("Ecker2015") rtol = 1e-6 atol = 1e-6 - if pybamm.have_idaklu(): + if pybamm.has_idaklu(): solver = pybamm.IDAKLUSolver(rtol=rtol, atol=atol) else: solver = pybamm.CasadiSolver(rtol=rtol, atol=atol) @@ -53,7 +53,7 @@ def test_optimisations(self): to_python = optimtest.evaluate_model(to_python=True) np.testing.assert_array_almost_equal(original, to_python) - if pybamm.have_jax(): + if pybamm.has_jax(): to_jax = optimtest.evaluate_model(to_jax=True) np.testing.assert_array_almost_equal(original, to_jax) diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py index 6e67f349fa..00bce1a9d7 100644 --- a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py +++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py @@ -25,7 +25,7 @@ def test_optimisations(self): to_python = optimtest.evaluate_model(to_python=True) np.testing.assert_array_almost_equal(original, to_python) - if pybamm.have_jax(): + if pybamm.has_jax(): to_jax = optimtest.evaluate_model(to_jax=True) np.testing.assert_array_almost_equal(original, to_jax) diff --git a/tests/integration/test_solvers/test_idaklu.py b/tests/integration/test_solvers/test_idaklu.py index d70b64c783..88faa80dde 100644 --- a/tests/integration/test_solvers/test_idaklu.py +++ b/tests/integration/test_solvers/test_idaklu.py @@ -3,7 +3,7 @@ import numpy as np -@pytest.mark.skipif(not pybamm.have_idaklu(), reason="idaklu solver is not installed") +@pytest.mark.skipif(not pybamm.has_idaklu(), reason="idaklu solver is not installed") class TestIDAKLUSolver: def test_on_spme(self): model = pybamm.lithium_ion.SPMe() diff --git a/tests/unit/test_citations.py b/tests/unit/test_citations.py index 0928cc993c..7133cf234a 100644 --- a/tests/unit/test_citations.py +++ b/tests/unit/test_citations.py @@ -423,14 +423,14 @@ def test_solver_citations(self): assert "Virtanen2020" in citations._papers_to_cite assert "Virtanen2020" in citations._citation_tags.keys() - if pybamm.have_idaklu(): + if pybamm.has_idaklu(): citations._reset() assert "Hindmarsh2005" not in citations._papers_to_cite pybamm.IDAKLUSolver() assert "Hindmarsh2005" in citations._papers_to_cite assert "Hindmarsh2005" in citations._citation_tags.keys() - @pytest.mark.skipif(not pybamm.have_jax(), reason="jax or jaxlib is not installed") + @pytest.mark.skipif(not pybamm.has_jax(), reason="jax or jaxlib is not installed") def test_jax_citations(self): citations = pybamm.citations citations._reset() diff --git a/tests/unit/test_experiments/test_simulation_with_experiment.py b/tests/unit/test_experiments/test_simulation_with_experiment.py index c4f55889a1..3507d6e5c1 100644 --- a/tests/unit/test_experiments/test_simulation_with_experiment.py +++ b/tests/unit/test_experiments/test_simulation_with_experiment.py @@ -168,7 +168,7 @@ def test_run_experiment_multiple_times(self): sol1["Voltage [V]"].data, sol2["Voltage [V]"].data ) - @unittest.skipIf(not pybamm.have_idaklu(), "idaklu solver is not installed") + @unittest.skipIf(not pybamm.has_idaklu(), "idaklu solver is not installed") def test_run_experiment_cccv_solvers(self): experiment_2step = pybamm.Experiment( [ diff --git a/tests/unit/test_expression_tree/test_operations/test_evaluate_python.py b/tests/unit/test_expression_tree/test_operations/test_evaluate_python.py index 518d8f8231..e6d8a0da83 100644 --- a/tests/unit/test_expression_tree/test_operations/test_evaluate_python.py +++ b/tests/unit/test_expression_tree/test_operations/test_evaluate_python.py @@ -12,7 +12,7 @@ from collections import OrderedDict import re -if pybamm.have_jax(): +if pybamm.has_jax(): import jax from tests import ( function_test, @@ -446,7 +446,7 @@ def test_evaluator_python(self): result = evaluator(t=t, y=y) np.testing.assert_allclose(result, expr.evaluate(t=t, y=y)) - @pytest.mark.skipif(not pybamm.have_jax(), reason="jax or jaxlib is not installed") + @pytest.mark.skipif(not pybamm.has_jax(), reason="jax or jaxlib is not installed") def test_find_symbols_jax(self): # test sparse conversion constant_symbols = OrderedDict() @@ -459,7 +459,7 @@ def test_find_symbols_jax(self): next(iter(constant_symbols.values())).toarray(), A.entries.toarray() ) - @pytest.mark.skipif(not pybamm.have_jax(), reason="jax or jaxlib is not installed") + @pytest.mark.skipif(not pybamm.has_jax(), reason="jax or jaxlib is not installed") def test_evaluator_jax(self): a = pybamm.StateVector(slice(0, 1)) b = pybamm.StateVector(slice(1, 2)) @@ -621,7 +621,7 @@ def test_evaluator_jax(self): result = evaluator(t=t, y=y) np.testing.assert_allclose(result, expr.evaluate(t=t, y=y)) - @pytest.mark.skipif(not pybamm.have_jax(), reason="jax or jaxlib is not installed") + @pytest.mark.skipif(not pybamm.has_jax(), reason="jax or jaxlib is not installed") def test_evaluator_jax_jacobian(self): a = pybamm.StateVector(slice(0, 1)) y_tests = [np.array([[2.0]]), np.array([[1.0]]), np.array([1.0])] @@ -636,7 +636,7 @@ def test_evaluator_jax_jacobian(self): result_true = evaluator_jac(t=None, y=y) np.testing.assert_allclose(result_test, result_true) - @pytest.mark.skipif(not pybamm.have_jax(), reason="jax or jaxlib is not installed") + @pytest.mark.skipif(not pybamm.has_jax(), reason="jax or jaxlib is not installed") def test_evaluator_jax_jvp(self): a = pybamm.StateVector(slice(0, 1)) y_tests = [np.array([[2.0]]), np.array([[1.0]]), np.array([1.0])] @@ -656,7 +656,7 @@ def test_evaluator_jax_jvp(self): np.testing.assert_allclose(result_test, result_true) np.testing.assert_allclose(result_test_times_v, result_true_times_v) - @pytest.mark.skipif(not pybamm.have_jax(), reason="jax or jaxlib is not installed") + @pytest.mark.skipif(not pybamm.has_jax(), reason="jax or jaxlib is not installed") def test_evaluator_jax_debug(self): a = pybamm.StateVector(slice(0, 1)) expr = a**2 @@ -664,7 +664,7 @@ def test_evaluator_jax_debug(self): evaluator = pybamm.EvaluatorJax(expr) evaluator.debug(y=y_test) - @pytest.mark.skipif(not pybamm.have_jax(), reason="jax or jaxlib is not installed") + @pytest.mark.skipif(not pybamm.has_jax(), reason="jax or jaxlib is not installed") def test_evaluator_jax_inputs(self): a = pybamm.InputParameter("a") expr = a**2 @@ -672,7 +672,7 @@ def test_evaluator_jax_inputs(self): result = evaluator(inputs={"a": 2}) assert result == 4 - @pytest.mark.skipif(not pybamm.have_jax(), reason="jax or jaxlib is not installed") + @pytest.mark.skipif(not pybamm.has_jax(), reason="jax or jaxlib is not installed") def test_evaluator_jax_demotion(self): for demote in [True, False]: pybamm.demote_expressions_to_32bit = demote # global flag @@ -734,7 +734,7 @@ def test_evaluator_jax_demotion(self): assert all(str(c_i.dtype)[-2:] == target_dtype for c_i in c_demoted.col) pybamm.demote_expressions_to_32bit = False - @pytest.mark.skipif(not pybamm.have_jax(), reason="jax or jaxlib is not installed") + @pytest.mark.skipif(not pybamm.has_jax(), reason="jax or jaxlib is not installed") def test_jax_coo_matrix(self): A = pybamm.JaxCooMatrix([0, 1], [0, 1], [1.0, 2.0], (2, 2)) Adense = jax.numpy.array([[1.0, 0], [0, 2.0]]) diff --git a/tests/unit/test_solvers/test_base_solver.py b/tests/unit/test_solvers/test_base_solver.py index e86b0f702e..a4b43e1dd2 100644 --- a/tests/unit/test_solvers/test_base_solver.py +++ b/tests/unit/test_solvers/test_base_solver.py @@ -355,12 +355,12 @@ def test_multiprocess_context(self): assert solver.get_platform_context("Linux") == "fork" assert solver.get_platform_context("Darwin") == "fork" - @unittest.skipIf(not pybamm.have_idaklu(), "idaklu solver is not installed") + @unittest.skipIf(not pybamm.has_idaklu(), "idaklu solver is not installed") def test_sensitivities(self): def exact_diff_a(y, a, b): return np.array([[y[0] ** 2 + 2 * a], [y[0]]]) - @unittest.skipIf(not pybamm.have_jax(), "jax or jaxlib is not installed") + @unittest.skipIf(not pybamm.has_jax(), "jax or jaxlib is not installed") def exact_diff_b(y, a, b): return np.array([[y[0]], [0]]) diff --git a/tests/unit/test_solvers/test_idaklu_jax.py b/tests/unit/test_solvers/test_idaklu_jax.py index d985991929..a99f108f40 100644 --- a/tests/unit/test_solvers/test_idaklu_jax.py +++ b/tests/unit/test_solvers/test_idaklu_jax.py @@ -9,7 +9,7 @@ import unittest testcase = [] -if pybamm.have_idaklu() and pybamm.have_jax(): +if pybamm.has_idaklu() and pybamm.has_jax(): from jax.tree_util import tree_flatten import jax import jax.numpy as jnp @@ -87,7 +87,7 @@ def no_jit(f): # Check the interface throws an appropriate error if either IDAKLU or JAX not available @unittest.skipIf( - pybamm.have_idaklu() and pybamm.have_jax(), + pybamm.has_idaklu() and pybamm.has_jax(), "Both IDAKLU and JAX are available", ) class TestIDAKLUJax_NoJax(unittest.TestCase): @@ -97,7 +97,7 @@ def test_instantiate_fails(self): @unittest.skipIf( - not pybamm.have_idaklu() or not pybamm.have_jax(), + not pybamm.has_idaklu() or not pybamm.has_jax(), "IDAKLU Solver and/or JAX are not available", ) class TestIDAKLUJax(unittest.TestCase): diff --git a/tests/unit/test_solvers/test_idaklu_solver.py b/tests/unit/test_solvers/test_idaklu_solver.py index 5da4e7e628..5d71b5f945 100644 --- a/tests/unit/test_solvers/test_idaklu_solver.py +++ b/tests/unit/test_solvers/test_idaklu_solver.py @@ -13,14 +13,14 @@ @pytest.mark.cibw -@unittest.skipIf(not pybamm.have_idaklu(), "idaklu solver is not installed") +@unittest.skipIf(not pybamm.has_idaklu(), "idaklu solver is not installed") class TestIDAKLUSolver(unittest.TestCase): def test_ida_roberts_klu(self): # this test implements a python version of the ida Roberts # example provided in sundials # see sundials ida examples pdf for form in ["casadi", "iree"]: - if (form == "iree") and (not pybamm.have_jax() or not pybamm.have_iree()): + if (form == "iree") and (not pybamm.has_jax() or not pybamm.has_iree()): continue if form == "casadi": root_method = "casadi" @@ -66,7 +66,7 @@ def test_ida_roberts_klu(self): def test_model_events(self): for form in ["casadi", "iree"]: - if (form == "iree") and (not pybamm.have_jax() or not pybamm.have_iree()): + if (form == "iree") and (not pybamm.has_jax() or not pybamm.has_iree()): continue if form == "casadi": root_method = "casadi" @@ -187,7 +187,7 @@ def test_model_events(self): def test_input_params(self): # test a mix of scalar and vector input params for form in ["casadi", "iree"]: - if (form == "iree") and (not pybamm.have_jax() or not pybamm.have_iree()): + if (form == "iree") and (not pybamm.has_jax() or not pybamm.has_iree()): continue if form == "casadi": root_method = "casadi" @@ -245,9 +245,7 @@ def test_input_params(self): def test_sensitivities_initial_condition(self): for form in ["casadi", "iree"]: for output_variables in [[], ["2v"]]: - if (form == "iree") and ( - not pybamm.have_jax() or not pybamm.have_iree() - ): + if (form == "iree") and (not pybamm.has_jax() or not pybamm.has_iree()): continue if form == "casadi": root_method = "casadi" @@ -298,7 +296,7 @@ def test_ida_roberts_klu_sensitivities(self): # example provided in sundials # see sundials ida examples pdf for form in ["casadi", "iree"]: - if (form == "iree") and (not pybamm.have_jax() or not pybamm.have_iree()): + if (form == "iree") and (not pybamm.has_jax() or not pybamm.has_iree()): continue if form == "casadi": root_method = "casadi" @@ -404,7 +402,7 @@ def test_ida_roberts_consistent_initialization(self): # example provided in sundials # see sundials ida examples pdf for form in ["casadi", "iree"]: - if (form == "iree") and (not pybamm.have_jax() or not pybamm.have_iree()): + if (form == "iree") and (not pybamm.has_jax() or not pybamm.has_iree()): continue if form == "casadi": root_method = "casadi" @@ -446,7 +444,7 @@ def test_sensitivities_with_events(self): # example provided in sundials # see sundials ida examples pdf for form in ["casadi", "iree"]: - if (form == "iree") and (not pybamm.have_jax() or not pybamm.have_iree()): + if (form == "iree") and (not pybamm.has_jax() or not pybamm.has_iree()): continue if form == "casadi": root_method = "casadi" @@ -605,7 +603,7 @@ def test_failures(self): def test_dae_solver_algebraic_model(self): for form in ["casadi", "iree"]: - if (form == "iree") and (not pybamm.have_jax() or not pybamm.have_iree()): + if (form == "iree") and (not pybamm.has_jax() or not pybamm.has_iree()): continue if form == "casadi": root_method = "casadi" @@ -914,7 +912,7 @@ def test_with_output_variables_and_sensitivities(self): # equivalence for form in ["casadi", "iree"]: - if (form == "iree") and (not pybamm.have_jax() or not pybamm.have_iree()): + if (form == "iree") and (not pybamm.has_jax() or not pybamm.has_iree()): continue if form == "casadi": root_method = "casadi" @@ -1087,7 +1085,7 @@ def test_interpolate_time_step_start_offset(self): def test_python_idaklu_deprecation_errors(self): for form in ["python", "", "jax"]: - if form == "jax" and not pybamm.have_jax(): + if form == "jax" and not pybamm.has_jax(): continue model = pybamm.BaseModel() diff --git a/tests/unit/test_solvers/test_jax_bdf_solver.py b/tests/unit/test_solvers/test_jax_bdf_solver.py index e02bdb2510..e0064ae463 100644 --- a/tests/unit/test_solvers/test_jax_bdf_solver.py +++ b/tests/unit/test_solvers/test_jax_bdf_solver.py @@ -5,11 +5,11 @@ import sys import numpy as np -if pybamm.have_jax(): +if pybamm.has_jax(): import jax -@unittest.skipIf(not pybamm.have_jax(), "jax or jaxlib is not installed") +@unittest.skipIf(not pybamm.has_jax(), "jax or jaxlib is not installed") class TestJaxBDFSolver(unittest.TestCase): def test_solver_(self): # Trailing _ manipulates the random seed # Create model diff --git a/tests/unit/test_solvers/test_jax_solver.py b/tests/unit/test_solvers/test_jax_solver.py index 4f34497626..b1c293c2f2 100644 --- a/tests/unit/test_solvers/test_jax_solver.py +++ b/tests/unit/test_solvers/test_jax_solver.py @@ -5,11 +5,11 @@ import sys import numpy as np -if pybamm.have_jax(): +if pybamm.has_jax(): import jax -@unittest.skipIf(not pybamm.have_jax(), "jax or jaxlib is not installed") +@unittest.skipIf(not pybamm.has_jax(), "jax or jaxlib is not installed") class TestJaxSolver(unittest.TestCase): def test_model_solver(self): # Create model diff --git a/tests/unit/test_solvers/test_scipy_solver.py b/tests/unit/test_solvers/test_scipy_solver.py index c6afd16704..446206e95c 100644 --- a/tests/unit/test_solvers/test_scipy_solver.py +++ b/tests/unit/test_solvers/test_scipy_solver.py @@ -11,7 +11,7 @@ class TestScipySolver(unittest.TestCase): def test_model_solver_python_and_jax(self): - if pybamm.have_jax(): + if pybamm.has_jax(): formats = ["python", "jax"] else: formats = ["python"] @@ -339,7 +339,7 @@ def test_model_solver_multiple_inputs_initial_conditions_error(self): solver.solve(model, t_eval, inputs=inputs_list, nproc=2) def test_model_solver_multiple_inputs_jax_format(self): - if pybamm.have_jax(): + if pybamm.has_jax(): # Create model model = pybamm.BaseModel() model.convert_to_format = "jax" diff --git a/tests/unit/test_util.py b/tests/unit/test_util.py index 1b621d98f0..058b7d4a14 100644 --- a/tests/unit/test_util.py +++ b/tests/unit/test_util.py @@ -88,7 +88,7 @@ def test_get_parameters_filepath(self): path = os.path.join(package_dir, tempfile_obj.name) assert pybamm.get_parameters_filepath(tempfile_obj.name) == path - @pytest.mark.skipif(not pybamm.have_jax(), reason="JAX is not installed") + @pytest.mark.skipif(not pybamm.has_jax(), reason="JAX is not installed") def test_is_jax_compatible(self): assert pybamm.is_jax_compatible() From 16e06f598dc60d7469d617a05a2fd1a68d22e76b Mon Sep 17 00:00:00 2001 From: Santhosh <52504160+santacodes@users.noreply.github.com> Date: Sun, 1 Sep 2024 22:24:58 +0530 Subject: [PATCH 77/82] `uv` support in CI (#4353) * Using uv for testing and benchmark workflows * fix failing notebook tests --- .github/workflows/benchmark_on_push.yml | 7 +++++-- .github/workflows/periodic_benchmarks.yml | 7 +++++-- .github/workflows/run_periodic_tests.yml | 20 ++++++++++++++---- .github/workflows/test_on_push.yml | 25 ++++++++++++++++++----- noxfile.py | 4 ++-- 5 files changed, 48 insertions(+), 15 deletions(-) diff --git a/.github/workflows/benchmark_on_push.yml b/.github/workflows/benchmark_on_push.yml index b0da71461e..2883eb5f26 100644 --- a/.github/workflows/benchmark_on_push.yml +++ b/.github/workflows/benchmark_on_push.yml @@ -23,10 +23,13 @@ jobs: sudo apt-get update sudo apt install gfortran gcc libopenblas-dev + - name: Set up uv + run: python -m pip install uv + - name: Install python dependencies run: | - python -m pip install --upgrade pip wheel setuptools wget cmake casadi numpy - python -m pip install asv[virtualenv] + python -m uv pip install --upgrade pip wheel setuptools wget cmake casadi numpy + python -m uv pip install asv[virtualenv] - name: Install SuiteSparse and SUNDIALS run: python scripts/install_KLU_Sundials.py diff --git a/.github/workflows/periodic_benchmarks.yml b/.github/workflows/periodic_benchmarks.yml index faa008ff05..641627c0ba 100644 --- a/.github/workflows/periodic_benchmarks.yml +++ b/.github/workflows/periodic_benchmarks.yml @@ -31,10 +31,13 @@ jobs: sudo apt-get update sudo apt-get install gfortran gcc libopenblas-dev + - name: Set up uv + run: python -m pip install uv + - name: Install python dependencies run: | - python -m pip install --upgrade pip wheel setuptools wget cmake casadi numpy - python -m pip install asv[virtualenv] + python -m uv pip install --upgrade pip wheel setuptools wget cmake casadi numpy + python -m uv pip install asv[virtualenv] - name: Install SuiteSparse and SUNDIALS run: python scripts/install_KLU_Sundials.py diff --git a/.github/workflows/run_periodic_tests.yml b/.github/workflows/run_periodic_tests.yml index 2dd9ef8a89..9f10a9c6f7 100644 --- a/.github/workflows/run_periodic_tests.yml +++ b/.github/workflows/run_periodic_tests.yml @@ -68,8 +68,11 @@ jobs: with: python-version: ${{ matrix.python-version }} + - name: Set up uv + run: python -m pip install uv + - name: Install nox - run: python -m pip install nox + run: python -m uv pip install nox[uv] - name: Install SuiteSparse and SUNDIALS on GNU/Linux and macOS timeout-minutes: 10 @@ -114,8 +117,11 @@ jobs: with: python-version: 3.11 + - name: Set up uv + run: python -m pip install uv + - name: Install nox - run: python -m pip install nox + run: python -m uv pip install nox[uv] - name: Install docs dependencies and run doctests for GNU/Linux run: python -m nox -s doctests @@ -141,8 +147,11 @@ jobs: with: python-version: 3.12 + - name: Set up uv + run: python -m pip install uv + - name: Install nox - run: python -m pip install nox + run: python -m uv pip install nox[uv] - name: Install SuiteSparse and SUNDIALS on GNU/Linux timeout-minutes: 10 @@ -169,8 +178,11 @@ jobs: with: python-version: 3.12 + - name: Set up uv + run: python -m pip install uv + - name: Install nox - run: python -m pip install nox + run: python -m uv pip install nox[uv] - name: Install SuiteSparse and SUNDIALS on GNU/Linux timeout-minutes: 10 diff --git a/.github/workflows/test_on_push.yml b/.github/workflows/test_on_push.yml index adfb698a69..9224b7df36 100644 --- a/.github/workflows/test_on_push.yml +++ b/.github/workflows/test_on_push.yml @@ -27,9 +27,12 @@ jobs: with: python-version: 3.12 + - name: Set up uv + run: python -m pip install uv + - name: Check style run: | - python -m pip install pre-commit + python -m uv pip install pre-commit pre-commit run -a run_unit_integration_and_coverage_tests: @@ -86,8 +89,11 @@ jobs: python-version: ${{ matrix.python-version }} cache: 'pip' + - name: Set up uv + run: python -m pip install uv + - name: Install nox - run: python -m pip install nox + run: python -m uv pip install nox[uv] - name: Cache pybamm-requires nox environment for GNU/Linux and macOS uses: actions/cache@v4 @@ -158,8 +164,11 @@ jobs: python-version: 3.11 cache: 'pip' + - name: Set up uv + run: python -m pip install uv + - name: Install nox - run: python -m pip install nox + run: python -m uv pip install nox[uv] - name: Install docs dependencies and run doctests for GNU/Linux run: python -m nox -s doctests @@ -198,8 +207,11 @@ jobs: python-version: 3.12 cache: 'pip' + - name: Set up uv + run: python -m pip install uv + - name: Install nox - run: python -m pip install nox + run: python -m uv pip install nox[uv] - name: Cache pybamm-requires nox environment for GNU/Linux uses: actions/cache@v4 @@ -251,8 +263,11 @@ jobs: python-version: 3.12 cache: 'pip' + - name: Set up uv + run: python -m pip install uv + - name: Install nox - run: python -m pip install nox + run: python -m uv pip install nox[uv] - name: Cache pybamm-requires nox environment for GNU/Linux uses: actions/cache@v4 diff --git a/noxfile.py b/noxfile.py index a34d6e81f4..6567ed167c 100644 --- a/noxfile.py +++ b/noxfile.py @@ -7,7 +7,7 @@ # Options to modify nox behaviour -nox.options.default_venv_backend = "virtualenv" +nox.options.default_venv_backend = "uv|virtualenv" nox.options.reuse_existing_virtualenvs = True if sys.platform != "win32": nox.options.sessions = ["pre-commit", "pybamm-requires", "unit"] @@ -207,7 +207,7 @@ def run_examples(session): """Run the examples tests for Jupyter notebooks.""" set_environment_variables(PYBAMM_ENV, session=session) session.install("setuptools", silent=False) - session.install("-e", ".[all,dev]", silent=False) + session.install("-e", ".[all,dev,jax]", silent=False) notebooks_to_test = session.posargs if session.posargs else [] session.run( "pytest", "--nbmake", *notebooks_to_test, "docs/source/examples/", external=True From 4b4b98befd41dc9a0b18faf8dfc3fe70375cb801 Mon Sep 17 00:00:00 2001 From: "Eric G. Kratz" Date: Mon, 2 Sep 2024 10:20:26 -0400 Subject: [PATCH 78/82] Streamline release process (#4376) * Modify update version workflow * Simplify workflow * A few more fixes * Style * Fix link errors * Remove manual trigger * Remove redundant work * Fix workflow text * Test workflow * Change target branch * Fix * Remove some test code --- .github/release_workflow.md | 97 +++++++++++++++------------- .github/workflows/update_version.yml | 79 +++------------------- 2 files changed, 60 insertions(+), 116 deletions(-) diff --git a/.github/release_workflow.md b/.github/release_workflow.md index 89a22e7d38..00c7b73dbf 100644 --- a/.github/release_workflow.md +++ b/.github/release_workflow.md @@ -1,10 +1,13 @@ # Release workflow -This file contains the workflow required to make a `PyBaMM` release on GitHub, PyPI, and conda-forge by the maintainers. +This file contains the workflow required to make a `PyBaMM` release on +GitHub, PyPI, and conda-forge by the maintainers. -## rc0 releases (automated) +## Initial release (automated) -1. The `update_version.yml` workflow will run on every 1st of January, May and September, updating incrementing the version to `vYY.MMrc0` by running `scripts/update_version.py` in the following files - +1. The `update_version.yml` workflow will run on every 1st of January, May + and September, updating incrementing the version to `vYY.MM.0` by running + `scripts/update_version.py` in the following files: - `pybamm/version.py` - `docs/conf.py` @@ -13,21 +16,27 @@ This file contains the workflow required to make a `PyBaMM` release on GitHub, P - `vcpkg.json` - `CHANGELOG.md` - These changes will be automatically pushed to a new branch `vYY.MM` and a PR from `vvYY.MM` to `develop` will be created (to sync the branches). + These changes will be automatically pushed to a new branch `vYY.MM` + and a PR from `vYY.MM` to `main` will be created. -2. Create a new GitHub _pre-release_ with the tag `vYY.MMrc0` from the `vYY.MM` branch and a description copied from `CHANGELOG.md`. +2. Create a new GitHub _release_ with the tag `vYY.MM.0` from the `vYY.MM` + branch and a description copied from `CHANGELOG.md`. -3. This release will automatically trigger `publish_pypi.yml` and create a _pre-release_ on PyPI. +3. This release will automatically trigger `publish_pypi.yml` and create a + _release_ on PyPI. -## rcX releases (manual) +## Bug fix releases (manual) -If a new release candidate is required after the release of `rc{X-1}` - +If a new release is required after the release of `vYY.MM.{x-1}` - -1. Cherry-pick the bug fix (no new features should be added to `vYY.MM` once `rc{X-1}` is released) commit to `vYY.MM` branch once the fix is merged into `develop`. The CHANGELOG entry for such fixes should go under the `rc{X-1}` heading in `CHANGELOG.md` +1. Create a new branch for the `vYY.MM.x` release using the `vYY.MM.{x-1}` tag. -2. Run `update_version.yml` manually while using `append_to_tag` to specify the release candidate version number (`rc1`, `rc2`, ...). +2. Cherry-pick the bug fixes to `vYY.MM.x` branch once the fix is + merged into `develop`. The CHANGELOG entry for such fixes should go under the + `YY.MM.x` heading in `CHANGELOG.md` -3. This will increment the version to `vYY.MMrcX` by running `scripts/update_version.py` in the following files - +3. Run `scripts/update_version.py` manually while setting `VERSION=vYY.MM.x` + in your environment. This will update the version in the following files: - `pybamm/version.py` - `docs/conf.py` @@ -36,45 +45,41 @@ If a new release candidate is required after the release of `rc{X-1}` - - `vcpkg.json` - `CHANGELOG.md` - These changes will be automatically pushed to the existing `vYY.MM` branch and a PR will be created to update version strings in `develop`. + Commit the changes to your release branch. -4. Create a new GitHub _pre-release_ with the same tag (`vYY.MMrcX`) from the `vYY.MM` branch and a description copied from `CHANGELOG.md`. +4. Create a PR for the release and configure it to merge into the `main` branch. -5. This release will automatically trigger `publish_pypi.yml` and create a _pre-release_ on PyPI. - -## Actual release (manual) - -Once satisfied with the release candidates - - -1. Run `update_version.yml` manually, leaving the `append_to_tag` field blank ("") for an actual release. - -2. This will increment the version to `vYY.MMrcX` by running `scripts/update_version.py` in the following files - - - - `pybamm/version.py` - - `docs/conf.py` - - `CITATION.cff` - - `pyproject.toml` - - `vcpkg.json` - - `CHANGELOG.md` - - These changes will be automatically pushed to the existing `vYY.MM` branch and a PR will be created to update version strings in `develop`. - -3. Next, a PR from `vYY.MM` to `main` will be generated that should be merged once all the tests pass. - -4. Create a new GitHub _release_ with the same tag from the `main` branch and a description copied from `CHANGELOG.md`. - -5. This release will automatically trigger `publish_pypi.yml` and create a _release_ on PyPI. +5. Create a new GitHub release with the same tag (`YY.MM.x`) from the `main` + branch and a description copied from `CHANGELOG.md`. This release will + automatically trigger `publish_pypi.yml` and create a _release_ on PyPI. ## Other checks Some other essential things to check throughout the release process - -- If updating our custom vcpkg registory entries [pybamm-team/sundials-vcpkg-registry](https://github.com/pybamm-team/sundials-vcpkg-registry) or [pybamm-team/casadi-vcpkg-registry](https://github.com/pybamm-team/casadi-vcpkg-registry) (used to build Windows wheels), make sure to update the baseline of the registories in vcpkg-configuration.json to the latest commit id. -- Update jax and jaxlib to the latest version in `pybamm.util` and `pyproject.toml`, fixing any bugs that arise -- As the release workflow is initiated by the `release` event, it's important to note that the default `GITHUB_REF` used by `actions/checkout` during the checkout process will correspond to the tag created during the release process. Consequently, the workflows will consistently build PyBaMM based on the commit associated with this tag. Should new commits be introduced to the `vYY.MM` branch, such as those addressing build issues, it becomes necessary to manually update this tag to point to the most recent commit - - ``` - git tag -f - git push -f # can only be carried out by the maintainers - ``` -- If changes are made to the API, console scripts, entry points, new optional dependencies are added, support for major Python versions is dropped or added, or core project information and metadata are modified at the time of the release, make sure to update the `meta.yaml` file in the `recipe/` folder of the [conda-forge/pybamm-feedstock](https://github.com/conda-forge/pybamm-feedstock) repository accordingly by following the instructions in the [conda-forge documentation](https://conda-forge.org/docs/maintainer/updating_pkgs.html#updating-the-feedstock-repository) and re-rendering the recipe -- The conda-forge release workflow will automatically be triggered following a stable PyPI release, and the aforementioned updates should be carried out directly in the main repository by pushing changes to the automated PR created by the conda-forge-bot. A manual PR can also be created if the updates are not included in the automated PR for some reason. This manual PR **must** bump the build number in `meta.yaml` and **must** be from a personal fork of the repository. +- If updating our custom vcpkg registry entries + [sundials-vcpkg-registry][SUNDIALS_VCPKG] + or [casadi-vcpkg-registry][CASADI_VCPKG] (used to build Windows + wheels), make sure to update the baseline of the registries in + vcpkg-configuration.json to the latest commit id. +- Update jax and jaxlib to the latest version in `pybamm.util` and + `pyproject.toml`, fixing any bugs that arise. +- If changes are made to the API, console scripts, entry points, new optional + dependencies are added, support for major Python versions is dropped or + added, or core project information and metadata are modified at the time + of the release, make sure to update the `meta.yaml` file in the `recipe/` + folder of the [pybamm-feedstock][PYBAMM_FEED] repository accordingly by + following the instructions in the [conda-forge documentation][FEED_GUIDE] and + re-rendering the recipe. +- The conda-forge release workflow will automatically be triggered following + a stable PyPI release, and the aforementioned updates should be carried + out directly in the main repository by pushing changes to the automated PR + created by the conda-forge-bot. A manual PR can also be created if the + updates are not included in the automated PR for some reason. This manual + PR **must** bump the build number in `meta.yaml` and **must** be from a + personal fork of the repository. + +[SUNDIALS_VCPKG]: https://github.com/pybamm-team/sundials-vcpkg-registry +[CASADI_VCPKG]: https://github.com/pybamm-team/casadi-vcpkg-registry +[PYBAMM_FEED]: https://github.com/conda-forge/pybamm-feedstock +[FEED_GUIDE]: https://conda-forge.org/docs/maintainer/updating_pkgs.html#updating-the-feedstock-repository diff --git a/.github/workflows/update_version.yml b/.github/workflows/update_version.yml index eb62469778..899667d2de 100644 --- a/.github/workflows/update_version.yml +++ b/.github/workflows/update_version.yml @@ -1,11 +1,6 @@ name: Update version on: - workflow_dispatch: - inputs: - append_to_tag: - description: 'Leave blank for an actual release or "rc1", "rc2", ..., for release candidates."' - default: "" schedule: # Run at 10 am UTC on day-of-month 1 in January, May, and September. - cron: "0 10 1 1,5,9 *" @@ -18,29 +13,13 @@ jobs: steps: - name: Get current date for the first release candidate - if: github.event_name == 'schedule' run: | - echo "VERSION=$(date +'v%y.%-m')rc0" >> $GITHUB_ENV - echo "NON_RC_VERSION=$(date +'v%y.%-m')" >> $GITHUB_ENV + echo "VERSION=$(date +'v%y.%-m').0" >> $GITHUB_ENV - - name: Get current date for a manual release - if: github.event_name == 'workflow_dispatch' - run: | - echo "VERSION=$(date +'v%y.%-m')${{ github.event.inputs.append_to_tag }}" >> $GITHUB_ENV - echo "NON_RC_VERSION=$(date +'v%y.%-m')" >> $GITHUB_ENV - - # the schedule workflow is for rc0 release - uses: actions/checkout@v4 - if: github.event_name == 'schedule' with: ref: 'develop' - # the dispatch workflow is for rcX and final releases - - uses: actions/checkout@v4 - if: github.event_name == 'workflow_dispatch' - with: - ref: '${{ env.NON_RC_VERSION }}' - - name: Set up Python uses: actions/setup-python@v5 with: @@ -48,65 +27,25 @@ jobs: - name: Install dependencies run: | - pip install wheel - pip install --editable ".[all]" + pip install -e ".[all]" - # update all the version strings and add CHANGELOG headings + # Update all the version strings and add CHANGELOG headings - name: Update version run: python scripts/update_version.py - # create a new version branch for rc0 release and commit + # Create a new version branch for the release and commit - uses: EndBug/add-and-commit@v9 - if: github.event_name == 'schedule' with: message: 'Bump to ${{ env.VERSION }}' - new_branch: '${{ env.NON_RC_VERSION }}' - - # use the already created release branch for rcX + final releases - # and commit - - uses: EndBug/add-and-commit@v9 - if: github.event_name == 'workflow_dispatch' - with: - message: 'Bump to ${{ env.VERSION }}' - - # checkout to develop for updating versions in the same - - uses: actions/checkout@v4 - with: - ref: 'develop' - - # update all the version strings - - name: Update version - if: github.event_name == 'workflow_dispatch' - run: python scripts/update_version.py - - # create a pull request updating versions in develop - - name: Create Pull Request - id: version_pr - uses: peter-evans/create-pull-request@v6 - with: - delete-branch: true - branch-suffix: short-commit-hash - base: develop - commit-message: Update version to ${{ env.VERSION }} - title: Bump to ${{ env.VERSION }} - body: | - - [x] Update to ${{ env.VERSION }} - - [ ] Check the [release workflow](https://github.com/pybamm-team/PyBaMM/blob/develop/.github/release_workflow.md) - - # checkout to the version branch for the final release - - uses: actions/checkout@v4 - if: github.event_name == 'workflow_dispatch' && !startsWith(github.event.inputs.append_to_tag, 'rc') - with: - ref: '${{ env.NON_RC_VERSION }}' + new_branch: '${{ env.VERSION }}' - # for final releases, create a PR from version branch to main - - name: Make a PR from ${{ env.NON_RC_VERSION }} to main + # Create a PR from version branch to main + - name: Make a PR from ${{ env.VERSION }} to main id: release_pr - if: github.event_name == 'workflow_dispatch' && !startsWith(github.event.inputs.append_to_tag, 'rc') uses: repo-sync/pull-request@v2 with: - source_branch: '${{ env.NON_RC_VERSION }}' + source_branch: '${{ env.VERSION }}' destination_branch: "main" - pr_title: "Make release ${{ env.NON_RC_VERSION }}" + pr_title: "Make release ${{ env.VERSION }}" pr_body: "**Check the [release workflow](https://github.com/pybamm-team/PyBaMM/blob/develop/.github/release_workflow.md)**" github_token: ${{ secrets.GITHUB_TOKEN }} From 396741eec6076a83c3104aa9b73d2f7957201ab7 Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Mon, 2 Sep 2024 22:04:34 +0530 Subject: [PATCH 79/82] Fix CasADi libs workaround for macOS wheels, update to CasADi 3.6.6 for Windows wheels (#4391) * Fix CasADi libs workaround for macOS wheels * Bump to CasADi 3.6.6 * Update vcpkg baseline for CasADi 3.6.6 update * libc++ is still not fixed * Fix a typo in the build-system table * Fix GHA macOS versions for images * Fix Windows access violation with `msvcp140.dll` --------- Co-authored-by: Eric G. Kratz --- .github/workflows/publish_pypi.yml | 11 ++++++----- pyproject.toml | 6 +++--- vcpkg-configuration.json | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/workflows/publish_pypi.yml b/.github/workflows/publish_pypi.yml index 7482f03003..9ca277b653 100644 --- a/.github/workflows/publish_pypi.yml +++ b/.github/workflows/publish_pypi.yml @@ -84,7 +84,9 @@ jobs: CMAKE_BUILD_PARALLEL_LEVEL=${{ steps.get_num_cores.outputs.count }} CIBW_ARCHS: AMD64 CIBW_BEFORE_BUILD: python -m pip install setuptools wheel delvewheel # skip CasADi and CMake - CIBW_REPAIR_WHEEL_COMMAND: delvewheel repair -w {dest_dir} {wheel} + # Fix access violation because GHA runners have modified PATH that picks wrong + # msvcp140.dll, see https://github.com/adang1345/delvewheel/issues/54 + CIBW_REPAIR_WHEEL_COMMAND: delvewheel repair --add-path C:/Windows/System32 -w {dest_dir} {wheel} CIBW_TEST_EXTRAS: "all,dev,jax" CIBW_TEST_COMMAND: | python -c "import pybamm; print(pybamm.IDAKLUSolver())" @@ -149,9 +151,6 @@ jobs: - name: Clone pybind11 repo (no history) run: git clone --depth 1 --branch v2.12.0 https://github.com/pybind/pybind11.git -c advice.detachedHead=false - - name: Set macOS-specific environment variables - run: echo "MACOSX_DEPLOYMENT_TARGET=11.0" >> $GITHUB_ENV - - name: Install cibuildwheel run: python -m pip install cibuildwheel @@ -243,13 +242,15 @@ jobs: python scripts/install_KLU_Sundials.py python -m cibuildwheel --output-dir wheelhouse env: + # 10.13 for Intel (macos-12/macos-13), 11.0 for Apple Silicon (macos-14 and macos-latest) + MACOSX_DEPLOYMENT_TARGET: ${{ matrix.os == 'macos-14' && '11.0' || '10.13' }} CIBW_ARCHS_MACOS: auto CIBW_BEFORE_BUILD: python -m pip install cmake casadi setuptools wheel delocate CIBW_REPAIR_WHEEL_COMMAND: | if [[ $(uname -m) == "x86_64" ]]; then delocate-listdeps {wheel} && delocate-wheel -v -w {dest_dir} {wheel} elif [[ $(uname -m) == "arm64" ]]; then - # Use higher macOS target for now: https://github.com/casadi/casadi/issues/3698 + # Use higher macOS target for now since casadi/libc++.1.0.dylib is still not fixed delocate-listdeps {wheel} && delocate-wheel -v -w {dest_dir} {wheel} --require-target-macos-version 11.1 for file in {dest_dir}/*.whl; do mv "$file" "${file//macosx_11_1/macosx_11_0}"; done fi diff --git a/pyproject.toml b/pyproject.toml index e4cce3eccd..34ccd05e20 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,9 +3,9 @@ requires = [ "setuptools>=64", "wheel", # On Windows, use the CasADi vcpkg registry and CMake bundled from MSVC - "casadi>=3.6.5; platform_system!='Windows'", + "casadi>=3.6.6; platform_system!='Windows'", # Note: the version of CasADi as a build-time dependency should be matched - # cross platforms, so updates to its minimum version here should be accompanied + # across platforms, so updates to its minimum version here should be accompanied # by a version bump in https://github.com/pybamm-team/casadi-vcpkg-registry. "cmake; platform_system!='Windows'", ] @@ -37,7 +37,7 @@ classifiers = [ dependencies = [ "numpy>=1.23.5,<2.0.0", "scipy>=1.11.4", - "casadi>=3.6.5", + "casadi>=3.6.6", "xarray>=2022.6.0", "anytree>=2.8.0", "sympy>=1.12", diff --git a/vcpkg-configuration.json b/vcpkg-configuration.json index 11505be46e..8d5c64ac47 100644 --- a/vcpkg-configuration.json +++ b/vcpkg-configuration.json @@ -13,7 +13,7 @@ { "kind": "git", "repository": "https://github.com/pybamm-team/casadi-vcpkg-registry.git", - "baseline": "ceee3ed50246744cdef43517d7d7617b8ac291e7", + "baseline": "1cb93f2fb71be26c874db724940ef8e604ee558e", "packages": ["casadi"] } ] From 1ab27d1e3721e1d150d7f548e14f343c2cbdfdfc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 3 Sep 2024 03:26:10 +0530 Subject: [PATCH 80/82] chore: update pre-commit hooks (#4404) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.6.2 → v0.6.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.2...v0.6.3) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 835678e034..43928bbc56 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.6.2" + rev: "v0.6.3" hooks: - id: ruff args: [--fix, --show-fixes] From bac7570b9bb85c3372dd32f09c8059a899b31c0e Mon Sep 17 00:00:00 2001 From: kratman Date: Tue, 3 Sep 2024 11:46:41 -0400 Subject: [PATCH 81/82] Fix update version script --- scripts/update_version.py | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/scripts/update_version.py b/scripts/update_version.py index 3543f0b07b..a8fd884537 100644 --- a/scripts/update_version.py +++ b/scripts/update_version.py @@ -6,9 +6,6 @@ import os import re from datetime import date -from dateutil.relativedelta import relativedelta - - import pybamm @@ -17,11 +14,7 @@ def update_version(): Opens file and updates the version number """ release_version = os.getenv("VERSION")[1:] - release_date = ( - date.today() - if "rc" in release_version - else date.today() + relativedelta(day=31) - ) + release_date = date.today() # pybamm/version.py with open( @@ -84,15 +77,7 @@ def update_version(): with open(os.path.join(pybamm.root_dir(), "CHANGELOG.md"), "r+") as file: output_list = file.readlines() output_list[0] = changelog_line1 - # add a new heading for rc0 releases - if "rc0" in release_version: - output_list.insert(2, changelog_line2) - else: - # for rcX and final releases, update the already existing rc - # release heading - for i in range(0, len(output_list)): - if re.search("[v]\d\d\.\drc\d", output_list[i]): - output_list[i] = changelog_line2[:-1] + output_list.insert(2, changelog_line2) file.truncate(0) file.seek(0) file.writelines(output_list) From c212bb03b3e6bfb13fd2005b479586ee187d951a Mon Sep 17 00:00:00 2001 From: kratman Date: Tue, 3 Sep 2024 12:24:46 -0400 Subject: [PATCH 82/82] Update change log --- CHANGELOG.md | 2 ++ CITATION.cff | 2 +- pyproject.toml | 2 +- src/pybamm/version.py | 2 +- vcpkg.json | 2 +- 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c532e839c..f5a959748f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # [Unreleased](https://github.com/pybamm-team/PyBaMM/) +# [v24.9.0](https://github.com/pybamm-team/PyBaMM/tree/v24.9.0) - 2024-09-03 + ## Features - Added additional user-configurable options to the (`IDAKLUSolver`) and adjusted the default values to improve performance. ([#4282](https://github.com/pybamm-team/PyBaMM/pull/4282)) diff --git a/CITATION.cff b/CITATION.cff index cc2479e8f3..d128cf485e 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -24,6 +24,6 @@ keywords: - "expression tree" - "python" - "symbolic differentiation" -version: "24.5" +version: "24.9.0" repository-code: "https://github.com/pybamm-team/PyBaMM" title: "Python Battery Mathematical Modelling (PyBaMM)" diff --git a/pyproject.toml b/pyproject.toml index 34ccd05e20..7fb1a5ce95 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ build-backend = "setuptools.build_meta" [project] name = "pybamm" -version = "24.5" +version = "24.9.0" license = { file = "LICENSE.txt" } description = "Python Battery Mathematical Modelling" authors = [{name = "The PyBaMM Team", email = "pybamm@pybamm.org"}] diff --git a/src/pybamm/version.py b/src/pybamm/version.py index edeca1094b..ca0cfd956e 100644 --- a/src/pybamm/version.py +++ b/src/pybamm/version.py @@ -1 +1 @@ -__version__ = "24.5" +__version__ = "24.9.0" diff --git a/vcpkg.json b/vcpkg.json index a4ae73f302..6c50d65524 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,6 +1,6 @@ { "name": "pybamm", - "version-string": "24.5", + "version-string": "24.9.0", "dependencies": [ "casadi", {