From 78c24106f45f9f04787856ed1908710ce210356c Mon Sep 17 00:00:00 2001 From: Pradyot Ranjan <99216956+prady0t@users.noreply.github.com> Date: Tue, 1 Oct 2024 18:53:55 +0530 Subject: [PATCH 01/24] Removing all instances of unittest (#4472) * Removing all instances of unittest Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> * style: pre-commit fixes * Adding ruff rules 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> --- pyproject.toml | 2 + .../test_lithium_ion/test_compare_outputs.py | 12 +--- .../test_interface/test_butler_volmer.py | 57 +++++++------------ tests/testcase.py | 17 ------ .../test_concatenations.py | 7 +-- .../test_expression_tree/test_functions.py | 5 +- .../test_input_parameter.py | 5 +- .../test_operations/test_evaluate_python.py | 11 ---- .../test_parameter_sets/test_Ecker2015.py | 18 ++---- 9 files changed, 33 insertions(+), 101 deletions(-) delete mode 100644 tests/testcase.py diff --git a/pyproject.toml b/pyproject.toml index d2c487ede4..328388bed7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -196,6 +196,8 @@ extend-select = [ "YTT", # flake8-2020 "TID252", # relative-imports "S101", # to identify use of assert statement + "PT027", # remove unittest style assertion + "PT009", # Use pytest.raises instead of unittest-style ] ignore = [ "E741", # Ambiguous variable name 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 57067d6e3b..8ae7393f83 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 @@ -4,11 +4,10 @@ import pybamm import numpy as np -import unittest from tests import StandardOutputComparison -class TestCompareOutputs(unittest.TestCase): +class TestCompareOutputs: def test_compare_outputs_surface_form(self): # load models options = [ @@ -142,12 +141,3 @@ def test_compare_narrow_size_distribution(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_submodels/test_interface/test_butler_volmer.py b/tests/integration/test_models/test_submodels/test_interface/test_butler_volmer.py index f1f02350cd..7b2100792e 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 @@ -2,15 +2,15 @@ # Tests for the electrode-electrolyte interface equations # +import pytest import pybamm from tests import get_discretisation_for_testing -import unittest import numpy as np -class TestButlerVolmer(unittest.TestCase): - def setUp(self): +class TestButlerVolmer: + def setup_method(self): self.delta_phi_s_n = pybamm.Variable( "surface potential difference [V]", ["negative electrode"], @@ -73,7 +73,7 @@ def setUp(self): "reaction source terms [A.m-3]": 1, } - def tearDown(self): + def teardown_method(self): del self.variables del self.c_e_n del self.c_e_p @@ -114,12 +114,12 @@ def test_creation(self): ] # negative electrode Butler-Volmer is Multiplication - self.assertIsInstance(j_n, pybamm.Multiplication) - self.assertEqual(j_n.domain, ["negative electrode"]) + assert isinstance(j_n, pybamm.Multiplication) + assert j_n.domain == ["negative electrode"] # positive electrode Butler-Volmer is Multiplication - self.assertIsInstance(j_p, pybamm.Multiplication) - self.assertEqual(j_p.domain, ["positive electrode"]) + assert isinstance(j_p, pybamm.Multiplication) + assert j_p.domain == ["positive electrode"] def test_set_parameters(self): param = pybamm.LithiumIonParameters() @@ -159,9 +159,9 @@ def test_set_parameters(self): j_p = parameter_values.process_symbol(j_p) # Test for x in j_n.pre_order(): - self.assertNotIsInstance(x, pybamm.Parameter) + assert not isinstance(x, pybamm.Parameter) for x in j_p.pre_order(): - self.assertNotIsInstance(x, pybamm.Parameter) + assert not isinstance(x, pybamm.Parameter) def test_discretisation(self): param = pybamm.LithiumIonParameters() @@ -219,17 +219,13 @@ def test_discretisation(self): [mesh["negative electrode"].nodes, mesh["positive electrode"].nodes] ) y = np.concatenate([submesh**2, submesh**3, submesh**4]) - self.assertEqual( - j_n.evaluate(None, y).shape, (mesh["negative electrode"].npts, 1) - ) - self.assertEqual( - j_p.evaluate(None, y).shape, (mesh["positive electrode"].npts, 1) - ) + assert j_n.evaluate(None, y).shape == (mesh["negative electrode"].npts, 1) + assert j_p.evaluate(None, y).shape == (mesh["positive electrode"].npts, 1) # test concatenated butler-volmer whole_cell = ["negative electrode", "separator", "positive electrode"] whole_cell_mesh = disc.mesh[whole_cell] - self.assertEqual(j.evaluate(None, y).shape, (whole_cell_mesh.npts, 1)) + assert j.evaluate(None, y).shape == (whole_cell_mesh.npts, 1) def test_diff_c_e_lead_acid(self): # With intercalation @@ -361,27 +357,12 @@ def j_p(delta_phi): j_n_FD = parameter_values.process_symbol( (j_n(delta_phi + h) - j_n(delta_phi - h)) / (2 * h) ) - self.assertAlmostEqual( - j_n_diff.evaluate(inputs={"delta_phi": 0.5}) - / j_n_FD.evaluate(inputs={"delta_phi": 0.5}), - 1, - places=5, - ) + assert j_n_diff.evaluate(inputs={"delta_phi": 0.5}) / j_n_FD.evaluate( + inputs={"delta_phi": 0.5} + ) == pytest.approx(1, abs=1e-05) j_p_FD = parameter_values.process_symbol( (j_p(delta_phi + h) - j_p(delta_phi - h)) / (2 * h) ) - self.assertAlmostEqual( - j_p_diff.evaluate(inputs={"delta_phi": 0.5}) - / j_p_FD.evaluate(inputs={"delta_phi": 0.5}), - 1, - places=5, - ) - - -if __name__ == "__main__": - print("Add -v for more debug output") - import sys - - if "-v" in sys.argv: - debug = True - unittest.main() + assert j_p_diff.evaluate(inputs={"delta_phi": 0.5}) / j_p_FD.evaluate( + inputs={"delta_phi": 0.5} + ) == pytest.approx(1, abs=1e-05) diff --git a/tests/testcase.py b/tests/testcase.py deleted file mode 100644 index 0b9b1f5dee..0000000000 --- a/tests/testcase.py +++ /dev/null @@ -1,17 +0,0 @@ -# -# Custom TestCase class for pybamm -# -import unittest - - -class TestCase(unittest.TestCase): - """ - Custom TestCase class for PyBaMM - TO BE REMOVED - """ - - def assertDomainEqual(self, 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 != []} - self.assertEqual(a_dict, b_dict) diff --git a/tests/unit/test_expression_tree/test_concatenations.py b/tests/unit/test_expression_tree/test_concatenations.py index 1d7ccef610..9d653a4d51 100644 --- a/tests/unit/test_expression_tree/test_concatenations.py +++ b/tests/unit/test_expression_tree/test_concatenations.py @@ -2,7 +2,6 @@ # Tests for the Concatenation class and subclasses # import pytest -import unittest.mock as mock from tests import assert_domain_equal @@ -377,7 +376,7 @@ def test_to_equation(self): # Test concat_sym assert pybamm.Concatenation(a, b).to_equation() == func_symbol - def test_to_from_json(self): + def test_to_from_json(self, mocker): # test DomainConcatenation mesh = get_mesh_for_testing() a = pybamm.Symbol("a", domain=["negative electrode"]) @@ -386,7 +385,7 @@ def test_to_from_json(self): json_dict = { "name": "domain_concatenation", - "id": mock.ANY, + "id": mocker.ANY, "domains": { "primary": ["negative electrode", "separator", "positive electrode"], "secondary": [], @@ -429,7 +428,7 @@ def test_to_from_json(self): np_json = { "name": "numpy_concatenation", - "id": mock.ANY, + "id": mocker.ANY, "domains": { "primary": [], "secondary": [], diff --git a/tests/unit/test_expression_tree/test_functions.py b/tests/unit/test_expression_tree/test_functions.py index 5f5324c0ae..fca7d7ee67 100644 --- a/tests/unit/test_expression_tree/test_functions.py +++ b/tests/unit/test_expression_tree/test_functions.py @@ -3,7 +3,6 @@ # import pytest -import unittest.mock as mock import numpy as np from scipy import special @@ -399,7 +398,7 @@ def test_tanh(self): abs=1e-05, ) - def test_erf(self): + def test_erf(self, mocker): a = pybamm.InputParameter("a") fun = pybamm.erf(a) assert fun.evaluate(inputs={"a": 3}) == special.erf(3) @@ -416,7 +415,7 @@ def test_erf(self): # test creation from json input_json = { "name": "erf", - "id": mock.ANY, + "id": mocker.ANY, "function": "erf", "children": [a], } diff --git a/tests/unit/test_expression_tree/test_input_parameter.py b/tests/unit/test_expression_tree/test_input_parameter.py index 87cbe79a31..884341cb4f 100644 --- a/tests/unit/test_expression_tree/test_input_parameter.py +++ b/tests/unit/test_expression_tree/test_input_parameter.py @@ -4,7 +4,6 @@ import numpy as np import pybamm import pytest -import unittest.mock as mock class TestInputParameter: @@ -49,12 +48,12 @@ def test_errors(self): with pytest.raises(KeyError): a.evaluate() - def test_to_from_json(self): + def test_to_from_json(self, mocker): a = pybamm.InputParameter("a") json_dict = { "name": "a", - "id": mock.ANY, + "id": mocker.ANY, "domain": [], "expected_size": 1, } 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 e6d8a0da83..14b980b358 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 @@ -6,7 +6,6 @@ import pybamm from tests import get_discretisation_for_testing, get_1p1d_discretisation_for_testing -import unittest import numpy as np import scipy.sparse from collections import OrderedDict @@ -746,13 +745,3 @@ def test_jax_coo_matrix(self): with pytest.raises(NotImplementedError): A.multiply(v) - - -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.py b/tests/unit/test_parameters/test_parameter_sets/test_Ecker2015.py index 4be67175d7..d703b46200 100644 --- a/tests/unit/test_parameters/test_parameter_sets/test_Ecker2015.py +++ b/tests/unit/test_parameters/test_parameter_sets/test_Ecker2015.py @@ -2,11 +2,11 @@ # Tests for O'Kane (2022) parameter set # +import pytest import pybamm -import unittest -class TestEcker2015(unittest.TestCase): +class TestEcker2015: def test_functions(self): param = pybamm.ParameterValues("Ecker2015") sto = pybamm.Scalar(0.5) @@ -40,16 +40,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() From 591315406c1342b0b3015b1a0eaee4d475ecdbbb Mon Sep 17 00:00:00 2001 From: "Eric G. Kratz" Date: Tue, 1 Oct 2024 09:56:30 -0400 Subject: [PATCH 02/24] Require setuptools for citations (#4478) * Require setuptools for citations * Update pyproject.toml --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 328388bed7..a70f712e3e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,6 +84,7 @@ plot = [ "matplotlib>=3.6.0", ] cite = [ + "setuptools", # Fix for a pybtex issue "pybtex>=0.24.0", ] # Battery Parameter eXchange format From 5cf68bdbdb93191a42f7a4ec2b79debcb3faa73e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 15:02:04 +0100 Subject: [PATCH 03/24] chore: update pre-commit hooks (#4476) 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.7 → v0.6.8](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.7...v0.6.8) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Eric G. Kratz --- .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 306118e254..51e4d7c23e 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.7" + rev: "v0.6.8" hooks: - id: ruff args: [--fix, --show-fixes] From 08e5cf24f2c3f60afbba851b3b22e514aac80c27 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 10:20:26 -0400 Subject: [PATCH 04/24] Build(deps): bump github/codeql-action in the actions group (#4474) Bumps the actions group with 1 update: [github/codeql-action](https://github.com/github/codeql-action). Updates `github/codeql-action` from 3.26.8 to 3.26.10 - [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/294a9d92911152fe08befb9ec03e240add280cb3...e2b3eafc8d227b0241d48be5f425d47c2d750a13) --- 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 3186f0e49b..017919c4e7 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@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8 + uses: github/codeql-action/upload-sarif@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.26.10 with: sarif_file: results.sarif From 444ecc13295dee5b8e4cb2813d9220d5724b94b3 Mon Sep 17 00:00:00 2001 From: Marc Berliner <34451391+MarcBerliner@users.noreply.github.com> Date: Wed, 2 Oct 2024 11:54:40 -0400 Subject: [PATCH 05/24] Fast Hermite interpolation and observables (#4464) * fast observables * fix release and types * good * faster interp * `double` -> `realtype` * clean separation * good again * private members * cleanup * fix codecov, tests * naming * codecov * codacy, cse/expand * fix `try/except` * Update CHANGELOG.md * address comments * initialize `save_hermite` * fix codecov --------- Co-authored-by: Eric G. Kratz --- CHANGELOG.md | 1 + CMakeLists.txt | 2 + setup.py | 2 + src/pybamm/__init__.py | 2 +- src/pybamm/plotting/quick_plot.py | 17 +- src/pybamm/solvers/c_solvers/idaklu.cpp | 26 + .../c_solvers/idaklu/IDAKLUSolverOpenMP.hpp | 43 +- .../c_solvers/idaklu/IDAKLUSolverOpenMP.inl | 224 ++++- .../solvers/c_solvers/idaklu/Options.cpp | 1 + .../solvers/c_solvers/idaklu/Options.hpp | 1 + .../solvers/c_solvers/idaklu/Solution.hpp | 6 +- .../solvers/c_solvers/idaklu/SolutionData.cpp | 34 +- .../solvers/c_solvers/idaklu/SolutionData.hpp | 9 + .../solvers/c_solvers/idaklu/common.hpp | 1 + .../solvers/c_solvers/idaklu/observe.cpp | 343 +++++++ .../solvers/c_solvers/idaklu/observe.hpp | 42 + src/pybamm/solvers/idaklu_solver.py | 50 +- src/pybamm/solvers/processed_variable.py | 914 ++++++++++++++---- .../solvers/processed_variable_computed.py | 60 +- src/pybamm/solvers/solution.py | 181 +++- .../test_models/standard_output_comparison.py | 1 + .../test_simulation_with_experiment.py | 8 +- tests/unit/test_solvers/test_idaklu_solver.py | 58 +- .../test_solvers/test_processed_variable.py | 580 +++++++---- .../test_processed_variable_computed.py | 19 +- tests/unit/test_solvers/test_solution.py | 6 +- 26 files changed, 2021 insertions(+), 610 deletions(-) create mode 100644 src/pybamm/solvers/c_solvers/idaklu/observe.cpp create mode 100644 src/pybamm/solvers/c_solvers/idaklu/observe.hpp diff --git a/CHANGELOG.md b/CHANGELOG.md index f70aae0272..fd590fbc12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Features +- Added Hermite interpolation to the (`IDAKLUSolver`) that improves the accuracy and performance of post-processing variables. ([#4464](https://github.com/pybamm-team/PyBaMM/pull/4464)) - Added sensitivity calculation support for `pybamm.Simulation` and `pybamm.Experiment` ([#4415](https://github.com/pybamm-team/PyBaMM/pull/4415)) - Added OpenMP parallelization to IDAKLU solver for lists of input parameters ([#4449](https://github.com/pybamm-team/PyBaMM/pull/4449)) - Added phase-dependent particle options to LAM diff --git a/CMakeLists.txt b/CMakeLists.txt index a7f68ce7a0..ec594e5ca5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -105,6 +105,8 @@ pybind11_add_module(idaklu 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/observe.hpp + src/pybamm/solvers/c_solvers/idaklu/observe.cpp # IDAKLU expressions - concrete implementations ${IDAKLU_EXPR_CASADI_SOURCE_FILES} ${IDAKLU_EXPR_IREE_SOURCE_FILES} diff --git a/setup.py b/setup.py index 74de1baca4..8a49bfd715 100644 --- a/setup.py +++ b/setup.py @@ -327,6 +327,8 @@ def compile_KLU(): "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/observe.hpp", + "src/pybamm/solvers/c_solvers/idaklu/observe.cpp", "src/pybamm/solvers/c_solvers/idaklu.cpp", ], ) diff --git a/src/pybamm/__init__.py b/src/pybamm/__init__.py index 36ad0b137a..51c7f49969 100644 --- a/src/pybamm/__init__.py +++ b/src/pybamm/__init__.py @@ -157,7 +157,7 @@ # Solver classes from .solvers.solution import Solution, EmptySolution, make_cycle_solution -from .solvers.processed_variable import ProcessedVariable +from .solvers.processed_variable import ProcessedVariable, process_variable from .solvers.processed_variable_computed import ProcessedVariableComputed from .solvers.base_solver import BaseSolver from .solvers.dummy_solver import DummySolver diff --git a/src/pybamm/plotting/quick_plot.py b/src/pybamm/plotting/quick_plot.py index 39dc974f9b..cddce58d77 100644 --- a/src/pybamm/plotting/quick_plot.py +++ b/src/pybamm/plotting/quick_plot.py @@ -419,14 +419,14 @@ def reset_axis(self): spatial_vars = self.spatial_variable_dict[key] var_min = np.min( [ - ax_min(var(self.ts_seconds[i], **spatial_vars, warn=False)) + ax_min(var(self.ts_seconds[i], **spatial_vars)) for i, variable_list in enumerate(variable_lists) for var in variable_list ] ) var_max = np.max( [ - ax_max(var(self.ts_seconds[i], **spatial_vars, warn=False)) + ax_max(var(self.ts_seconds[i], **spatial_vars)) for i, variable_list in enumerate(variable_lists) for var in variable_list ] @@ -512,7 +512,7 @@ def plot(self, t, dynamic=False): full_t = self.ts_seconds[i] (self.plots[key][i][j],) = ax.plot( full_t / self.time_scaling_factor, - variable(full_t, warn=False), + variable(full_t), color=self.colors[i], linestyle=linestyle, ) @@ -548,7 +548,7 @@ def plot(self, t, dynamic=False): linestyle = self.linestyles[j] (self.plots[key][i][j],) = ax.plot( self.first_spatial_variable[key], - variable(t_in_seconds, **spatial_vars, warn=False), + variable(t_in_seconds, **spatial_vars), color=self.colors[i], linestyle=linestyle, zorder=10, @@ -570,13 +570,13 @@ def plot(self, t, dynamic=False): y_name = next(iter(spatial_vars.keys()))[0] x = self.second_spatial_variable[key] y = self.first_spatial_variable[key] - var = variable(t_in_seconds, **spatial_vars, warn=False) + var = variable(t_in_seconds, **spatial_vars) else: x_name = next(iter(spatial_vars.keys()))[0] y_name = list(spatial_vars.keys())[1][0] x = self.first_spatial_variable[key] y = self.second_spatial_variable[key] - var = variable(t_in_seconds, **spatial_vars, warn=False).T + var = variable(t_in_seconds, **spatial_vars).T ax.set_xlabel(f"{x_name} [{self.spatial_unit}]") ax.set_ylabel(f"{y_name} [{self.spatial_unit}]") vmin, vmax = self.variable_limits[key] @@ -710,7 +710,6 @@ def slider_update(self, t): var = variable( time_in_seconds, **self.spatial_variable_dict[key], - warn=False, ) plot[i][j].set_ydata(var) var_min = min(var_min, ax_min(var)) @@ -729,11 +728,11 @@ def slider_update(self, t): if self.x_first_and_y_second[key] is False: x = self.second_spatial_variable[key] y = self.first_spatial_variable[key] - var = variable(time_in_seconds, **spatial_vars, warn=False) + var = variable(time_in_seconds, **spatial_vars) else: x = self.first_spatial_variable[key] y = self.second_spatial_variable[key] - var = variable(time_in_seconds, **spatial_vars, warn=False).T + var = variable(time_in_seconds, **spatial_vars).T # store the plot and the var data (for testing) as cant access # z data from QuadMesh or QuadContourSet object if self.is_y_z[key] is True: diff --git a/src/pybamm/solvers/c_solvers/idaklu.cpp b/src/pybamm/solvers/c_solvers/idaklu.cpp index db7147feb2..82a3cbe91c 100644 --- a/src/pybamm/solvers/c_solvers/idaklu.cpp +++ b/src/pybamm/solvers/c_solvers/idaklu.cpp @@ -9,6 +9,7 @@ #include #include "idaklu/idaklu_solver.hpp" +#include "idaklu/observe.hpp" #include "idaklu/IDAKLUSolverGroup.hpp" #include "idaklu/IdakluJax.hpp" #include "idaklu/common.hpp" @@ -27,6 +28,7 @@ casadi::Function generate_casadi_function(const std::string &data) namespace py = pybind11; PYBIND11_MAKE_OPAQUE(std::vector); +PYBIND11_MAKE_OPAQUE(std::vector); PYBIND11_MAKE_OPAQUE(std::vector); PYBIND11_MODULE(idaklu, m) @@ -34,6 +36,7 @@ PYBIND11_MODULE(idaklu, m) m.doc() = "sundials solvers"; // optional module docstring py::bind_vector>(m, "VectorNdArray"); + py::bind_vector>(m, "VectorRealtypeNdArray"); py::bind_vector>(m, "VectorSolution"); py::class_(m, "IDAKLUSolverGroup") @@ -72,6 +75,27 @@ PYBIND11_MODULE(idaklu, m) py::arg("options"), py::return_value_policy::take_ownership); + m.def("observe", &observe, + "Observe variables", + py::arg("ts"), + py::arg("ys"), + py::arg("inputs"), + py::arg("funcs"), + py::arg("is_f_contiguous"), + py::arg("shape"), + py::return_value_policy::take_ownership); + + m.def("observe_hermite_interp", &observe_hermite_interp, + "Observe and Hermite interpolate variables", + py::arg("t_interp"), + py::arg("ts"), + py::arg("ys"), + py::arg("yps"), + py::arg("inputs"), + py::arg("funcs"), + py::arg("shape"), + py::return_value_policy::take_ownership); + #ifdef IREE_ENABLE m.def("create_iree_solver_group", &create_idaklu_solver_group, "Create a group of iree idaklu solver objects", @@ -167,7 +191,9 @@ PYBIND11_MODULE(idaklu, m) py::class_(m, "solution") .def_readwrite("t", &Solution::t) .def_readwrite("y", &Solution::y) + .def_readwrite("yp", &Solution::yp) .def_readwrite("yS", &Solution::yS) + .def_readwrite("ypS", &Solution::ypS) .def_readwrite("y_term", &Solution::y_term) .def_readwrite("flag", &Solution::flag); } diff --git a/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.hpp b/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.hpp index 92eede3643..ee2c03abff 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.hpp +++ b/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.hpp @@ -54,9 +54,9 @@ class IDAKLUSolverOpenMP : public IDAKLUSolver int const number_of_events; // cppcheck-suppress unusedStructMember int number_of_timesteps; int precon_type; // cppcheck-suppress unusedStructMember - N_Vector yy, yp, y_cache, avtol; // y, y', y cache vector, and absolute tolerance + N_Vector yy, yyp, y_cache, avtol; // y, y', y cache vector, and absolute tolerance N_Vector *yyS; // cppcheck-suppress unusedStructMember - N_Vector *ypS; // cppcheck-suppress unusedStructMember + N_Vector *yypS; // cppcheck-suppress unusedStructMember N_Vector id; // rhs_alg_id realtype rtol; int const jac_times_cjmass_nnz; // cppcheck-suppress unusedStructMember @@ -70,11 +70,14 @@ class IDAKLUSolverOpenMP : public IDAKLUSolver vector res_dvar_dp; bool const sensitivity; // cppcheck-suppress unusedStructMember bool const save_outputs_only; // cppcheck-suppress unusedStructMember + bool save_hermite; // cppcheck-suppress unusedStructMember bool is_ODE; // cppcheck-suppress unusedStructMember int length_of_return_vector; // cppcheck-suppress unusedStructMember vector t; // cppcheck-suppress unusedStructMember vector> y; // cppcheck-suppress unusedStructMember + vector> yp; // cppcheck-suppress unusedStructMember vector>> yS; // cppcheck-suppress unusedStructMember + vector>> ypS; // cppcheck-suppress unusedStructMember SetupOptions const setup_opts; SolverOptions const solver_opts; @@ -144,6 +147,11 @@ class IDAKLUSolverOpenMP : public IDAKLUSolver */ void InitializeStorage(int const N); + /** + * @brief Initialize the storage for Hermite interpolation + */ + void InitializeHermiteStorage(int const N); + /** * @brief Apply user-configurable IDA options */ @@ -190,13 +198,20 @@ class IDAKLUSolverOpenMP : public IDAKLUSolver */ void ExtendAdaptiveArrays(); + /** + * @brief Extend the Hermite interpolation info by 1 + */ + void ExtendHermiteArrays(); + /** * @brief Set the step values */ void SetStep( - realtype &t_val, + realtype &tval, realtype *y_val, + realtype *yp_val, vector const &yS_val, + vector const &ypS_val, int &i_save ); @@ -211,7 +226,9 @@ class IDAKLUSolverOpenMP : public IDAKLUSolver realtype &t_prev, realtype const &t_next, realtype *y_val, + realtype *yp_val, vector const &yS_val, + vector const &ypS_val, int &i_save ); @@ -255,6 +272,26 @@ class IDAKLUSolverOpenMP : public IDAKLUSolver int &i_save ); + /** + * @brief Save the output function results at the requested time + */ + void SetStepHermite( + realtype &t_val, + realtype *yp_val, + const vector &ypS_val, + int &i_save + ); + + /** + * @brief Save the output function sensitivities at the requested time + */ + void SetStepHermiteSensitivities( + realtype &t_val, + realtype *yp_val, + const vector &ypS_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 56f546facf..d128ae1809 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl +++ b/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl @@ -1,7 +1,6 @@ #include "Expressions/Expressions.hpp" #include "sundials_functions.hpp" #include - #include "common.hpp" #include "SolutionData.hpp" @@ -48,7 +47,7 @@ IDAKLUSolverOpenMP::IDAKLUSolverOpenMP( AllocateVectors(); if (sensitivity) { yyS = N_VCloneVectorArray(number_of_parameters, yy); - ypS = N_VCloneVectorArray(number_of_parameters, yp); + yypS = N_VCloneVectorArray(number_of_parameters, yyp); } // set initial values realtype *atval = N_VGetArrayPointer(avtol); @@ -58,14 +57,14 @@ IDAKLUSolverOpenMP::IDAKLUSolverOpenMP( for (int is = 0; is < number_of_parameters; is++) { N_VConst(RCONST(0.0), yyS[is]); - N_VConst(RCONST(0.0), ypS[is]); + N_VConst(RCONST(0.0), yypS[is]); } // create Matrix objects SetMatrix(); // initialise solver - IDAInit(ida_mem, residual_eval, 0, yy, yp); + IDAInit(ida_mem, residual_eval, 0, yy, yyp); // set tolerances rtol = RCONST(rel_tol); @@ -87,6 +86,9 @@ IDAKLUSolverOpenMP::IDAKLUSolverOpenMP( // The default is to solve a DAE for generality. This may be changed // to an ODE during the Initialize() call is_ODE = false; + + // Will be overwritten during the solve() call + save_hermite = solver_opts.hermite_interpolation; } template @@ -95,14 +97,14 @@ void IDAKLUSolverOpenMP::AllocateVectors() { // Create vectors if (setup_opts.num_threads == 1) { yy = N_VNew_Serial(number_of_states, sunctx); - yp = N_VNew_Serial(number_of_states, sunctx); + yyp = N_VNew_Serial(number_of_states, sunctx); y_cache = N_VNew_Serial(number_of_states, sunctx); avtol = N_VNew_Serial(number_of_states, sunctx); id = N_VNew_Serial(number_of_states, sunctx); } else { DEBUG("IDAKLUSolverOpenMP::AllocateVectors OpenMP"); yy = N_VNew_OpenMP(number_of_states, setup_opts.num_threads, sunctx); - yp = N_VNew_OpenMP(number_of_states, setup_opts.num_threads, sunctx); + yyp = N_VNew_OpenMP(number_of_states, setup_opts.num_threads, sunctx); y_cache = 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); @@ -127,6 +129,26 @@ void IDAKLUSolverOpenMP::InitializeStorage(int const N) { vector(length_of_return_vector, 0.0) ) ); + + if (save_hermite) { + InitializeHermiteStorage(N); + } +} + +template +void IDAKLUSolverOpenMP::InitializeHermiteStorage(int const N) { + yp = vector>( + N, + vector(number_of_states, 0.0) + ); + + ypS = vector>>( + N, + vector>( + number_of_parameters, + vector(number_of_states, 0.0) + ) + ); } template @@ -285,7 +307,7 @@ void IDAKLUSolverOpenMP::Initialize() { if (sensitivity) { CheckErrors(IDASensInit(ida_mem, number_of_parameters, IDA_SIMULTANEOUS, - sensitivities_eval, yyS, ypS)); + sensitivities_eval, yyS, yypS)); CheckErrors(IDASensEEtolerances(ida_mem)); } @@ -321,13 +343,13 @@ IDAKLUSolverOpenMP::~IDAKLUSolverOpenMP() { SUNMatDestroy(J); N_VDestroy(avtol); N_VDestroy(yy); - N_VDestroy(yp); + N_VDestroy(yyp); N_VDestroy(y_cache); N_VDestroy(id); if (sensitivity) { N_VDestroyVectorArray(yyS, number_of_parameters); - N_VDestroyVectorArray(ypS, number_of_parameters); + N_VDestroyVectorArray(yypS, number_of_parameters); } IDAFree(&ida_mem); @@ -349,9 +371,15 @@ SolutionData IDAKLUSolverOpenMP::solve( const int number_of_evals = t_eval.size(); const int number_of_interps = t_interp.size(); - if (t.size() < number_of_evals + number_of_interps) { - InitializeStorage(number_of_evals + number_of_interps); - } + // Hermite interpolation is only available when saving + // 1. adaptive steps and 2. the full solution + save_hermite = ( + solver_opts.hermite_interpolation && + save_adaptive_steps && + !save_outputs_only + ); + + InitializeStorage(number_of_evals + number_of_interps); int i_save = 0; @@ -378,12 +406,12 @@ SolutionData IDAKLUSolverOpenMP::solve( // Setup consistent initialization realtype *y_val = N_VGetArrayPointer(yy); - realtype *yp_val = N_VGetArrayPointer(yp); + realtype *yp_val = N_VGetArrayPointer(yyp); vector yS_val(number_of_parameters); vector ypS_val(number_of_parameters); for (int p = 0 ; p < number_of_parameters; p++) { yS_val[p] = N_VGetArrayPointer(yyS[p]); - ypS_val[p] = N_VGetArrayPointer(ypS[p]); + ypS_val[p] = N_VGetArrayPointer(yypS[p]); for (int i = 0; i < number_of_states; i++) { yS_val[p][i] = y0[i + (p + 1) * number_of_states]; ypS_val[p][i] = yp0[i + (p + 1) * number_of_states]; @@ -409,23 +437,27 @@ SolutionData IDAKLUSolverOpenMP::solve( ConsistentInitialization(t0, t_eval_next, init_type); } + // Set the initial stop time + IDASetStopTime(ida_mem, t_eval_next); + + // Progress one step. This must be done before the while loop to ensure + // that we can run IDAGetDky at t0 for dky = 1 + int retval = IDASolve(ida_mem, tf, &t_val, yy, yyp, IDA_ONE_STEP); + + // Store consistent initialization + CheckErrors(IDAGetDky(ida_mem, t0, 0, yy)); if (sensitivity) { - CheckErrors(IDAGetSensDky(ida_mem, t_val, 0, yyS)); + CheckErrors(IDAGetSensDky(ida_mem, t0, 0, yyS)); } - // Store Consistent initialization - SetStep(t0, y_val, yS_val, i_save); + SetStep(t0, y_val, yp_val, yS_val, ypS_val, i_save); - // Set the initial stop time - IDASetStopTime(ida_mem, t_eval_next); + // Reset the states at t = t_val. Sensitivities are handled in the while-loop + CheckErrors(IDAGetDky(ida_mem, t_val, 0, yy)); // 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; @@ -448,18 +480,21 @@ SolutionData IDAKLUSolverOpenMP::solve( if (hit_tinterp) { // Save the interpolated state at t_prev < t < t_val, for all t in t_interp - SetStepInterp(i_interp, + SetStepInterp( + i_interp, t_interp_next, t_interp, t_val, t_prev, t_eval_next, y_val, + yp_val, yS_val, + ypS_val, i_save); } - if (hit_adaptive || hit_teval || hit_event) { + if (hit_adaptive || hit_teval || hit_event || hit_final_time) { if (hit_tinterp) { // Reset the states and sensitivities at t = t_val CheckErrors(IDAGetDky(ida_mem, t_val, 0, yy)); @@ -469,11 +504,16 @@ SolutionData IDAKLUSolverOpenMP::solve( } // Save the current state at t_val - if (hit_adaptive) { - // Dynamically allocate memory for the adaptive step - ExtendAdaptiveArrays(); + // First, check to make sure that the t_val is not equal to the current t value + // If it is, we don't want to save the current state twice + if (!hit_tinterp || t_val != t.back()) { + if (hit_adaptive) { + // Dynamically allocate memory for the adaptive step + ExtendAdaptiveArrays(); + } + + SetStep(t_val, y_val, yp_val, yS_val, ypS_val, i_save); } - SetStep(t_val, y_val, yS_val, i_save); } if (hit_final_time || hit_event) { @@ -481,7 +521,7 @@ SolutionData IDAKLUSolverOpenMP::solve( break; } else if (hit_teval) { // Set the next stop time - i_eval += 1; + i_eval++; t_eval_next = t_eval[i_eval]; CheckErrors(IDASetStopTime(ida_mem, t_eval_next)); @@ -491,6 +531,9 @@ SolutionData IDAKLUSolverOpenMP::solve( } t_prev = t_val; + + // Progress one step + retval = IDASolve(ida_mem, tf, &t_val, yy, yyp, IDA_ONE_STEP); } int const length_of_final_sv_slice = save_outputs_only ? number_of_states : 0; @@ -547,7 +590,50 @@ SolutionData IDAKLUSolverOpenMP::solve( } } - return SolutionData(retval, number_of_timesteps, length_of_return_vector, arg_sens0, arg_sens1, arg_sens2, length_of_final_sv_slice, t_return, y_return, yS_return, yterm_return); + realtype *yp_return = new realtype[(save_hermite ? 1 : 0) * (number_of_timesteps * number_of_states)]; + realtype *ypS_return = new realtype[(save_hermite ? 1 : 0) * (arg_sens0 * arg_sens1 * arg_sens2)]; + if (save_hermite) { + count = 0; + for (size_t i = 0; i < number_of_timesteps; i++) { + for (size_t j = 0; j < number_of_states; j++) { + yp_return[count] = yp[i][j]; + count++; + } + } + + // Sensitivity states, ypS + // Note: Ordering of vector is different if computing outputs vs returning + // the complete state vector + 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); + + ypS_return[count] = ypS[i][k][j]; + count++; + } + } + } + } + + return SolutionData( + retval, + number_of_timesteps, + length_of_return_vector, + arg_sens0, + arg_sens1, + arg_sens2, + length_of_final_sv_slice, + save_hermite, + t_return, + y_return, + yp_return, + yS_return, + ypS_return, + yterm_return); } template @@ -563,14 +649,30 @@ void IDAKLUSolverOpenMP::ExtendAdaptiveArrays() { if (sensitivity) { yS.emplace_back(number_of_parameters, vector(length_of_return_vector, 0.0)); } + + if (save_hermite) { + ExtendHermiteArrays(); + } +} + +template +void IDAKLUSolverOpenMP::ExtendHermiteArrays() { + DEBUG("IDAKLUSolver::ExtendHermiteArrays"); + // States + yp.emplace_back(number_of_states, 0.0); + + // Sensitivity + if (sensitivity) { + ypS.emplace_back(number_of_parameters, vector(number_of_states, 0.0)); + } } template void IDAKLUSolverOpenMP::ReinitializeIntegrator(const realtype& t_val) { DEBUG("IDAKLUSolver::ReinitializeIntegrator"); - CheckErrors(IDAReInit(ida_mem, t_val, yy, yp)); + CheckErrors(IDAReInit(ida_mem, t_val, yy, yyp)); if (sensitivity) { - CheckErrors(IDASensReInit(ida_mem, IDA_SIMULTANEOUS, yyS, ypS)); + CheckErrors(IDASensReInit(ida_mem, IDA_SIMULTANEOUS, yyS, yypS)); } } @@ -609,14 +711,16 @@ void IDAKLUSolverOpenMP::ConsistentInitializationODE( realtype *y_cache_val = N_VGetArrayPointer(y_cache); std::memset(y_cache_val, 0, number_of_states * sizeof(realtype)); // Overwrite yp - residual_eval(t_val, yy, y_cache, yp, functions.get()); + residual_eval(t_val, yy, y_cache, yyp, functions.get()); } template void IDAKLUSolverOpenMP::SetStep( realtype &tval, realtype *y_val, + realtype *yp_val, vector const &yS_val, + vector const &ypS_val, int &i_save ) { // Set adaptive step results for y and yS @@ -629,6 +733,10 @@ void IDAKLUSolverOpenMP::SetStep( SetStepOutput(tval, y_val, yS_val, i_save); } else { SetStepFull(tval, y_val, yS_val, i_save); + + if (save_hermite) { + SetStepHermite(tval, yp_val, ypS_val, i_save); + } } i_save++; @@ -644,7 +752,9 @@ void IDAKLUSolverOpenMP::SetStepInterp( realtype &t_prev, realtype const &t_eval_next, realtype *y_val, + realtype *yp_val, vector const &yS_val, + vector const &ypS_val, int &i_save ) { // Save the state at the requested time @@ -657,7 +767,7 @@ void IDAKLUSolverOpenMP::SetStepInterp( } // Memory is already allocated for the interpolated values - SetStep(t_interp_next, y_val, yS_val, i_save); + SetStep(t_interp_next, y_val, yp_val, yS_val, ypS_val, i_save); i_interp++; if (i_interp == (t_interp.size())) { @@ -769,6 +879,50 @@ void IDAKLUSolverOpenMP::SetStepOutputSensitivities( } } +template +void IDAKLUSolverOpenMP::SetStepHermite( + realtype &tval, + realtype *yp_val, + vector const &ypS_val, + int &i_save +) { + // Set adaptive step results for yp and ypS + DEBUG("IDAKLUSolver::SetStepHermite"); + + // States + CheckErrors(IDAGetDky(ida_mem, tval, 1, yyp)); + auto &yp_back = yp[i_save]; + for (size_t j = 0; j < length_of_return_vector; ++j) { + yp_back[j] = yp_val[j]; + + } + + // Sensitivity + if (sensitivity) { + SetStepHermiteSensitivities(tval, yp_val, ypS_val, i_save); + } +} + +template +void IDAKLUSolverOpenMP::SetStepHermiteSensitivities( + realtype &tval, + realtype *yp_val, + vector const &ypS_val, + int &i_save +) { + DEBUG("IDAKLUSolver::SetStepHermiteSensitivities"); + + // Calculate sensitivities for the full ypS array + CheckErrors(IDAGetSensDky(ida_mem, tval, 1, yypS)); + for (size_t j = 0; j < number_of_parameters; ++j) { + auto &ypS_back_j = ypS[i_save][j]; + auto &ypSval_j = ypS_val[j]; + for (size_t k = 0; k < number_of_states; ++k) { + ypS_back_j[k] = ypSval_j[k]; + } + } +} + template void IDAKLUSolverOpenMP::CheckErrors(int const & flag) { if (flag < 0) { diff --git a/src/pybamm/solvers/c_solvers/idaklu/Options.cpp b/src/pybamm/solvers/c_solvers/idaklu/Options.cpp index 51544040ee..8eb605fe77 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/Options.cpp +++ b/src/pybamm/solvers/c_solvers/idaklu/Options.cpp @@ -149,6 +149,7 @@ SolverOptions::SolverOptions(py::dict &py_opts) 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()), + hermite_interpolation(py_opts["hermite_interpolation"].cast()), // IDA initial conditions calculation calc_ic(py_opts["calc_ic"].cast()), init_all_y_ic(py_opts["init_all_y_ic"].cast()), diff --git a/src/pybamm/solvers/c_solvers/idaklu/Options.hpp b/src/pybamm/solvers/c_solvers/idaklu/Options.hpp index d0c0c1d766..7418c68ec3 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/Options.hpp +++ b/src/pybamm/solvers/c_solvers/idaklu/Options.hpp @@ -38,6 +38,7 @@ struct SolverOptions { double nonlinear_convergence_coefficient; double nonlinear_convergence_coefficient_ic; sunbooleantype suppress_algebraic_error; + bool hermite_interpolation; // IDA initial conditions calculation bool calc_ic; bool init_all_y_ic; diff --git a/src/pybamm/solvers/c_solvers/idaklu/Solution.hpp b/src/pybamm/solvers/c_solvers/idaklu/Solution.hpp index a43e6a7174..8227bb9da8 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/Solution.hpp +++ b/src/pybamm/solvers/c_solvers/idaklu/Solution.hpp @@ -17,8 +17,8 @@ class Solution /** * @brief Constructor */ - 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) + Solution(int &retval, np_array &t_np, np_array &y_np, np_array &yp_np, np_array &yS_np, np_array &ypS_np, np_array &y_term_np) + : flag(retval), t(t_np), y(y_np), yp(yp_np), yS(yS_np), ypS(ypS_np), y_term(y_term_np) { } @@ -30,7 +30,9 @@ class Solution int flag; np_array t; np_array y; + np_array yp; np_array yS; + np_array ypS; np_array y_term; }; diff --git a/src/pybamm/solvers/c_solvers/idaklu/SolutionData.cpp b/src/pybamm/solvers/c_solvers/idaklu/SolutionData.cpp index 00c2ddbccc..bc48c646d3 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/SolutionData.cpp +++ b/src/pybamm/solvers/c_solvers/idaklu/SolutionData.cpp @@ -29,6 +29,20 @@ Solution SolutionData::generate_solution() { free_y_when_done ); + py::capsule free_yp_when_done( + yp_return, + [](void *f) { + realtype *vect = reinterpret_cast(f); + delete[] vect; + } + ); + + np_array yp_ret = np_array( + (save_hermite ? 1 : 0) * number_of_timesteps * length_of_return_vector, + &yp_return[0], + free_yp_when_done + ); + py::capsule free_yS_when_done( yS_return, [](void *f) { @@ -47,6 +61,24 @@ Solution SolutionData::generate_solution() { free_yS_when_done ); + py::capsule free_ypS_when_done( + ypS_return, + [](void *f) { + realtype *vect = reinterpret_cast(f); + delete[] vect; + } + ); + + np_array ypS_ret = np_array( + std::vector { + (save_hermite ? 1 : 0) * arg_sens0, + arg_sens1, + arg_sens2 + }, + &ypS_return[0], + free_ypS_when_done + ); + // Final state slice, yterm py::capsule free_yterm_when_done( yterm_return, @@ -63,5 +95,5 @@ Solution SolutionData::generate_solution() { ); // Store the solution - return Solution(flag, t_ret, y_ret, yS_ret, y_term); + return Solution(flag, t_ret, y_ret, yp_ret, yS_ret, ypS_ret, y_term); } diff --git a/src/pybamm/solvers/c_solvers/idaklu/SolutionData.hpp b/src/pybamm/solvers/c_solvers/idaklu/SolutionData.hpp index 815e41daca..81ca7f5221 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/SolutionData.hpp +++ b/src/pybamm/solvers/c_solvers/idaklu/SolutionData.hpp @@ -27,9 +27,12 @@ class SolutionData int arg_sens1, int arg_sens2, int length_of_final_sv_slice, + bool save_hermite, realtype *t_return, realtype *y_return, + realtype *yp_return, realtype *yS_return, + realtype *ypS_return, realtype *yterm_return): flag(flag), number_of_timesteps(number_of_timesteps), @@ -38,9 +41,12 @@ class SolutionData arg_sens1(arg_sens1), arg_sens2(arg_sens2), length_of_final_sv_slice(length_of_final_sv_slice), + save_hermite(save_hermite), t_return(t_return), y_return(y_return), + yp_return(yp_return), yS_return(yS_return), + ypS_return(ypS_return), yterm_return(yterm_return) {} @@ -64,9 +70,12 @@ class SolutionData int arg_sens1; int arg_sens2; int length_of_final_sv_slice; + bool save_hermite; realtype *t_return; realtype *y_return; + realtype *yp_return; realtype *yS_return; + realtype *ypS_return; realtype *yterm_return; }; diff --git a/src/pybamm/solvers/c_solvers/idaklu/common.hpp b/src/pybamm/solvers/c_solvers/idaklu/common.hpp index 58be90932e..90672080b6 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/common.hpp +++ b/src/pybamm/solvers/c_solvers/idaklu/common.hpp @@ -33,6 +33,7 @@ namespace py = pybind11; // note: we rely on c_style ordering for numpy arrays so don't change this! using np_array = py::array_t; +using np_array_realtype = py::array_t; using np_array_int = py::array_t; /** diff --git a/src/pybamm/solvers/c_solvers/idaklu/observe.cpp b/src/pybamm/solvers/c_solvers/idaklu/observe.cpp new file mode 100644 index 0000000000..8f1d90e55d --- /dev/null +++ b/src/pybamm/solvers/c_solvers/idaklu/observe.cpp @@ -0,0 +1,343 @@ +#include "observe.hpp" + +int _setup_len_spatial(const std::vector& shape) { + // Calculate the product of all dimensions except the last (spatial dimensions) + int size_spatial = 1; + for (size_t i = 0; i < shape.size() - 1; ++i) { + size_spatial *= shape[i]; + } + + if (size_spatial == 0 || shape.back() == 0) { + throw std::invalid_argument("output array must have at least one element"); + } + + return size_spatial; +} + +// Coupled observe and Hermite interpolation of variables +class HermiteInterpolator { +public: + HermiteInterpolator(const py::detail::unchecked_reference& t, + const py::detail::unchecked_reference& y, + const py::detail::unchecked_reference& yp) + : t(t), y(y), yp(yp) {} + + void compute_knots(const size_t j, vector& c, vector& d) const { + // Called at the start of each interval + const realtype h_full = t(j + 1) - t(j); + const realtype inv_h = 1.0 / h_full; + const realtype inv_h2 = inv_h * inv_h; + const realtype inv_h3 = inv_h2 * inv_h; + + for (size_t i = 0; i < y.shape(0); ++i) { + realtype y_ij = y(i, j); + realtype yp_ij = yp(i, j); + realtype y_ijp1 = y(i, j + 1); + realtype yp_ijp1 = yp(i, j + 1); + + c[i] = 3.0 * (y_ijp1 - y_ij) * inv_h2 - (2.0 * yp_ij + yp_ijp1) * inv_h; + d[i] = 2.0 * (y_ij - y_ijp1) * inv_h3 + (yp_ij + yp_ijp1) * inv_h2; + } + } + + void interpolate(vector& entries, + realtype t_interp, + const size_t j, + vector& c, + vector& d) const { + // Must be called after compute_knots + const realtype h = t_interp - t(j); + const realtype h2 = h * h; + const realtype h3 = h2 * h; + + for (size_t i = 0; i < entries.size(); ++i) { + realtype y_ij = y(i, j); + realtype yp_ij = yp(i, j); + entries[i] = y_ij + yp_ij * h + c[i] * h2 + d[i] * h3; + } + } + +private: + const py::detail::unchecked_reference& t; + const py::detail::unchecked_reference& y; + const py::detail::unchecked_reference& yp; +}; + +class TimeSeriesInterpolator { +public: + TimeSeriesInterpolator(const np_array_realtype& _t_interp, + const vector& _ts_data, + const vector& _ys_data, + const vector& _yps_data, + const vector& _inputs, + const vector>& _funcs, + realtype* _entries, + const int _size_spatial) + : t_interp_np(_t_interp), ts_data_np(_ts_data), ys_data_np(_ys_data), + yps_data_np(_yps_data), inputs_np(_inputs), funcs(_funcs), + entries(_entries), size_spatial(_size_spatial) {} + + void process() { + auto t_interp = t_interp_np.unchecked<1>(); + ssize_t i_interp = 0; + int i_entries = 0; + const ssize_t N_interp = t_interp.size(); + + // Main processing within bounds + process_within_bounds(i_interp, i_entries, t_interp, N_interp); + + // Extrapolation for remaining points + if (i_interp < N_interp) { + extrapolate_remaining(i_interp, i_entries, t_interp, N_interp); + } + } + + void process_within_bounds( + ssize_t& i_interp, + int& i_entries, + const py::detail::unchecked_reference& t_interp, + const ssize_t N_interp + ) { + for (size_t i = 0; i < ts_data_np.size(); i++) { + const auto& t_data = ts_data_np[i].unchecked<1>(); + const realtype t_data_final = t_data(t_data.size() - 1); + realtype t_interp_next = t_interp(i_interp); + // Continue if the next interpolation point is beyond the final data point + if (t_interp_next > t_data_final) { + continue; + } + + const auto& y_data = ys_data_np[i].unchecked<2>(); + const auto& yp_data = yps_data_np[i].unchecked<2>(); + const auto input = inputs_np[i].data(); + const auto func = *funcs[i]; + + resize_arrays(y_data.shape(0), funcs[i]); + args[1] = y_buffer.data(); + args[2] = input; + + ssize_t j = 0; + ssize_t j_prev = -1; + const auto itp = HermiteInterpolator(t_data, y_data, yp_data); + while (t_interp_next <= t_data_final) { + for (; j < t_data.size() - 2; ++j) { + if (t_data(j) <= t_interp_next && t_interp_next <= t_data(j + 1)) { + break; + } + } + + if (j != j_prev) { + // Compute c and d for the new interval + itp.compute_knots(j, c, d); + } + + itp.interpolate(y_buffer, t_interp(i_interp), j, c, d); + + args[0] = &t_interp(i_interp); + results[0] = &entries[i_entries]; + func(args.data(), results.data(), iw.data(), w.data(), 0); + + ++i_interp; + if (i_interp == N_interp) { + return; + } + t_interp_next = t_interp(i_interp); + i_entries += size_spatial; + j_prev = j; + } + } + } + + void extrapolate_remaining( + ssize_t& i_interp, + int& i_entries, + const py::detail::unchecked_reference& t_interp, + const ssize_t N_interp + ) { + const auto& t_data = ts_data_np.back().unchecked<1>(); + const auto& y_data = ys_data_np.back().unchecked<2>(); + const auto& yp_data = yps_data_np.back().unchecked<2>(); + const auto input = inputs_np.back().data(); + const auto func = *funcs.back(); + const ssize_t j = t_data.size() - 2; + + resize_arrays(y_data.shape(0), funcs.back()); + args[1] = y_buffer.data(); + args[2] = input; + + const auto itp = HermiteInterpolator(t_data, y_data, yp_data); + itp.compute_knots(j, c, d); + + for (; i_interp < N_interp; ++i_interp) { + const realtype t_interp_next = t_interp(i_interp); + itp.interpolate(y_buffer, t_interp_next, j, c, d); + + args[0] = &t_interp_next; + results[0] = &entries[i_entries]; + func(args.data(), results.data(), iw.data(), w.data(), 0); + + i_entries += size_spatial; + } + } + + void resize_arrays(const int M, std::shared_ptr func) { + args.resize(func->sz_arg()); + results.resize(func->sz_res()); + iw.resize(func->sz_iw()); + w.resize(func->sz_w()); + if (y_buffer.size() < M) { + y_buffer.resize(M); + c.resize(M); + d.resize(M); + } + } + +private: + const np_array_realtype& t_interp_np; + const vector& ts_data_np; + const vector& ys_data_np; + const vector& yps_data_np; + const vector& inputs_np; + const vector>& funcs; + realtype* entries; + const int size_spatial; + vector c; + vector d; + vector y_buffer; + vector args; + vector results; + vector iw; + vector w; +}; + +// Observe the raw data +class TimeSeriesProcessor { +public: + TimeSeriesProcessor(const vector& _ts, + const vector& _ys, + const vector& _inputs, + const vector>& _funcs, + realtype* _entries, + const bool _is_f_contiguous, + const int _size_spatial) + : ts(_ts), ys(_ys), inputs(_inputs), funcs(_funcs), + entries(_entries), is_f_contiguous(_is_f_contiguous), size_spatial(_size_spatial) {} + + void process() { + int i_entries = 0; + for (size_t i = 0; i < ts.size(); i++) { + const auto& t = ts[i].unchecked<1>(); + const auto& y = ys[i].unchecked<2>(); + const auto input = inputs[i].data(); + const auto func = *funcs[i]; + + resize_arrays(y.shape(0), funcs[i]); + args[2] = input; + + for (size_t j = 0; j < t.size(); j++) { + const realtype t_val = t(j); + const realtype* y_val = is_f_contiguous ? &y(0, j) : copy_to_buffer(y_buffer, y, j); + + args[0] = &t_val; + args[1] = y_val; + results[0] = &entries[i_entries]; + + func(args.data(), results.data(), iw.data(), w.data(), 0); + + i_entries += size_spatial; + } + } + } + +private: + const realtype* copy_to_buffer( + vector& entries, + const py::detail::unchecked_reference& y, + size_t j) { + for (size_t i = 0; i < entries.size(); ++i) { + entries[i] = y(i, j); + } + + return entries.data(); + } + + void resize_arrays(const int M, std::shared_ptr func) { + args.resize(func->sz_arg()); + results.resize(func->sz_res()); + iw.resize(func->sz_iw()); + w.resize(func->sz_w()); + if (!is_f_contiguous && y_buffer.size() < M) { + y_buffer.resize(M); + } + } + + const vector& ts; + const vector& ys; + const vector& inputs; + const vector>& funcs; + realtype* entries; + const bool is_f_contiguous; + int size_spatial; + vector y_buffer; + vector args; + vector results; + vector iw; + vector w; +}; + +const np_array_realtype observe_hermite_interp( + const np_array_realtype& t_interp_np, + const vector& ts_np, + const vector& ys_np, + const vector& yps_np, + const vector& inputs_np, + const vector& strings, + const vector& shape +) { + const int size_spatial = _setup_len_spatial(shape); + const auto& funcs = setup_casadi_funcs(strings); + py::array_t out_array(shape); + auto entries = out_array.mutable_data(); + + TimeSeriesInterpolator(t_interp_np, ts_np, ys_np, yps_np, inputs_np, funcs, entries, size_spatial).process(); + + return out_array; +} + +const np_array_realtype observe( + const vector& ts_np, + const vector& ys_np, + const vector& inputs_np, + const vector& strings, + const bool is_f_contiguous, + const vector& shape +) { + const int size_spatial = _setup_len_spatial(shape); + const auto& funcs = setup_casadi_funcs(strings); + py::array_t out_array(shape); + auto entries = out_array.mutable_data(); + + TimeSeriesProcessor(ts_np, ys_np, inputs_np, funcs, entries, is_f_contiguous, size_spatial).process(); + + return out_array; +} + +const vector> setup_casadi_funcs(const vector& strings) { + std::unordered_map> function_cache; + vector> funcs(strings.size()); + + for (size_t i = 0; i < strings.size(); ++i) { + const std::string& str = strings[i]; + + // Check if function is already in the local cache + if (function_cache.find(str) == function_cache.end()) { + // If not in the cache, create a new casadi::Function::deserialize and store it + function_cache[str] = std::make_shared(casadi::Function::deserialize(str)); + } + + // Retrieve the function from the cache as a shared pointer + funcs[i] = function_cache[str]; + } + + return funcs; +} diff --git a/src/pybamm/solvers/c_solvers/idaklu/observe.hpp b/src/pybamm/solvers/c_solvers/idaklu/observe.hpp new file mode 100644 index 0000000000..e8dc432240 --- /dev/null +++ b/src/pybamm/solvers/c_solvers/idaklu/observe.hpp @@ -0,0 +1,42 @@ +#ifndef PYBAMM_CREATE_OBSERVE_HPP +#define PYBAMM_CREATE_OBSERVE_HPP + +#include +#include +#include +#include "common.hpp" +#include +#include +using std::vector; + +/** + * @brief Observe and Hermite interpolate ND variables + */ +const np_array_realtype observe_hermite_interp( + const np_array_realtype& t_interp, + const vector& ts, + const vector& ys, + const vector& yps, + const vector& inputs, + const vector& strings, + const vector& shape +); + + +/** + * @brief Observe ND variables + */ +const np_array_realtype observe( + const vector& ts_np, + const vector& ys_np, + const vector& inputs_np, + const vector& strings, + const bool is_f_contiguous, + const vector& shape +); + +const vector> setup_casadi_funcs(const vector& strings); + +int _setup_len_spatial(const vector& shape); + +#endif // PYBAMM_CREATE_OBSERVE_HPP diff --git a/src/pybamm/solvers/idaklu_solver.py b/src/pybamm/solvers/idaklu_solver.py index 2b2d852697..80eaffebf4 100644 --- a/src/pybamm/solvers/idaklu_solver.py +++ b/src/pybamm/solvers/idaklu_solver.py @@ -30,6 +30,7 @@ if idaklu_spec.loader: idaklu_spec.loader.exec_module(idaklu) except ImportError as e: # pragma: no cover + idaklu = None print(f"Error loading idaklu: {e}") idaklu_spec = None @@ -133,6 +134,10 @@ class IDAKLUSolver(pybamm.BaseSolver): "nonlinear_convergence_coefficient": 0.33, # Suppress algebraic variables from error test "suppress_algebraic_error": False, + # Store Hermite interpolation data for the solution. + # Note: this option is always disabled if output_variables are given + # or if t_interp values are specified + "hermite_interpolation": True, ## Initial conditions calculation # Positive constant in the Newton iteration convergence test within the # initial condition calculation @@ -201,6 +206,7 @@ def __init__( "max_convergence_failures": 100, "nonlinear_convergence_coefficient": 0.33, "suppress_algebraic_error": False, + "hermite_interpolation": True, "nonlinear_convergence_coefficient_ic": 0.0033, "max_num_steps_ic": 50, "max_num_jacobians_ic": 40, @@ -756,6 +762,16 @@ def _integrate(self, model, t_eval, inputs_list=None, t_interp=None): The times (in seconds) at which to interpolate the solution. Defaults to `None`, which returns the adaptive time-stepping times. """ + if not ( + model.convert_to_format == "casadi" + or ( + model.convert_to_format == "jax" + and self._options["jax_evaluator"] == "iree" + ) + ): # pragma: no cover + # Shouldn't ever reach this point + raise pybamm.SolverError("Unsupported IDAKLU solver configuration.") + inputs_list = inputs_list or [{}] # stack inputs so that they are a 2D array of shape (number_of_inputs, number_of_parameters) @@ -779,20 +795,13 @@ def _integrate(self, model, t_eval, inputs_list=None, t_interp=None): atol = self._check_atol_type(atol, y0full.size) timer = pybamm.Timer() - if model.convert_to_format == "casadi" or ( - model.convert_to_format == "jax" - and self._options["jax_evaluator"] == "iree" - ): - solns = self._setup["solver"].solve( - t_eval, - t_interp, - y0full, - ydot0full, - inputs, - ) - else: # pragma: no cover - # Shouldn't ever reach this point - raise pybamm.SolverError("Unsupported IDAKLU solver configuration.") + solns = self._setup["solver"].solve( + t_eval, + t_interp, + y0full, + ydot0full, + inputs, + ) integration_time = timer.time() return [ @@ -807,7 +816,8 @@ def _post_process_solution(self, sol, model, integration_time, inputs_dict): sensitivity_names = self._setup["sensitivity_names"] number_of_timesteps = sol.t.size number_of_states = model.len_rhs_and_alg - if self.output_variables: + save_outputs_only = self.output_variables + if save_outputs_only: # Substitute empty vectors for state vector 'y' y_out = np.zeros((number_of_timesteps * number_of_states, 0)) y_event = sol.y_term @@ -838,6 +848,11 @@ def _post_process_solution(self, sol, model, integration_time, inputs_dict): else: raise pybamm.SolverError(f"FAILURE {self._solver_flag(sol.flag)}") + if sol.yp.size > 0: + yp = sol.yp.reshape((number_of_timesteps, number_of_states)).T + else: + yp = None + newsol = pybamm.Solution( sol.t, np.transpose(y_out), @@ -847,10 +862,11 @@ def _post_process_solution(self, sol, model, integration_time, inputs_dict): np.transpose(y_event)[:, np.newaxis], termination, all_sensitivities=yS_out, + all_yps=yp, ) + newsol.integration_time = integration_time - if not self.output_variables: - # print((newsol.y).shape) + if not save_outputs_only: return newsol # Populate variables and sensititivies dictionaries directly diff --git a/src/pybamm/solvers/processed_variable.py b/src/pybamm/solvers/processed_variable.py index 2464466348..5cf928ca7f 100644 --- a/src/pybamm/solvers/processed_variable.py +++ b/src/pybamm/solvers/processed_variable.py @@ -6,6 +6,7 @@ import pybamm from scipy.integrate import cumulative_trapezoid import xarray as xr +import bisect class ProcessedVariable: @@ -23,14 +24,11 @@ class ProcessedVariable: Note that this can be any kind of node in the expression tree, not just a :class:`pybamm.Variable`. When evaluated, returns an array of size (m,n) - base_variable_casadis : list of :class:`casadi.Function` + base_variables_casadi : list of :class:`casadi.Function` A list of casadi functions. When evaluated, returns the same thing as `base_Variable.evaluate` (but more efficiently). solution : :class:`pybamm.Solution` The solution object to be used to create the processed variables - warn : bool, optional - Whether to raise warnings when trying to evaluate time and length scales. - Default is True. """ def __init__( @@ -38,7 +36,6 @@ def __init__( base_variables, base_variables_casadi, solution, - warn=True, cumtrapz_ic=None, ): self.base_variables = base_variables @@ -46,13 +43,13 @@ def __init__( self.all_ts = solution.all_ts self.all_ys = solution.all_ys + self.all_yps = solution.all_yps self.all_inputs = solution.all_inputs self.all_inputs_casadi = solution.all_inputs_casadi self.mesh = base_variables[0].mesh self.domain = base_variables[0].domain self.domains = base_variables[0].domains - self.warn = warn self.cumtrapz_ic = cumtrapz_ic # Process spatial variables @@ -75,46 +72,431 @@ 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 + self._xr_array_raw = None + self._entries_raw = None + self._entries_for_interp_raw = None + self._coords_raw = None - # handle 2D (in space) finite element variables differently - if ( - self.mesh - and "current collector" in self.domain - and isinstance(self.mesh, pybamm.ScikitSubMesh2D) + def initialise(self): + if self.entries_raw_initialized: + return + + entries = self.observe_raw() + + t = self.t_pts + entries_for_interp, coords = self._interp_setup(entries, t) + + self._entries_raw = entries + self._entries_for_interp_raw = entries_for_interp + self._coords_raw = coords + + def observe_and_interp(self, t, fill_value): + """ + Interpolate the variable at the given time points and y values. + t must be a sorted array of time points. + """ + + entries = self._observe_hermite_cpp(t) + processed_entries = self._observe_postfix(entries, t) + + tf = self.t_pts[-1] + if t[-1] > tf and fill_value != "extrapolate": + # fill the rest + idx = np.searchsorted(t, tf, side="right") + processed_entries[..., idx:] = fill_value + + return processed_entries + + def observe_raw(self): + """ + Evaluate the base variable at the given time points and y values. + """ + t = self.t_pts + + # For small number of points, use Python + if pybamm.has_idaklu(): + entries = self._observe_raw_cpp() + else: + # Fallback method for when IDAKLU is not available. To be removed + # when the C++ code is migrated to a new repo + entries = self._observe_raw_python() # pragma: no cover + + return self._observe_postfix(entries, t) + + def _setup_cpp_inputs(self, t, full_range): + pybamm.logger.debug("Setting up C++ interpolation inputs") + + ts = self.all_ts + ys = self.all_ys + yps = self.all_yps + inputs = self.all_inputs_casadi + # Find the indices of the time points to observe + if full_range: + idxs = range(len(ts)) + else: + idxs = _find_ts_indices(ts, t) + + if isinstance(idxs, list): + # Extract the time points and inputs + ts = [ts[idx] for idx in idxs] + ys = [ys[idx] for idx in idxs] + if self.hermite_interpolation: + yps = [yps[idx] for idx in idxs] + inputs = [self.all_inputs_casadi[idx] for idx in idxs] + + is_f_contiguous = _is_f_contiguous(ys) + + ts = pybamm.solvers.idaklu_solver.idaklu.VectorRealtypeNdArray(ts) + ys = pybamm.solvers.idaklu_solver.idaklu.VectorRealtypeNdArray(ys) + if self.hermite_interpolation: + yps = pybamm.solvers.idaklu_solver.idaklu.VectorRealtypeNdArray(yps) + else: + yps = None + inputs = pybamm.solvers.idaklu_solver.idaklu.VectorRealtypeNdArray(inputs) + + # Generate the serialized C++ functions only once + funcs_unique = {} + funcs = [None] * len(idxs) + for i in range(len(idxs)): + vars = self.base_variables_casadi[idxs[i]] + if vars not in funcs_unique: + funcs_unique[vars] = vars.serialize() + funcs[i] = funcs_unique[vars] + + return ts, ys, yps, funcs, inputs, is_f_contiguous + + def _observe_hermite_cpp(self, t): + pybamm.logger.debug("Observing and Hermite interpolating the variable in C++") + + ts, ys, yps, funcs, inputs, _ = self._setup_cpp_inputs(t, full_range=False) + shapes = self._shape(t) + return pybamm.solvers.idaklu_solver.idaklu.observe_hermite_interp( + t, ts, ys, yps, inputs, funcs, shapes + ) + + def _observe_raw_cpp(self): + pybamm.logger.debug("Observing the variable raw data in C++") + t = self.t_pts + ts, ys, _, funcs, inputs, is_f_contiguous = self._setup_cpp_inputs( + t, full_range=True + ) + shapes = self._shape(self.t_pts) + + return pybamm.solvers.idaklu_solver.idaklu.observe( + ts, ys, inputs, funcs, is_f_contiguous, shapes + ) + + def _observe_raw_python(self): + raise NotImplementedError # pragma: no cover + + def _observe_postfix(self, entries, t): + return entries + + def _interp_setup(self, entries, t): + raise NotImplementedError # pragma: no cover + + def _shape(self, t): + raise NotImplementedError # pragma: no cover + + def _process_spatial_variable_names(self, spatial_variable): + if len(spatial_variable) == 0: + return None + + # Extract names + raw_names = [] + for var in spatial_variable: + # Ignore tabs in domain names + if var == "tabs": + continue + if isinstance(var, str): + raw_names.append(var) + else: + raw_names.append(var.name) + + # Rename battery variables to match PyBaMM convention + if all([var.startswith("r") for var in raw_names]): + return "r" + elif all([var.startswith("x") for var in raw_names]): + return "x" + elif all([var.startswith("R") for var in raw_names]): + return "R" + elif len(raw_names) == 1: + return raw_names[0] + else: + raise NotImplementedError( + f"Spatial variable name not recognized for {spatial_variable}" + ) + + def __call__( + self, + t=None, + x=None, + r=None, + y=None, + z=None, + R=None, + fill_value=np.nan, + ): + # Check to see if we are interpolating exactly onto the raw solution time points + t_observe, observe_raw = self._check_observe_raw(t) + + # Check if the time points are sorted and unique + is_sorted = observe_raw or _is_sorted(t_observe) + + # Sort them if not + if not is_sorted: + idxs_sort = np.argsort(t_observe) + t_observe = t_observe[idxs_sort] + + hermite_time_interp = ( + pybamm.has_idaklu() and self.hermite_interpolation and not observe_raw + ) + + if hermite_time_interp: + entries = self.observe_and_interp(t_observe, fill_value) + + spatial_interp = any(a is not None for a in [x, r, y, z, R]) + + xr_interp = spatial_interp or not hermite_time_interp + + if xr_interp: + if hermite_time_interp: + # Already interpolated in time + t = None + entries_for_interp, coords = self._interp_setup(entries, t_observe) + else: + self.initialise() + entries_for_interp, coords = ( + self._entries_for_interp_raw, + self._coords_raw, + ) + + processed_entries = self._xr_interpolate( + entries_for_interp, + coords, + observe_raw, + t, + x, + r, + y, + z, + R, + fill_value, + ) + else: + processed_entries = entries + + if not is_sorted: + idxs_unsort = np.zeros_like(idxs_sort) + idxs_unsort[idxs_sort] = np.arange(len(t_observe)) + + processed_entries = processed_entries[..., idxs_unsort] + + # Remove a singleton time dimension if we interpolate in time with hermite + if hermite_time_interp and t_observe.size == 1: + processed_entries = np.squeeze(processed_entries, axis=-1) + + return processed_entries + + def _xr_interpolate( + self, + entries_for_interp, + coords, + observe_raw, + t=None, + x=None, + r=None, + y=None, + z=None, + R=None, + fill_value=None, + ): + """ + Evaluate the variable at arbitrary *dimensional* t (and x, r, y, z and/or R), + using interpolation + """ + if observe_raw: + if not self.xr_array_raw_initialized: + self._xr_array_raw = xr.DataArray(entries_for_interp, coords=coords) + xr_data_array = self._xr_array_raw + else: + xr_data_array = xr.DataArray(entries_for_interp, coords=coords) + + kwargs = {"t": t, "x": x, "r": r, "y": y, "z": z, "R": R} + + # Remove any None arguments + kwargs = {key: value for key, value in kwargs.items() if value is not None} + + # Use xarray interpolation, return numpy array + out = xr_data_array.interp(**kwargs, kwargs={"fill_value": fill_value}).values + + return out + + def _check_observe_raw(self, t): + """ + Checks if the raw data should be observed exactly at the solution time points + + Args: + t (np.ndarray, list, None): time points to observe + + Returns: + t_observe (np.ndarray): time points to observe + observe_raw (bool): True if observing the raw data + """ + observe_raw = (t is None) or ( + np.asarray(t).size == len(self.t_pts) and np.all(t == self.t_pts) + ) + + if observe_raw: + t_observe = self.t_pts + elif not isinstance(t, np.ndarray): + if not isinstance(t, list): + t = [t] + t_observe = np.array(t) + else: + t_observe = t + + if t_observe[0] < self.t_pts[0]: + raise ValueError( + "The interpolation points must be greater than or equal to the initial solution time" + ) + + return t_observe, observe_raw + + @property + def entries(self): + """ + Returns the raw data entries of the processed variable. If the processed + variable has not been initialized (i.e. the entries have not been + calculated), then the processed variable is initialized first. + """ + self.initialise() + return self._entries_raw + + @property + def data(self): + """Same as entries, but different name""" + return self.entries + + @property + def entries_raw_initialized(self): + return self._entries_raw is not None + + @property + def xr_array_raw_initialized(self): + return self._xr_array_raw is not None + + @property + def sensitivities(self): + """ + Returns a dictionary of sensitivities for each input parameter. + The keys are the input parameters, and the value is a matrix of size + (n_x * n_t, n_p), where n_x is the number of states, n_t is the number of time + points, and n_p is the size of the input parameter + """ + # No sensitivities if there are no inputs + if len(self.all_inputs[0]) == 0: + return {} + # Otherwise initialise and return sensitivities + if self._sensitivities is None: + if self.all_solution_sensitivities: + self.initialise_sensitivity_explicit_forward() + else: + raise ValueError( + "Cannot compute sensitivities. The 'calculate_sensitivities' " + "argument of the solver.solve should be changed from 'None' to " + "allow sensitivities calculations. Check solver documentation for " + "details." + ) + return self._sensitivities + + def initialise_sensitivity_explicit_forward(self): + "Set up the sensitivity dictionary" + + all_S_var = [] + for ts, ys, inputs_stacked, inputs, base_variable, dy_dp in zip( + self.all_ts, + self.all_ys, + self.all_inputs_casadi, + self.all_inputs, + self.base_variables, + self.all_solution_sensitivities["all"], ): - return self.initialise_2D_scikit_fem() + # Set up symbolic variables + t_casadi = casadi.MX.sym("t") + y_casadi = casadi.MX.sym("y", ys.shape[0]) + p_casadi = { + name: casadi.MX.sym(name, value.shape[0]) + for name, value in inputs.items() + } - # check variable shape - if len(self.base_eval_shape) == 0 or self.base_eval_shape[0] == 1: - return self.initialise_0D() + p_casadi_stacked = casadi.vertcat(*[p for p in p_casadi.values()]) - 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() + # Convert variable to casadi format for differentiating + var_casadi = base_variable.to_casadi(t_casadi, y_casadi, inputs=p_casadi) + dvar_dy = casadi.jacobian(var_casadi, y_casadi) + dvar_dp = casadi.jacobian(var_casadi, p_casadi_stacked) - # 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)" + # Convert to functions and evaluate index-by-index + dvar_dy_func = casadi.Function( + "dvar_dy", [t_casadi, y_casadi, p_casadi_stacked], [dvar_dy] + ) + dvar_dp_func = casadi.Function( + "dvar_dp", [t_casadi, y_casadi, p_casadi_stacked], [dvar_dp] + ) + for idx, t in enumerate(ts): + u = ys[:, idx] + next_dvar_dy_eval = dvar_dy_func(t, u, inputs_stacked) + next_dvar_dp_eval = dvar_dp_func(t, u, inputs_stacked) + if idx == 0: + dvar_dy_eval = next_dvar_dy_eval + dvar_dp_eval = next_dvar_dp_eval + else: + dvar_dy_eval = casadi.diagcat(dvar_dy_eval, next_dvar_dy_eval) + dvar_dp_eval = casadi.vertcat(dvar_dp_eval, next_dvar_dp_eval) + + # Compute sensitivity + S_var = dvar_dy_eval @ dy_dp + dvar_dp_eval + all_S_var.append(S_var) + + S_var = casadi.vertcat(*all_S_var) + sensitivities = {"all": S_var} + + # Add the individual sensitivity + start = 0 + for name, inp in self.all_inputs[0].items(): + end = start + inp.shape[0] + sensitivities[name] = S_var[:, start:end] + start = end + + # Save attribute + self._sensitivities = sensitivities + + @property + def hermite_interpolation(self): + return self.all_yps is not None + + +class ProcessedVariable0D(ProcessedVariable): + def __init__( + self, + base_variables, + base_variables_casadi, + solution, + cumtrapz_ic=None, + ): + self.dimensions = 0 + super().__init__( + base_variables, + base_variables_casadi, + solution, + cumtrapz_ic=cumtrapz_ic, ) - def initialise_0D(self): + def _observe_raw_python(self): + pybamm.logger.debug("Observing the variable raw data in Python") # initialise empty array of the correct size - entries = np.empty(len(self.t_pts)) + entries = np.empty(self._shape(self.t_pts)) idx = 0 # Evaluate the base_variable index-by-index for ts, ys, inputs, base_var_casadi in zip( @@ -126,22 +508,67 @@ def initialise_0D(self): entries[idx] = float(base_var_casadi(t, y, inputs)) idx += 1 + return entries - if self.cumtrapz_ic is not None: - entries = cumulative_trapezoid( - entries, self.t_pts, initial=float(self.cumtrapz_ic) - ) + def _observe_postfix(self, entries, _): + if self.cumtrapz_ic is None: + return entries + + return cumulative_trapezoid( + entries, self.t_pts, initial=float(self.cumtrapz_ic) + ) + def _interp_setup(self, entries, t): # save attributes for interpolation - self.entries_for_interp = entries - self.coords_for_interp = {"t": self.t_pts} + entries_for_interp = entries + coords_for_interp = {"t": t} - self.entries = entries - self.dimensions = 0 + return entries_for_interp, coords_for_interp - def initialise_1D(self, fixed_t=False): - len_space = self.base_eval_shape[0] - entries = np.empty((len_space, len(self.t_pts))) + def _shape(self, t): + return [len(t)] + + +class ProcessedVariable1D(ProcessedVariable): + """ + An object that can be evaluated at arbitrary (scalars or vectors) t and x, and + returns the (interpolated) value of the base variable at that t and x. + + Parameters + ---------- + base_variables : list of :class:`pybamm.Symbol` + A list of base variables with a method `evaluate(t,y)`, each entry of which + returns the value of that variable for that particular sub-solution. + A Solution can be comprised of sub-solutions which are the solutions of + different models. + Note that this can be any kind of node in the expression tree, not + just a :class:`pybamm.Variable`. + When evaluated, returns an array of size (m,n) + base_variables_casadi : list of :class:`casadi.Function` + A list of casadi functions. When evaluated, returns the same thing as + `base_Variable.evaluate` (but more efficiently). + solution : :class:`pybamm.Solution` + The solution object to be used to create the processed variables + """ + + def __init__( + self, + base_variables, + base_variables_casadi, + solution, + cumtrapz_ic=None, + ): + self.dimensions = 1 + super().__init__( + base_variables, + base_variables_casadi, + solution, + cumtrapz_ic=cumtrapz_ic, + ) + + def _observe_raw_python(self): + pybamm.logger.debug("Observing the variable raw data in Python") + entries = np.empty(self._shape(self.t_pts)) # Evaluate the base_variable index-by-index idx = 0 @@ -153,7 +580,9 @@ def initialise_1D(self, fixed_t=False): y = ys[:, inner_idx] entries[:, idx] = base_var_casadi(t, y, inputs).full()[:, 0] idx += 1 + return entries + def _interp_setup(self, entries, t): # Get node and edge values nodes = self.mesh.nodes edges = self.mesh.edges @@ -173,8 +602,6 @@ def initialise_1D(self, fixed_t=False): ) # assign attributes for reference (either x_sol or r_sol) - self.entries = entries - self.dimensions = 1 self.spatial_variable_names = { k: self._process_spatial_variable_names(v) for k, v in self.spatial_variables.items() @@ -189,26 +616,71 @@ def initialise_1D(self, fixed_t=False): self.first_dim_pts = edges # save attributes for interpolation - self.entries_for_interp = entries_for_interp - self.coords_for_interp = {self.first_dimension: pts_for_interp, "t": self.t_pts} + coords_for_interp = {self.first_dimension: pts_for_interp, "t": t} - def initialise_2D(self): - """ - Initialise a 2D object that depends on x and r, x and z, x and R, or R and r. - """ + return entries_for_interp, coords_for_interp + + def _shape(self, t): + t_size = len(t) + space_size = self.base_eval_shape[0] + return [space_size, t_size] + + +class ProcessedVariable2D(ProcessedVariable): + """ + An object that can be evaluated at arbitrary (scalars or vectors) t and x, and + returns the (interpolated) value of the base variable at that t and x. + + Parameters + ---------- + base_variables : list of :class:`pybamm.Symbol` + A list of base variables with a method `evaluate(t,y)`, each entry of which + returns the value of that variable for that particular sub-solution. + A Solution can be comprised of sub-solutions which are the solutions of + different models. + Note that this can be any kind of node in the expression tree, not + just a :class:`pybamm.Variable`. + When evaluated, returns an array of size (m,n) + base_variables_casadi : list of :class:`casadi.Function` + A list of casadi functions. When evaluated, returns the same thing as + `base_Variable.evaluate` (but more efficiently). + solution : :class:`pybamm.Solution` + The solution object to be used to create the processed variables + """ + + def __init__( + self, + base_variables, + base_variables_casadi, + solution, + cumtrapz_ic=None, + ): + self.dimensions = 2 + super().__init__( + base_variables, + base_variables_casadi, + solution, + cumtrapz_ic=cumtrapz_ic, + ) first_dim_nodes = self.mesh.nodes first_dim_edges = self.mesh.edges second_dim_nodes = self.base_variables[0].secondary_mesh.nodes - second_dim_edges = self.base_variables[0].secondary_mesh.edges if self.base_eval_size // len(second_dim_nodes) == len(first_dim_nodes): first_dim_pts = first_dim_nodes elif self.base_eval_size // len(second_dim_nodes) == len(first_dim_edges): first_dim_pts = first_dim_edges second_dim_pts = second_dim_nodes - first_dim_size = len(first_dim_pts) - second_dim_size = len(second_dim_pts) - entries = np.empty((first_dim_size, second_dim_size, len(self.t_pts))) + self.first_dim_size = len(first_dim_pts) + self.second_dim_size = len(second_dim_pts) + + def _observe_raw_python(self): + """ + Initialise a 2D object that depends on x and r, x and z, x and R, or R and r. + """ + pybamm.logger.debug("Observing the variable raw data in Python") + first_dim_size, second_dim_size, t_size = self._shape(self.t_pts) + entries = np.empty((first_dim_size, second_dim_size, t_size)) # Evaluate the base_variable index-by-index idx = 0 @@ -224,6 +696,22 @@ def initialise_2D(self): order="F", ) idx += 1 + return entries + + def _interp_setup(self, entries, t): + """ + Initialise a 2D object that depends on x and r, x and z, x and R, or R and r. + """ + first_dim_nodes = self.mesh.nodes + first_dim_edges = self.mesh.edges + second_dim_nodes = self.base_variables[0].secondary_mesh.nodes + second_dim_edges = self.base_variables[0].secondary_mesh.edges + if self.base_eval_size // len(second_dim_nodes) == len(first_dim_nodes): + first_dim_pts = first_dim_nodes + elif self.base_eval_size // len(second_dim_nodes) == len(first_dim_edges): + first_dim_pts = first_dim_edges + + second_dim_pts = second_dim_nodes # add points outside first dimension domain for extrapolation to # boundaries @@ -281,8 +769,6 @@ def initialise_2D(self): self.second_dimension = self.spatial_variable_names["secondary"] # assign attributes for reference - self.entries = entries - self.dimensions = 2 first_dim_pts_for_interp = first_dim_pts second_dim_pts_for_interp = second_dim_pts @@ -291,38 +777,68 @@ def initialise_2D(self): self.second_dim_pts = second_dim_edges # save attributes for interpolation - self.entries_for_interp = entries_for_interp - self.coords_for_interp = { + coords_for_interp = { self.first_dimension: first_dim_pts_for_interp, self.second_dimension: second_dim_pts_for_interp, - "t": self.t_pts, + "t": t, } - def initialise_2D_scikit_fem(self): + return entries_for_interp, coords_for_interp + + def _shape(self, t): + first_dim_size = self.first_dim_size + second_dim_size = self.second_dim_size + t_size = len(t) + return [first_dim_size, second_dim_size, t_size] + + +class ProcessedVariable2DSciKitFEM(ProcessedVariable2D): + """ + An object that can be evaluated at arbitrary (scalars or vectors) t and x, and + returns the (interpolated) value of the base variable at that t and x. + + Parameters + ---------- + base_variables : list of :class:`pybamm.Symbol` + A list of base variables with a method `evaluate(t,y)`, each entry of which + returns the value of that variable for that particular sub-solution. + A Solution can be comprised of sub-solutions which are the solutions of + different models. + Note that this can be any kind of node in the expression tree, not + just a :class:`pybamm.Variable`. + When evaluated, returns an array of size (m,n) + base_variables_casadi : list of :class:`casadi.Function` + A list of casadi functions. When evaluated, returns the same thing as + `base_Variable.evaluate` (but more efficiently). + solution : :class:`pybamm.Solution` + The solution object to be used to create the processed variables + """ + + def __init__( + self, + base_variables, + base_variables_casadi, + solution, + cumtrapz_ic=None, + ): + self.dimensions = 2 + super(ProcessedVariable2D, self).__init__( + base_variables, + base_variables_casadi, + solution, + cumtrapz_ic=cumtrapz_ic, + ) y_sol = self.mesh.edges["y"] - len_y = len(y_sol) z_sol = self.mesh.edges["z"] - len_z = len(z_sol) - entries = np.empty((len_y, len_z, len(self.t_pts))) - # Evaluate the base_variable index-by-index - idx = 0 - for ts, ys, inputs, base_var_casadi in zip( - self.all_ts, self.all_ys, self.all_inputs_casadi, self.base_variables_casadi - ): - for inner_idx, t in enumerate(ts): - t = ts[inner_idx] - y = ys[:, inner_idx] - entries[:, :, idx] = np.reshape( - base_var_casadi(t, y, inputs).full(), - [len_y, len_z], - order="C", - ) - idx += 1 + self.first_dim_size = len(y_sol) + self.second_dim_size = len(z_sol) + + def _interp_setup(self, entries, t): + y_sol = self.mesh.edges["y"] + z_sol = self.mesh.edges["z"] # assign attributes for reference - self.entries = entries - self.dimensions = 2 self.y_sol = y_sol self.z_sol = z_sol self.first_dimension = "y" @@ -331,148 +847,116 @@ def initialise_2D_scikit_fem(self): self.second_dim_pts = z_sol # save attributes for interpolation - self.entries_for_interp = entries - self.coords_for_interp = {"y": y_sol, "z": z_sol, "t": self.t_pts} + coords_for_interp = {"y": y_sol, "z": z_sol, "t": t} - def _process_spatial_variable_names(self, spatial_variable): - if len(spatial_variable) == 0: - return None + return entries, coords_for_interp - # Extract names - raw_names = [] - for var in spatial_variable: - # Ignore tabs in domain names - if var == "tabs": - continue - if isinstance(var, str): - raw_names.append(var) - else: - raw_names.append(var.name) - # Rename battery variables to match PyBaMM convention - if all([var.startswith("r") for var in raw_names]): - return "r" - elif all([var.startswith("x") for var in raw_names]): - return "x" - elif all([var.startswith("R") for var in raw_names]): - return "R" - elif len(raw_names) == 1: - return raw_names[0] - else: - raise NotImplementedError( - f"Spatial variable name not recognized for {spatial_variable}" - ) +def process_variable(base_variables, *args, **kwargs): + mesh = base_variables[0].mesh + domain = base_variables[0].domain - def _initialize_xr_data_array(self): - """ - Initialize the xarray DataArray for interpolation. We don't do this by - default as it has some overhead (~75 us) and sometimes we only need the entries - of the processed variable, not the xarray object for interpolation. - """ - entries = self.entries_for_interp - coords = self.coords_for_interp - self._xr_data_array = xr.DataArray(entries, coords=coords) + # Evaluate base variable at initial time + base_eval_shape = base_variables[0].shape + base_eval_size = base_variables[0].size - def __call__(self, t=None, x=None, r=None, y=None, z=None, R=None, warn=True): - """ - Evaluate the variable at arbitrary *dimensional* t (and x, r, y, z and/or R), - using interpolation - """ - if self._xr_data_array is None: - self._initialize_xr_data_array() - kwargs = {"t": t, "x": x, "r": r, "y": y, "z": z, "R": R} - # Remove any None arguments - kwargs = {key: value for key, value in kwargs.items() if value is not None} - # Use xarray interpolation, return numpy array - return self._xr_data_array.interp(**kwargs).values + # handle 2D (in space) finite element variables differently + if ( + mesh + and "current collector" in domain + and isinstance(mesh, pybamm.ScikitSubMesh2D) + ): + return ProcessedVariable2DSciKitFEM(base_variables, *args, **kwargs) + + # check variable shape + if len(base_eval_shape) == 0 or base_eval_shape[0] == 1: + return ProcessedVariable0D(base_variables, *args, **kwargs) + + n = mesh.npts + base_shape = base_eval_shape[0] + # Try some shapes that could make the variable a 1D variable + if base_shape in [n, n + 1]: + return ProcessedVariable1D(base_variables, *args, **kwargs) + + # Try some shapes that could make the variable a 2D variable + first_dim_nodes = mesh.nodes + first_dim_edges = mesh.edges + second_dim_pts = base_variables[0].secondary_mesh.nodes + if base_eval_size // len(second_dim_pts) in [ + len(first_dim_nodes), + len(first_dim_edges), + ]: + return ProcessedVariable2D(base_variables, *args, **kwargs) + + # 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 _is_f_contiguous(all_ys): + """ + Check if all the ys are f-contiguous in memory - @property - def data(self): - """Same as entries, but different name""" - return self.entries + Args: + all_ys (list of np.ndarray): list of all ys - @property - def sensitivities(self): - """ - Returns a dictionary of sensitivities for each input parameter. - The keys are the input parameters, and the value is a matrix of size - (n_x * n_t, n_p), where n_x is the number of states, n_t is the number of time - points, and n_p is the size of the input parameter - """ - # No sensitivities if there are no inputs - if len(self.all_inputs[0]) == 0: - return {} - # Otherwise initialise and return sensitivities - if self._sensitivities is None: - if self.all_solution_sensitivities: - self.initialise_sensitivity_explicit_forward() - else: - raise ValueError( - "Cannot compute sensitivities. The 'calculate_sensitivities' " - "argument of the solver.solve should be changed from 'None' to " - "allow sensitivities calculations. Check solver documentation for " - "details." - ) - return self._sensitivities + Returns: + bool: True if all ys are f-contiguous + """ - def initialise_sensitivity_explicit_forward(self): - "Set up the sensitivity dictionary" + return all(isinstance(y, np.ndarray) and y.data.f_contiguous for y in all_ys) - all_S_var = [] - for ts, ys, inputs_stacked, inputs, base_variable, dy_dp in zip( - self.all_ts, - self.all_ys, - self.all_inputs_casadi, - self.all_inputs, - self.base_variables, - self.all_solution_sensitivities["all"], - ): - # Set up symbolic variables - t_casadi = casadi.MX.sym("t") - y_casadi = casadi.MX.sym("y", ys.shape[0]) - p_casadi = { - name: casadi.MX.sym(name, value.shape[0]) - for name, value in inputs.items() - } - p_casadi_stacked = casadi.vertcat(*[p for p in p_casadi.values()]) +def _is_sorted(t): + """ + Check if an array is sorted - # Convert variable to casadi format for differentiating - var_casadi = base_variable.to_casadi(t_casadi, y_casadi, inputs=p_casadi) - dvar_dy = casadi.jacobian(var_casadi, y_casadi) - dvar_dp = casadi.jacobian(var_casadi, p_casadi_stacked) + Args: + t (np.ndarray): array to check - # Convert to functions and evaluate index-by-index - dvar_dy_func = casadi.Function( - "dvar_dy", [t_casadi, y_casadi, p_casadi_stacked], [dvar_dy] - ) - dvar_dp_func = casadi.Function( - "dvar_dp", [t_casadi, y_casadi, p_casadi_stacked], [dvar_dp] - ) - for idx, t in enumerate(ts): - u = ys[:, idx] - next_dvar_dy_eval = dvar_dy_func(t, u, inputs_stacked) - next_dvar_dp_eval = dvar_dp_func(t, u, inputs_stacked) - if idx == 0: - dvar_dy_eval = next_dvar_dy_eval - dvar_dp_eval = next_dvar_dp_eval - else: - dvar_dy_eval = casadi.diagcat(dvar_dy_eval, next_dvar_dy_eval) - dvar_dp_eval = casadi.vertcat(dvar_dp_eval, next_dvar_dp_eval) + Returns: + bool: True if array is sorted + """ + return np.all(t[:-1] <= t[1:]) - # Compute sensitivity - S_var = dvar_dy_eval @ dy_dp + dvar_dp_eval - all_S_var.append(S_var) - S_var = casadi.vertcat(*all_S_var) - sensitivities = {"all": S_var} +def _find_ts_indices(ts, t): + """ + Parameters: + - ts: A list of numpy arrays (each sorted) whose values are successively increasing. + - t: A sorted list or array of values to find within ts. - # Add the individual sensitivity - start = 0 - for name, inp in self.all_inputs[0].items(): - end = start + inp.shape[0] - sensitivities[name] = S_var[:, start:end] - start = end + Returns: + - indices: A list of indices from `ts` such that at least one value of `t` falls within ts[idx]. + """ - # Save attribute - self._sensitivities = sensitivities + indices = [] + + # Get the minimum and maximum values of the target values `t` + t_min, t_max = t[0], t[-1] + + # Step 1: Use binary search to find the range of `ts` arrays where t_min and t_max could lie + low_idx = bisect.bisect_left([ts_arr[-1] for ts_arr in ts], t_min) + high_idx = bisect.bisect_right([ts_arr[0] for ts_arr in ts], t_max) + + # Step 2: Iterate over the identified range + for idx in range(low_idx, high_idx): + ts_min, ts_max = ts[idx][0], ts[idx][-1] + + # Binary search within `t` to check if any value falls within [ts_min, ts_max] + i = bisect.bisect_left(t, ts_min) + if i < len(t) and t[i] <= ts_max: + # At least one value of t is within ts[idx] + indices.append(idx) + + # extrapolating + if (t[-1] > ts[-1][-1]) and (len(indices) == 0 or indices[-1] != len(ts) - 1): + indices.append(len(ts) - 1) + + if len(indices) == len(ts): + # All indices are included + return range(len(ts)) + + return indices diff --git a/src/pybamm/solvers/processed_variable_computed.py b/src/pybamm/solvers/processed_variable_computed.py index a717c8b0cb..4f0cccc8c3 100644 --- a/src/pybamm/solvers/processed_variable_computed.py +++ b/src/pybamm/solvers/processed_variable_computed.py @@ -27,7 +27,7 @@ class ProcessedVariableComputed: Note that this can be any kind of node in the expression tree, not just a :class:`pybamm.Variable`. When evaluated, returns an array of size (m,n) - base_variable_casadis : list of :class:`casadi.Function` + base_variables_casadi : list of :class:`casadi.Function` A list of casadi functions. When evaluated, returns the same thing as `base_Variable.evaluate` (but more efficiently). base_variable_data : list of :numpy:array @@ -45,7 +45,6 @@ def __init__( base_variables_casadi, base_variables_data, solution, - warn=True, cumtrapz_ic=None, ): self.base_variables = base_variables @@ -60,7 +59,6 @@ def __init__( self.mesh = base_variables[0].mesh self.domain = base_variables[0].domain self.domains = base_variables[0].domains - self.warn = warn self.cumtrapz_ic = cumtrapz_ic # Sensitivity starts off uninitialized, only set when called @@ -82,33 +80,35 @@ def __init__( and isinstance(self.mesh, pybamm.ScikitSubMesh2D) ): self.initialise_2D_scikit_fem() + return # 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)" - ) + if len(self.base_eval_shape) == 0 or self.base_eval_shape[0] == 1: + self.initialise_0D() + return + + 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() + return + + # 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) not in [ + len(first_dim_nodes), + len(first_dim_edges), + ]: + # Raise error for 3D variable + raise NotImplementedError( + f"Shape not recognized for {base_variables[0]} " + + "(note processing of 3D variables is not yet implemented)" + ) + + self.initialise_2D() def add_sensitivity(self, param, data): # unroll from sparse representation into n-d matrix @@ -203,7 +203,7 @@ def initialise_0D(self): self.entries = entries self.dimensions = 0 - def initialise_1D(self, fixed_t=False): + def initialise_1D(self): entries = self.unroll_1D() # Get node and edge values @@ -422,7 +422,7 @@ def initialise_2D_scikit_fem(self): coords={"y": y_sol, "z": z_sol, "t": self.t_pts}, ) - def __call__(self, t=None, x=None, r=None, y=None, z=None, R=None, warn=True): + def __call__(self, t=None, x=None, r=None, y=None, z=None, R=None): """ Evaluate the variable at arbitrary *dimensional* t (and x, r, y, z and/or R), using interpolation diff --git a/src/pybamm/solvers/solution.py b/src/pybamm/solvers/solution.py index 74d9ce7baf..c884e79e34 100644 --- a/src/pybamm/solvers/solution.py +++ b/src/pybamm/solvers/solution.py @@ -75,6 +75,7 @@ def __init__( y_event=None, termination="final time", all_sensitivities=False, + all_yps=None, check_solution=True, ): if not isinstance(all_ts, list): @@ -88,6 +89,10 @@ def __init__( self._all_ys_and_sens = all_ys self._all_models = all_models + if (all_yps is not None) and not isinstance(all_yps, list): + all_yps = [all_yps] + self._all_yps = all_yps + # Set up inputs if not isinstance(all_inputs, list): all_inputs_copy = dict(all_inputs) @@ -128,7 +133,7 @@ def __init__( # initialize empty variables and data self._variables = pybamm.FuzzyDict() - self.data = pybamm.FuzzyDict() + self._data = pybamm.FuzzyDict() # Add self as sub-solution for compatibility with ProcessedVariable self._sub_solutions = [self] @@ -298,6 +303,13 @@ def y(self): return self._y + @property + def data(self): + for k, v in self._variables.items(): + if k not in self._data: + self._data[k] = v.data + return self._data + @property def sensitivities(self): """Values of the sensitivities. Returns a dict of param_name: np_array""" @@ -400,6 +412,14 @@ def all_models(self): def all_inputs_casadi(self): return [casadi.vertcat(*inp.values()) for inp in self.all_inputs] + @property + def all_yps(self): + return self._all_yps + + @property + def hermite_interpolation(self): + return self.all_yps is not None + @property def t_event(self): """Time at which the event happens""" @@ -434,6 +454,12 @@ def first_state(self): n_states = self.all_models[0].len_rhs_and_alg for key in self._all_sensitivities: sensitivities[key] = self._all_sensitivities[key][0][-n_states:, :] + + if self.all_yps is None: + all_yps = None + else: + all_yps = self.all_yps[0][:, :1] + new_sol = Solution( self.all_ts[0][:1], self.all_ys[0][:, :1], @@ -443,6 +469,7 @@ def first_state(self): None, "final time", all_sensitivities=sensitivities, + all_yps=all_yps, ) new_sol._all_inputs_casadi = self.all_inputs_casadi[:1] new_sol._sub_solutions = self.sub_solutions[:1] @@ -467,6 +494,12 @@ def last_state(self): n_states = self.all_models[-1].len_rhs_and_alg for key in self._all_sensitivities: sensitivities[key] = self._all_sensitivities[key][-1][-n_states:, :] + + if self.all_yps is None: + all_yps = None + else: + all_yps = self.all_yps[-1][:, -1:] + new_sol = Solution( self.all_ts[-1][-1:], self.all_ys[-1][:, -1:], @@ -476,6 +509,7 @@ def last_state(self): self.y_event, self.termination, all_sensitivities=sensitivities, + all_yps=all_yps, ) new_sol._all_inputs_casadi = self.all_inputs_casadi[-1:] new_sol._sub_solutions = self.sub_solutions[-1:] @@ -528,57 +562,61 @@ def update(self, variables): if isinstance(self._all_sensitivities, bool) and self._all_sensitivities: self.extract_explicit_sensitivities() - # Convert single entry to list + # Single variable if isinstance(variables, str): variables = [variables] + # Process - for key in variables: - cumtrapz_ic = None - pybamm.logger.debug(f"Post-processing {key}") - vars_pybamm = [model.variables_and_events[key] for model in self.all_models] - - # Iterate through all models, some may be in the list several times and - # therefore only get set up once - vars_casadi = [] - for i, (model, ys, inputs, var_pybamm) in enumerate( - zip(self.all_models, self.all_ys, self.all_inputs, vars_pybamm) + for variable in variables: + self._update_variable(variable) + + def _update_variable(self, variable): + cumtrapz_ic = None + pybamm.logger.debug(f"Post-processing {variable}") + vars_pybamm = [ + model.variables_and_events[variable] for model in self.all_models + ] + + # Iterate through all models, some may be in the list several times and + # therefore only get set up once + vars_casadi = [] + for i, (model, ys, inputs, var_pybamm) in enumerate( + zip(self.all_models, self.all_ys, self.all_inputs, vars_pybamm) + ): + if ys.size == 0 and var_pybamm.has_symbol_of_classes( + pybamm.expression_tree.state_vector.StateVector ): - if ys.size == 0 and var_pybamm.has_symbol_of_classes( - pybamm.expression_tree.state_vector.StateVector - ): - raise KeyError( - f"Cannot process variable '{key}' as it was not part of the " - "solve. Please re-run the solve with `output_variables` set to " - "include this variable." - ) - elif isinstance(var_pybamm, pybamm.ExplicitTimeIntegral): - cumtrapz_ic = var_pybamm.initial_condition - cumtrapz_ic = cumtrapz_ic.evaluate() - var_pybamm = var_pybamm.child - var_casadi = self.process_casadi_var( - var_pybamm, - inputs, - ys.shape, - ) - model._variables_casadi[key] = var_casadi - vars_pybamm[i] = var_pybamm - elif key in model._variables_casadi: - var_casadi = model._variables_casadi[key] - else: - var_casadi = self.process_casadi_var( - var_pybamm, - inputs, - ys.shape, - ) - model._variables_casadi[key] = var_casadi - vars_casadi.append(var_casadi) - var = pybamm.ProcessedVariable( - vars_pybamm, vars_casadi, self, cumtrapz_ic=cumtrapz_ic - ) + raise KeyError( + f"Cannot process variable '{variable}' as it was not part of the " + "solve. Please re-run the solve with `output_variables` set to " + "include this variable." + ) + elif isinstance(var_pybamm, pybamm.ExplicitTimeIntegral): + cumtrapz_ic = var_pybamm.initial_condition + cumtrapz_ic = cumtrapz_ic.evaluate() + var_pybamm = var_pybamm.child + var_casadi = self.process_casadi_var( + var_pybamm, + inputs, + ys.shape, + ) + model._variables_casadi[variable] = var_casadi + vars_pybamm[i] = var_pybamm + elif variable in model._variables_casadi: + var_casadi = model._variables_casadi[variable] + else: + var_casadi = self.process_casadi_var( + var_pybamm, + inputs, + ys.shape, + ) + model._variables_casadi[variable] = var_casadi + vars_casadi.append(var_casadi) + var = pybamm.process_variable( + vars_pybamm, vars_casadi, self, cumtrapz_ic=cumtrapz_ic + ) - # Save variable and data - self._variables[key] = var - self.data[key] = var.data + self._variables[variable] = var def process_casadi_var(self, var_pybamm, inputs, ys_shape): t_MX = casadi.MX.sym("t") @@ -588,8 +626,40 @@ def process_casadi_var(self, var_pybamm, inputs, ys_shape): } inputs_MX = casadi.vertcat(*[p for p in inputs_MX_dict.values()]) var_sym = var_pybamm.to_casadi(t_MX, y_MX, inputs=inputs_MX_dict) - var_casadi = casadi.Function("variable", [t_MX, y_MX, inputs_MX], [var_sym]) - return var_casadi + + opts = { + "cse": True, + "inputs_check": False, + "is_diff_in": [False, False, False], + "is_diff_out": [False], + "regularity_check": False, + "error_on_fail": False, + "enable_jacobian": False, + } + + # Casadi has a bug where it does not correctly handle arrays with + # zeros padded at the beginning or end. To avoid this, we add and + # subtract the same number to the variable to reinforce the + # variable bounds. This does not affect the answer + epsilon = 1.0 + var_sym = (var_sym - epsilon) + epsilon + + var_casadi = casadi.Function( + "variable", + [t_MX, y_MX, inputs_MX], + [var_sym], + opts, + ) + + # Some variables, like interpolants, cannot be expanded + try: + var_casadi_out = var_casadi.expand() + except RuntimeError as error: + if "'eval_sx' not defined for" not in str(error): + raise error # pragma: no cover + var_casadi_out = var_casadi + + return var_casadi_out def __getitem__(self, key): """Read a variable from the solution. Variables are created 'just in time', i.e. @@ -818,13 +888,23 @@ def __add__(self, other): return new_sol # Update list of sub-solutions + hermite_interpolation = ( + other.hermite_interpolation and self.hermite_interpolation + ) if other.all_ts[0][0] == self.all_ts[-1][-1]: # Skip first time step if it is repeated all_ts = self.all_ts + [other.all_ts[0][1:]] + other.all_ts[1:] all_ys = self.all_ys + [other.all_ys[0][:, 1:]] + other.all_ys[1:] + if hermite_interpolation: + all_yps = self.all_yps + [other.all_yps[0][:, 1:]] + other.all_yps[1:] else: all_ts = self.all_ts + other.all_ts all_ys = self.all_ys + other.all_ys + if hermite_interpolation: + all_yps = self.all_yps + other.all_yps + + if not hermite_interpolation: + all_yps = None # sensitivities can be: # - bool if not using sensitivities or using explicit sensitivities which still @@ -859,6 +939,7 @@ def __add__(self, other): other.y_event, other.termination, all_sensitivities=all_sensitivities, + all_yps=all_yps, ) new_sol.closest_event_idx = other.closest_event_idx @@ -891,6 +972,7 @@ def copy(self): self.y_event, self.termination, self._all_sensitivities, + self.all_yps, ) new_sol._all_inputs_casadi = self.all_inputs_casadi new_sol._sub_solutions = self.sub_solutions @@ -1001,6 +1083,7 @@ def make_cycle_solution( sum_sols.y_event, sum_sols.termination, sum_sols._all_sensitivities, + sum_sols.all_yps, ) cycle_solution._all_inputs_casadi = sum_sols.all_inputs_casadi cycle_solution._sub_solutions = sum_sols.sub_solutions diff --git a/tests/integration/test_models/standard_output_comparison.py b/tests/integration/test_models/standard_output_comparison.py index 4d4d16e5ca..6c56894314 100644 --- a/tests/integration/test_models/standard_output_comparison.py +++ b/tests/integration/test_models/standard_output_comparison.py @@ -66,6 +66,7 @@ def compare(self, var, atol=0, rtol=0.02): # Get variable for each model model_variables = [solution[var] for solution in self.solutions] var0 = model_variables[0] + var0.initialise() spatial_pts = {} if var0.dimensions >= 1: diff --git a/tests/unit/test_experiments/test_simulation_with_experiment.py b/tests/unit/test_experiments/test_simulation_with_experiment.py index 394d64b257..b455e72393 100644 --- a/tests/unit/test_experiments/test_simulation_with_experiment.py +++ b/tests/unit/test_experiments/test_simulation_with_experiment.py @@ -83,8 +83,12 @@ def test_run_experiment(self): assert len(sol.cycles) == 1 # Test outputs - np.testing.assert_array_equal(sol.cycles[0].steps[0]["C-rate"].data, 1 / 20) - np.testing.assert_array_equal(sol.cycles[0].steps[1]["Current [A]"].data, -1) + np.testing.assert_array_almost_equal( + sol.cycles[0].steps[0]["C-rate"].data, 1 / 20 + ) + np.testing.assert_array_almost_equal( + sol.cycles[0].steps[1]["Current [A]"].data, -1 + ) np.testing.assert_array_almost_equal( sol.cycles[0].steps[2]["Voltage [V]"].data, 4.1, decimal=5 ) diff --git a/tests/unit/test_solvers/test_idaklu_solver.py b/tests/unit/test_solvers/test_idaklu_solver.py index b049729ae3..39918d73a4 100644 --- a/tests/unit/test_solvers/test_idaklu_solver.py +++ b/tests/unit/test_solvers/test_idaklu_solver.py @@ -43,9 +43,8 @@ def test_ida_roberts_klu(self): ) # Test - t_eval = np.linspace(0, 3, 100) - t_interp = t_eval - solution = solver.solve(model, t_eval, t_interp=t_interp) + t_eval = [0, 3] + solution = solver.solve(model, t_eval) # test that final time is time of event # y = 0.1 t + y0 so y=0.2 when t=2 @@ -311,8 +310,7 @@ def test_sensitivities_initial_condition(self): options={"jax_evaluator": "iree"} if form == "iree" else {}, ) - t_interp = np.linspace(0, 3, 100) - t_eval = [t_interp[0], t_interp[-1]] + t_eval = [0, 3] a_value = 0.1 @@ -321,7 +319,6 @@ def test_sensitivities_initial_condition(self): t_eval, inputs={"a": a_value}, calculate_sensitivities=True, - t_interp=t_interp, ) np.testing.assert_array_almost_equal( @@ -638,7 +635,7 @@ def test_failures(self): solver = pybamm.IDAKLUSolver() t_eval = [0, 3] - with pytest.raises(pybamm.SolverError, match="FAILURE IDA"): + with pytest.raises(ValueError, match="std::exception"): solver.solve(model, t_eval) def test_dae_solver_algebraic_model(self): @@ -944,7 +941,7 @@ def construct_model(): # Mock a 1D current collector and initialise (none in the model) sol["x_s [m]"].domain = ["current collector"] - sol["x_s [m]"].initialise_1D() + sol["x_s [m]"].entries def test_with_output_variables_and_sensitivities(self): # Construct a model and solve for all variables, then test @@ -1040,7 +1037,7 @@ def test_with_output_variables_and_sensitivities(self): # Mock a 1D current collector and initialise (none in the model) sol["x_s [m]"].domain = ["current collector"] - sol["x_s [m]"].initialise_1D() + sol["x_s [m]"].entries def test_bad_jax_evaluator(self): model = pybamm.lithium_ion.DFN() @@ -1108,19 +1105,40 @@ def test_simulation_period(self): 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", - ) + + def experiment_setup(period=None): + return pybamm.Experiment( + [ + "Discharge at C/10 for 10 seconds", + "Charge at C/10 for 10 seconds", + ], + period=period, + ) + + experiment_1s = experiment_setup(period="1 seconds") solver = pybamm.IDAKLUSolver() - sim = pybamm.Simulation(model, experiment=experiment, solver=solver) - sol = sim.solve() + sim_1s = pybamm.Simulation(model, experiment=experiment_1s, solver=solver) + sol_1s = sim_1s.solve() np.testing.assert_equal( - np.nextafter(sol.sub_solutions[0].t[-1], np.inf), - sol.sub_solutions[1].t[0], + np.nextafter(sol_1s.sub_solutions[0].t[-1], np.inf), + sol_1s.sub_solutions[1].t[0], + ) + + assert not sol_1s.hermite_interpolation + + experiment = experiment_setup(period=None) + sim = pybamm.Simulation(model, experiment=experiment, solver=solver) + sol = sim.solve(model) + + assert sol.hermite_interpolation + + rtol = solver.rtol + atol = solver.atol + np.testing.assert_allclose( + sol_1s["Voltage [V]"].data, + sol["Voltage [V]"](sol_1s.t), + rtol=rtol, + atol=atol, ) def test_python_idaklu_deprecation_errors(self): diff --git a/tests/unit/test_solvers/test_processed_variable.py b/tests/unit/test_solvers/test_processed_variable.py index 6cd456347d..04de88963d 100644 --- a/tests/unit/test_solvers/test_processed_variable.py +++ b/tests/unit/test_solvers/test_processed_variable.py @@ -8,6 +8,13 @@ import numpy as np import pytest +from scipy.interpolate import CubicHermiteSpline + + +if pybamm.has_idaklu(): + _hermite_args = [True, False] +else: + _hermite_args = [False] def to_casadi(var_pybamm, y, inputs=None): @@ -27,55 +34,91 @@ def to_casadi(var_pybamm, y, inputs=None): return var_casadi -def process_and_check_2D_variable( - var, first_spatial_var, second_spatial_var, disc=None, geometry_options=None -): - # first_spatial_var should be on the "smaller" domain, i.e "r" for an "r-x" variable - if geometry_options is None: - geometry_options = {} - if disc is None: - disc = tests.get_discretisation_for_testing() - disc.set_variable_slices([var]) - - first_sol = disc.process_symbol(first_spatial_var).entries[:, 0] - second_sol = disc.process_symbol(second_spatial_var).entries[:, 0] - - # Keep only the first iteration of entries - first_sol = first_sol[: len(first_sol) // len(second_sol)] - var_sol = disc.process_symbol(var) - t_sol = np.linspace(0, 1) - y_sol = np.ones(len(second_sol) * len(first_sol))[:, np.newaxis] * np.linspace(0, 5) - - var_casadi = to_casadi(var_sol, y_sol) - model = tests.get_base_model_with_battery_geometry(**geometry_options) - processed_var = pybamm.ProcessedVariable( - [var_sol], - [var_casadi], - pybamm.Solution(t_sol, y_sol, model, {}), - warn=False, - ) - np.testing.assert_array_equal( - processed_var.entries, - np.reshape(y_sol, [len(first_sol), len(second_sol), len(t_sol)]), - ) - return y_sol, first_sol, second_sol, t_sol +class TestProcessedVariable: + @staticmethod + def _get_yps(y, hermite_interp, values=1): + if hermite_interp: + yp_sol = values * np.ones_like(y) + else: + yp_sol = None + return yp_sol + + @staticmethod + def _sol_default(t_sol, y_sol, yp_sol=None, model=None, inputs=None): + if inputs is None: + inputs = {} + if model is None: + model = tests.get_base_model_with_battery_geometry() + return pybamm.Solution( + t_sol, + y_sol, + model, + inputs, + all_yps=yp_sol, + ) + + def _process_and_check_2D_variable( + self, + var, + first_spatial_var, + second_spatial_var, + disc=None, + geometry_options=None, + hermite_interp=False, + ): + # first_spatial_var should be on the "smaller" domain, i.e "r" for an "r-x" variable + if geometry_options is None: + geometry_options = {} + if disc is None: + disc = tests.get_discretisation_for_testing() + disc.set_variable_slices([var]) + first_sol = disc.process_symbol(first_spatial_var).entries[:, 0] + second_sol = disc.process_symbol(second_spatial_var).entries[:, 0] -class TestProcessedVariable: - def test_processed_variable_0D(self): + # Keep only the first iteration of entries + first_sol = first_sol[: len(first_sol) // len(second_sol)] + var_sol = disc.process_symbol(var) + t_sol = np.linspace(0, 1) + y_sol = 5 * t_sol * np.ones(len(second_sol) * len(first_sol))[:, np.newaxis] + yp_sol = self._get_yps(y_sol, hermite_interp, values=5) + + var_casadi = to_casadi(var_sol, y_sol) + model = tests.get_base_model_with_battery_geometry(**geometry_options) + processed_var = pybamm.process_variable( + [var_sol], + [var_casadi], + self._sol_default(t_sol, y_sol, yp_sol, model), + ) + np.testing.assert_array_equal( + processed_var.entries, + np.reshape(y_sol, [len(first_sol), len(second_sol), len(t_sol)]), + ) + + # check that C++ and Python give the same result + if pybamm.has_idaklu(): + np.testing.assert_array_equal( + processed_var._observe_raw_cpp(), processed_var._observe_raw_python() + ) + + return y_sol, first_sol, second_sol, t_sol, yp_sol + + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_variable_0D(self, hermite_interp): # without space t = pybamm.t y = pybamm.StateVector(slice(0, 1)) var = t * y var.mesh = None + model = pybamm.BaseModel() t_sol = np.linspace(0, 1) y_sol = np.array([np.linspace(0, 5)]) + yp_sol = self._get_yps(y_sol, hermite_interp) var_casadi = to_casadi(var, y_sol) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [var], [var_casadi], - pybamm.Solution(t_sol, y_sol, pybamm.BaseModel(), {}), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol, model), ) np.testing.assert_array_equal(processed_var.entries, t_sol * y_sol[0]) @@ -84,18 +127,41 @@ def test_processed_variable_0D(self): var.mesh = None t_sol = np.array([0]) y_sol = np.array([1])[:, np.newaxis] + yp_sol = np.array([1])[:, np.newaxis] + sol = self._sol_default(t_sol, y_sol, yp_sol, model) var_casadi = to_casadi(var, y_sol) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [var], [var_casadi], - pybamm.Solution(t_sol, y_sol, pybamm.BaseModel(), {}), - warn=False, + sol, ) np.testing.assert_array_equal(processed_var.entries, y_sol[0]) - # check empty sensitivity works + # check that repeated calls return the same data + data1 = processed_var.data + + assert processed_var.entries_raw_initialized + + data2 = processed_var.data + + np.testing.assert_array_equal(data1, data2) + + data_t1 = processed_var(sol.t) + + assert processed_var.xr_array_raw_initialized - def test_processed_variable_0D_no_sensitivity(self): + data_t2 = processed_var(sol.t) + + np.testing.assert_array_equal(data_t1, data_t2) + + # check that C++ and Python give the same result + if pybamm.has_idaklu(): + np.testing.assert_array_equal( + processed_var._observe_raw_cpp(), processed_var._observe_raw_python() + ) + + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_variable_0D_no_sensitivity(self, hermite_interp): # without space t = pybamm.t y = pybamm.StateVector(slice(0, 1)) @@ -103,12 +169,12 @@ def test_processed_variable_0D_no_sensitivity(self): var.mesh = None t_sol = np.linspace(0, 1) y_sol = np.array([np.linspace(0, 5)]) + yp_sol = self._get_yps(y_sol, hermite_interp) var_casadi = to_casadi(var, y_sol) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [var], [var_casadi], - pybamm.Solution(t_sol, y_sol, pybamm.BaseModel(), {}), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol, pybamm.BaseModel()), ) # test no inputs (i.e. no sensitivity) @@ -124,18 +190,18 @@ def test_processed_variable_0D_no_sensitivity(self): y_sol = np.array([np.linspace(0, 5)]) inputs = {"a": np.array([1.0])} var_casadi = to_casadi(var, y_sol, inputs=inputs) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [var], [var_casadi], pybamm.Solution(t_sol, y_sol, pybamm.BaseModel(), inputs), - warn=False, ) # test no sensitivity raises error with pytest.raises(ValueError, match="Cannot compute sensitivities"): print(processed_var.sensitivities) - def test_processed_variable_1D(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_variable_1D(self, hermite_interp): t = pybamm.t var = pybamm.Variable("var", domain=["negative electrode", "separator"]) x = pybamm.SpatialVariable("x", domain=["negative electrode", "separator"]) @@ -149,26 +215,21 @@ def test_processed_variable_1D(self): eqn_sol = disc.process_symbol(eqn) t_sol = np.linspace(0, 1) y_sol = np.ones_like(x_sol)[:, np.newaxis] * np.linspace(0, 5) + yp_sol = self._get_yps(y_sol, hermite_interp, values=5) var_casadi = to_casadi(var_sol, y_sol) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [var_sol], [var_casadi], - pybamm.Solution( - t_sol, y_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol), ) np.testing.assert_array_equal(processed_var.entries, y_sol) np.testing.assert_array_almost_equal(processed_var(t_sol, x_sol), y_sol) eqn_casadi = to_casadi(eqn_sol, y_sol) - processed_eqn = pybamm.ProcessedVariable( + processed_eqn = pybamm.process_variable( [eqn_sol], [eqn_casadi], - pybamm.Solution( - t_sol, y_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol), ) np.testing.assert_array_almost_equal( processed_eqn(t_sol, x_sol), t_sol * y_sol + x_sol[:, np.newaxis] @@ -184,13 +245,10 @@ def test_processed_variable_1D(self): x_s_edge = pybamm.Matrix(disc.mesh["separator"].edges, domain="separator") x_s_edge.mesh = disc.mesh["separator"] x_s_casadi = to_casadi(x_s_edge, y_sol) - processed_x_s_edge = pybamm.ProcessedVariable( + processed_x_s_edge = pybamm.process_variable( [x_s_edge], [x_s_casadi], - pybamm.Solution( - t_sol, y_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol), ) np.testing.assert_array_equal( x_s_edge.entries[:, 0], processed_x_s_edge.entries[:, 0] @@ -201,20 +259,25 @@ def test_processed_variable_1D(self): eqn_sol = disc.process_symbol(eqn) t_sol = np.array([0]) y_sol = np.ones_like(x_sol)[:, np.newaxis] + yp_sol = self._get_yps(y_sol, hermite_interp, values=0) eqn_casadi = to_casadi(eqn_sol, y_sol) - processed_eqn2 = pybamm.ProcessedVariable( + processed_eqn2 = pybamm.process_variable( [eqn_sol], [eqn_casadi], - pybamm.Solution( - t_sol, y_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol), ) np.testing.assert_array_equal( processed_eqn2.entries, y_sol + x_sol[:, np.newaxis] ) - def test_processed_variable_1D_unknown_domain(self): + # check that C++ and Python give the same result + if pybamm.has_idaklu(): + np.testing.assert_array_equal( + processed_eqn2._observe_raw_cpp(), processed_eqn2._observe_raw_python() + ) + + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_variable_1D_unknown_domain(self, hermite_interp): x = pybamm.SpatialVariable("x", domain="SEI layer", coord_sys="cartesian") geometry = pybamm.Geometry( {"SEI layer": {x: {"min": pybamm.Scalar(0), "max": pybamm.Scalar(1)}}} @@ -227,6 +290,7 @@ def test_processed_variable_1D_unknown_domain(self): nt = 100 y_sol = np.zeros((var_pts[x], nt)) + yp_sol = self._get_yps(y_sol, hermite_interp) model = tests.get_base_model_with_battery_geometry() model._geometry = geometry solution = pybamm.Solution( @@ -237,14 +301,16 @@ def test_processed_variable_1D_unknown_domain(self): np.linspace(0, 1, 1), np.zeros(var_pts[x]), "test", + all_yps=yp_sol, ) c = pybamm.StateVector(slice(0, var_pts[x]), domain=["SEI layer"]) c.mesh = mesh["SEI layer"] c_casadi = to_casadi(c, y_sol) - pybamm.ProcessedVariable([c], [c_casadi], solution, warn=False) + pybamm.process_variable([c], [c_casadi], solution) - def test_processed_variable_2D_x_r(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_variable_2D_x_r(self, hermite_interp): var = pybamm.Variable( "var", domain=["negative particle"], @@ -258,9 +324,12 @@ def test_processed_variable_2D_x_r(self): ) disc = tests.get_p2d_discretisation_for_testing() - process_and_check_2D_variable(var, r, x, disc=disc) + self._process_and_check_2D_variable( + var, r, x, disc=disc, hermite_interp=hermite_interp + ) - def test_processed_variable_2D_R_x(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_variable_2D_R_x(self, hermite_interp): var = pybamm.Variable( "var", domain=["negative particle size"], @@ -274,15 +343,17 @@ def test_processed_variable_2D_R_x(self): x = pybamm.SpatialVariable("x", domain=["negative electrode"]) disc = tests.get_size_distribution_disc_for_testing() - process_and_check_2D_variable( + self._process_and_check_2D_variable( var, R, x, disc=disc, geometry_options={"options": {"particle size": "distribution"}}, + hermite_interp=hermite_interp, ) - def test_processed_variable_2D_R_z(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_variable_2D_R_z(self, hermite_interp): var = pybamm.Variable( "var", domain=["negative particle size"], @@ -296,15 +367,17 @@ def test_processed_variable_2D_R_z(self): z = pybamm.SpatialVariable("z", domain=["current collector"]) disc = tests.get_size_distribution_disc_for_testing() - process_and_check_2D_variable( + self._process_and_check_2D_variable( var, R, z, disc=disc, geometry_options={"options": {"particle size": "distribution"}}, + hermite_interp=hermite_interp, ) - def test_processed_variable_2D_r_R(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_variable_2D_r_R(self, hermite_interp): var = pybamm.Variable( "var", domain=["negative particle"], @@ -318,15 +391,17 @@ def test_processed_variable_2D_r_R(self): R = pybamm.SpatialVariable("R", domain=["negative particle size"]) disc = tests.get_size_distribution_disc_for_testing() - process_and_check_2D_variable( + self._process_and_check_2D_variable( var, r, R, disc=disc, geometry_options={"options": {"particle size": "distribution"}}, + hermite_interp=hermite_interp, ) - def test_processed_variable_2D_x_z(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_variable_2D_x_z(self, hermite_interp): var = pybamm.Variable( "var", domain=["negative electrode", "separator"], @@ -340,7 +415,9 @@ def test_processed_variable_2D_x_z(self): z = pybamm.SpatialVariable("z", domain=["current collector"]) disc = tests.get_1p1d_discretisation_for_testing() - y_sol, x_sol, z_sol, t_sol = process_and_check_2D_variable(var, x, z, disc=disc) + y_sol, x_sol, z_sol, t_sol, yp_sol = self._process_and_check_2D_variable( + var, x, z, disc=disc, hermite_interp=hermite_interp + ) del x_sol # On edges @@ -352,19 +429,17 @@ def test_processed_variable_2D_x_z(self): x_s_edge.mesh = disc.mesh["separator"] x_s_edge.secondary_mesh = disc.mesh["current collector"] x_s_casadi = to_casadi(x_s_edge, y_sol) - processed_x_s_edge = pybamm.ProcessedVariable( + processed_x_s_edge = pybamm.process_variable( [x_s_edge], [x_s_casadi], - pybamm.Solution( - t_sol, y_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol), ) np.testing.assert_array_equal( x_s_edge.entries.flatten(), processed_x_s_edge.entries[:, :, 0].T.flatten() ) - def test_processed_variable_2D_space_only(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_variable_2D_space_only(self, hermite_interp): var = pybamm.Variable( "var", domain=["negative particle"], @@ -386,22 +461,21 @@ def test_processed_variable_2D_space_only(self): var_sol = disc.process_symbol(var) t_sol = np.array([0]) y_sol = np.ones(len(x_sol) * len(r_sol))[:, np.newaxis] + yp_sol = self._get_yps(y_sol, hermite_interp) var_casadi = to_casadi(var_sol, y_sol) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [var_sol], [var_casadi], - pybamm.Solution( - t_sol, y_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol), ) np.testing.assert_array_equal( processed_var.entries, np.reshape(y_sol, [len(r_sol), len(x_sol), len(t_sol)]), ) - def test_processed_variable_2D_scikit(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_variable_2D_scikit(self, hermite_interp): var = pybamm.Variable("var", domain=["current collector"]) disc = tests.get_2p1d_discretisation_for_testing() @@ -412,21 +486,20 @@ def test_processed_variable_2D_scikit(self): var_sol.mesh = disc.mesh["current collector"] t_sol = np.linspace(0, 1) u_sol = np.ones(var_sol.shape[0])[:, np.newaxis] * np.linspace(0, 5) + yp_sol = self._get_yps(u_sol, hermite_interp) var_casadi = to_casadi(var_sol, u_sol) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [var_sol], [var_casadi], - pybamm.Solution( - t_sol, u_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, u_sol, yp_sol), ) np.testing.assert_array_equal( processed_var.entries, np.reshape(u_sol, [len(y), len(z), len(t_sol)]) ) - def test_processed_variable_2D_fixed_t_scikit(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_variable_2D_fixed_t_scikit(self, hermite_interp): var = pybamm.Variable("var", domain=["current collector"]) disc = tests.get_2p1d_discretisation_for_testing() @@ -437,22 +510,23 @@ def test_processed_variable_2D_fixed_t_scikit(self): var_sol.mesh = disc.mesh["current collector"] t_sol = np.array([0]) u_sol = np.ones(var_sol.shape[0])[:, np.newaxis] + yp_sol = self._get_yps(u_sol, hermite_interp) var_casadi = to_casadi(var_sol, u_sol) model = tests.get_base_model_with_battery_geometry( options={"dimensionality": 2} ) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [var_sol], [var_casadi], - pybamm.Solution(t_sol, u_sol, model, {}), - warn=False, + pybamm.Solution(t_sol, u_sol, model, {}, all_yps=yp_sol), ) np.testing.assert_array_equal( processed_var.entries, np.reshape(u_sol, [len(y), len(z), len(t_sol)]) ) - def test_processed_var_0D_interpolation(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_var_0D_interpolation(self, hermite_interp): # without spatial dependence t = pybamm.t y = pybamm.StateVector(slice(0, 1)) @@ -462,40 +536,39 @@ def test_processed_var_0D_interpolation(self): eqn.mesh = None t_sol = np.linspace(0, 1, 1000) - y_sol = np.array([np.linspace(0, 5, 1000)]) + y_sol = np.array([5 * t_sol]) + yp_sol = self._get_yps(y_sol, hermite_interp, values=5) var_casadi = to_casadi(var, y_sol) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [var], [var_casadi], - pybamm.Solution( - t_sol, y_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol), ) # vector np.testing.assert_array_equal(processed_var(t_sol), y_sol[0]) # scalar - np.testing.assert_array_equal(processed_var(0.5), 2.5) - np.testing.assert_array_equal(processed_var(0.7), 3.5) + np.testing.assert_array_almost_equal(processed_var(0.5), 2.5) + np.testing.assert_array_almost_equal(processed_var(0.7), 3.5) eqn_casadi = to_casadi(eqn, y_sol) - processed_eqn = pybamm.ProcessedVariable( + processed_eqn = pybamm.process_variable( [eqn], [eqn_casadi], - pybamm.Solution( - t_sol, y_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol), ) np.testing.assert_array_equal(processed_eqn(t_sol), t_sol * y_sol[0]) - np.testing.assert_array_almost_equal(processed_eqn(0.5), 0.5 * 2.5) + assert processed_eqn(0.5).shape == () + + np.testing.assert_array_almost_equal(processed_eqn(0.5), 0.5 * 2.5) + np.testing.assert_array_equal(processed_eqn(2, fill_value=100), 100) # Suppress warning for this test pybamm.set_logging_level("ERROR") np.testing.assert_array_equal(processed_eqn(2), np.nan) pybamm.set_logging_level("WARNING") - def test_processed_var_0D_fixed_t_interpolation(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_var_0D_fixed_t_interpolation(self, hermite_interp): y = pybamm.StateVector(slice(0, 1)) var = y eqn = 2 * y @@ -504,17 +577,18 @@ def test_processed_var_0D_fixed_t_interpolation(self): t_sol = np.array([10]) y_sol = np.array([[100]]) + yp_sol = self._get_yps(y_sol, hermite_interp) eqn_casadi = to_casadi(eqn, y_sol) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [eqn], [eqn_casadi], - pybamm.Solution(t_sol, y_sol, pybamm.BaseModel(), {}), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol, pybamm.BaseModel()), ) - np.testing.assert_array_equal(processed_var(), 200) + assert processed_var() == 200 - def test_processed_var_1D_interpolation(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_var_1D_interpolation(self, hermite_interp): t = pybamm.t var = pybamm.Variable("var", domain=["negative electrode", "separator"]) x = pybamm.SpatialVariable("x", domain=["negative electrode", "separator"]) @@ -526,36 +600,33 @@ def test_processed_var_1D_interpolation(self): var_sol = disc.process_symbol(var) eqn_sol = disc.process_symbol(eqn) t_sol = np.linspace(0, 1) - y_sol = x_sol[:, np.newaxis] * np.linspace(0, 5) + y_sol = x_sol[:, np.newaxis] * (5 * t_sol) + yp_sol = self._get_yps(y_sol, hermite_interp, values=5) var_casadi = to_casadi(var_sol, y_sol) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [var_sol], [var_casadi], - pybamm.Solution( - t_sol, y_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol), ) + # 2 vectors np.testing.assert_array_almost_equal(processed_var(t_sol, x_sol), y_sol) # 1 vector, 1 scalar np.testing.assert_array_almost_equal(processed_var(0.5, x_sol), 2.5 * x_sol) - np.testing.assert_array_equal( - processed_var(t_sol, x_sol[-1]), x_sol[-1] * np.linspace(0, 5) + np.testing.assert_array_almost_equal( + processed_var(t_sol, x_sol[-1]), + x_sol[-1] * np.linspace(0, 5), ) # 2 scalars np.testing.assert_array_almost_equal( processed_var(0.5, x_sol[-1]), 2.5 * x_sol[-1] ) eqn_casadi = to_casadi(eqn_sol, y_sol) - processed_eqn = pybamm.ProcessedVariable( + processed_eqn = pybamm.process_variable( [eqn_sol], [eqn_casadi], - pybamm.Solution( - t_sol, y_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol), ) # 2 vectors np.testing.assert_array_almost_equal( @@ -571,13 +642,10 @@ def test_processed_var_1D_interpolation(self): x_disc = disc.process_symbol(x) x_casadi = to_casadi(x_disc, y_sol) - processed_x = pybamm.ProcessedVariable( + processed_x = pybamm.process_variable( [x_disc], [x_casadi], - pybamm.Solution( - t_sol, y_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol), ) np.testing.assert_array_almost_equal(processed_x(t=0, x=x_sol), x_sol) @@ -587,13 +655,10 @@ def test_processed_var_1D_interpolation(self): ) r_n.mesh = disc.mesh["negative particle"] r_n_casadi = to_casadi(r_n, y_sol) - processed_r_n = pybamm.ProcessedVariable( + processed_r_n = pybamm.process_variable( [r_n], [r_n_casadi], - pybamm.Solution( - t_sol, y_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol), ) np.testing.assert_array_equal(r_n.entries[:, 0], processed_r_n.entries[:, 0]) r_test = np.linspace(0, 0.5) @@ -608,17 +673,17 @@ def test_processed_var_1D_interpolation(self): model = tests.get_base_model_with_battery_geometry( options={"particle size": "distribution"} ) - processed_R_n = pybamm.ProcessedVariable( + processed_R_n = pybamm.process_variable( [R_n], [R_n_casadi], pybamm.Solution(t_sol, y_sol, model, {}), - warn=False, ) np.testing.assert_array_equal(R_n.entries[:, 0], processed_R_n.entries[:, 0]) R_test = np.linspace(0, 1) np.testing.assert_array_almost_equal(processed_R_n(0, R=R_test), R_test) - def test_processed_var_1D_fixed_t_interpolation(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_var_1D_fixed_t_interpolation(self, hermite_interp): var = pybamm.Variable("var", domain=["negative electrode", "separator"]) x = pybamm.SpatialVariable("x", domain=["negative electrode", "separator"]) eqn = var + x @@ -629,15 +694,13 @@ def test_processed_var_1D_fixed_t_interpolation(self): eqn_sol = disc.process_symbol(eqn) t_sol = np.array([1]) y_sol = x_sol[:, np.newaxis] + yp_sol = self._get_yps(y_sol, hermite_interp) eqn_casadi = to_casadi(eqn_sol, y_sol) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [eqn_sol], [eqn_casadi], - pybamm.Solution( - t_sol, y_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol), ) # vector @@ -647,7 +710,8 @@ def test_processed_var_1D_fixed_t_interpolation(self): # scalar np.testing.assert_array_almost_equal(processed_var(x=0.5), 1) - def test_processed_var_wrong_spatial_variable_names(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_var_wrong_spatial_variable_names(self, hermite_interp): var = pybamm.Variable( "var", domain=["domain A", "domain B"], @@ -677,6 +741,7 @@ def test_processed_var_wrong_spatial_variable_names(self): var_sol = disc.process_symbol(var) t_sol = np.linspace(0, 1) y_sol = np.ones(len(a_sol) * len(b_sol))[:, np.newaxis] * np.linspace(0, 5) + yp_sol = self._get_yps(y_sol, hermite_interp) var_casadi = to_casadi(var_sol, y_sol) model = pybamm.BaseModel() @@ -687,14 +752,14 @@ def test_processed_var_wrong_spatial_variable_names(self): } ) with pytest.raises(NotImplementedError, match="Spatial variable name"): - pybamm.ProcessedVariable( + pybamm.process_variable( [var_sol], [var_casadi], - pybamm.Solution(t_sol, y_sol, model, {}), - warn=False, - ) + pybamm.Solution(t_sol, y_sol, model, {}, all_yps=yp_sol), + ).initialise() - def test_processed_var_2D_interpolation(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_var_2D_interpolation(self, hermite_interp): var = pybamm.Variable( "var", domain=["negative particle"], @@ -716,15 +781,13 @@ def test_processed_var_2D_interpolation(self): var_sol = disc.process_symbol(var) t_sol = np.linspace(0, 1) y_sol = np.ones(len(x_sol) * len(r_sol))[:, np.newaxis] * np.linspace(0, 5) + yp_sol = self._get_yps(y_sol, hermite_interp) var_casadi = to_casadi(var_sol, y_sol) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [var_sol], [var_casadi], - pybamm.Solution( - t_sol, y_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol), ) # 3 vectors np.testing.assert_array_equal( @@ -768,20 +831,18 @@ def test_processed_var_2D_interpolation(self): y_sol = np.ones(len(x_sol) * len(r_sol))[:, np.newaxis] * np.linspace(0, 5) var_casadi = to_casadi(var_sol, y_sol) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [var_sol], [var_casadi], - pybamm.Solution( - t_sol, y_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol), ) # 3 vectors np.testing.assert_array_equal( processed_var(t_sol, x_sol, r_sol).shape, (10, 35, 50) ) - def test_processed_var_2D_fixed_t_interpolation(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_var_2D_fixed_t_interpolation(self, hermite_interp): var = pybamm.Variable( "var", domain=["negative particle"], @@ -803,15 +864,13 @@ def test_processed_var_2D_fixed_t_interpolation(self): var_sol = disc.process_symbol(var) t_sol = np.array([0]) y_sol = np.ones(len(x_sol) * len(r_sol))[:, np.newaxis] + yp_sol = self._get_yps(y_sol, hermite_interp) var_casadi = to_casadi(var_sol, y_sol) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [var_sol], [var_casadi], - pybamm.Solution( - t_sol, y_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol), ) # 2 vectors np.testing.assert_array_equal( @@ -823,7 +882,8 @@ def test_processed_var_2D_fixed_t_interpolation(self): # 2 scalars np.testing.assert_array_equal(processed_var(t=0, x=0.2, r=0.2).shape, ()) - def test_processed_var_2D_secondary_broadcast(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_var_2D_secondary_broadcast(self, hermite_interp): var = pybamm.Variable("var", domain=["negative particle"]) broad_var = pybamm.SecondaryBroadcast(var, "negative electrode") x = pybamm.SpatialVariable("x", domain=["negative electrode"]) @@ -836,15 +896,13 @@ def test_processed_var_2D_secondary_broadcast(self): var_sol = disc.process_symbol(broad_var) t_sol = np.linspace(0, 1) y_sol = np.ones(len(x_sol) * len(r_sol))[:, np.newaxis] * np.linspace(0, 5) + yp_sol = self._get_yps(y_sol, hermite_interp) var_casadi = to_casadi(var_sol, y_sol) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [var_sol], [var_casadi], - pybamm.Solution( - t_sol, y_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol), ) # 3 vectors np.testing.assert_array_equal( @@ -877,22 +935,21 @@ def test_processed_var_2D_secondary_broadcast(self): var_sol = disc.process_symbol(broad_var) t_sol = np.linspace(0, 1) y_sol = np.ones(len(x_sol) * len(r_sol))[:, np.newaxis] * np.linspace(0, 5) + yp_sol = self._get_yps(y_sol, hermite_interp, values=5) var_casadi = to_casadi(var_sol, y_sol) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [var_sol], [var_casadi], - pybamm.Solution( - t_sol, y_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol), ) # 3 vectors np.testing.assert_array_equal( processed_var(t_sol, x_sol, r_sol).shape, (10, 35, 50) ) - def test_processed_var_2_d_scikit_interpolation(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_var_2_d_scikit_interpolation(self, hermite_interp): var = pybamm.Variable("var", domain=["current collector"]) disc = tests.get_2p1d_discretisation_for_testing() @@ -903,15 +960,13 @@ def test_processed_var_2_d_scikit_interpolation(self): var_sol.mesh = disc.mesh["current collector"] t_sol = np.linspace(0, 1) u_sol = np.ones(var_sol.shape[0])[:, np.newaxis] * np.linspace(0, 5) + yp_sol = self._get_yps(u_sol, hermite_interp) var_casadi = to_casadi(var_sol, u_sol) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [var_sol], [var_casadi], - pybamm.Solution( - t_sol, u_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, u_sol, yp_sol), ) # 3 vectors np.testing.assert_array_equal( @@ -938,7 +993,8 @@ def test_processed_var_2_d_scikit_interpolation(self): # 3 scalars np.testing.assert_array_equal(processed_var(0.2, y=0.2, z=0.2).shape, ()) - def test_processed_var_2D_fixed_t_scikit_interpolation(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_var_2D_fixed_t_scikit_interpolation(self, hermite_interp): var = pybamm.Variable("var", domain=["current collector"]) disc = tests.get_2p1d_discretisation_for_testing() @@ -949,15 +1005,13 @@ def test_processed_var_2D_fixed_t_scikit_interpolation(self): var_sol.mesh = disc.mesh["current collector"] t_sol = np.array([0]) u_sol = np.ones(var_sol.shape[0])[:, np.newaxis] + yp_sol = self._get_yps(u_sol, hermite_interp) var_casadi = to_casadi(var_sol, u_sol) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [var_sol], [var_casadi], - pybamm.Solution( - t_sol, u_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, u_sol, yp_sol), ) # 2 vectors np.testing.assert_array_equal( @@ -969,7 +1023,8 @@ def test_processed_var_2D_fixed_t_scikit_interpolation(self): # 2 scalars np.testing.assert_array_equal(processed_var(t=0, y=0.2, z=0.2).shape, ()) - def test_processed_var_2D_unknown_domain(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_var_2D_unknown_domain(self, hermite_interp): var = pybamm.Variable( "var", domain=["domain B"], @@ -1007,6 +1062,7 @@ def test_processed_var_2D_unknown_domain(self): var_sol = disc.process_symbol(var) t_sol = np.linspace(0, 1) y_sol = np.ones(len(x_sol) * len(z_sol))[:, np.newaxis] * np.linspace(0, 5) + yp_sol = self._get_yps(y_sol, hermite_interp, values=5) var_casadi = to_casadi(var_sol, y_sol) model = pybamm.BaseModel() @@ -1016,11 +1072,10 @@ def test_processed_var_2D_unknown_domain(self): "domain B": {z: {"min": 0, "max": 1}}, } ) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [var_sol], [var_casadi], - pybamm.Solution(t_sol, y_sol, model, {}), - warn=False, + pybamm.Solution(t_sol, y_sol, model, {}, all_yps=yp_sol), ) # 3 vectors np.testing.assert_array_equal( @@ -1047,7 +1102,8 @@ def test_processed_var_2D_unknown_domain(self): # 3 scalars np.testing.assert_array_equal(processed_var(t=0.2, x=0.2, z=0.2).shape, ()) - def test_3D_raises_error(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_3D_raises_error(self, hermite_interp): var = pybamm.Variable( "var", domain=["negative electrode"], @@ -1059,16 +1115,14 @@ def test_3D_raises_error(self): var_sol = disc.process_symbol(var) t_sol = np.array([0, 1, 2]) u_sol = np.ones(var_sol.shape[0] * 3)[:, np.newaxis] + yp_sol = self._get_yps(u_sol, hermite_interp, values=0) var_casadi = to_casadi(var_sol, u_sol) with pytest.raises(NotImplementedError, match="Shape not recognized"): - pybamm.ProcessedVariable( + pybamm.process_variable( [var_sol], [var_casadi], - pybamm.Solution( - t_sol, u_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, u_sol, yp_sol), ) def test_process_spatial_variable_names(self): @@ -1080,11 +1134,10 @@ def test_process_spatial_variable_names(self): t_sol = np.linspace(0, 1) y_sol = np.array([np.linspace(0, 5)]) var_casadi = to_casadi(var, y_sol) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [var], [var_casadi], pybamm.Solution(t_sol, y_sol, pybamm.BaseModel(), {}), - warn=False, ) # Test empty list returns None @@ -1108,3 +1161,110 @@ def test_process_spatial_variable_names(self): # Test error raised if spatial variable name not recognised with pytest.raises(NotImplementedError, match="Spatial variable name"): processed_var._process_spatial_variable_names(["var1", "var2"]) + + def test_hermite_interpolator(self): + if not pybamm.has_idaklu(): + pytest.skip("Cannot test Hermite interpolation without IDAKLU") + + # initialise dummy solution to access method + def solution_setup(t_sol, sign): + y_sol = np.array([sign * np.sin(t_sol)]) + yp_sol = np.array([sign * np.cos(t_sol)]) + sol = self._sol_default(t_sol, y_sol, yp_sol) + return sol + + # without spatial dependence + t = pybamm.t + y = pybamm.StateVector(slice(0, 1)) + var = y + eqn = t * y + var.mesh = None + eqn.mesh = None + + sign1 = +1 + t_sol1 = np.linspace(0, 1, 100) + sol1 = solution_setup(t_sol1, sign1) + + # Discontinuity in the solution + sign2 = -1 + t_sol2 = np.linspace(np.nextafter(t_sol1[-1], np.inf), t_sol1[-1] + 3, 99) + sol2 = solution_setup(t_sol2, sign2) + + sol = sol1 + sol2 + var_casadi = to_casadi(var, sol.all_ys[0]) + processed_var = pybamm.process_variable( + [var] * len(sol.all_ts), + [var_casadi] * len(sol.all_ts), + sol, + ) + + # Ground truth spline interpolants from scipy + spls = [ + CubicHermiteSpline(t, y, yp, axis=1) + for t, y, yp in zip(sol.all_ts, sol.all_ys, sol.all_yps) + ] + + def spl(t): + t = np.array(t) + out = np.zeros(len(t)) + for i, spl in enumerate(spls): + t0 = sol.all_ts[i][0] + tf = sol.all_ts[i][-1] + + mask = t >= t0 + # Extrapolation is allowed for the final solution + if i < len(spls) - 1: + mask &= t <= tf + + out[mask] = spl(t[mask]).flatten() + return out + + t0 = sol.t[0] + tf = sol.t[-1] + + # Test extrapolation before the first solution time + t_left_extrap = t0 - 1 + with pytest.raises( + ValueError, match="interpolation points must be greater than" + ): + processed_var(t_left_extrap) + + # Test extrapolation after the last solution time + t_right_extrap = [tf + 1] + np.testing.assert_almost_equal( + spl(t_right_extrap), + processed_var(t_right_extrap, fill_value="extrapolate"), + decimal=8, + ) + + t_dense = np.linspace(t0, tf + 1, 1000) + np.testing.assert_almost_equal( + spl(t_dense), + processed_var(t_dense, fill_value="extrapolate"), + decimal=8, + ) + + t_extended = np.union1d(sol.t, sol.t[-1] + 1) + np.testing.assert_almost_equal( + spl(t_extended), + processed_var(t_extended, fill_value="extrapolate"), + decimal=8, + ) + + ## Unsorted arrays + t_unsorted = np.array([0.5, 0.4, 0.6, 0, 1]) + idxs_sort = np.argsort(t_unsorted) + t_sorted = np.sort(t_unsorted) + + y_sorted = processed_var(t_sorted) + + idxs_unsort = np.zeros_like(idxs_sort) + idxs_unsort[idxs_sort] = np.arange(len(t_unsorted)) + + # Check that the unsorted and sorted arrays are the same + assert np.all(t_sorted == t_unsorted[idxs_sort]) + + y_unsorted = processed_var(t_unsorted) + + # Check that the unsorted and sorted arrays are the same + assert np.all(y_unsorted == y_sorted[idxs_unsort]) diff --git a/tests/unit/test_solvers/test_processed_variable_computed.py b/tests/unit/test_solvers/test_processed_variable_computed.py index 59a062b199..0fa46b4414 100644 --- a/tests/unit/test_solvers/test_processed_variable_computed.py +++ b/tests/unit/test_solvers/test_processed_variable_computed.py @@ -57,7 +57,6 @@ def process_and_check_2D_variable( [var_casadi], [y_sol], pybamm.Solution(t_sol, y_sol, model, {}), - warn=False, ) # NB: ProcessedVariableComputed does not interpret y in the same way as # ProcessedVariable; a better test of equivalence is to check that the @@ -82,7 +81,6 @@ def test_processed_variable_0D(self): [var_casadi], [y_sol], pybamm.Solution(t_sol, y_sol, pybamm.BaseModel(), {}), - warn=False, ) # Assert that the processed variable is the same as the solution np.testing.assert_array_equal(processed_var.entries, y_sol[0]) @@ -94,7 +92,7 @@ def test_processed_variable_0D(self): # Check cumtrapz workflow produces no errors processed_var.cumtrapz_ic = 1 - processed_var.initialise_0D() + processed_var.entries # check empty sensitivity works def test_processed_variable_0D_no_sensitivity(self): @@ -111,7 +109,6 @@ def test_processed_variable_0D_no_sensitivity(self): [var_casadi], [y_sol], pybamm.Solution(t_sol, y_sol, pybamm.BaseModel(), {}), - warn=False, ) # test no inputs (i.e. no sensitivity) @@ -132,7 +129,6 @@ def test_processed_variable_0D_no_sensitivity(self): [var_casadi], [y_sol], pybamm.Solution(t_sol, y_sol, pybamm.BaseModel(), inputs), - warn=False, ) # test no sensitivity raises error @@ -157,7 +153,6 @@ def test_processed_variable_1D(self): [var_casadi], [y_sol], sol, - warn=False, ) # Ordering from idaklu with output_variables set is different to @@ -175,7 +170,7 @@ def test_processed_variable_1D(self): processed_var.mesh.edges, processed_var.mesh.nodes, ) - processed_var.initialise_1D() + processed_var.entries processed_var.mesh.nodes, processed_var.mesh.edges = ( processed_var.mesh.edges, processed_var.mesh.nodes, @@ -192,7 +187,7 @@ def test_processed_variable_1D(self): ] for domain in domain_list: processed_var.domain[0] = domain - processed_var.initialise_1D() + processed_var.entries def test_processed_variable_1D_unknown_domain(self): x = pybamm.SpatialVariable("x", domain="SEI layer", coord_sys="cartesian") @@ -220,7 +215,7 @@ def test_processed_variable_1D_unknown_domain(self): c = pybamm.StateVector(slice(0, var_pts[x]), domain=["SEI layer"]) c.mesh = mesh["SEI layer"] c_casadi = to_casadi(c, y_sol) - pybamm.ProcessedVariableComputed([c], [c_casadi], [y_sol], solution, warn=False) + pybamm.ProcessedVariableComputed([c], [c_casadi], [y_sol], solution) def test_processed_variable_2D_x_r(self): var = pybamm.Variable( @@ -330,13 +325,12 @@ def test_processed_variable_2D_x_z(self): x_s_edge.mesh = disc.mesh["separator"] x_s_edge.secondary_mesh = disc.mesh["current collector"] x_s_casadi = to_casadi(x_s_edge, y_sol) - processed_x_s_edge = pybamm.ProcessedVariable( + processed_x_s_edge = pybamm.process_variable( [x_s_edge], [x_s_casadi], pybamm.Solution( t_sol, y_sol, tests.get_base_model_with_battery_geometry(), {} ), - warn=False, ) np.testing.assert_array_equal( x_s_edge.entries.flatten(), processed_x_s_edge.entries[:, :, 0].T.flatten() @@ -371,7 +365,6 @@ def test_processed_variable_2D_space_only(self): [var_casadi], [y_sol], pybamm.Solution(t_sol, y_sol, pybamm.BaseModel(), {}), - warn=False, ) np.testing.assert_array_equal( processed_var.entries, @@ -408,7 +401,6 @@ def test_processed_variable_2D_fixed_t_scikit(self): [var_casadi], [u_sol], pybamm.Solution(t_sol, u_sol, pybamm.BaseModel(), {}), - warn=False, ) np.testing.assert_array_equal( processed_var.entries, np.reshape(u_sol, [len(y), len(z), len(t_sol)]) @@ -434,5 +426,4 @@ def test_3D_raises_error(self): [var_casadi], [u_sol], pybamm.Solution(t_sol, u_sol, pybamm.BaseModel(), {}), - warn=False, ) diff --git a/tests/unit/test_solvers/test_solution.py b/tests/unit/test_solvers/test_solution.py index 3aff012d5b..4ac9312531 100644 --- a/tests/unit/test_solvers/test_solution.py +++ b/tests/unit/test_solvers/test_solution.py @@ -77,14 +77,16 @@ def test_add_solutions(self): # Set up first solution t1 = np.linspace(0, 1) y1 = np.tile(t1, (20, 1)) - sol1 = pybamm.Solution(t1, y1, pybamm.BaseModel(), {"a": 1}) + yp1 = np.tile(t1, (30, 1)) + sol1 = pybamm.Solution(t1, y1, pybamm.BaseModel(), {"a": 1}, all_yps=yp1) sol1.solve_time = 1.5 sol1.integration_time = 0.3 # Set up second solution t2 = np.linspace(1, 2) y2 = np.tile(t2, (20, 1)) - sol2 = pybamm.Solution(t2, y2, pybamm.BaseModel(), {"a": 2}) + yp2 = np.tile(t1, (30, 1)) + sol2 = pybamm.Solution(t2, y2, pybamm.BaseModel(), {"a": 2}, all_yps=yp2) sol2.solve_time = 1 sol2.integration_time = 0.5 From 2ca16ac3a575eec020563e95a3710092afd5bfd7 Mon Sep 17 00:00:00 2001 From: "Eric G. Kratz" Date: Thu, 3 Oct 2024 11:19:51 -0400 Subject: [PATCH 06/24] Fix a type issue for windows (#4486) --- src/pybamm/solvers/c_solvers/idaklu/observe.hpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/pybamm/solvers/c_solvers/idaklu/observe.hpp b/src/pybamm/solvers/c_solvers/idaklu/observe.hpp index e8dc432240..52a0cfdf84 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/observe.hpp +++ b/src/pybamm/solvers/c_solvers/idaklu/observe.hpp @@ -9,6 +9,11 @@ #include using std::vector; +#if defined(_MSC_VER) + #include + typedef SSIZE_T ssize_t; +#endif + /** * @brief Observe and Hermite interpolate ND variables */ From 9751574ffd1de7b6f83996efcc0666fcd29ae293 Mon Sep 17 00:00:00 2001 From: "Eric G. Kratz" Date: Mon, 7 Oct 2024 14:47:11 -0400 Subject: [PATCH 07/24] Fix doc failures in notebooks (#4498) * Update a path * Update docs/conf.py * Try a different cast --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 55a4ac3f61..76dcffb18b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -341,7 +341,7 @@ {% set github_docname = 'github/pybamm-team/pybamm/blob/develop/docs/' + -env.doc2path(env.docname, base=None) %} +env.doc2path(env.docname, base=None) | string() %} {% set notebooks_version = env.config.html_context.notebooks_version %} {% set github_download_url = env.config.html_context.github_download_url %} From a7d476ea4f21a64d1cf3a39f8bc687035b39621d Mon Sep 17 00:00:00 2001 From: Ferran Brosa Planella Date: Mon, 7 Oct 2024 20:24:10 +0100 Subject: [PATCH 08/24] Implement sodium-ion DFN (#4451) * add sodium-ion model, parameters and examples * update citation keys to be consistent * rename electrolyte conductivity from kappa to sigma * add tests for sodium ion * fix doctests * fix doctest * remove accidentally added example * commented better the volume fraction * write volume fractions as a single number, not a substraction * improve basic tests * update CHANGELOG --------- Co-authored-by: Eric G. Kratz --- CHANGELOG.md | 1 + docs/source/examples/index.rst | 1 + .../notebooks/models/sodium-ion.ipynb | 185 +++++++ pyproject.toml | 1 + src/pybamm/CITATIONS.bib | 459 +++++++++--------- src/pybamm/__init__.py | 1 + src/pybamm/input/parameters/__init__.py | 2 +- .../input/parameters/lithium_ion/Ai2020.py | 6 +- .../input/parameters/lithium_ion/Chen2020.py | 4 +- .../input/parameters/lithium_ion/Ecker2015.py | 4 +- .../parameters/lithium_ion/Marquis2019.py | 4 +- .../parameters/lithium_ion/Mohtat2020.py | 4 +- .../parameters/lithium_ion/NCA_Kim2011.py | 4 +- .../parameters/lithium_ion/ORegan2022.py | 2 +- .../parameters/lithium_ion/Ramadass2004.py | 2 +- .../input/parameters/lithium_ion/Xu2019.py | 4 +- .../parameters/sodium_ion/Chayambuka2022.py | 341 +++++++++++++ .../input/parameters/sodium_ion/__init__.py | 1 + .../input/parameters/sodium_ion/data/D_e.csv | 14 + .../input/parameters/sodium_ion/data/D_n.csv | 40 ++ .../input/parameters/sodium_ion/data/D_p.csv | 35 ++ .../input/parameters/sodium_ion/data/U_n.csv | 21 + .../input/parameters/sodium_ion/data/U_p.csv | 28 ++ .../input/parameters/sodium_ion/data/k_n.csv | 32 ++ .../input/parameters/sodium_ion/data/k_p.csv | 24 + .../parameters/sodium_ion/data/sigma_e.csv | 6 + .../models/full_battery_models/__init__.py | 2 +- .../sodium_ion/__init__.py | 6 + .../sodium_ion/basic_dfn.py | 273 +++++++++++ .../submodels/thermal/surface/lumped.py | 2 +- .../transport_efficiency/bruggeman.py | 6 +- .../cation_exchange_membrane.py | 6 +- .../heterogeneous_catalyst.py | 6 +- .../hyperbola_of_revolution.py | 6 +- .../transport_efficiency/ordered_packing.py | 6 +- .../overlapping_spheres.py | 6 +- .../random_overlapping_cylinders.py | 6 +- src/pybamm/parameters/parameter_sets.py | 4 +- src/pybamm/solvers/jax_bdf_solver.py | 20 +- src/pybamm/spatial_methods/spectral_volume.py | 2 +- .../test_lithium_ion/test_basic_models.py | 11 +- .../test_sodium_ion/__init__.py | 0 .../test_sodium_ion/test_basic_models.py | 34 ++ .../test_sodium_ion/__init__.py | 0 .../test_sodium_ion/test_basic_models.py | 14 + .../test_Chayambuka2022.py | 41 ++ 46 files changed, 1402 insertions(+), 275 deletions(-) create mode 100644 docs/source/examples/notebooks/models/sodium-ion.ipynb create mode 100644 src/pybamm/input/parameters/sodium_ion/Chayambuka2022.py create mode 100644 src/pybamm/input/parameters/sodium_ion/__init__.py create mode 100755 src/pybamm/input/parameters/sodium_ion/data/D_e.csv create mode 100755 src/pybamm/input/parameters/sodium_ion/data/D_n.csv create mode 100755 src/pybamm/input/parameters/sodium_ion/data/D_p.csv create mode 100755 src/pybamm/input/parameters/sodium_ion/data/U_n.csv create mode 100755 src/pybamm/input/parameters/sodium_ion/data/U_p.csv create mode 100755 src/pybamm/input/parameters/sodium_ion/data/k_n.csv create mode 100755 src/pybamm/input/parameters/sodium_ion/data/k_p.csv create mode 100644 src/pybamm/input/parameters/sodium_ion/data/sigma_e.csv create mode 100644 src/pybamm/models/full_battery_models/sodium_ion/__init__.py create mode 100644 src/pybamm/models/full_battery_models/sodium_ion/basic_dfn.py create mode 100644 tests/integration/test_models/test_full_battery_models/test_sodium_ion/__init__.py create mode 100644 tests/integration/test_models/test_full_battery_models/test_sodium_ion/test_basic_models.py create mode 100644 tests/unit/test_models/test_full_battery_models/test_sodium_ion/__init__.py create mode 100644 tests/unit/test_models/test_full_battery_models/test_sodium_ion/test_basic_models.py create mode 100644 tests/unit/test_parameters/test_parameter_sets/test_Chayambuka2022.py diff --git a/CHANGELOG.md b/CHANGELOG.md index fd590fbc12..8ffc210a74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Features - Added Hermite interpolation to the (`IDAKLUSolver`) that improves the accuracy and performance of post-processing variables. ([#4464](https://github.com/pybamm-team/PyBaMM/pull/4464)) +- Added `BasicDFN` model for sodium-ion batteries ([#4451](https://github.com/pybamm-team/PyBaMM/pull/4451)) - Added sensitivity calculation support for `pybamm.Simulation` and `pybamm.Experiment` ([#4415](https://github.com/pybamm-team/PyBaMM/pull/4415)) - Added OpenMP parallelization to IDAKLU solver for lists of input parameters ([#4449](https://github.com/pybamm-team/PyBaMM/pull/4449)) - Added phase-dependent particle options to LAM diff --git a/docs/source/examples/index.rst b/docs/source/examples/index.rst index dab8247ddf..3f77578ef5 100644 --- a/docs/source/examples/index.rst +++ b/docs/source/examples/index.rst @@ -68,6 +68,7 @@ The notebooks are organised into subfolders, and can be viewed in the galleries notebooks/models/SEI-on-cracks.ipynb notebooks/models/simulate-3E-cell.ipynb notebooks/models/simulating-ORegan-2022-parameter-set.ipynb + notebooks/models/sodium-ion.ipynb notebooks/models/SPM.ipynb notebooks/models/SPMe.ipynb notebooks/models/submodel_cracking_DFN_or_SPM.ipynb diff --git a/docs/source/examples/notebooks/models/sodium-ion.ipynb b/docs/source/examples/notebooks/models/sodium-ion.ipynb new file mode 100644 index 0000000000..671e7923e9 --- /dev/null +++ b/docs/source/examples/notebooks/models/sodium-ion.ipynb @@ -0,0 +1,185 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# DFN model for sodium-ion batteries\n", + "\n", + "In this notebook we use the DFN model to simulate sodium-ion batteries. The parameters are based on the article\n", + "> K. Chayambuka, G. Mulder, D.L. Danilov, P.H.L. Notten, Physics-based modeling of sodium-ion batteries part II. Model and validation, Electrochimica Acta 404 (2022) 139764. https://doi.org/10.1016/j.electacta.2021.139764.\n", + "\n", + "However, the specific values (including the data for the interpolants) are taken from the COMSOL implementation presented in [this example](https://www.comsol.com/model/1d-isothermal-sodium-ion-battery-117341). As usual, we start by importing PyBaMM." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "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": [ + "import pybamm" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We now need to define the model. In this case we take the `BasicDFN` model for sodium-ion batteries (note how it is called from the `pybamm.sodium_ion` submodule). Note that, at the moment, the model is identical to the one for lithium-ion batteries, but uses different parameter values." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "model = pybamm.sodium_ion.BasicDFN()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In order to replicate the results in the COMSOL example, we discharge at different C-rates and compare the solutions. We loop over the C-rate dictionary and solve the model for each C-rate. We append the solutions into a list so we can later plots the results." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "8ca3353c637e48d28c3b02f42d25fa03", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "interactive(children=(FloatSlider(value=0.0, description='t', max=10.80914150213347, step=0.1080914150213347),…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "C_rates = [1 / 12, 5 / 12, 10 / 12, 1]\n", + "solutions = []\n", + "\n", + "for C_rate in C_rates:\n", + " sim = pybamm.Simulation(model, solver=pybamm.IDAKLUSolver(), C_rate=C_rate)\n", + " sol = sim.solve([0, 4000 / C_rate])\n", + " solutions.append(sol)\n", + "\n", + "pybamm.dynamic_plot(solutions)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can now perform a manual plot of voltage versus capacity, to compare the results with the COMSOL example." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "for solution, C_rate in zip(solutions, C_rates):\n", + " capacity = [i * 1000 for i in solution[\"Discharge capacity [A.h]\"].entries]\n", + " voltage = solution[\"Voltage [V]\"].entries\n", + " plt.plot(capacity, voltage, label=f\"{(12 * C_rate)} A.m-2\")\n", + "\n", + "plt.xlabel(\"Discharge Capacity [mA.h]\")\n", + "plt.ylabel(\"Voltage [V]\");" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "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] Kudakwashe Chayambuka, Grietus Mulder, Dmitri L Danilov, and Peter HL Notten. Physics-based modeling of sodium-ion batteries part ii. model and validation. Electrochimica Acta, 404:139764, 2022.\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] Alan C. Hindmarsh. The PVODE and IDA algorithms. Technical Report, Lawrence Livermore National Lab., CA (US), 2000. doi:10.2172/802599.\n", + "[5] Alan C. Hindmarsh, Peter N. Brown, Keith E. Grant, Steven L. Lee, Radu Serban, Dan E. Shumaker, and Carol S. Woodward. SUNDIALS: Suite of nonlinear and differential/algebraic equation solvers. ACM Transactions on Mathematical Software (TOMS), 31(3):363–396, 2005. doi:10.1145/1089014.1089020.\n", + "[6] 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", + "[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", + "\n" + ] + } + ], + "source": [ + "pybamm.print_citations()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/pyproject.toml b/pyproject.toml index a70f712e3e..1db7c927a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -152,6 +152,7 @@ Ramadass2004 = "pybamm.input.parameters.lithium_ion.Ramadass2004:get_parameter_v Xu2019 = "pybamm.input.parameters.lithium_ion.Xu2019:get_parameter_values" ECM_Example = "pybamm.input.parameters.ecm.example_set:get_parameter_values" MSMR_Example = "pybamm.input.parameters.lithium_ion.MSMR_example_set:get_parameter_values" +Chayambuka2022 = "pybamm.input.parameters.sodium_ion.Chayambuka2022:get_parameter_values" [tool.setuptools] include-package-data = true diff --git a/src/pybamm/CITATIONS.bib b/src/pybamm/CITATIONS.bib index 3d853738b4..26919f7c66 100644 --- a/src/pybamm/CITATIONS.bib +++ b/src/pybamm/CITATIONS.bib @@ -22,6 +22,17 @@ @article{Ai2022 author = {Weilong Ai and Niall Kirkaldy and Yang Jiang and Gregory Offer and Huizhi Wang and Billy Wu}, } +@article{Akanni1987, + title={Effective transport coefficients in heterogeneous media}, + author={Akanni, KA and Evans, JW and Abramson, IS}, + journal={Chemical Engineering Science}, + volume={42}, + number={8}, + pages={1945--1954}, + year={1987}, + publisher={Elsevier} +} + @article{Andersson2019, author = {Andersson, Joel A. E. and Gillis, Joris and Horn, Greg and Rawlings, James B. and Diehl, Moritz}, @@ -47,6 +58,39 @@ @article{Baker2018 publisher={IOP Publishing} } +@article{Baltensperger2003, + title={Spectral differencing with a twist}, + author={Baltensperger, Richard and Trummer, Manfred R}, + journal={SIAM journal on scientific computing}, + volume={24}, + number={5}, + pages={1465--1487}, + year={2003}, + publisher={SIAM} +} + +@article{Barletta2022thevenin, + title={Th{\'e}venin’s Battery Model Parameter Estimation Based on Simulink}, + author={Barletta, Giulio and DiPrima, Piera and Papurello, Davide}, + journal={Energies}, + volume={15}, + number={17}, + pages={6207}, + year={2022}, + publisher={MDPI} +} + +@article{Beeckman1990, + title={Mathematical description of heterogeneous materials}, + author={Beeckman, JW}, + journal={Chemical engineering science}, + volume={45}, + number={8}, + pages={2603--2610}, + year={1990}, + publisher={Elsevier} +} + @article{BrosaPlanella2021, title = {Systematic derivation and validation of a reduced thermal-electrochemical model for lithium-ion batteries using asymptotic methods}, author = {Brosa Planella, Ferran and Sheikh, Muhammad and Widanage, W. Dhammika}, @@ -70,6 +114,38 @@ @article{BrosaPlanella2022 doi = {}, } +@article{Bruggeman1935, + title={Berechnung verschiedener physikalischer Konstanten von heterogenen Substanzen. I. Dielektrizit{\"a}tskonstanten und Leitf{\"a}higkeiten der Mischk{\"o}rper aus isotropen Substanzen}, + author={Bruggeman, Von DAG}, + journal={Annalen der physik}, + volume={416}, + number={7}, + pages={636--664}, + year={1935}, + publisher={Wiley Online Library} +} + +@article{Byrne1975, + title={A polyalgorithm for the numerical solution of ordinary differential equations}, + author={Byrne, George D. and Hindmarsh, Alan C.}, + journal={ACM Transactions on Mathematical Software (TOMS)}, + volume={1}, + number={1}, + pages={71--96}, + year={1975}, + publisher={ACM New York, NY, USA} +} + +@article{Chayambuka2022, + title={Physics-based modeling of sodium-ion batteries part II. Model and validation}, + author={Chayambuka, Kudakwashe and Mulder, Grietus and Danilov, Dmitri L and Notten, Peter HL}, + journal={Electrochimica Acta}, + volume={404}, + pages={139764}, + year={2022}, + publisher={Elsevier} +} + @article{Chen2020, author = {Chen, Chang-Hui and Brosa Planella, Ferran and O'Regan, Kieran and Gastol, Dominika and Widanage, W. Dhammika and Kendrick, Emma}, title = {{Development of Experimental Techniques for Parameterization of Multi-scale Lithium-ion Battery Models}}, @@ -140,6 +216,16 @@ @article{Ecker2015ii doi = {10.1149/2.0541509jes}, } +@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} +} + @article{Gustafsson2020, doi = {10.21105/joss.02369}, year = {2020}, @@ -152,6 +238,13 @@ @article{Gustafsson2020 journal = {Journal of Open Source Software}, } +@book{Hairer1993, + title={Solving ordinary differential equations. 1, Nonstiff problems}, + author={Hairer, Ernst and N{\o}rsett, Syvert P and Wanner, Gerhard}, + year={1993}, + publisher={Springer-Vlg} +} + @article{Hales2019, title={The cell cooling coefficient: a standard to define heat rejection from lithium-ion batteries}, author={Hales, Alastair and Diaz, Laura Bravo and Marzook, Mohamed Waseem and Zhao, Yan and Patel, Yatish and Offer, Gregory}, @@ -252,7 +345,18 @@ @article{Lain2019 doi = {10.3390/batteries5040064}, } -@article{lin2014lumped, +@article{Landesfeind2019, + title={Temperature and concentration dependence of the ionic transport properties of lithium-ion battery electrolytes}, + author={Landesfeind, Johannes and Gasteiger, Hubert A}, + journal={Journal of The Electrochemical Society}, + volume={166}, + number={14}, + pages={A3079--A3097}, + year={2019}, + publisher={The Electrochemical Society} +} + +@article{Lin2014, 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}, @@ -262,6 +366,17 @@ @article{lin2014lumped publisher={Elsevier} } +@article{Mackie1955, + title={The diffusion of electrolytes in a cation-exchange resin membrane I. Theoretical}, + author={Mackie, JS and Meares, P}, + journal={Proceedings of the Royal Society of London. Series A. Mathematical and Physical Sciences}, + volume={232}, + number={1191}, + pages={498--509}, + year={1955}, + publisher={The Royal Society London} +} + @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}, @@ -321,6 +436,17 @@ @article{Newman1962 publisher={IOP Publishing} } +@article{Nieto2012, +author = {Nieto, Nerea and Diaz, Luis and Gastelurrutia, Jon and Alava, Isabel and Blanco, Francisco and Ramos, Juan and Rivas, Alejandro}, +year = {2012}, +month = {11}, +pages = {A212-A217}, +title = {Thermal Modeling of Large Format Lithium-Ion Cells}, +volume = {160}, +journal = {Journal of the Electrochemical Society}, +doi = {10.1149/2.042302jes} +} + @article{Nyman2008, title={Electrochemical characterisation and modelling of the mass transport phenomena in LiPF6--EC--EMC electrolyte}, author={Nyman, Andreas and Behm, M{\aa}rten and Lindbergh, G{\"o}ran}, @@ -370,6 +496,28 @@ @article{ORegan2022 doi = {10.1016/j.electacta.2022.140700}, } +@article{Petersen1958, + title={Diffusion in a pore of varying cross section}, + author={Petersen, EE}, + journal={AIChE Journal}, + volume={4}, + number={3}, + pages={343--345}, + year={1958}, + publisher={Wiley Online Library} +} + +@article{Ploehn2004, + title={Solvent diffusion model for aging of lithium-ion battery cells}, + author={Ploehn, Harry J and Ramadass, Premanand and White, Ralph E}, + journal={Journal of The Electrochemical Society}, + volume={151}, + number={3}, + pages={A456}, + year={2004}, + publisher={IOP Publishing} +} + @article{Prada2013, title = {{A simplified electrochemical and thermal aging model of LiFePO4-graphite Li-ion batteries: power and capacity fade simulations}}, author = {Prada, Eric and Di Domenico, D. and Creff, Y. and Bernard, J. and Sauvant-Moynot, Val{\'{e}}rie and Huet, Fran{\c{c}}ois}, @@ -439,6 +587,61 @@ @article{Richardson2021 doi = {10.1016/j.electacta.2021.138909}, } +@article{Rieger2016, + title={A new method to model the thickness change of a commercial pouch cell during discharge}, + author={Rieger, Bernhard and Erhard, Simon V and Rumpf, Katharina and Jossen, Andreas}, + journal={Journal of The Electrochemical Society}, + volume={163}, + number={8}, + pages={A1566}, + year={2016}, + publisher={IOP Publishing} +} + +@article{Safari2008, + title={Multimodal physics-based aging model for life prediction of Li-ion batteries}, + author={Safari, M and Morcrette, Mathieu and Teyssot, A and Delacourt, Charles}, + journal={Journal of The Electrochemical Society}, + volume={156}, + number={3}, + pages={A145}, + year={2008}, + publisher={IOP Publishing} +} + +@article{Shampine1997, + title={The matlab ode suite}, + author={Shampine, Lawrence F and Reichelt, Mark W}, + journal={SIAM journal on scientific computing}, + volume={18}, + number={1}, + pages={1--22}, + year={1997}, + publisher={SIAM} +} + +@article{Shen2007, + title={Critical review of the impact of tortuosity on diffusion}, + author={Shen, Lihua and Chen, Zhangxin}, + journal={Chemical Engineering Science}, + volume={62}, + number={14}, + pages={3748--3755}, + year={2007}, + publisher={Elsevier} +} + +@article{Single2018, + title={Identifying the mechanism of continued growth of the solid--electrolyte interphase}, + author={Single, Fabian and Latz, Arnulf and Horstmann, Birger}, + journal={ChemSusChem}, + volume={11}, + number={12}, + pages={1950--1955}, + year={2018}, + publisher={Wiley Online Library} +} + @article{Sripad2020, title={Kinetics of lithium electrodeposition and stripping}, author={Sripad, Shashank and Korff, Daniel and DeCaluwe, Steven C and Viswanathan, Venkatasubramanian}, @@ -510,6 +713,17 @@ @article{Timms2021 doi = {10.1137/20M1336898}, } +@article{Tomadakis1993, + title={Transport properties of random arrays of freely overlapping cylinders with various orientation distributions}, + author={Tomadakis, Manolis M and Sotirchos, Stratis V}, + journal={The Journal of chemical physics}, + volume={98}, + number={1}, + pages={616--626}, + year={1993}, + publisher={American Institute of Physics} +} + @article{Valoen2005, title={Transport properties of LiPF6-based Li-ion battery electrolytes}, author={Val{\o}en, Lars Ole and Reimers, Jan N}, @@ -556,6 +770,17 @@ @article{Wang2002 doi = {10.1006/jcph.2002.7041}, } +@article{Weissberg1963, + title={Effective diffusion coefficient in porous media}, + author={Weissberg, Harold L}, + journal={Journal of Applied Physics}, + volume={34}, + number={9}, + pages={2636--2639}, + year={1963}, + publisher={American Institute of Physics} +} + @article{Weng2023, title={Differential voltage analysis for battery manufacturing process control}, author={Weng, Andrew and Siegel, Jason B and Stefanopoulou, Anna}, @@ -563,6 +788,19 @@ @article{Weng2023 year={2023} } +@article{Wycisk2022, + title = {Modified Plett-model for modeling voltage hysteresis in lithium-ion cells}, + journal = {Journal of Energy Storage}, + volume = {52}, + pages = {105016}, + year = {2022}, + issn = {2352-152X}, + doi = {https://doi.org/10.1016/j.est.2022.105016}, + url = {https://www.sciencedirect.com/science/article/pii/S2352152X22010192}, + 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{Xu2019, title={Evolution of Dead Lithium Growth in Lithium Metal Batteries: Experimentally Validated Model of the Apparent Capacity Loss}, author={Xu, Shanshan and Chen, Kuan-Hung and Dasgupta, Neil P and Siegel, Jason B and Stefanopoulou, Anna G}, @@ -595,222 +833,3 @@ @article{Zhao2018 year={2018}, publisher={IOP Publishing} } - -@article{Barletta2022thevenin, - title={Th{\'e}venin’s Battery Model Parameter Estimation Based on Simulink}, - author={Barletta, Giulio and DiPrima, Piera and Papurello, Davide}, - journal={Energies}, - volume={15}, - number={17}, - pages={6207}, - year={2022}, - publisher={MDPI} -} - -@article{Nieto2012, -author = {Nieto, Nerea and Diaz, Luis and Gastelurrutia, Jon and Alava, Isabel and Blanco, Francisco and Ramos, Juan and Rivas, Alejandro}, -year = {2012}, -month = {11}, -pages = {A212-A217}, -title = {Thermal Modeling of Large Format Lithium-Ion Cells}, -volume = {160}, -journal = {Journal of the Electrochemical Society}, -doi = {10.1149/2.042302jes} -} - -@article{shampine1997matlab, - title={The matlab ode suite}, - author={Shampine, Lawrence F and Reichelt, Mark W}, - journal={SIAM journal on scientific computing}, - volume={18}, - number={1}, - pages={1--22}, - year={1997}, - publisher={SIAM} -} - -@article{byrne1975polyalgorithm, - title={A polyalgorithm for the numerical solution of ordinary differential equations}, - author={Byrne, George D. and Hindmarsh, Alan C.}, - journal={ACM Transactions on Mathematical Software (TOMS)}, - volume={1}, - number={1}, - pages={71--96}, - year={1975}, - publisher={ACM New York, NY, USA} -} - -@book{hairer1993solving, - title={Solving ordinary differential equations. 1, Nonstiff problems}, - author={Hairer, Ernst and N{\o}rsett, Syvert P and Wanner, Gerhard}, - year={1993}, - publisher={Springer-Vlg} -} - -@article{baltensperger2003spectral, - title={Spectral differencing with a twist}, - author={Baltensperger, Richard and Trummer, Manfred R}, - journal={SIAM journal on scientific computing}, - volume={24}, - number={5}, - pages={1465--1487}, - year={2003}, - publisher={SIAM} -} - -@article{rieger2016new, - title={A new method to model the thickness change of a commercial pouch cell during discharge}, - author={Rieger, Bernhard and Erhard, Simon V and Rumpf, Katharina and Jossen, Andreas}, - journal={Journal of The Electrochemical Society}, - volume={163}, - number={8}, - pages={A1566}, - year={2016}, - publisher={IOP Publishing} -} - -@article{ploehn2004solvent, - title={Solvent diffusion model for aging of lithium-ion battery cells}, - author={Ploehn, Harry J and Ramadass, Premanand and White, Ralph E}, - journal={Journal of The Electrochemical Society}, - volume={151}, - number={3}, - pages={A456}, - year={2004}, - publisher={IOP Publishing} -} - -@article{single2018identifying, - title={Identifying the mechanism of continued growth of the solid--electrolyte interphase}, - author={Single, Fabian and Latz, Arnulf and Horstmann, Birger}, - journal={ChemSusChem}, - volume={11}, - number={12}, - pages={1950--1955}, - year={2018}, - publisher={Wiley Online Library} -} - -@article{safari2008multimodal, - title={Multimodal physics-based aging model for life prediction of Li-ion batteries}, - author={Safari, M and Morcrette, Mathieu and Teyssot, A and Delacourt, Charles}, - journal={Journal of The Electrochemical Society}, - volume={156}, - number={3}, - pages={A145}, - year={2008}, - publisher={IOP Publishing} -} - -@article{landesfeind2019temperature, - title={Temperature and concentration dependence of the ionic transport properties of lithium-ion battery electrolytes}, - author={Landesfeind, Johannes and Gasteiger, Hubert A}, - journal={Journal of The Electrochemical Society}, - volume={166}, - number={14}, - pages={A3079--A3097}, - year={2019}, - publisher={The Electrochemical Society} -} -@article{akanni1987effective, - title={Effective transport coefficients in heterogeneous media}, - author={Akanni, KA and Evans, JW and Abramson, IS}, - journal={Chemical Engineering Science}, - volume={42}, - number={8}, - pages={1945--1954}, - year={1987}, - publisher={Elsevier} -} -@article{petersen1958diffusion, - title={Diffusion in a pore of varying cross section}, - author={Petersen, EE}, - journal={AIChE Journal}, - volume={4}, - number={3}, - pages={343--345}, - year={1958}, - publisher={Wiley Online Library} -} -@article{bruggeman1935berechnung, - title={Berechnung verschiedener physikalischer Konstanten von heterogenen Substanzen. I. Dielektrizit{\"a}tskonstanten und Leitf{\"a}higkeiten der Mischk{\"o}rper aus isotropen Substanzen}, - author={Bruggeman, Von DAG}, - journal={Annalen der physik}, - volume={416}, - number={7}, - pages={636--664}, - year={1935}, - publisher={Wiley Online Library} -} -@article{weissberg1963effective, - title={Effective diffusion coefficient in porous media}, - author={Weissberg, Harold L}, - journal={Journal of Applied Physics}, - volume={34}, - number={9}, - pages={2636--2639}, - year={1963}, - publisher={American Institute of Physics} -} -@article{tomadakis1993transport, - title={Transport properties of random arrays of freely overlapping cylinders with various orientation distributions}, - author={Tomadakis, Manolis M and Sotirchos, Stratis V}, - journal={The Journal of chemical physics}, - volume={98}, - number={1}, - pages={616--626}, - year={1993}, - publisher={American Institute of Physics} -} -@article{beeckman1990mathematical, - title={Mathematical description of heterogeneous materials}, - author={Beeckman, JW}, - journal={Chemical engineering science}, - volume={45}, - number={8}, - pages={2603--2610}, - year={1990}, - publisher={Elsevier} -} -@article{mackie1955diffusion, - title={The diffusion of electrolytes in a cation-exchange resin membrane I. Theoretical}, - author={Mackie, JS and Meares, P}, - journal={Proceedings of the Royal Society of London. Series A. Mathematical and Physical Sciences}, - volume={232}, - number={1191}, - pages={498--509}, - year={1955}, - publisher={The Royal Society London} -} -@article{shen2007critical, - title={Critical review of the impact of tortuosity on diffusion}, - author={Shen, Lihua and Chen, Zhangxin}, - journal={Chemical Engineering Science}, - volume={62}, - number={14}, - pages={3748--3755}, - year={2007}, - publisher={Elsevier} -} -@article{Wycisk2022, - title = {Modified Plett-model for modeling voltage hysteresis in lithium-ion cells}, - journal = {Journal of Energy Storage}, - volume = {52}, - pages = {105016}, - year = {2022}, - issn = {2352-152X}, - doi = {https://doi.org/10.1016/j.est.2022.105016}, - url = {https://www.sciencedirect.com/science/article/pii/S2352152X22010192}, - 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/src/pybamm/__init__.py b/src/pybamm/__init__.py index 51c7f49969..f51ba05d02 100644 --- a/src/pybamm/__init__.py +++ b/src/pybamm/__init__.py @@ -75,6 +75,7 @@ from .models.full_battery_models import lead_acid from .models.full_battery_models import lithium_ion from .models.full_battery_models import equivalent_circuit +from .models.full_battery_models import sodium_ion # Submodel classes from .models.submodels.base_submodel import BaseSubModel diff --git a/src/pybamm/input/parameters/__init__.py b/src/pybamm/input/parameters/__init__.py index 9ef23b743d..3c21058270 100644 --- a/src/pybamm/input/parameters/__init__.py +++ b/src/pybamm/input/parameters/__init__.py @@ -1 +1 @@ -__all__ = ['ecm', 'lead_acid', 'lithium_ion'] +__all__ = ['ecm', 'lead_acid', 'lithium_ion', 'sodium_ion'] diff --git a/src/pybamm/input/parameters/lithium_ion/Ai2020.py b/src/pybamm/input/parameters/lithium_ion/Ai2020.py index f578d59fa5..4bf51f3440 100644 --- a/src/pybamm/input/parameters/lithium_ion/Ai2020.py +++ b/src/pybamm/input/parameters/lithium_ion/Ai2020.py @@ -517,11 +517,11 @@ def lico2_ocp_Ai2020(sto): def get_parameter_values(): """ Parameters for the Enertech cell (Ai2020), from the papers :footcite:t:`Ai2019`, - :footcite:t:`rieger2016new` and references therein. + :footcite:t:`Rieger2016` and references therein. SEI parameters are example parameters for SEI growth from the papers - :footcite:t:`Ramadass2004`, :footcite:t:`ploehn2004solvent`, - :footcite:t:`single2018identifying`, :footcite:t:`safari2008multimodal`, and + :footcite:t:`Ramadass2004`, :footcite:t:`Ploehn2004`, + :footcite:t:`Single2018`, :footcite:t:`Safari2008`, and :footcite:t:`Yang2017` .. note:: diff --git a/src/pybamm/input/parameters/lithium_ion/Chen2020.py b/src/pybamm/input/parameters/lithium_ion/Chen2020.py index b3655513a1..eccac74615 100644 --- a/src/pybamm/input/parameters/lithium_ion/Chen2020.py +++ b/src/pybamm/input/parameters/lithium_ion/Chen2020.py @@ -213,8 +213,8 @@ def get_parameter_values(): therein. SEI parameters are example parameters for SEI growth from the papers - :footcite:t:`Ramadass2004`, :footcite:t:`ploehn2004solvent`, - :footcite:t:`single2018identifying`, :footcite:t:`safari2008multimodal`, and + :footcite:t:`Ramadass2004`, :footcite:t:`Ploehn2004`, + :footcite:t:`Single2018`, :footcite:t:`Safari2008`, and :footcite:t:`Yang2017` .. note:: diff --git a/src/pybamm/input/parameters/lithium_ion/Ecker2015.py b/src/pybamm/input/parameters/lithium_ion/Ecker2015.py index 05fbbb2fd7..30ca2ef827 100644 --- a/src/pybamm/input/parameters/lithium_ion/Ecker2015.py +++ b/src/pybamm/input/parameters/lithium_ion/Ecker2015.py @@ -488,8 +488,8 @@ def get_parameter_values(): by Dr. Simon O'Kane in the paper :footcite:t:`Richardson2020` SEI parameters are example parameters for SEI growth from the papers - :footcite:t:`Ramadass2004`, :footcite:t:`ploehn2004solvent`, - :footcite:t:`single2018identifying`, :footcite:t:`safari2008multimodal`, and + :footcite:t:`Ramadass2004`, :footcite:t:`Ploehn2004`, + :footcite:t:`Single2018`, :footcite:t:`Safari2008`, and :footcite:t:`Yang2017` .. note:: diff --git a/src/pybamm/input/parameters/lithium_ion/Marquis2019.py b/src/pybamm/input/parameters/lithium_ion/Marquis2019.py index 16591eac2d..13b8f57966 100644 --- a/src/pybamm/input/parameters/lithium_ion/Marquis2019.py +++ b/src/pybamm/input/parameters/lithium_ion/Marquis2019.py @@ -338,8 +338,8 @@ def get_parameter_values(): and references therein. SEI parameters are example parameters for SEI growth from the papers - :footcite:t:`Ramadass2004`, :footcite:t:`ploehn2004solvent`, - :footcite:t:`single2018identifying`, :footcite:t:`safari2008multimodal`, and + :footcite:t:`Ramadass2004`, :footcite:t:`Ploehn2004`, + :footcite:t:`Single2018`, :footcite:t:`Safari2008`, and :footcite:t:`Yang2017` .. note:: diff --git a/src/pybamm/input/parameters/lithium_ion/Mohtat2020.py b/src/pybamm/input/parameters/lithium_ion/Mohtat2020.py index 0176c3f6a0..044cebe3c5 100644 --- a/src/pybamm/input/parameters/lithium_ion/Mohtat2020.py +++ b/src/pybamm/input/parameters/lithium_ion/Mohtat2020.py @@ -319,8 +319,8 @@ def get_parameter_values(): and references therein. SEI parameters are example parameters for SEI growth from the papers - :footcite:t:`Ramadass2004`, :footcite:t:`ploehn2004solvent`, - :footcite:t:`single2018identifying`, :footcite:t:`safari2008multimodal`, and + :footcite:t:`Ramadass2004`, :footcite:t:`Ploehn2004`, + :footcite:t:`Single2018`, :footcite:t:`Safari2008`, and :footcite:t:`Yang2017` SEI parameters diff --git a/src/pybamm/input/parameters/lithium_ion/NCA_Kim2011.py b/src/pybamm/input/parameters/lithium_ion/NCA_Kim2011.py index 1af610f58a..da1191fa8c 100644 --- a/src/pybamm/input/parameters/lithium_ion/NCA_Kim2011.py +++ b/src/pybamm/input/parameters/lithium_ion/NCA_Kim2011.py @@ -297,8 +297,8 @@ def get_parameter_values(): for the planar effective thermal conductivity. SEI parameters are example parameters for SEI growth from the papers - :footcite:t:`Ramadass2004`, :footcite:t:`ploehn2004solvent`, - :footcite:t:`single2018identifying`, :footcite:t:`safari2008multimodal`, and + :footcite:t:`Ramadass2004`, :footcite:t:`Ploehn2004`, + :footcite:t:`Single2018`, :footcite:t:`Safari2008`, and :footcite:t:`Yang2017` .. note:: diff --git a/src/pybamm/input/parameters/lithium_ion/ORegan2022.py b/src/pybamm/input/parameters/lithium_ion/ORegan2022.py index 45c8cdbcbd..d7e240a7b6 100644 --- a/src/pybamm/input/parameters/lithium_ion/ORegan2022.py +++ b/src/pybamm/input/parameters/lithium_ion/ORegan2022.py @@ -921,7 +921,7 @@ def get_parameter_values(): Parameters for an LG M50 cell, from the paper :footcite:t:`ORegan2022` Parameters for a LiPF6 in EC:EMC (3:7 w:w) electrolyte are from the paper - :footcite:t:`landesfeind2019temperature` and references therein. + :footcite:t:`Landesfeind2019` and references therein. """ return { diff --git a/src/pybamm/input/parameters/lithium_ion/Ramadass2004.py b/src/pybamm/input/parameters/lithium_ion/Ramadass2004.py index a1e24da7e3..82c0df76bf 100644 --- a/src/pybamm/input/parameters/lithium_ion/Ramadass2004.py +++ b/src/pybamm/input/parameters/lithium_ion/Ramadass2004.py @@ -355,7 +355,7 @@ def get_parameter_values(): :footcite:t:`Zhao2018` Parameters for SEI growth are from the papers :footcite:t:`Ramadass2004` and - :footcite:t:`safari2008multimodal` + :footcite:t:`Safari2008` .. note:: Ramadass 2004 has mistakes in units and values of SEI parameters, corrected by diff --git a/src/pybamm/input/parameters/lithium_ion/Xu2019.py b/src/pybamm/input/parameters/lithium_ion/Xu2019.py index caee487339..d1c5edea98 100644 --- a/src/pybamm/input/parameters/lithium_ion/Xu2019.py +++ b/src/pybamm/input/parameters/lithium_ion/Xu2019.py @@ -201,8 +201,8 @@ def get_parameter_values(): ^^^^^^^^^^^^^^^^^^^^^^ SEI parameters are example parameters for SEI growth from the papers - :footcite:t:`Ramadass2004`, :footcite:t:`ploehn2004solvent`, - :footcite:t:`single2018identifying`, :footcite:t:`safari2008multimodal`, and + :footcite:t:`Ramadass2004`, :footcite:t:`Ploehn2004`, + :footcite:t:`Single2018`, :footcite:t:`Safari2008`, and :footcite:t:`Yang2017`. .. note:: diff --git a/src/pybamm/input/parameters/sodium_ion/Chayambuka2022.py b/src/pybamm/input/parameters/sodium_ion/Chayambuka2022.py new file mode 100644 index 0000000000..f8c423cf76 --- /dev/null +++ b/src/pybamm/input/parameters/sodium_ion/Chayambuka2022.py @@ -0,0 +1,341 @@ +import pybamm +import os + +path, _ = os.path.split(os.path.abspath(__file__)) + +U_n_data = pybamm.parameters.process_1D_data("U_n.csv", path=path) +U_p_data = pybamm.parameters.process_1D_data("U_p.csv", path=path) +D_n_data = pybamm.parameters.process_1D_data("D_n.csv", path=path) +D_p_data = pybamm.parameters.process_1D_data("D_p.csv", path=path) +k_n_data = pybamm.parameters.process_1D_data("k_n.csv", path=path) +k_p_data = pybamm.parameters.process_1D_data("k_p.csv", path=path) +D_e_data = pybamm.parameters.process_1D_data("D_e.csv", path=path) +sigma_e_data = pybamm.parameters.process_1D_data("sigma_e.csv", path=path) + + +def HC_ocp_Chayambuka2022(sto): + """ + HC open-circuit potential as a function of stochiometry, data taken + from [1]. + + References + ---------- + .. [1] K. Chayambuka, G. Mulder, D.L. Danilov, P.H.L. Notten, Physics-based + modeling of sodium-ion batteries part II. Model and validation, Electrochimica + Acta 404 (2022) 139764. https://doi.org/10.1016/j.electacta.2021.139764. + + Parameters + ---------- + sto: :class:`pybamm.Symbol` + Electrode stochiometry + + Returns + ------- + :class:`pybamm.Symbol` + Open-circuit potential + """ + + name, (x, y) = U_n_data + return pybamm.Interpolant(x, y, sto, name) + + +def HC_diffusivity_Chayambuka2022(sto, T): + """ + HC diffusivity as a function of stochiometry, the data is taken from [1]. + + References + ---------- + .. [1] K. Chayambuka, G. Mulder, D.L. Danilov, P.H.L. Notten, Physics-based + modeling of sodium-ion batteries part II. Model and validation, Electrochimica + Acta 404 (2022) 139764. https://doi.org/10.1016/j.electacta.2021.139764. + + Parameters + ---------- + sto: :class:`pybamm.Symbol` + Electrode stochiometry + T: :class:`pybamm.Symbol` + Dimensional temperature + + Returns + ------- + :class:`pybamm.Symbol` + Solid diffusivity + """ + + name, (x, y) = D_n_data + c_max = pybamm.Parameter("Maximum concentration in negative electrode [mol.m-3]") + return pybamm.Interpolant(x, y, sto * c_max, name) + + +def HC_electrolyte_exchange_current_density_Chayambuka2022(c_e, c_s_surf, c_s_max, T): + """ + Exchange-current density for Butler-Volmer reactions between HC and NaPF6 in + EC:PC. + + References + ---------- + .. [1] K. Chayambuka, G. Mulder, D.L. Danilov, P.H.L. Notten, Physics-based + modeling of sodium-ion batteries part II. Model and validation, Electrochimica + Acta 404 (2022) 139764. https://doi.org/10.1016/j.electacta.2021.139764. + + Parameters + ---------- + c_e : :class:`pybamm.Symbol` + Electrolyte concentration [mol.m-3] + c_s_surf : :class:`pybamm.Symbol` + Particle concentration [mol.m-3] + c_s_max : :class:`pybamm.Symbol` + Maximum particle concentration [mol.m-3] + T : :class:`pybamm.Symbol` + Temperature [K] + + Returns + ------- + :class:`pybamm.Symbol` + Exchange-current density [A.m-2] + """ + name, (x, y) = k_n_data + k_n = pybamm.Interpolant(x, y, c_s_surf, name) + c_e0 = pybamm.Parameter("Initial concentration in electrolyte [mol.m-3]") + + return ( + pybamm.constants.F + * k_n + * (c_e / c_e0) ** 0.5 + * c_s_surf**0.5 + * (c_s_max - c_s_surf) ** 0.5 + / 2 + ) + + +def NVPF_ocp_Chayambuka2022(sto): + """ + NVPF open-circuit potential as a function of stochiometry, data taken + from [1]. + + References + ---------- + .. [1] K. Chayambuka, G. Mulder, D.L. Danilov, P.H.L. Notten, Physics-based + modeling of sodium-ion batteries part II. Model and validation, Electrochimica + Acta 404 (2022) 139764. https://doi.org/10.1016/j.electacta.2021.139764. + + Parameters + ---------- + sto: :class:`pybamm.Symbol` + Electrode stochiometry + + Returns + ------- + :class:`pybamm.Symbol` + Open-circuit potential + """ + + name, (x, y) = U_p_data + return pybamm.Interpolant(x, y, sto, name) + + +def NVPF_diffusivity_Chayambuka2022(sto, T): + """ + NVPF diffusivity as a function of stochiometry, the data is taken from [1]. + + References + ---------- + .. [1] K. Chayambuka, G. Mulder, D.L. Danilov, P.H.L. Notten, Physics-based + modeling of sodium-ion batteries part II. Model and validation, Electrochimica + Acta 404 (2022) 139764. https://doi.org/10.1016/j.electacta.2021.139764. + + Parameters + ---------- + sto: :class:`pybamm.Symbol` + Electrode stochiometry + T: :class:`pybamm.Symbol` + Dimensional temperature + + Returns + ------- + :class:`pybamm.Symbol` + Solid diffusivity + """ + + name, (x, y) = D_p_data + c_max = pybamm.Parameter("Initial concentration in electrolyte [mol.m-3]") + return pybamm.Interpolant(x, y, sto * c_max, name) + + +def NVPF_electrolyte_exchange_current_density_Chayambuka2022(c_e, c_s_surf, c_s_max, T): + """ + Exchange-current density for Butler-Volmer reactions between NVPF and NaPF6 in + EC:PC. + + References + ---------- + .. [1] K. Chayambuka, G. Mulder, D.L. Danilov, P.H.L. Notten, Physics-based + modeling of sodium-ion batteries part II. Model and validation, Electrochimica + Acta 404 (2022) 139764. https://doi.org/10.1016/j.electacta.2021.139764. + + Parameters + ---------- + c_e : :class:`pybamm.Symbol` + Electrolyte concentration [mol.m-3] + c_s_surf : :class:`pybamm.Symbol` + Particle concentration [mol.m-3] + c_s_max : :class:`pybamm.Symbol` + Maximum particle concentration [mol.m-3] + T : :class:`pybamm.Symbol` + Temperature [K] + + Returns + ------- + :class:`pybamm.Symbol` + Exchange-current density [A.m-2] + """ + name, (x, y) = k_p_data + k_p = pybamm.Interpolant(x, y, c_s_surf, name) + c_e0 = pybamm.Parameter("Initial concentration in electrolyte [mol.m-3]") + + return ( + pybamm.constants.F + * k_p + * (c_e / c_e0) ** 0.5 + * c_s_surf**0.5 + * (c_s_max - c_s_surf) ** 0.5 + / 2 + ) + + +def electrolyte_diffusivity_Chayambuka2022(c_e, T): + """ + Diffusivity of NaPF6 in EC:PC (1:1) as a function of ion concentration. The data + comes from [1] + + References + ---------- + .. [1] K. Chayambuka, G. Mulder, D.L. Danilov, P.H.L. Notten, Physics-based + modeling of sodium-ion batteries part II. Model and validation, Electrochimica + Acta 404 (2022) 139764. https://doi.org/10.1016/j.electacta.2021.139764. + + Parameters + ---------- + c_e: :class:`pybamm.Symbol` + Dimensional electrolyte concentration + T: :class:`pybamm.Symbol` + Dimensional temperature + + Returns + ------- + :class:`pybamm.Symbol` + Solid diffusivity + """ + + name, (x, y) = D_e_data + D_e = pybamm.Interpolant(x, y, c_e, name) + + # Chayambuka et al. (2022) does not provide temperature dependence + + return D_e + + +def electrolyte_conductivity_Chayambuka2022(c_e, T): + """ + Conductivity of NaPF6 in EC:PC (1:1) as a function of ion concentration. The data + comes from [1]. + + References + ---------- + .. [1] K. Chayambuka, G. Mulder, D.L. Danilov, P.H.L. Notten, Physics-based + modeling of sodium-ion batteries part II. Model and validation, Electrochimica + Acta 404 (2022) 139764. https://doi.org/10.1016/j.electacta.2021.139764. + + Parameters + ---------- + c_e: :class:`pybamm.Symbol` + Dimensional electrolyte concentration + T: :class:`pybamm.Symbol` + Dimensional temperature + + Returns + ------- + :class:`pybamm.Symbol` + Solid diffusivity + """ + + name, (x, y) = sigma_e_data + sigma_e = pybamm.Interpolant(x, y, c_e, name) + + # Chayambuka et al. (2022) does not provide temperature dependence + + return sigma_e + + +# Call dict via a function to avoid errors when editing in place +def get_parameter_values(): + """ + Parameters for a sodium-ion cell, from the paper :footcite:t:`Chayambuka2022` and references + therein. The specific parameter values are taken from the COMSOL implementation presented in + [this example](https://www.comsol.com/model/1d-isothermal-sodium-ion-battery-117341). + + """ + + return { + "chemistry": "sodium_ion", + # cell + "Negative electrode thickness [m]": 64e-6, + "Separator thickness [m]": 25e-6, + "Positive electrode thickness [m]": 68e-6, + "Electrode height [m]": 2.54e-4, + "Electrode width [m]": 1, + "Nominal cell capacity [A.h]": 3e-3, + "Current function [A]": 3e-3, + "Contact resistance [Ohm]": 0, + # negative electrode + "Negative electrode conductivity [S.m-1]": 256, + "Maximum concentration in negative electrode [mol.m-3]": 14540, + "Negative particle diffusivity [m2.s-1]": HC_diffusivity_Chayambuka2022, + "Negative electrode OCP [V]": HC_ocp_Chayambuka2022, + "Negative electrode porosity": 0.51, + "Negative electrode active material volume fraction": 0.489, # 1 - 0.51 - 0.001 + "Negative particle radius [m]": 3.48e-6, + "Negative electrode Bruggeman coefficient (electrolyte)": 1.5, + "Negative electrode Bruggeman coefficient (electrode)": 0, + "Negative electrode charge transfer coefficient": 0.5, + "Negative electrode exchange-current density [A.m-2]" + "": HC_electrolyte_exchange_current_density_Chayambuka2022, + "Negative electrode OCP entropic change [V.K-1]": 0, + # positive electrode + "Positive electrode conductivity [S.m-1]": 50, + "Maximum concentration in positive electrode [mol.m-3]": 15320, + "Positive particle diffusivity [m2.s-1]": NVPF_diffusivity_Chayambuka2022, + "Positive electrode OCP [V]": NVPF_ocp_Chayambuka2022, + "Positive electrode porosity": 0.23, + "Positive electrode active material volume fraction": 0.55, # 1 - 0.23 - 0.22 + "Positive particle radius [m]": 0.59e-6, + "Positive electrode Bruggeman coefficient (electrolyte)": 1.5, + "Positive electrode Bruggeman coefficient (electrode)": 0, + "Positive electrode charge transfer coefficient": 0.5, + "Positive electrode exchange-current density [A.m-2]" + "": NVPF_electrolyte_exchange_current_density_Chayambuka2022, + "Positive electrode OCP entropic change [V.K-1]": 0, + # separator + "Separator porosity": 0.55, + "Separator Bruggeman coefficient (electrolyte)": 1.5, + # electrolyte + "Initial concentration in electrolyte [mol.m-3]": 1000, + "Cation transference number": 0.45, + "Thermodynamic factor": 1, + "Electrolyte diffusivity [m2.s-1]": electrolyte_diffusivity_Chayambuka2022, + "Electrolyte conductivity [S.m-1]": electrolyte_conductivity_Chayambuka2022, + # experiment + "Reference temperature [K]": 298.15, + "Ambient temperature [K]": 298.15, + "Number of electrodes connected in parallel to make a cell": 1.0, + "Number of cells connected in series to make a battery": 1.0, + "Lower voltage cut-off [V]": 2.0, + "Upper voltage cut-off [V]": 4.2, + "Open-circuit voltage at 0% SOC [V]": 2.0, + "Open-circuit voltage at 100% SOC [V]": 4.2, + "Initial concentration in negative electrode [mol.m-3]": 13520, + "Initial concentration in positive electrode [mol.m-3]": 3320, + "Initial temperature [K]": 298.15, + # citations + "citations": ["Chayambuka2022"], + } diff --git a/src/pybamm/input/parameters/sodium_ion/__init__.py b/src/pybamm/input/parameters/sodium_ion/__init__.py new file mode 100644 index 0000000000..7591ba5554 --- /dev/null +++ b/src/pybamm/input/parameters/sodium_ion/__init__.py @@ -0,0 +1 @@ +__all__ = ['Chayambuka2022'] diff --git a/src/pybamm/input/parameters/sodium_ion/data/D_e.csv b/src/pybamm/input/parameters/sodium_ion/data/D_e.csv new file mode 100755 index 0000000000..3fe74a8cb8 --- /dev/null +++ b/src/pybamm/input/parameters/sodium_ion/data/D_e.csv @@ -0,0 +1,14 @@ +Electrolyte concentration [mol.m-3],Electrolyte diffusivity [m2.s-1] +1.131153,4.14E-11 +124.121064,3.87E-11 +249.37328,3.62E-11 +387.618465,3.33E-11 +538.734332,3.11E-11 +741.699786,2.81E-11 +1013.696117,2.49E-11 +1272.69948,2.25E-11 +1514.27698,2.14E-11 +1811.953531,1.99E-11 +2113.940691,1.86E-11 +2320.97218,1.79E-11 +2471.904616,1.76E-11 diff --git a/src/pybamm/input/parameters/sodium_ion/data/D_n.csv b/src/pybamm/input/parameters/sodium_ion/data/D_n.csv new file mode 100755 index 0000000000..12d42cce5d --- /dev/null +++ b/src/pybamm/input/parameters/sodium_ion/data/D_n.csv @@ -0,0 +1,40 @@ +Negative particle concentration [mol.m-3],Negative electrode difusivity [m2.s-1] +73.891626,2.57E-16 +246.305419,3.03E-16 +418.719212,3.54E-16 +714.285714,3.99E-16 +1108.374384,4.50E-16 +1477.832512,4.83E-16 +1847.29064,5.31E-16 +2241.37931,6.21E-16 +2733.990148,7.60E-16 +3177.339901,9.20E-16 +3620.689655,1.11E-15 +4113.300493,1.36E-15 +4630.541872,1.57E-15 +5197.044335,1.79E-15 +5714.285714,1.93E-15 +6280.788177,2.02E-15 +6896.551724,1.97E-15 +7413.793103,1.84E-15 +7832.512315,1.67E-15 +8325.123153,1.45E-15 +8669.950739,1.27E-15 +9039.408867,1.06E-15 +9384.236453,8.77E-16 +9729.064039,7.08E-16 +10073.89163,5.44E-16 +10394.08867,4.39E-16 +10714.28571,3.58E-16 +10960.59113,3.14E-16 +11206.89655,2.96E-16 +11502.46305,3.14E-16 +11847.29064,3.80E-16 +12142.85714,4.72E-16 +12487.68473,6.13E-16 +12832.51232,7.78E-16 +13226.60099,1.01E-15 +13719.21182,1.21E-15 +14014.77833,1.28E-15 +14261.08374,1.28E-15 +14433.49754,1.24E-15 diff --git a/src/pybamm/input/parameters/sodium_ion/data/D_p.csv b/src/pybamm/input/parameters/sodium_ion/data/D_p.csv new file mode 100755 index 0000000000..51665bcea9 --- /dev/null +++ b/src/pybamm/input/parameters/sodium_ion/data/D_p.csv @@ -0,0 +1,35 @@ +Positive particle concentration [mol.m-3],Positive electrode difusivity [m2.s-1] +131.578947,2.51E-15 +592.105263,1.71E-15 +1052.631579,1.16E-15 +1535.087719,7.41E-16 +2017.54386,4.61E-16 +2521.929825,2.62E-16 +2982.45614,1.56E-16 +3355.263158,9.92E-17 +3618.421053,7.92E-17 +3925.438596,6.61E-17 +4254.385965,5.65E-17 +4495.614035,5.53E-17 +4890.350877,6.07E-17 +5263.157895,6.65E-17 +5679.824561,7.64E-17 +6096.491228,8.38E-17 +6578.947368,8.79E-17 +7039.473684,8.81E-17 +7587.719298,8.44E-17 +8201.754386,7.52E-17 +8837.719298,6.46E-17 +9495.614035,5.05E-17 +10109.64912,4.13E-17 +10657.89474,3.45E-17 +11184.21053,3.16E-17 +11776.31579,3.17E-17 +12346.49123,3.40E-17 +12828.94737,3.57E-17 +13333.33333,3.66E-17 +13750,3.58E-17 +14254.38596,3.35E-17 +14649.12281,2.74E-17 +15000,2.29E-17 +15197.36842,1.95E-17 diff --git a/src/pybamm/input/parameters/sodium_ion/data/U_n.csv b/src/pybamm/input/parameters/sodium_ion/data/U_n.csv new file mode 100755 index 0000000000..ddb213b3db --- /dev/null +++ b/src/pybamm/input/parameters/sodium_ion/data/U_n.csv @@ -0,0 +1,21 @@ +Negative particle stoichiometry,Negative electrode open-circuit potential [V] +0.001436794,1.318963892 +0.001643334,1.21982507 +0.00811789,1.112038542 +0.01027308,1.077546494 +0.035479844,0.978299913 +0.060758448,0.844570264 +0.090185795,0.719443422 +0.127982471,0.577039126 +0.169900951,0.456168787 +0.232688871,0.317967115 +0.320485995,0.1753473 +0.403954777,0.110332349 +0.483167055,0.088439192 +0.574861484,0.075112923 +0.687380455,0.066007238 +0.799899424,0.056901553 +0.891593855,0.043575284 +0.960353451,0.038968561 +0.987446008,0.034541438 +0.995806356,0.021574368 diff --git a/src/pybamm/input/parameters/sodium_ion/data/U_p.csv b/src/pybamm/input/parameters/sodium_ion/data/U_p.csv new file mode 100755 index 0000000000..a2698567e6 --- /dev/null +++ b/src/pybamm/input/parameters/sodium_ion/data/U_p.csv @@ -0,0 +1,28 @@ +Positive particle stoichiometry,Positive electrode open-circuit potential [V] +0.21,4.288102031 +0.21004478,4.210892773 +0.219269532,4.175283892 +0.261497402,4.172472367 +0.332354842,4.172738781 +0.409317336,4.158176665 +0.474174208,4.152479924 +0.526985168,4.143767596 +0.560316674,4.11121965 +0.575706188,4.048901275 +0.582273973,3.941995276 +0.585975815,3.805375531 +0.590901654,3.725196032 +0.598588947,3.695521965 +0.655863013,3.698707605 +0.696598205,3.69292017 +0.741871138,3.684179499 +0.802205196,3.678465753 +0.864031932,3.675727917 +0.930500897,3.649245158 +0.959279735,3.62262069 +0.979341333,3.530616911 +0.980053837,3.450437411 +0.983058101,3.391026925 +0.992282853,3.355418044 +0.996268304,3.162363722 +0.999940293,3.031684459 diff --git a/src/pybamm/input/parameters/sodium_ion/data/k_n.csv b/src/pybamm/input/parameters/sodium_ion/data/k_n.csv new file mode 100755 index 0000000000..152ebfac4e --- /dev/null +++ b/src/pybamm/input/parameters/sodium_ion/data/k_n.csv @@ -0,0 +1,32 @@ +Negative particle concentration [mol.m-3],Negative electrode exchange-current density rate [m.s-1] +121.359223,3.33E-11 +412.621359,2.15E-11 +631.067961,1.39E-11 +800.970874,9.40E-12 +970.873786,6.88E-12 +1140.776699,5.71E-12 +1262.135922,5.26E-12 +1529.126214,7.03E-12 +1796.116505,1.04E-11 +2063.106796,1.58E-11 +2500,2.65E-11 +3058.252427,4.18E-11 +3713.592233,5.83E-11 +4660.194175,7.48E-11 +5412.621359,7.96E-11 +6067.961165,7.48E-11 +6868.932039,6.47E-11 +7718.446602,4.94E-11 +8422.330097,3.47E-11 +9004.854369,2.39E-11 +9538.834951,1.61E-11 +10024.27184,1.21E-11 +10388.34951,1.06E-11 +10752.42718,1.13E-11 +11213.59223,1.51E-11 +11723.30097,2.25E-11 +12378.64078,3.54E-11 +12912.62136,4.94E-11 +13470.87379,6.34E-11 +14004.85437,7.48E-11 +14417.47573,7.96E-11 diff --git a/src/pybamm/input/parameters/sodium_ion/data/k_p.csv b/src/pybamm/input/parameters/sodium_ion/data/k_p.csv new file mode 100755 index 0000000000..7c228e907c --- /dev/null +++ b/src/pybamm/input/parameters/sodium_ion/data/k_p.csv @@ -0,0 +1,24 @@ +Positive particle concentration [mol.m-3],Positive electrode exchange-current density rate [m.s-1] +21.929825,2.27E-10 +372.807018,1.74E-10 +833.333333,1.24E-10 +1293.859649,8.51E-11 +1907.894737,4.83E-11 +2456.140351,2.96E-11 +2960.526316,1.95E-11 +3442.982456,1.45E-11 +3969.298246,1.16E-11 +4802.631579,1.08E-11 +5548.245614,1.17E-11 +6381.578947,1.18E-11 +7192.982456,1.10E-11 +8092.105263,8.79E-12 +8969.298246,6.65E-12 +9649.122807,5.03E-12 +10350.87719,3.87E-12 +11074.5614,3.34E-12 +12171.05263,3.37E-12 +13223.68421,3.60E-12 +14188.59649,3.30E-12 +14934.21053,2.35E-12 +15328.94737,1.91E-12 diff --git a/src/pybamm/input/parameters/sodium_ion/data/sigma_e.csv b/src/pybamm/input/parameters/sodium_ion/data/sigma_e.csv new file mode 100644 index 0000000000..e8a1104901 --- /dev/null +++ b/src/pybamm/input/parameters/sodium_ion/data/sigma_e.csv @@ -0,0 +1,6 @@ +Electrolyte concentration [mol.m-3],Electrolyte conductivity [S.m-1] +150,0.404 +500,0.72 +1000,0.883 +1500,0.861 +2000,0.76 diff --git a/src/pybamm/models/full_battery_models/__init__.py b/src/pybamm/models/full_battery_models/__init__.py index 135f678289..0260f4dd07 100644 --- a/src/pybamm/models/full_battery_models/__init__.py +++ b/src/pybamm/models/full_battery_models/__init__.py @@ -1,2 +1,2 @@ __all__ = ['base_battery_model', 'equivalent_circuit', 'lead_acid', - 'lithium_ion'] + 'lithium_ion', 'sodium_ion'] diff --git a/src/pybamm/models/full_battery_models/sodium_ion/__init__.py b/src/pybamm/models/full_battery_models/sodium_ion/__init__.py new file mode 100644 index 0000000000..52e4e54952 --- /dev/null +++ b/src/pybamm/models/full_battery_models/sodium_ion/__init__.py @@ -0,0 +1,6 @@ +# +# Root of the sodium-ion models module. +# +from .basic_dfn import BasicDFN + +__all__ = ['basic_dfn'] diff --git a/src/pybamm/models/full_battery_models/sodium_ion/basic_dfn.py b/src/pybamm/models/full_battery_models/sodium_ion/basic_dfn.py new file mode 100644 index 0000000000..c6f618d338 --- /dev/null +++ b/src/pybamm/models/full_battery_models/sodium_ion/basic_dfn.py @@ -0,0 +1,273 @@ +# +# Basic Doyle-Fuller-Newman (DFN) Model +# +import pybamm + + +class BasicDFN(pybamm.lithium_ion.BaseModel): + """Doyle-Fuller-Newman (DFN) model of a sodium-ion battery, from + :footcite:t:`Marquis2019`. + + Parameters + ---------- + name : str, optional + The name of the model. + + """ + + def __init__(self, name="Doyle-Fuller-Newman model"): + super().__init__(name=name) + pybamm.citations.register("Marquis2019") + # `param` is a class containing all the relevant parameters and functions for + # this model. These are purely symbolic at this stage, and will be set by the + # `ParameterValues` class when the model is processed. + param = self.param + + ###################### + # Variables + ###################### + # Variables that depend on time only are created without a domain + Q = pybamm.Variable("Discharge capacity [A.h]") + + # Variables that vary spatially are created with a domain + c_e_n = pybamm.Variable( + "Negative electrolyte concentration [mol.m-3]", + domain="negative electrode", + ) + c_e_s = pybamm.Variable( + "Separator electrolyte concentration [mol.m-3]", + domain="separator", + ) + c_e_p = pybamm.Variable( + "Positive electrolyte concentration [mol.m-3]", + domain="positive electrode", + ) + # Concatenations combine several variables into a single variable, to simplify + # implementing equations that hold over several domains + c_e = pybamm.concatenation(c_e_n, c_e_s, c_e_p) + + # Electrolyte potential + phi_e_n = pybamm.Variable( + "Negative electrolyte potential [V]", + domain="negative electrode", + ) + phi_e_s = pybamm.Variable( + "Separator electrolyte potential [V]", + domain="separator", + ) + phi_e_p = pybamm.Variable( + "Positive electrolyte potential [V]", + domain="positive electrode", + ) + phi_e = pybamm.concatenation(phi_e_n, phi_e_s, phi_e_p) + + # Electrode potential + phi_s_n = pybamm.Variable( + "Negative electrode potential [V]", domain="negative electrode" + ) + phi_s_p = pybamm.Variable( + "Positive electrode potential [V]", + domain="positive electrode", + ) + # Particle concentrations are variables on the particle domain, but also vary in + # the x-direction (electrode domain) and so must be provided with auxiliary + # domains + c_s_n = pybamm.Variable( + "Negative particle concentration [mol.m-3]", + domain="negative particle", + auxiliary_domains={"secondary": "negative electrode"}, + ) + c_s_p = pybamm.Variable( + "Positive particle concentration [mol.m-3]", + domain="positive particle", + auxiliary_domains={"secondary": "positive electrode"}, + ) + + # Constant temperature + T = param.T_init + + ###################### + # Other set-up + ###################### + + # Current density + i_cell = param.current_density_with_time + + # Porosity + # Primary broadcasts are used to broadcast scalar quantities across a domain + # into a vector of the right shape, for multiplying with other vectors + eps_n = pybamm.PrimaryBroadcast( + pybamm.Parameter("Negative electrode porosity"), "negative electrode" + ) + eps_s = pybamm.PrimaryBroadcast( + pybamm.Parameter("Separator porosity"), "separator" + ) + eps_p = pybamm.PrimaryBroadcast( + pybamm.Parameter("Positive electrode porosity"), "positive electrode" + ) + eps = pybamm.concatenation(eps_n, eps_s, eps_p) + + # Active material volume fraction (eps + eps_s + eps_inactive = 1) + eps_s_n = pybamm.Parameter("Negative electrode active material volume fraction") + eps_s_p = pybamm.Parameter("Positive electrode active material volume fraction") + + # transport_efficiency + tor = pybamm.concatenation( + eps_n**param.n.b_e, eps_s**param.s.b_e, eps_p**param.p.b_e + ) + a_n = 3 * param.n.prim.epsilon_s_av / param.n.prim.R_typ + a_p = 3 * param.p.prim.epsilon_s_av / param.p.prim.R_typ + + # Interfacial reactions + # Surf takes the surface value of a variable, i.e. its boundary value on the + # right side. This is also accessible via `boundary_value(x, "right")`, with + # "left" providing the boundary value of the left side + c_s_surf_n = pybamm.surf(c_s_n) + sto_surf_n = c_s_surf_n / param.n.prim.c_max + j0_n = param.n.prim.j0(c_e_n, c_s_surf_n, T) + eta_n = phi_s_n - phi_e_n - param.n.prim.U(sto_surf_n, T) + Feta_RT_n = param.F * eta_n / (param.R * T) + j_n = 2 * j0_n * pybamm.sinh(param.n.prim.ne / 2 * Feta_RT_n) + + c_s_surf_p = pybamm.surf(c_s_p) + sto_surf_p = c_s_surf_p / param.p.prim.c_max + j0_p = param.p.prim.j0(c_e_p, c_s_surf_p, T) + eta_p = phi_s_p - phi_e_p - param.p.prim.U(sto_surf_p, T) + Feta_RT_p = param.F * eta_p / (param.R * T) + j_s = pybamm.PrimaryBroadcast(0, "separator") + j_p = 2 * j0_p * pybamm.sinh(param.p.prim.ne / 2 * Feta_RT_p) + + a_j_n = a_n * j_n + a_j_p = a_p * j_p + a_j = pybamm.concatenation(a_j_n, j_s, a_j_p) + + ###################### + # State of Charge + ###################### + I = param.current_with_time + # The `rhs` dictionary contains differential equations, with the key being the + # variable in the d/dt + self.rhs[Q] = I / 3600 + # Initial conditions must be provided for the ODEs + self.initial_conditions[Q] = pybamm.Scalar(0) + + ###################### + # Particles + ###################### + + # The div and grad operators will be converted to the appropriate matrix + # multiplication at the discretisation stage + N_s_n = -param.n.prim.D(c_s_n, T) * pybamm.grad(c_s_n) + N_s_p = -param.p.prim.D(c_s_p, T) * pybamm.grad(c_s_p) + self.rhs[c_s_n] = -pybamm.div(N_s_n) + self.rhs[c_s_p] = -pybamm.div(N_s_p) + # Boundary conditions must be provided for equations with spatial derivatives + self.boundary_conditions[c_s_n] = { + "left": (pybamm.Scalar(0), "Neumann"), + "right": ( + -j_n / (param.F * pybamm.surf(param.n.prim.D(c_s_n, T))), + "Neumann", + ), + } + self.boundary_conditions[c_s_p] = { + "left": (pybamm.Scalar(0), "Neumann"), + "right": ( + -j_p / (param.F * pybamm.surf(param.p.prim.D(c_s_p, T))), + "Neumann", + ), + } + self.initial_conditions[c_s_n] = param.n.prim.c_init + self.initial_conditions[c_s_p] = param.p.prim.c_init + ###################### + # Current in the solid + ###################### + sigma_eff_n = param.n.sigma(T) * eps_s_n**param.n.b_s + i_s_n = -sigma_eff_n * pybamm.grad(phi_s_n) + sigma_eff_p = param.p.sigma(T) * eps_s_p**param.p.b_s + i_s_p = -sigma_eff_p * pybamm.grad(phi_s_p) + # The `algebraic` dictionary contains differential equations, with the key being + # the main scalar variable of interest in the equation + # multiply by Lx**2 to improve conditioning + self.algebraic[phi_s_n] = param.L_x**2 * (pybamm.div(i_s_n) + a_j_n) + self.algebraic[phi_s_p] = param.L_x**2 * (pybamm.div(i_s_p) + a_j_p) + self.boundary_conditions[phi_s_n] = { + "left": (pybamm.Scalar(0), "Dirichlet"), + "right": (pybamm.Scalar(0), "Neumann"), + } + self.boundary_conditions[phi_s_p] = { + "left": (pybamm.Scalar(0), "Neumann"), + "right": (i_cell / pybamm.boundary_value(-sigma_eff_p, "right"), "Neumann"), + } + # Initial conditions must also be provided for algebraic equations, as an + # initial guess for a root-finding algorithm which calculates consistent initial + # conditions + self.initial_conditions[phi_s_n] = pybamm.Scalar(0) + self.initial_conditions[phi_s_p] = param.ocv_init + + ###################### + # Current in the electrolyte + ###################### + i_e = (param.kappa_e(c_e, T) * tor) * ( + param.chiRT_over_Fc(c_e, T) * pybamm.grad(c_e) - pybamm.grad(phi_e) + ) + # multiply by Lx**2 to improve conditioning + self.algebraic[phi_e] = param.L_x**2 * (pybamm.div(i_e) - a_j) + self.boundary_conditions[phi_e] = { + "left": (pybamm.Scalar(0), "Neumann"), + "right": (pybamm.Scalar(0), "Neumann"), + } + self.initial_conditions[phi_e] = -param.n.prim.U_init + + ###################### + # Electrolyte concentration + ###################### + N_e = -tor * param.D_e(c_e, T) * pybamm.grad(c_e) + self.rhs[c_e] = (1 / eps) * ( + -pybamm.div(N_e) + (1 - param.t_plus(c_e, T)) * a_j / param.F + ) + self.boundary_conditions[c_e] = { + "left": (pybamm.Scalar(0), "Neumann"), + "right": (pybamm.Scalar(0), "Neumann"), + } + self.initial_conditions[c_e] = param.c_e_init + + ###################### + # (Some) variables + ###################### + voltage = pybamm.boundary_value(phi_s_p, "right") + num_cells = pybamm.Parameter( + "Number of cells connected in series to make a battery" + ) + # The `variables` dictionary contains all variables that might be useful for + # visualising the solution of the model + self.variables = { + "Negative particle concentration [mol.m-3]": c_s_n, + "Negative particle surface concentration [mol.m-3]": c_s_surf_n, + "Electrolyte concentration [mol.m-3]": c_e, + "Negative electrolyte concentration [mol.m-3]": c_e_n, + "Separator electrolyte concentration [mol.m-3]": c_e_s, + "Positive electrolyte concentration [mol.m-3]": c_e_p, + "Positive particle concentration [mol.m-3]": c_s_p, + "Positive particle surface concentration [mol.m-3]": c_s_surf_p, + "Current [A]": I, + "Current variable [A]": I, # for compatibility with pybamm.Experiment + "Negative electrode potential [V]": phi_s_n, + "Electrolyte potential [V]": phi_e, + "Negative electrolyte potential [V]": phi_e_n, + "Separator electrolyte potential [V]": phi_e_s, + "Positive electrolyte potential [V]": phi_e_p, + "Positive electrode potential [V]": phi_s_p, + "Voltage [V]": voltage, + "Battery voltage [V]": voltage * num_cells, + "Time [s]": pybamm.t, + "Discharge capacity [A.h]": Q, + } + # Events specify points at which a solution should terminate + self.events += [ + pybamm.Event("Minimum voltage [V]", voltage - param.voltage_low_cut), + pybamm.Event("Maximum voltage [V]", param.voltage_high_cut - voltage), + ] + + @property + def default_parameter_values(self): + return pybamm.ParameterValues("Chayambuka2022") diff --git a/src/pybamm/models/submodels/thermal/surface/lumped.py b/src/pybamm/models/submodels/thermal/surface/lumped.py index dc481947e8..9fe3118e2f 100644 --- a/src/pybamm/models/submodels/thermal/surface/lumped.py +++ b/src/pybamm/models/submodels/thermal/surface/lumped.py @@ -19,7 +19,7 @@ class Lumped(pybamm.BaseSubModel): def __init__(self, param, options=None): super().__init__(param, options=options) - pybamm.citations.register("lin2014lumped") + pybamm.citations.register("Lin2014") def get_fundamental_variables(self): T_surf = pybamm.Variable("Surface temperature [K]") diff --git a/src/pybamm/models/submodels/transport_efficiency/bruggeman.py b/src/pybamm/models/submodels/transport_efficiency/bruggeman.py index ec26d7955d..a960f20e66 100644 --- a/src/pybamm/models/submodels/transport_efficiency/bruggeman.py +++ b/src/pybamm/models/submodels/transport_efficiency/bruggeman.py @@ -7,7 +7,7 @@ class Bruggeman(BaseModel): """Submodel for Bruggeman transport_efficiency, - :footcite:t:`bruggeman1935berechnung` + :footcite:t:`Bruggeman1935` Parameters ---------- @@ -28,7 +28,7 @@ def get_coupled_variables(self, variables): for domain in self.options.whole_cell_domains: Domain = domain.capitalize() eps_k = variables[f"{Domain} porosity"] - pybamm.citations.register("bruggeman1935berechnung") + pybamm.citations.register("Bruggeman1935") b_k = self.param.domain_params[domain.split()[0]].b_e tor_k = eps_k**b_k tor_dict[domain] = tor_k @@ -40,7 +40,7 @@ def get_coupled_variables(self, variables): else: Domain = domain.capitalize() phi_k = 1 - variables[f"{Domain} porosity"] - pybamm.citations.register("bruggeman1935berechnung") + pybamm.citations.register("Bruggeman1935") b_k = self.param.domain_params[domain.split()[0]].b_s tor_k = phi_k**b_k tor_dict[domain] = tor_k diff --git a/src/pybamm/models/submodels/transport_efficiency/cation_exchange_membrane.py b/src/pybamm/models/submodels/transport_efficiency/cation_exchange_membrane.py index 3ffb57e7de..b9165cf255 100644 --- a/src/pybamm/models/submodels/transport_efficiency/cation_exchange_membrane.py +++ b/src/pybamm/models/submodels/transport_efficiency/cation_exchange_membrane.py @@ -7,7 +7,7 @@ class CationExchangeMembrane(BaseModel): """Submodel for Cation Exchange Membrane transport_efficiency, - :footcite:t:`bruggeman1935berechnung`, :footcite:t:`shen2007critical` + :footcite:t:`Bruggeman1935`, :footcite:t:`Shen2007` Parameters ---------- @@ -23,8 +23,8 @@ def __init__(self, param, component, options=None): super().__init__(param, component, options=options) def get_coupled_variables(self, variables): - pybamm.citations.register("shen2007critical") - pybamm.citations.register("mackie1955diffusion") + pybamm.citations.register("Shen2007") + pybamm.citations.register("Mackie1955") if self.component == "Electrolyte": tor_dict = {} for domain in self.options.whole_cell_domains: diff --git a/src/pybamm/models/submodels/transport_efficiency/heterogeneous_catalyst.py b/src/pybamm/models/submodels/transport_efficiency/heterogeneous_catalyst.py index 7ec8bc3580..f60f71765c 100644 --- a/src/pybamm/models/submodels/transport_efficiency/heterogeneous_catalyst.py +++ b/src/pybamm/models/submodels/transport_efficiency/heterogeneous_catalyst.py @@ -7,7 +7,7 @@ class HeterogeneousCatalyst(BaseModel): """Submodel for Heterogeneous Catalyst transport_efficiency - :footcite:t:`beeckman1990mathematical`, :footcite:t:`shen2007critical` + :footcite:t:`Beeckman1990`, :footcite:t:`Shen2007` Parameters ---------- @@ -23,8 +23,8 @@ def __init__(self, param, component, options=None): super().__init__(param, component, options=options) def get_coupled_variables(self, variables): - pybamm.citations.register("shen2007critical") - pybamm.citations.register("beeckman1990mathematical") + pybamm.citations.register("Shen2007") + pybamm.citations.register("Beeckman1990") if self.component == "Electrolyte": tor_dict = {} for domain in self.options.whole_cell_domains: diff --git a/src/pybamm/models/submodels/transport_efficiency/hyperbola_of_revolution.py b/src/pybamm/models/submodels/transport_efficiency/hyperbola_of_revolution.py index 306c66b774..fe7e8dfb1d 100644 --- a/src/pybamm/models/submodels/transport_efficiency/hyperbola_of_revolution.py +++ b/src/pybamm/models/submodels/transport_efficiency/hyperbola_of_revolution.py @@ -7,7 +7,7 @@ class HyperbolaOfRevolution(BaseModel): """Submodel for Hyperbola of revolution transport_efficiency - :footcite:t:`petersen1958diffusion`, :footcite:t:`shen2007critical` + :footcite:t:`Petersen1958`, :footcite:t:`Shen2007` Parameters ---------- @@ -23,8 +23,8 @@ def __init__(self, param, component, options=None): super().__init__(param, component, options=options) def get_coupled_variables(self, variables): - pybamm.citations.register("shen2007critical") - pybamm.citations.register("petersen1958diffusion") + pybamm.citations.register("Shen2007") + pybamm.citations.register("Petersen1958") if self.component == "Electrolyte": tor_dict = {} for domain in self.options.whole_cell_domains: diff --git a/src/pybamm/models/submodels/transport_efficiency/ordered_packing.py b/src/pybamm/models/submodels/transport_efficiency/ordered_packing.py index 13b3a3515e..4b9e9b5dc5 100644 --- a/src/pybamm/models/submodels/transport_efficiency/ordered_packing.py +++ b/src/pybamm/models/submodels/transport_efficiency/ordered_packing.py @@ -7,7 +7,7 @@ class OrderedPacking(BaseModel): """Submodel for Ordered Packing transport_efficiency - :footcite:t:`akanni1987effective`, :footcite:t:`shen2007critical` + :footcite:t:`Akanni1987`, :footcite:t:`Shen2007` Parameters ---------- @@ -23,8 +23,8 @@ def __init__(self, param, component, options=None): super().__init__(param, component, options=options) def get_coupled_variables(self, variables): - pybamm.citations.register("shen2007critical") - pybamm.citations.register("akanni1987effective") + pybamm.citations.register("Shen2007") + pybamm.citations.register("Akanni1987") if self.component == "Electrolyte": tor_dict = {} for domain in self.options.whole_cell_domains: diff --git a/src/pybamm/models/submodels/transport_efficiency/overlapping_spheres.py b/src/pybamm/models/submodels/transport_efficiency/overlapping_spheres.py index 9bbed1fd05..ae2dbc590d 100644 --- a/src/pybamm/models/submodels/transport_efficiency/overlapping_spheres.py +++ b/src/pybamm/models/submodels/transport_efficiency/overlapping_spheres.py @@ -7,7 +7,7 @@ class OverlappingSpheres(BaseModel): """Submodel for Overlapping Spheres transport_efficiency - :footcite:t:`weissberg1963effective`, :footcite:t:`shen2007critical` + :footcite:t:`Weissberg1963`, :footcite:t:`Shen2007` Parameters ---------- @@ -23,8 +23,8 @@ def __init__(self, param, component, options=None): super().__init__(param, component, options=options) def get_coupled_variables(self, variables): - pybamm.citations.register("shen2007critical") - pybamm.citations.register("weissberg1963effective") + pybamm.citations.register("Shen2007") + pybamm.citations.register("Weissberg1963") if self.component == "Electrolyte": tor_dict = {} for domain in self.options.whole_cell_domains: diff --git a/src/pybamm/models/submodels/transport_efficiency/random_overlapping_cylinders.py b/src/pybamm/models/submodels/transport_efficiency/random_overlapping_cylinders.py index da32f2f4fe..b9eb49a54e 100644 --- a/src/pybamm/models/submodels/transport_efficiency/random_overlapping_cylinders.py +++ b/src/pybamm/models/submodels/transport_efficiency/random_overlapping_cylinders.py @@ -7,7 +7,7 @@ class RandomOverlappingCylinders(BaseModel): """Submodel for Random Overlapping Cylinders transport_efficiency, - :footcite:t:`tomadakis1993transport`, :footcite:t:`shen2007critical` + :footcite:t:`Tomadakis1993`, :footcite:t:`Shen2007` Parameters ---------- @@ -23,8 +23,8 @@ def __init__(self, param, component, options=None): super().__init__(param, component, options=options) def get_coupled_variables(self, variables): - pybamm.citations.register("shen2007critical") - pybamm.citations.register("tomadakis1993transport") + pybamm.citations.register("Shen2007") + pybamm.citations.register("Tomadakis1993") if self.component == "Electrolyte": tor_dict = {} for domain in self.options.whole_cell_domains: diff --git a/src/pybamm/parameters/parameter_sets.py b/src/pybamm/parameters/parameter_sets.py index a3ddd0ed2e..22b476f4e0 100644 --- a/src/pybamm/parameters/parameter_sets.py +++ b/src/pybamm/parameters/parameter_sets.py @@ -18,7 +18,7 @@ class ParameterSets(Mapping): >>> import pybamm >>> list(pybamm.parameter_sets) - ['Ai2020', 'Chen2020', ...] + ['Ai2020', 'Chayambuka2022', ...] Get the docstring for a parameter set: @@ -26,7 +26,7 @@ class ParameterSets(Mapping): >>> print(pybamm.parameter_sets.get_docstring("Ai2020")) Parameters for the Enertech cell (Ai2020), from the papers :footcite:t:`Ai2019`, - :footcite:t:`rieger2016new` and references therein. + :footcite:t:`Rieger2016` and references therein. ... See also: :ref:`adding-parameter-sets` diff --git a/src/pybamm/solvers/jax_bdf_solver.py b/src/pybamm/solvers/jax_bdf_solver.py index 3db82ca0da..a07ad8505b 100644 --- a/src/pybamm/solvers/jax_bdf_solver.py +++ b/src/pybamm/solvers/jax_bdf_solver.py @@ -68,7 +68,7 @@ def caller(*args): def _bdf_odeint(fun, mass, rtol, atol, y0, t_eval, *args): """ Implements a Backward Difference formula (BDF) implicit multistep integrator. - The basic algorithm is derived in :footcite:t:`byrne1975polyalgorithm`. This + The basic algorithm is derived in :footcite:t:`Byrne1975`. This particular implementation follows that implemented in the Matlab routine ode15s described in :footcite:t:`shamphine1997matlab` and the SciPy implementation :footcite:t:`Virtanen2020`, which features the NDF formulas for improved @@ -362,7 +362,7 @@ def _select_initial_step(atol, rtol, fun, t0, y0, f0, h0): comparing the predicted state against that using the provided function. Optimal step size based on the selected order is obtained using formula (4.12) - in :footcite:t:`hairer1993solving`. + in :footcite:t:`Hairer1993`. """ scale = atol + jnp.abs(y0) * rtol @@ -926,14 +926,14 @@ def ravel_first_arg_(unravel, y_flat, *args): def jax_bdf_integrate(func, y0, t_eval, *args, rtol=1e-6, atol=1e-6, mass=None): """ Backward Difference formula (BDF) implicit multistep integrator. The basic algorithm - is derived in :footcite:t:`byrne1975polyalgorithm`. This particular implementation - follows the Matlab routine ode15s described in :footcite:t:`shampine1997matlab` - and the SciPy implementation :footcite:t:`Virtanen2020` which features - the NDF formulas for improved stability, with associated differences in the - error constants, and calculates the jacobian at J(t_{n+1}, y^0_{n+1}). This - implementation was based on that implemented in the SciPy library - :footcite:t:`Virtanen2020`, which also mainly follows :footcite:t:`shampine1997matlab` - but uses the more standard jacobian update. + is derived in :footcite:t:`Byrne1975`. This particular implementation + follows that implemented in the Matlab routine ode15s described in + :footcite:t:`Shampine1997` and the SciPy implementation + :footcite:t:`Virtanen2020` which features the NDF formulas for improved stability, + with associated differences in the error constants, and calculates the jacobian at + J(t_{n+1}, y^0_{n+1}). This implementation was based on that implemented in the + SciPy library :footcite:t:`Virtanen2020`, which also mainly follows + :footcite:t:`Shampine1997` but uses the more standard jacobian update. Parameters ---------- diff --git a/src/pybamm/spatial_methods/spectral_volume.py b/src/pybamm/spatial_methods/spectral_volume.py index 11c6dfd6d2..8699045dca 100644 --- a/src/pybamm/spatial_methods/spectral_volume.py +++ b/src/pybamm/spatial_methods/spectral_volume.py @@ -176,7 +176,7 @@ def cv_boundary_reconstruction_matrix(self, domains): def chebyshev_differentiation_matrices(self, noe, dod): """ Chebyshev differentiation matrices, from - :footcite:t:`baltensperger2003spectral`. + :footcite:t:`Baltensperger2003`. Parameters ---------- 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 abb0169d06..3288db75fe 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 @@ -16,7 +16,16 @@ def test_with_experiment(self): ] ) sim = pybamm.Simulation(model, experiment=experiment) - sim.solve(calc_esoh=False) + sol = sim.solve(calc_esoh=False) + + # Check the solve returned a solution + assert sol is not None + + # Check that the solution contains the expected number of cycles + assert len(sol.cycles) == 3 + + # Check that the solution terminated because it reached final time + assert sol.termination == "final time" class TestBasicSPM(BaseBasicModelTest): diff --git a/tests/integration/test_models/test_full_battery_models/test_sodium_ion/__init__.py b/tests/integration/test_models/test_full_battery_models/test_sodium_ion/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integration/test_models/test_full_battery_models/test_sodium_ion/test_basic_models.py b/tests/integration/test_models/test_full_battery_models/test_sodium_ion/test_basic_models.py new file mode 100644 index 0000000000..2e0321cb1b --- /dev/null +++ b/tests/integration/test_models/test_full_battery_models/test_sodium_ion/test_basic_models.py @@ -0,0 +1,34 @@ +# +# Test basic model classes +# +import pybamm +import pytest + + +class BaseBasicModelTest: + def test_with_experiment(self): + model = self.model + experiment = pybamm.Experiment( + [ + "Discharge at C/3 until 3.5V", + "Hold at 3.5V for 1 hour", + "Rest for 10 min", + ] + ) + sim = pybamm.Simulation(model, experiment=experiment) + sol = sim.solve(calc_esoh=False) + + # Check the solve returned a solution + assert sol is not None + + # Check that the solution contains the expected number of cycles + assert len(sol.cycles) == 3 + + # Check that the solution terminated because it reached final time + assert sol.termination == "final time" + + +class TestBasicDFN(BaseBasicModelTest): + @pytest.fixture(autouse=True) + def setup(self): + self.model = pybamm.sodium_ion.BasicDFN() diff --git a/tests/unit/test_models/test_full_battery_models/test_sodium_ion/__init__.py b/tests/unit/test_models/test_full_battery_models/test_sodium_ion/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/test_models/test_full_battery_models/test_sodium_ion/test_basic_models.py b/tests/unit/test_models/test_full_battery_models/test_sodium_ion/test_basic_models.py new file mode 100644 index 0000000000..6b085d3a79 --- /dev/null +++ b/tests/unit/test_models/test_full_battery_models/test_sodium_ion/test_basic_models.py @@ -0,0 +1,14 @@ +# +# Tests for the basic sodium-ion models +# +import pybamm + + +class TestBasicModels: + def test_dfn_well_posed(self): + model = pybamm.sodium_ion.BasicDFN() + model.check_well_posedness() + + def test_default_parameters(self): + model = pybamm.sodium_ion.BasicDFN() + assert "Chayambuka2022" in model.default_parameter_values["citations"] diff --git a/tests/unit/test_parameters/test_parameter_sets/test_Chayambuka2022.py b/tests/unit/test_parameters/test_parameter_sets/test_Chayambuka2022.py new file mode 100644 index 0000000000..db2eea0d65 --- /dev/null +++ b/tests/unit/test_parameters/test_parameter_sets/test_Chayambuka2022.py @@ -0,0 +1,41 @@ +# +# Tests for Chayambuka et al (2022) parameter set +# + +import pytest +import pybamm + + +class TestChayambuka2022: + def test_functions(self): + param = pybamm.ParameterValues("Chayambuka2022") + sto = pybamm.Scalar(0.5) + T = pybamm.Scalar(298.15) + c_e = 1000 + c_n_max = 14540 + c_p_max = 15320 + + fun_test = { + # Negative electrode + "Negative particle diffusivity [m2.s-1]": ([sto, T], 1.8761e-15), + "Negative electrode OCP [V]": ([sto], 0.0859), + "Negative electrode exchange-current density [A.m-2]" "": ( + [c_e, sto * c_n_max, c_n_max, T], + 0.0202, + ), + # Positive electrode + "Positive particle diffusivity [m2.s-1]": ([sto, T], 1.8700e-15), + "Positive electrode OCP [V]": ([sto], 4.1482), + "Positive electrode exchange-current density [A.m-2]" "": ( + [c_e, sto * c_p_max, c_p_max, T], + 0.0036, + ), + # Electrolyte + "Electrolyte diffusivity [m2.s-1]": ([c_e, T], 2.5061e-10), + "Electrolyte conductivity [S.m-1]": ([c_e, T], 0.8830), + } + + for name, value in fun_test.items(): + assert param.evaluate(param[name](*value[0])) == pytest.approx( + value[1], abs=0.0001 + ) From 0636e947b0bbd4db5a32a79bda4662b6bedbcf3e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 17:21:32 -0400 Subject: [PATCH 09/24] Build(deps): bump the actions group with 3 updates (#4499) Bumps the actions group with 3 updates: [actions/upload-artifact](https://github.com/actions/upload-artifact), [codecov/codecov-action](https://github.com/codecov/codecov-action) and [github/codeql-action](https://github.com/github/codeql-action). Updates `actions/upload-artifact` from 4.4.0 to 4.4.1 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4.4.0...v4.4.1) Updates `codecov/codecov-action` from 4.5.0 to 4.6.0 - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v4.5.0...v4.6.0) Updates `github/codeql-action` from 3.26.10 to 3.26.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/e2b3eafc8d227b0241d48be5f425d47c2d750a13...c36620d31ac7c881962c3d9dd939c40ec9434f2b) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions - dependency-name: codecov/codecov-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/periodic_benchmarks.yml | 2 +- .github/workflows/publish_pypi.yml | 8 ++++---- .github/workflows/run_benchmarks_over_history.yml | 2 +- .github/workflows/run_periodic_tests.yml | 2 +- .github/workflows/scorecard.yml | 4 ++-- .github/workflows/test_on_push.yml | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/periodic_benchmarks.yml b/.github/workflows/periodic_benchmarks.yml index b99e2ab7b0..ef66cec238 100644 --- a/.github/workflows/periodic_benchmarks.yml +++ b/.github/workflows/periodic_benchmarks.yml @@ -51,7 +51,7 @@ jobs: LD_LIBRARY_PATH: $HOME/.local/lib - name: Upload results as artifact - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v4.4.1 with: name: asv_periodic_results path: results diff --git a/.github/workflows/publish_pypi.yml b/.github/workflows/publish_pypi.yml index 5f68bd2b45..27e6d1162f 100644 --- a/.github/workflows/publish_pypi.yml +++ b/.github/workflows/publish_pypi.yml @@ -92,7 +92,7 @@ jobs: python -c "import pybamm; print(pybamm.IDAKLUSolver())" python -m pytest -m cibw {project}/tests/unit - name: Upload Windows wheels - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v4.4.1 with: name: wheels_windows path: ./wheelhouse/*.whl @@ -129,7 +129,7 @@ jobs: python -m pytest -m cibw {project}/tests/unit - name: Upload wheels for Linux - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v4.4.1 with: name: wheels_manylinux path: ./wheelhouse/*.whl @@ -261,7 +261,7 @@ jobs: python -m pytest -m cibw {project}/tests/unit - name: Upload wheels for macOS (amd64, arm64) - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v4.4.1 with: name: wheels_${{ matrix.os }} path: ./wheelhouse/*.whl @@ -281,7 +281,7 @@ jobs: run: pipx run build --sdist - name: Upload SDist - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v4.4.1 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 71687a8b02..ce8eb72ce0 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.4.0 + uses: actions/upload-artifact@v4.4.1 with: name: asv_over_history_results path: results diff --git a/.github/workflows/run_periodic_tests.yml b/.github/workflows/run_periodic_tests.yml index 9f10a9c6f7..68a8a1ae8b 100644 --- a/.github/workflows/run_periodic_tests.yml +++ b/.github/workflows/run_periodic_tests.yml @@ -89,7 +89,7 @@ jobs: - name: Upload coverage report if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12' - uses: codecov/codecov-action@v4.5.0 + uses: codecov/codecov-action@v4.6.0 with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 017919c4e7..beb849e9fc 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@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4.4.1 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@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.26.10 + uses: github/codeql-action/upload-sarif@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12 with: sarif_file: results.sarif diff --git a/.github/workflows/test_on_push.yml b/.github/workflows/test_on_push.yml index 9224b7df36..9be0c7b3ea 100644 --- a/.github/workflows/test_on_push.yml +++ b/.github/workflows/test_on_push.yml @@ -123,7 +123,7 @@ jobs: - name: Upload coverage report if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12' - uses: codecov/codecov-action@v4.5.0 + uses: codecov/codecov-action@v4.6.0 with: token: ${{ secrets.CODECOV_TOKEN }} From 9e62b66c53f423ceb5bb337d4fd893f71358c8d4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 18:44:16 -0400 Subject: [PATCH 10/24] chore: update pre-commit hooks (#4500) 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.8 → v0.6.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.8...v0.6.9) - [github.com/pre-commit/pre-commit-hooks: v4.6.0 → v5.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.6.0...v5.0.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 51e4d7c23e..b66b38b4db 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.8" + rev: "v0.6.9" hooks: - id: ruff args: [--fix, --show-fixes] @@ -19,7 +19,7 @@ repos: additional_dependencies: [black==23.*] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: check-added-large-files - id: check-case-conflict From e4eb82a3d84f9bfc392cc5796f42de2e38b214ad Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Wed, 9 Oct 2024 15:13:34 +0100 Subject: [PATCH 11/24] feat: add discrete time sum expression tree node (#4501) * feat: add discrete time sum expression tree node #4485 * docs: fix math syntax in docstring * remove prints * test casadi solver as well * coverage * coverage * add to changelog and tidy solution test --- CHANGELOG.md | 1 + src/pybamm/__init__.py | 2 + src/pybamm/expression_tree/__init__.py | 2 +- .../expression_tree/discrete_time_sum.py | 88 +++++++++++++++++++ src/pybamm/solvers/__init__.py | 2 +- src/pybamm/solvers/processed_variable.py | 78 ++++++++++------ .../processed_variable_time_integral.py | 28 ++++++ src/pybamm/solvers/solution.py | 28 +++--- .../test_unary_operators.py | 30 +++++++ .../test_solvers/test_processed_variable.py | 61 +++++++++++++ tests/unit/test_solvers/test_solution.py | 40 +++++++++ 11 files changed, 319 insertions(+), 41 deletions(-) create mode 100644 src/pybamm/expression_tree/discrete_time_sum.py create mode 100644 src/pybamm/solvers/processed_variable_time_integral.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ffc210a74..fd69942fd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Added phase-dependent particle options to LAM ([#4369](https://github.com/pybamm-team/PyBaMM/pull/4369)) - Added a lithium ion equivalent circuit model with split open circuit voltages for each electrode (`SplitOCVR`). ([#4330](https://github.com/pybamm-team/PyBaMM/pull/4330)) +- Added the `pybamm.DiscreteTimeSum` expression node to sum an expression over a sequence of data times, and accompanying `pybamm.DiscreteTimeData` class to store the data times and values ([#4501](https://github.com/pybamm-team/PyBaMM/pull/4501)) ## Optimizations diff --git a/src/pybamm/__init__.py b/src/pybamm/__init__.py index f51ba05d02..2deb30f305 100644 --- a/src/pybamm/__init__.py +++ b/src/pybamm/__init__.py @@ -36,6 +36,7 @@ from .expression_tree.broadcasts import * from .expression_tree.functions import * from .expression_tree.interpolant import Interpolant +from .expression_tree.discrete_time_sum import * from .expression_tree.input_parameter import InputParameter from .expression_tree.parameter import Parameter, FunctionParameter from .expression_tree.scalar import Scalar @@ -158,6 +159,7 @@ # Solver classes from .solvers.solution import Solution, EmptySolution, make_cycle_solution +from .solvers.processed_variable_time_integral import ProcessedVariableTimeIntegral from .solvers.processed_variable import ProcessedVariable, process_variable from .solvers.processed_variable_computed import ProcessedVariableComputed from .solvers.base_solver import BaseSolver diff --git a/src/pybamm/expression_tree/__init__.py b/src/pybamm/expression_tree/__init__.py index 0b06746e61..7ac80e5353 100644 --- a/src/pybamm/expression_tree/__init__.py +++ b/src/pybamm/expression_tree/__init__.py @@ -2,4 +2,4 @@ 'concatenations', 'exceptions', 'functions', 'independent_variable', 'input_parameter', 'interpolant', 'matrix', 'operations', 'parameter', 'printing', 'scalar', 'state_vector', 'symbol', - 'unary_operators', 'variable', 'vector'] + 'unary_operators', 'variable', 'vector', 'discrete_time_sum' ] diff --git a/src/pybamm/expression_tree/discrete_time_sum.py b/src/pybamm/expression_tree/discrete_time_sum.py new file mode 100644 index 0000000000..41cd14960d --- /dev/null +++ b/src/pybamm/expression_tree/discrete_time_sum.py @@ -0,0 +1,88 @@ +import pybamm +import numpy as np + + +class DiscreteTimeData(pybamm.Interpolant): + """ + A class for representing data that is only defined at discrete points in time. + This is implemented as a 1D interpolant with the time points as the nodes. + + Parameters + ---------- + + time_points : :class:`numpy.ndarray` + The time points at which the data is defined + data : :class:`numpy.ndarray` + The data to be interpolated + name : str + The name of the data + + """ + + def __init__(self, time_points: np.ndarray, data: np.ndarray, name: str): + super().__init__(time_points, data, pybamm.t, name) + + def create_copy(self, new_children=None, perform_simplifications=True): + """See :meth:`pybamm.Symbol.new_copy()`.""" + return pybamm.DiscreteTimeData(self.x[0], self.y, self.name) + + +class DiscreteTimeSum(pybamm.UnaryOperator): + """ + A node in the expression tree representing a discrete time sum operator. + + .. math:: + \\sum_{i=0}^{N} f(y(t_i), t_i) + + where f is the expression given by the child, and the sum is over the discrete + time points t_i. The set of time points is given by the :class:`pybamm.DiscreteTimeData` node, + which must be somewhere in the expression tree given by the child. If the child + does not contain a :class:`pybamm.DiscreteTimeData` node, then an error will be raised when + the node is created. If the child contains multiple :class:`pybamm.DiscreteTimeData` nodes, + an error will be raised when the node is created. + + + Parameters + ---------- + child: :class:`pybamm.Symbol` + The symbol to be summed + + Attributes + ---------- + data: :class:`pybamm.DiscreteTimeData` + The discrete time data node in the child + + Raises + ------ + :class:`pybamm.ModelError` + If the child does not contain a :class:`pybamm.DiscreteTimeData` node, or if the child + contains multiple :class:`pybamm.DiscreteTimeData` nodes. + """ + + def __init__(self, child: pybamm.Symbol): + self.data = None + for node in child.pre_order(): + if isinstance(node, DiscreteTimeData): + # Check that there is exactly one DiscreteTimeData node in the child + if self.data is not None: + raise pybamm.ModelError( + "DiscreteTimeSum can only have one DiscreteTimeData node in the child" + ) + self.data = node + if self.data is None: + raise pybamm.ModelError( + "DiscreteTimeSum must contain a DiscreteTimeData node" + ) + super().__init__("discrete time sum", child) + + @property + def sum_values(self): + return self.data.y + + @property + def sum_times(self): + return self.data.x[0] + + def _unary_evaluate(self, child): + # return result of evaluating the child, we'll only implement the sum once the model is solved (in pybamm.ProcessedVariable) + return child diff --git a/src/pybamm/solvers/__init__.py b/src/pybamm/solvers/__init__.py index fc8be7e2f8..e9d9d306f4 100644 --- a/src/pybamm/solvers/__init__.py +++ b/src/pybamm/solvers/__init__.py @@ -2,4 +2,4 @@ 'casadi_algebraic_solver', 'casadi_solver', 'dummy_solver', 'idaklu_jax', 'idaklu_solver', 'jax_bdf_solver', 'jax_solver', 'lrudict', 'processed_variable', 'processed_variable_computed', - 'scipy_solver', 'solution'] + 'scipy_solver', 'solution', 'processed_variable_time_integral'] diff --git a/src/pybamm/solvers/processed_variable.py b/src/pybamm/solvers/processed_variable.py index 5cf928ca7f..12cf140b38 100644 --- a/src/pybamm/solvers/processed_variable.py +++ b/src/pybamm/solvers/processed_variable.py @@ -1,6 +1,7 @@ # # Processed Variable class # +from typing import Optional import casadi import numpy as np import pybamm @@ -29,6 +30,8 @@ class ProcessedVariable: `base_Variable.evaluate` (but more efficiently). solution : :class:`pybamm.Solution` The solution object to be used to create the processed variables + time_integral : :class:`pybamm.ProcessedVariableTimeIntegral`, optional + Not none if the variable is to be time-integrated (default is None) """ def __init__( @@ -36,7 +39,7 @@ def __init__( base_variables, base_variables_casadi, solution, - cumtrapz_ic=None, + time_integral: Optional[pybamm.ProcessedVariableTimeIntegral] = None, ): self.base_variables = base_variables self.base_variables_casadi = base_variables_casadi @@ -50,7 +53,7 @@ def __init__( self.mesh = base_variables[0].mesh self.domain = base_variables[0].domain self.domains = base_variables[0].domains - self.cumtrapz_ic = cumtrapz_ic + self.time_integral = time_integral # Process spatial variables geometry = solution.all_models[0].geometry @@ -271,18 +274,21 @@ def __call__( self._coords_raw, ) - processed_entries = self._xr_interpolate( - entries_for_interp, - coords, - observe_raw, - t, - x, - r, - y, - z, - R, - fill_value, - ) + if self.time_integral is None: + processed_entries = self._xr_interpolate( + entries_for_interp, + coords, + observe_raw, + t, + x, + r, + y, + z, + R, + fill_value, + ) + else: + processed_entries = entries_for_interp else: processed_entries = entries @@ -343,6 +349,16 @@ def _check_observe_raw(self, t): t_observe (np.ndarray): time points to observe observe_raw (bool): True if observing the raw data """ + # if this is a time integral variable, t must be None and we observe either the + # data times (for a discrete sum) or the solution times (for a continuous sum) + if self.time_integral is not None: + if self.time_integral.method == "discrete": + # discrete sum should be observed at the discrete times + t = self.time_integral.discrete_times + else: + # assume we can do a sufficiently accurate trapezoidal integration at t_pts + t = self.t_pts + observe_raw = (t is None) or ( np.asarray(t).size == len(self.t_pts) and np.all(t == self.t_pts) ) @@ -483,14 +499,14 @@ def __init__( base_variables, base_variables_casadi, solution, - cumtrapz_ic=None, + time_integral: Optional[pybamm.ProcessedVariableTimeIntegral] = None, ): self.dimensions = 0 super().__init__( base_variables, base_variables_casadi, solution, - cumtrapz_ic=cumtrapz_ic, + time_integral=time_integral, ) def _observe_raw_python(self): @@ -510,13 +526,19 @@ def _observe_raw_python(self): idx += 1 return entries - def _observe_postfix(self, entries, _): - if self.cumtrapz_ic is None: + def _observe_postfix(self, entries, t): + if self.time_integral is None: return entries - - return cumulative_trapezoid( - entries, self.t_pts, initial=float(self.cumtrapz_ic) - ) + if self.time_integral.method == "discrete": + return np.sum(entries, axis=0, initial=self.time_integral.initial_condition) + elif self.time_integral.method == "continuous": + return cumulative_trapezoid( + entries, self.t_pts, initial=float(self.time_integral.initial_condition) + ) + else: + raise ValueError( + "time_integral method must be 'discrete' or 'continuous'" + ) # pragma: no cover def _interp_setup(self, entries, t): # save attributes for interpolation @@ -556,14 +578,14 @@ def __init__( base_variables, base_variables_casadi, solution, - cumtrapz_ic=None, + time_integral: Optional[pybamm.ProcessedVariableTimeIntegral] = None, ): self.dimensions = 1 super().__init__( base_variables, base_variables_casadi, solution, - cumtrapz_ic=cumtrapz_ic, + time_integral=time_integral, ) def _observe_raw_python(self): @@ -653,14 +675,14 @@ def __init__( base_variables, base_variables_casadi, solution, - cumtrapz_ic=None, + time_integral: Optional[pybamm.ProcessedVariableTimeIntegral] = None, ): self.dimensions = 2 super().__init__( base_variables, base_variables_casadi, solution, - cumtrapz_ic=cumtrapz_ic, + time_integral=time_integral, ) first_dim_nodes = self.mesh.nodes first_dim_edges = self.mesh.edges @@ -819,14 +841,14 @@ def __init__( base_variables, base_variables_casadi, solution, - cumtrapz_ic=None, + time_integral: Optional[pybamm.ProcessedVariableTimeIntegral] = None, ): self.dimensions = 2 super(ProcessedVariable2D, self).__init__( base_variables, base_variables_casadi, solution, - cumtrapz_ic=cumtrapz_ic, + time_integral=time_integral, ) y_sol = self.mesh.edges["y"] z_sol = self.mesh.edges["z"] diff --git a/src/pybamm/solvers/processed_variable_time_integral.py b/src/pybamm/solvers/processed_variable_time_integral.py new file mode 100644 index 0000000000..4fcdfb56ba --- /dev/null +++ b/src/pybamm/solvers/processed_variable_time_integral.py @@ -0,0 +1,28 @@ +from dataclasses import dataclass +from typing import Literal, Optional, Union +import numpy as np +import pybamm + + +@dataclass +class ProcessedVariableTimeIntegral: + method: Literal["discrete", "continuous"] + initial_condition: np.ndarray + discrete_times: Optional[np.ndarray] + + @staticmethod + def from_pybamm_var( + var: Union[pybamm.DiscreteTimeSum, pybamm.ExplicitTimeIntegral], + ) -> "ProcessedVariableTimeIntegral": + if isinstance(var, pybamm.DiscreteTimeSum): + return ProcessedVariableTimeIntegral( + method="discrete", initial_condition=0.0, discrete_times=var.sum_times + ) + elif isinstance(var, pybamm.ExplicitTimeIntegral): + return ProcessedVariableTimeIntegral( + method="continuous", + initial_condition=var.initial_condition.evaluate(), + discrete_times=None, + ) + else: + raise ValueError("Unsupported variable type") # pragma: no cover diff --git a/src/pybamm/solvers/solution.py b/src/pybamm/solvers/solution.py index c884e79e34..1aa540ab3c 100644 --- a/src/pybamm/solvers/solution.py +++ b/src/pybamm/solvers/solution.py @@ -571,7 +571,7 @@ def update(self, variables): self._update_variable(variable) def _update_variable(self, variable): - cumtrapz_ic = None + time_integral = None pybamm.logger.debug(f"Post-processing {variable}") vars_pybamm = [ model.variables_and_events[variable] for model in self.all_models @@ -591,16 +591,22 @@ def _update_variable(self, variable): "solve. Please re-run the solve with `output_variables` set to " "include this variable." ) - elif isinstance(var_pybamm, pybamm.ExplicitTimeIntegral): - cumtrapz_ic = var_pybamm.initial_condition - cumtrapz_ic = cumtrapz_ic.evaluate() - var_pybamm = var_pybamm.child - var_casadi = self.process_casadi_var( - var_pybamm, - inputs, - ys.shape, + elif isinstance( + var_pybamm, (pybamm.ExplicitTimeIntegral, pybamm.DiscreteTimeSum) + ): + time_integral = pybamm.ProcessedVariableTimeIntegral.from_pybamm_var( + var_pybamm ) - model._variables_casadi[variable] = var_casadi + var_pybamm = var_pybamm.child + if variable in model._variables_casadi: + var_casadi = model._variables_casadi[variable] + else: + var_casadi = self.process_casadi_var( + var_pybamm, + inputs, + ys.shape, + ) + model._variables_casadi[variable] = var_casadi vars_pybamm[i] = var_pybamm elif variable in model._variables_casadi: var_casadi = model._variables_casadi[variable] @@ -613,7 +619,7 @@ def _update_variable(self, variable): model._variables_casadi[variable] = var_casadi vars_casadi.append(var_casadi) var = pybamm.process_variable( - vars_pybamm, vars_casadi, self, cumtrapz_ic=cumtrapz_ic + vars_pybamm, vars_casadi, self, time_integral=time_integral ) self._variables[variable] = var diff --git a/tests/unit/test_expression_tree/test_unary_operators.py b/tests/unit/test_expression_tree/test_unary_operators.py index 812cbd8f6b..0dbafa38c5 100644 --- a/tests/unit/test_expression_tree/test_unary_operators.py +++ b/tests/unit/test_expression_tree/test_unary_operators.py @@ -724,6 +724,36 @@ def test_explicit_time_integral(self): assert expr.create_copy() == expr assert not expr.is_constant() + def test_discrete_time_sum(self): + times = np.array([1, 2, 3, 4, 5]) + values = np.array([2, 2, 3, 3, 1]) + data = pybamm.DiscreteTimeData(times, values, "test") + assert data.name == "test" + np.testing.assert_array_equal(data.x[0], times) + np.testing.assert_array_equal(data.y, values) + + y = pybamm.StateVector(slice(0, 1)) + + # check that raises error if data is not present + with pytest.raises(pybamm.ModelError, match="must contain a DiscreteTimeData"): + pybamm.DiscreteTimeSum(2 * y) + + # check that raises error if two data are present + data2 = pybamm.DiscreteTimeData(values, times, "test2") + with pytest.raises(pybamm.ModelError, match="only have one DiscreteTimeData"): + pybamm.DiscreteTimeSum(data + data2) + + sum = pybamm.DiscreteTimeSum(2 * data - y) + np.testing.assert_array_equal(sum.sum_times, times) + np.testing.assert_array_equal(sum.sum_values, values) + y = np.array([1]) + + # evaluate should return the values to sum up + for i in range(len(times)): + eval = sum.evaluate(y=y, t=times[i]) + expect = 2 * values[i] - y + np.testing.assert_array_equal(eval[0], expect) + def test_to_from_json(self, mocker): # UnaryOperator a = pybamm.Symbol("a", domain=["test"]) diff --git a/tests/unit/test_solvers/test_processed_variable.py b/tests/unit/test_solvers/test_processed_variable.py index 04de88963d..b3ab850607 100644 --- a/tests/unit/test_solvers/test_processed_variable.py +++ b/tests/unit/test_solvers/test_processed_variable.py @@ -160,6 +160,67 @@ def test_processed_variable_0D(self, hermite_interp): processed_var._observe_raw_cpp(), processed_var._observe_raw_python() ) + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_variable_0D_discrete_data(self, hermite_interp): + y = pybamm.StateVector(slice(0, 1)) + t_sol = np.linspace(0, 1) + y_sol = np.array([np.linspace(0, 5)]) + data_const = 3.6 + if hermite_interp: + yp_sol = 5 * np.ones_like(y_sol) + else: + yp_sol = None + + # hermite interpolation can do order 2 interpolation, otherwise make sure result is linear + order = 2 if hermite_interp else 1 + + # data is same timepoints as solution + data_t = t_sol + data_v = -data_const * data_t + data = pybamm.DiscreteTimeData(data_t, data_v, "test_data") + var = (y - data) ** order + expected_entries = (y_sol - data_v) ** order + var.mesh = None + model = pybamm.BaseModel() + var_casadi = to_casadi(var, y_sol) + processed_var = pybamm.process_variable( + [var], + [var_casadi], + self._sol_default(t_sol, y_sol, yp_sol, model), + ) + np.testing.assert_array_equal(processed_var.entries, expected_entries.flatten()) + np.testing.assert_array_equal(processed_var(data_t), expected_entries.flatten()) + + # data is different timepoints as solution + data_t = np.linspace(0, 1, 7) + data_v = -data_const * data_t + y_sol_interp = (np.interp(data_t, t_sol, y_sol[0]),) + data_v_interp = np.interp(t_sol, data_t, data_v) + data = pybamm.DiscreteTimeData(data_t, data_v, "test_data") + + # check data interp + np.testing.assert_array_almost_equal( + data.evaluate(t=t_sol).flatten(), data_v_interp + ) + + var = (y - data) ** order + expected = (y_sol_interp - data_v) ** order + expected_entries = (y_sol - data_v_interp) ** order + var.mesh = None + model = pybamm.BaseModel() + var_casadi = to_casadi(var, y_sol) + processed_var = pybamm.process_variable( + [var], + [var_casadi], + self._sol_default(t_sol, y_sol, yp_sol, model), + ) + np.testing.assert_array_almost_equal( + processed_var.entries, expected_entries.flatten(), decimal=10 + ) + np.testing.assert_array_almost_equal( + processed_var(t=data_t), expected.flatten(), decimal=10 + ) + @pytest.mark.parametrize("hermite_interp", _hermite_args) def test_processed_variable_0D_no_sensitivity(self, hermite_interp): # without space diff --git a/tests/unit/test_solvers/test_solution.py b/tests/unit/test_solvers/test_solution.py index 4ac9312531..2fb25f79a2 100644 --- a/tests/unit/test_solvers/test_solution.py +++ b/tests/unit/test_solvers/test_solution.py @@ -420,3 +420,43 @@ def test_solution_evals_with_inputs(self): sim.solve(t_eval=np.linspace(0, 10, 10), inputs=inputs) time = sim.solution["Time [h]"](sim.solution.t) assert len(time) == 10 + + _solver_classes = [pybamm.CasadiSolver] + if pybamm.has_idaklu(): + _solver_classes.append(pybamm.IDAKLUSolver) + + @pytest.mark.parametrize("solver_class", _solver_classes) + def test_discrete_data_sum(self, solver_class): + model = pybamm.BaseModel(name="test_model") + c = pybamm.Variable("c") + model.rhs = {c: -c} + model.initial_conditions = {c: 1} + model.variables["c"] = c + + data_times = np.linspace(0, 1, 10) + if solver_class == pybamm.IDAKLUSolver: + t_eval = [data_times[0], data_times[-1]] + t_interp = data_times + else: + t_eval = data_times + t_interp = None + solver = solver_class() + data_values = solver.solve(model, t_eval=t_eval, t_interp=t_interp)["c"].entries + + data = pybamm.DiscreteTimeData(data_times, data_values, "test_data") + data_comparison = pybamm.DiscreteTimeSum((c - data) ** 2) + + model = pybamm.BaseModel(name="test_model2") + a = pybamm.InputParameter("a") + model.rhs = {c: -a * c} + model.initial_conditions = {c: 1} + model.variables["data_comparison"] = data_comparison + + solver = solver_class() + for a in [0.5, 1.0, 2.0]: + sol = solver.solve(model, t_eval=t_eval, inputs={"a": a}) + y_sol = np.exp(-a * data_times) + expected = np.sum((y_sol - data_values) ** 2) + np.testing.assert_array_almost_equal( + sol["data_comparison"](), expected, decimal=2 + ) From 0efe5f6a4328d4bb35c440104139eed1595c3dfe Mon Sep 17 00:00:00 2001 From: Medha Bhardwaj <143182673+medha-14@users.noreply.github.com> Date: Thu, 10 Oct 2024 22:15:47 +0530 Subject: [PATCH 12/24] Removes `param = self.param` to use `self.param` directly (#4494) * 3 files done * all occurances changed * added changelog and fixed spectral_volume issue * fixed docs --------- Co-authored-by: Eric G. Kratz --- CHANGELOG.md | 1 + .../lead_acid/basic_full.py | 87 +++++++------- .../lithium_ion/base_lithium_ion_model.py | 21 ++-- .../lithium_ion/basic_dfn.py | 73 ++++++------ .../lithium_ion/basic_dfn_composite.py | 109 +++++++++--------- .../lithium_ion/basic_dfn_half_cell.py | 73 ++++++------ .../lithium_ion/basic_spm.py | 51 ++++---- .../lithium_ion/electrode_soh.py | 46 ++++---- .../active_material/base_active_material.py | 5 +- .../through_cell/explicit_convection.py | 27 +++-- .../through_cell/full_convection.py | 5 +- .../convection/transverse/full_convection.py | 8 +- .../transverse/uniform_convection.py | 7 +- .../effective_resistance_current_collector.py | 52 ++++----- .../current_collector/potential_pair.py | 24 ++-- .../submodels/electrode/base_electrode.py | 3 +- .../submodels/electrode/ohm/composite_ohm.py | 7 +- .../submodels/electrode/ohm/leading_ohm.py | 7 +- .../base_electrolyte_conductivity.py | 7 +- .../composite_conductivity.py | 27 +++-- .../full_conductivity.py | 5 +- .../integrated_conductivity.py | 24 ++-- .../leading_order_conductivity.py | 7 +- .../full_surface_form_conductivity.py | 16 +-- .../electrolyte_diffusion/full_diffusion.py | 10 +- .../leading_order_diffusion.py | 21 ++-- .../explicit_control_external_circuit.py | 12 +- .../function_control_external_circuit.py | 7 +- .../submodels/interface/base_interface.py | 7 +- .../interface/kinetics/diffusion_limited.py | 5 +- .../inverse_kinetics/inverse_butler_volmer.py | 5 +- .../submodels/interface/sei/sei_growth.py | 18 +-- .../oxygen_diffusion/full_oxygen_diffusion.py | 10 +- .../leading_oxygen_diffusion.py | 17 +-- .../submodels/particle/base_particle.py | 3 +- .../submodels/particle/fickian_diffusion.py | 3 +- .../submodels/particle/msmr_diffusion.py | 3 +- .../particle/x_averaged_polynomial_profile.py | 12 +- .../porosity/reaction_driven_porosity_ode.py | 6 +- .../models/submodels/thermal/base_thermal.py | 38 +++--- .../pouch_cell_1D_current_collectors.py | 25 ++-- 41 files changed, 441 insertions(+), 453 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd69942fd0..84f01d3797 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ ## Breaking changes +- Removed all instances of `param = self.param` and now directly access `self.param` across the codebase. This change simplifies parameter references and enhances readability. ([#4484](https://github.com/pybamm-team/PyBaMM/pull/4494)) - Removed the deprecation warning for the chemistry argument in ParameterValues ([#4466](https://github.com/pybamm-team/PyBaMM/pull/4466)) - The parameters "... electrode OCP entropic change [V.K-1]" and "... electrode volume change" are now expected to be functions of stoichiometry only instead of functions of both stoichiometry and maximum concentration ([#4427](https://github.com/pybamm-team/PyBaMM/pull/4427)) diff --git a/src/pybamm/models/full_battery_models/lead_acid/basic_full.py b/src/pybamm/models/full_battery_models/lead_acid/basic_full.py index 8caac98066..a67501fc72 100644 --- a/src/pybamm/models/full_battery_models/lead_acid/basic_full.py +++ b/src/pybamm/models/full_battery_models/lead_acid/basic_full.py @@ -26,7 +26,6 @@ def __init__(self, name="Basic full model"): # `param` is a class containing all the relevant parameters and functions for # this model. These are purely symbolic at this stage, and will be set by the # `ParameterValues` class when the model is processed. - param = self.param ###################### # Variables @@ -37,17 +36,17 @@ def __init__(self, name="Basic full model"): c_e_n = pybamm.Variable( "Negative electrolyte concentration [mol.m-3]", domain="negative electrode", - scale=param.c_e_init, + scale=self.param.c_e_init, ) c_e_s = pybamm.Variable( "Separator electrolyte concentration [mol.m-3]", domain="separator", - scale=param.c_e_init, + scale=self.param.c_e_init, ) c_e_p = pybamm.Variable( "Positive electrolyte concentration [mol.m-3]", domain="positive electrode", - scale=param.c_e_init, + scale=self.param.c_e_init, ) # Concatenations combine several variables into a single variable, to simplify # implementing equations that hold over several domains @@ -57,17 +56,17 @@ def __init__(self, name="Basic full model"): phi_e_n = pybamm.Variable( "Negative electrolyte potential [V]", domain="negative electrode", - reference=-param.n.prim.U_init, + reference=-self.param.n.prim.U_init, ) phi_e_s = pybamm.Variable( "Separator electrolyte potential [V]", domain="separator", - reference=-param.n.prim.U_init, + reference=-self.param.n.prim.U_init, ) phi_e_p = pybamm.Variable( "Positive electrolyte potential [V]", domain="positive electrode", - reference=-param.n.prim.U_init, + reference=-self.param.n.prim.U_init, ) phi_e = pybamm.concatenation(phi_e_n, phi_e_s, phi_e_p) @@ -78,7 +77,7 @@ def __init__(self, name="Basic full model"): phi_s_p = pybamm.Variable( "Positive electrode potential [V]", domain="positive electrode", - reference=param.ocv_init, + reference=self.param.ocv_init, ) # Porosity @@ -92,29 +91,29 @@ def __init__(self, name="Basic full model"): eps = pybamm.concatenation(eps_n, eps_s, eps_p) # Constant temperature - T = param.T_init + T = self.param.T_init ###################### # Other set-up ###################### # Current density - i_cell = param.current_density_with_time + i_cell = self.param.current_density_with_time # transport_efficiency tor = pybamm.concatenation( - eps_n**param.n.b_e, eps_s**param.s.b_e, eps_p**param.p.b_e + eps_n**self.param.n.b_e, eps_s**self.param.s.b_e, eps_p**self.param.p.b_e ) # Interfacial reactions - F_RT = param.F / (param.R * T) - Feta_RT_n = F_RT * (phi_s_n - phi_e_n - param.n.prim.U(c_e_n, T)) - j0_n = param.n.prim.j0(c_e_n, T) - j_n = 2 * j0_n * pybamm.sinh(param.n.prim.ne / 2 * Feta_RT_n) + F_RT = self.param.F / (self.param.R * T) + Feta_RT_n = F_RT * (phi_s_n - phi_e_n - self.param.n.prim.U(c_e_n, T)) + j0_n = self.param.n.prim.j0(c_e_n, T) + j_n = 2 * j0_n * pybamm.sinh(self.param.n.prim.ne / 2 * Feta_RT_n) j_s = pybamm.PrimaryBroadcast(0, "separator") - Feta_RT_p = F_RT * (phi_s_p - phi_e_p - param.p.prim.U(c_e_p, T)) - j0_p = param.p.prim.j0(c_e_p, T) - j_p = 2 * j0_p * pybamm.sinh(param.p.prim.ne / 2 * (Feta_RT_p)) + Feta_RT_p = F_RT * (phi_s_p - phi_e_p - self.param.p.prim.U(c_e_p, T)) + j0_p = self.param.p.prim.j0(c_e_p, T) + j_p = 2 * j0_p * pybamm.sinh(self.param.p.prim.ne / 2 * (Feta_RT_p)) a_n = pybamm.Parameter("Negative electrode surface area to volume ratio [m-1]") a_p = pybamm.Parameter("Positive electrode surface area to volume ratio [m-1]") @@ -125,7 +124,7 @@ def __init__(self, name="Basic full model"): ###################### # State of Charge ###################### - I = param.current_with_time + I = self.param.current_with_time # The `rhs` dictionary contains differential equations, with the key being the # variable in the d/dt self.rhs[Q] = I / 3600 @@ -135,28 +134,32 @@ def __init__(self, name="Basic full model"): ###################### # Current in the electrolyte ###################### - i_e = (param.kappa_e(c_e, T) * tor) * ( - param.chiRT_over_Fc(c_e, T) * pybamm.grad(c_e) - pybamm.grad(phi_e) + i_e = (self.param.kappa_e(c_e, T) * tor) * ( + self.param.chiRT_over_Fc(c_e, T) * pybamm.grad(c_e) - pybamm.grad(phi_e) ) # multiply by Lx**2 to improve conditioning - self.algebraic[phi_e] = (pybamm.div(i_e) - a_j) * param.L_x**2 + self.algebraic[phi_e] = (pybamm.div(i_e) - a_j) * self.param.L_x**2 self.boundary_conditions[phi_e] = { "left": (pybamm.Scalar(0), "Neumann"), "right": (pybamm.Scalar(0), "Neumann"), } - self.initial_conditions[phi_e] = -param.n.prim.U_init + self.initial_conditions[phi_e] = -self.param.n.prim.U_init ###################### # Current in the solid ###################### - i_s_n = -param.n.sigma(T) * (1 - eps_n) ** param.n.b_s * pybamm.grad(phi_s_n) - sigma_eff_p = param.p.sigma(T) * (1 - eps_p) ** param.p.b_s + i_s_n = ( + -self.param.n.sigma(T) + * (1 - eps_n) ** self.param.n.b_s + * pybamm.grad(phi_s_n) + ) + sigma_eff_p = self.param.p.sigma(T) * (1 - eps_p) ** self.param.p.b_s i_s_p = -sigma_eff_p * pybamm.grad(phi_s_p) # The `algebraic` dictionary contains differential equations, with the key being # the main scalar variable of interest in the equation # multiply by Lx**2 to improve conditioning - self.algebraic[phi_s_n] = (pybamm.div(i_s_n) + a_j_n) * param.L_x**2 - self.algebraic[phi_s_p] = (pybamm.div(i_s_p) + a_j_p) * param.L_x**2 + self.algebraic[phi_s_n] = (pybamm.div(i_s_n) + a_j_n) * self.param.L_x**2 + self.algebraic[phi_s_p] = (pybamm.div(i_s_p) + a_j_p) * self.param.L_x**2 self.boundary_conditions[phi_s_n] = { "left": (pybamm.Scalar(0), "Dirichlet"), "right": (pybamm.Scalar(0), "Neumann"), @@ -169,19 +172,19 @@ def __init__(self, name="Basic full model"): # initial guess for a root-finding algorithm which calculates consistent initial # conditions self.initial_conditions[phi_s_n] = pybamm.Scalar(0) - self.initial_conditions[phi_s_p] = param.ocv_init + self.initial_conditions[phi_s_p] = self.param.ocv_init ###################### # Porosity ###################### DeltaVsurf = pybamm.concatenation( - pybamm.PrimaryBroadcast(param.n.DeltaVsurf, "negative electrode"), + pybamm.PrimaryBroadcast(self.param.n.DeltaVsurf, "negative electrode"), pybamm.PrimaryBroadcast(0, "separator"), - pybamm.PrimaryBroadcast(param.p.DeltaVsurf, "positive electrode"), + pybamm.PrimaryBroadcast(self.param.p.DeltaVsurf, "positive electrode"), ) - deps_dt = DeltaVsurf * a_j / param.F + deps_dt = DeltaVsurf * a_j / self.param.F self.rhs[eps] = deps_dt - self.initial_conditions[eps] = param.epsilon_init + self.initial_conditions[eps] = self.param.epsilon_init self.events.extend( [ pybamm.Event( @@ -203,22 +206,22 @@ def __init__(self, name="Basic full model"): # Electrolyte concentration ###################### N_e = ( - -tor * param.D_e(c_e, T) * pybamm.grad(c_e) - + param.t_plus(c_e, T) * i_e / param.F + -tor * self.param.D_e(c_e, T) * pybamm.grad(c_e) + + self.param.t_plus(c_e, T) * i_e / self.param.F ) s = pybamm.concatenation( - pybamm.PrimaryBroadcast(param.n.prim.s_plus_S, "negative electrode"), + pybamm.PrimaryBroadcast(self.param.n.prim.s_plus_S, "negative electrode"), pybamm.PrimaryBroadcast(0, "separator"), - pybamm.PrimaryBroadcast(param.p.prim.s_plus_S, "positive electrode"), + pybamm.PrimaryBroadcast(self.param.p.prim.s_plus_S, "positive electrode"), ) self.rhs[c_e] = (1 / eps) * ( - -pybamm.div(N_e) + s * a_j / param.F - c_e * deps_dt + -pybamm.div(N_e) + s * a_j / self.param.F - c_e * deps_dt ) self.boundary_conditions[c_e] = { "left": (pybamm.Scalar(0), "Neumann"), "right": (pybamm.Scalar(0), "Neumann"), } - self.initial_conditions[c_e] = param.c_e_init + self.initial_conditions[c_e] = self.param.c_e_init self.events.append( pybamm.Event( "Zero electrolyte concentration cut-off", pybamm.min(c_e) - 0.002 @@ -242,7 +245,11 @@ def __init__(self, name="Basic full model"): } self.events.extend( [ - pybamm.Event("Minimum voltage [V]", voltage - param.voltage_low_cut), - pybamm.Event("Maximum voltage [V]", param.voltage_high_cut - voltage), + pybamm.Event( + "Minimum voltage [V]", voltage - self.param.voltage_low_cut + ), + pybamm.Event( + "Maximum voltage [V]", self.param.voltage_high_cut - voltage + ), ] ) 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 b1367f8300..9b801e1130 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 @@ -105,7 +105,6 @@ def set_standard_output_variables(self): def set_degradation_variables(self): """Sets variables that quantify degradation (LAM, LLI, etc)""" - param = self.param domains = [d for d in self.options.whole_cell_domains if d != "separator"] for domain in domains: @@ -135,8 +134,8 @@ def set_degradation_variables(self): # LLI is usually defined based only on the percentage lithium lost from # particles - LLI = (1 - n_Li_particles / param.n_Li_particles_init) * 100 - LLI_tot = (1 - n_Li / param.n_Li_init) * 100 + LLI = (1 - n_Li_particles / self.param.n_Li_particles_init) * 100 + LLI_tot = (1 - n_Li / self.param.n_Li_init) * 100 self.variables.update( { @@ -146,15 +145,16 @@ def set_degradation_variables(self): # Total lithium "Total lithium [mol]": n_Li, "Total lithium in particles [mol]": n_Li_particles, - "Total lithium capacity [A.h]": n_Li * param.F / 3600, + "Total lithium capacity [A.h]": n_Li * self.param.F / 3600, "Total lithium capacity in particles [A.h]": n_Li_particles - * param.F + * self.param.F / 3600, # Lithium lost - "Total lithium lost [mol]": param.n_Li_init - n_Li, - "Total lithium lost from particles [mol]": param.n_Li_particles_init + "Total lithium lost [mol]": self.param.n_Li_init - n_Li, + "Total lithium lost from particles [mol]": self.param.n_Li_particles_init - n_Li_particles, - "Total lithium lost from electrolyte [mol]": param.n_Li_e_init - n_Li_e, + "Total lithium lost from electrolyte [mol]": self.param.n_Li_e_init + - n_Li_e, } ) @@ -177,7 +177,7 @@ def set_degradation_variables(self): { "Total lithium lost to side reactions [mol]": n_Li_lost_reactions, "Total capacity lost to side reactions [A.h]": n_Li_lost_reactions - * param.F + * self.param.F / 3600, } ) @@ -502,9 +502,8 @@ def insert_reference_electrode(self, position=None): "electrode manually." ) - param = self.param if position is None: - position = param.n.L + param.s.L / 2 + position = self.param.n.L + self.param.s.L / 2 phi_e_ref = pybamm.EvaluateAt( self.variables["Electrolyte potential [V]"], position diff --git a/src/pybamm/models/full_battery_models/lithium_ion/basic_dfn.py b/src/pybamm/models/full_battery_models/lithium_ion/basic_dfn.py index 08809b645f..7865b84ff3 100644 --- a/src/pybamm/models/full_battery_models/lithium_ion/basic_dfn.py +++ b/src/pybamm/models/full_battery_models/lithium_ion/basic_dfn.py @@ -27,7 +27,6 @@ def __init__(self, name="Doyle-Fuller-Newman model"): # `param` is a class containing all the relevant parameters and functions for # this model. These are purely symbolic at this stage, and will be set by the # `ParameterValues` class when the model is processed. - param = self.param ###################### # Variables @@ -90,14 +89,14 @@ def __init__(self, name="Doyle-Fuller-Newman model"): ) # Constant temperature - T = param.T_init + T = self.param.T_init ###################### # Other set-up ###################### # Current density - i_cell = param.current_density_with_time + i_cell = self.param.current_density_with_time # Porosity # Primary broadcasts are used to broadcast scalar quantities across a domain @@ -119,29 +118,29 @@ def __init__(self, name="Doyle-Fuller-Newman model"): # transport_efficiency tor = pybamm.concatenation( - eps_n**param.n.b_e, eps_s**param.s.b_e, eps_p**param.p.b_e + eps_n**self.param.n.b_e, eps_s**self.param.s.b_e, eps_p**self.param.p.b_e ) - a_n = 3 * param.n.prim.epsilon_s_av / param.n.prim.R_typ - a_p = 3 * param.p.prim.epsilon_s_av / param.p.prim.R_typ + a_n = 3 * self.param.n.prim.epsilon_s_av / self.param.n.prim.R_typ + a_p = 3 * self.param.p.prim.epsilon_s_av / self.param.p.prim.R_typ # Interfacial reactions # Surf takes the surface value of a variable, i.e. its boundary value on the # right side. This is also accessible via `boundary_value(x, "right")`, with # "left" providing the boundary value of the left side c_s_surf_n = pybamm.surf(c_s_n) - sto_surf_n = c_s_surf_n / param.n.prim.c_max - j0_n = param.n.prim.j0(c_e_n, c_s_surf_n, T) - eta_n = phi_s_n - phi_e_n - param.n.prim.U(sto_surf_n, T) - Feta_RT_n = param.F * eta_n / (param.R * T) - j_n = 2 * j0_n * pybamm.sinh(param.n.prim.ne / 2 * Feta_RT_n) + sto_surf_n = c_s_surf_n / self.param.n.prim.c_max + j0_n = self.param.n.prim.j0(c_e_n, c_s_surf_n, T) + eta_n = phi_s_n - phi_e_n - self.param.n.prim.U(sto_surf_n, T) + Feta_RT_n = self.param.F * eta_n / (self.param.R * T) + j_n = 2 * j0_n * pybamm.sinh(self.param.n.prim.ne / 2 * Feta_RT_n) c_s_surf_p = pybamm.surf(c_s_p) - sto_surf_p = c_s_surf_p / param.p.prim.c_max - j0_p = param.p.prim.j0(c_e_p, c_s_surf_p, T) - eta_p = phi_s_p - phi_e_p - param.p.prim.U(sto_surf_p, T) - Feta_RT_p = param.F * eta_p / (param.R * T) + sto_surf_p = c_s_surf_p / self.param.p.prim.c_max + j0_p = self.param.p.prim.j0(c_e_p, c_s_surf_p, T) + eta_p = phi_s_p - phi_e_p - self.param.p.prim.U(sto_surf_p, T) + Feta_RT_p = self.param.F * eta_p / (self.param.R * T) j_s = pybamm.PrimaryBroadcast(0, "separator") - j_p = 2 * j0_p * pybamm.sinh(param.p.prim.ne / 2 * Feta_RT_p) + j_p = 2 * j0_p * pybamm.sinh(self.param.p.prim.ne / 2 * Feta_RT_p) a_j_n = a_n * j_n a_j_p = a_p * j_p @@ -150,7 +149,7 @@ def __init__(self, name="Doyle-Fuller-Newman model"): ###################### # State of Charge ###################### - I = param.current_with_time + I = self.param.current_with_time # The `rhs` dictionary contains differential equations, with the key being the # variable in the d/dt self.rhs[Q] = I / 3600 @@ -163,39 +162,39 @@ def __init__(self, name="Doyle-Fuller-Newman model"): # The div and grad operators will be converted to the appropriate matrix # multiplication at the discretisation stage - N_s_n = -param.n.prim.D(c_s_n, T) * pybamm.grad(c_s_n) - N_s_p = -param.p.prim.D(c_s_p, T) * pybamm.grad(c_s_p) + N_s_n = -self.param.n.prim.D(c_s_n, T) * pybamm.grad(c_s_n) + N_s_p = -self.param.p.prim.D(c_s_p, T) * pybamm.grad(c_s_p) self.rhs[c_s_n] = -pybamm.div(N_s_n) self.rhs[c_s_p] = -pybamm.div(N_s_p) # Boundary conditions must be provided for equations with spatial derivatives self.boundary_conditions[c_s_n] = { "left": (pybamm.Scalar(0), "Neumann"), "right": ( - -j_n / (param.F * pybamm.surf(param.n.prim.D(c_s_n, T))), + -j_n / (self.param.F * pybamm.surf(self.param.n.prim.D(c_s_n, T))), "Neumann", ), } self.boundary_conditions[c_s_p] = { "left": (pybamm.Scalar(0), "Neumann"), "right": ( - -j_p / (param.F * pybamm.surf(param.p.prim.D(c_s_p, T))), + -j_p / (self.param.F * pybamm.surf(self.param.p.prim.D(c_s_p, T))), "Neumann", ), } - self.initial_conditions[c_s_n] = param.n.prim.c_init - self.initial_conditions[c_s_p] = param.p.prim.c_init + self.initial_conditions[c_s_n] = self.param.n.prim.c_init + self.initial_conditions[c_s_p] = self.param.p.prim.c_init ###################### # Current in the solid ###################### - sigma_eff_n = param.n.sigma(T) * eps_s_n**param.n.b_s + sigma_eff_n = self.param.n.sigma(T) * eps_s_n**self.param.n.b_s i_s_n = -sigma_eff_n * pybamm.grad(phi_s_n) - sigma_eff_p = param.p.sigma(T) * eps_s_p**param.p.b_s + sigma_eff_p = self.param.p.sigma(T) * eps_s_p**self.param.p.b_s i_s_p = -sigma_eff_p * pybamm.grad(phi_s_p) # The `algebraic` dictionary contains differential equations, with the key being # the main scalar variable of interest in the equation # multiply by Lx**2 to improve conditioning - self.algebraic[phi_s_n] = param.L_x**2 * (pybamm.div(i_s_n) + a_j_n) - self.algebraic[phi_s_p] = param.L_x**2 * (pybamm.div(i_s_p) + a_j_p) + self.algebraic[phi_s_n] = self.param.L_x**2 * (pybamm.div(i_s_n) + a_j_n) + self.algebraic[phi_s_p] = self.param.L_x**2 * (pybamm.div(i_s_p) + a_j_p) self.boundary_conditions[phi_s_n] = { "left": (pybamm.Scalar(0), "Dirichlet"), "right": (pybamm.Scalar(0), "Neumann"), @@ -208,34 +207,34 @@ def __init__(self, name="Doyle-Fuller-Newman model"): # initial guess for a root-finding algorithm which calculates consistent initial # conditions self.initial_conditions[phi_s_n] = pybamm.Scalar(0) - self.initial_conditions[phi_s_p] = param.ocv_init + self.initial_conditions[phi_s_p] = self.param.ocv_init ###################### # Current in the electrolyte ###################### - i_e = (param.kappa_e(c_e, T) * tor) * ( - param.chiRT_over_Fc(c_e, T) * pybamm.grad(c_e) - pybamm.grad(phi_e) + i_e = (self.param.kappa_e(c_e, T) * tor) * ( + self.param.chiRT_over_Fc(c_e, T) * pybamm.grad(c_e) - pybamm.grad(phi_e) ) # multiply by Lx**2 to improve conditioning - self.algebraic[phi_e] = param.L_x**2 * (pybamm.div(i_e) - a_j) + self.algebraic[phi_e] = self.param.L_x**2 * (pybamm.div(i_e) - a_j) self.boundary_conditions[phi_e] = { "left": (pybamm.Scalar(0), "Neumann"), "right": (pybamm.Scalar(0), "Neumann"), } - self.initial_conditions[phi_e] = -param.n.prim.U_init + self.initial_conditions[phi_e] = -self.param.n.prim.U_init ###################### # Electrolyte concentration ###################### - N_e = -tor * param.D_e(c_e, T) * pybamm.grad(c_e) + N_e = -tor * self.param.D_e(c_e, T) * pybamm.grad(c_e) self.rhs[c_e] = (1 / eps) * ( - -pybamm.div(N_e) + (1 - param.t_plus(c_e, T)) * a_j / param.F + -pybamm.div(N_e) + (1 - self.param.t_plus(c_e, T)) * a_j / self.param.F ) self.boundary_conditions[c_e] = { "left": (pybamm.Scalar(0), "Neumann"), "right": (pybamm.Scalar(0), "Neumann"), } - self.initial_conditions[c_e] = param.c_e_init + self.initial_conditions[c_e] = self.param.c_e_init ###################### # (Some) variables @@ -270,6 +269,6 @@ def __init__(self, name="Doyle-Fuller-Newman model"): } # Events specify points at which a solution should terminate self.events += [ - pybamm.Event("Minimum voltage [V]", voltage - param.voltage_low_cut), - pybamm.Event("Maximum voltage [V]", param.voltage_high_cut - voltage), + pybamm.Event("Minimum voltage [V]", voltage - self.param.voltage_low_cut), + pybamm.Event("Maximum voltage [V]", self.param.voltage_high_cut - voltage), ] diff --git a/src/pybamm/models/full_battery_models/lithium_ion/basic_dfn_composite.py b/src/pybamm/models/full_battery_models/lithium_ion/basic_dfn_composite.py index 95f65f4d50..273d1c037c 100644 --- a/src/pybamm/models/full_battery_models/lithium_ion/basic_dfn_composite.py +++ b/src/pybamm/models/full_battery_models/lithium_ion/basic_dfn_composite.py @@ -28,7 +28,6 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"): # `param` is a class containing all the relevant parameters and functions for # this model. These are purely symbolic at this stage, and will be set by the # `ParameterValues` class when the model is processed. - param = self.param ###################### # Variables @@ -39,17 +38,17 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"): c_e_n = pybamm.Variable( "Negative electrolyte concentration [mol.m-3]", domain="negative electrode", - scale=param.c_e_init_av, + scale=self.param.c_e_init_av, ) c_e_s = pybamm.Variable( "Separator electrolyte concentration [mol.m-3]", domain="separator", - scale=param.c_e_init_av, + scale=self.param.c_e_init_av, ) c_e_p = pybamm.Variable( "Positive electrolyte concentration [mol.m-3]", domain="positive electrode", - scale=param.c_e_init_av, + scale=self.param.c_e_init_av, ) # Concatenations combine several variables into a single variable, to simplify # implementing equations that hold over several domains @@ -59,17 +58,17 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"): phi_e_n = pybamm.Variable( "Negative electrolyte potential [V]", domain="negative electrode", - reference=-param.n.prim.U_init, + reference=-self.param.n.prim.U_init, ) phi_e_s = pybamm.Variable( "Separator electrolyte potential [V]", domain="separator", - reference=-param.n.prim.U_init, + reference=-self.param.n.prim.U_init, ) phi_e_p = pybamm.Variable( "Positive electrolyte potential [V]", domain="positive electrode", - reference=-param.n.prim.U_init, + reference=-self.param.n.prim.U_init, ) phi_e = pybamm.concatenation(phi_e_n, phi_e_s, phi_e_p) @@ -80,7 +79,7 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"): phi_s_p = pybamm.Variable( "Positive electrode potential [V]", domain="positive electrode", - reference=param.ocv_init, + reference=self.param.ocv_init, ) # Particle concentrations are variables on the particle domain, but also vary in # the x-direction (electrode domain) and so must be provided with auxiliary @@ -89,30 +88,30 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"): "Negative primary particle concentration [mol.m-3]", domain="negative primary particle", auxiliary_domains={"secondary": "negative electrode"}, - scale=param.n.prim.c_max, + scale=self.param.n.prim.c_max, ) c_s_n_p2 = pybamm.Variable( "Negative secondary particle concentration [mol.m-3]", domain="negative secondary particle", auxiliary_domains={"secondary": "negative electrode"}, - scale=param.n.sec.c_max, + scale=self.param.n.sec.c_max, ) c_s_p = pybamm.Variable( "Positive particle concentration [mol.m-3]", domain="positive particle", auxiliary_domains={"secondary": "positive electrode"}, - scale=param.p.prim.c_max, + scale=self.param.p.prim.c_max, ) # Constant temperature - T = param.T_init + T = self.param.T_init ###################### # Other set-up ###################### # Current density - i_cell = param.current_density_with_time + i_cell = self.param.current_density_with_time # Porosity # Primary broadcasts are used to broadcast scalar quantities across a domain @@ -138,16 +137,16 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"): # Tortuosity tor = pybamm.concatenation( - eps_n**param.n.b_e, eps_s**param.s.b_e, eps_p**param.p.b_e + eps_n**self.param.n.b_e, eps_s**self.param.s.b_e, eps_p**self.param.p.b_e ) # Open-circuit potentials c_s_surf_n_p1 = pybamm.surf(c_s_n_p1) - sto_surf_n_p1 = c_s_surf_n_p1 / param.n.prim.c_max - ocp_n_p1 = param.n.prim.U(sto_surf_n_p1, T) + sto_surf_n_p1 = c_s_surf_n_p1 / self.param.n.prim.c_max + ocp_n_p1 = self.param.n.prim.U(sto_surf_n_p1, T) c_s_surf_n_p2 = pybamm.surf(c_s_n_p2) - sto_surf_n_p2 = c_s_surf_n_p2 / param.n.sec.c_max + sto_surf_n_p2 = c_s_surf_n_p2 / self.param.n.sec.c_max k = 100 m_lith = pybamm.sigmoid(i_cell, 0, k) # for lithation (current < 0) m_delith = 1 - m_lith # for delithiation (current > 0) @@ -156,38 +155,42 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"): ocp_n_p2 = m_lith * U_lith + m_delith * U_delith c_s_surf_p = pybamm.surf(c_s_p) - sto_surf_p = c_s_surf_p / param.p.prim.c_max - ocp_p = param.p.prim.U(sto_surf_p, T) + sto_surf_p = c_s_surf_p / self.param.p.prim.c_max + ocp_p = self.param.p.prim.U(sto_surf_p, T) # Interfacial reactions # Surf takes the surface value of a variable, i.e. its boundary value on the # right side. This is also accessible via `boundary_value(x, "right")`, with # "left" providing the boundary value of the left side - F_RT = param.F / (param.R * T) - j0_n_p1 = param.n.prim.j0(c_e_n, c_s_surf_n_p1, T) + F_RT = self.param.F / (self.param.R * T) + j0_n_p1 = self.param.n.prim.j0(c_e_n, c_s_surf_n_p1, T) j_n_p1 = ( 2 * j0_n_p1 - * pybamm.sinh(param.n.prim.ne / 2 * F_RT * (phi_s_n - phi_e_n - ocp_n_p1)) + * pybamm.sinh( + self.param.n.prim.ne / 2 * F_RT * (phi_s_n - phi_e_n - ocp_n_p1) + ) ) - j0_n_p2 = param.n.sec.j0(c_e_n, c_s_surf_n_p2, T) + j0_n_p2 = self.param.n.sec.j0(c_e_n, c_s_surf_n_p2, T) j_n_p2 = ( 2 * j0_n_p2 - * pybamm.sinh(param.n.sec.ne / 2 * F_RT * (phi_s_n - phi_e_n - ocp_n_p2)) + * pybamm.sinh( + self.param.n.sec.ne / 2 * F_RT * (phi_s_n - phi_e_n - ocp_n_p2) + ) ) - j0_p = param.p.prim.j0(c_e_p, c_s_surf_p, T) + j0_p = self.param.p.prim.j0(c_e_p, c_s_surf_p, T) a_j_s = pybamm.PrimaryBroadcast(0, "separator") j_p = ( 2 * j0_p - * pybamm.sinh(param.p.prim.ne / 2 * F_RT * (phi_s_p - phi_e_p - ocp_p)) + * pybamm.sinh(self.param.p.prim.ne / 2 * F_RT * (phi_s_p - phi_e_p - ocp_p)) ) # Volumetric - a_n_p1 = 3 * param.n.prim.epsilon_s_av / param.n.prim.R_typ - a_n_p2 = 3 * param.n.sec.epsilon_s_av / param.n.sec.R_typ - a_p = 3 * param.p.prim.epsilon_s_av / param.p.prim.R_typ + a_n_p1 = 3 * self.param.n.prim.epsilon_s_av / self.param.n.prim.R_typ + a_n_p2 = 3 * self.param.n.sec.epsilon_s_av / self.param.n.sec.R_typ + a_p = 3 * self.param.p.prim.epsilon_s_av / self.param.p.prim.R_typ a_j_n_p1 = a_n_p1 * j_n_p1 a_j_n_p2 = a_n_p2 * j_n_p2 a_j_n = a_j_n_p1 + a_j_n_p2 @@ -197,7 +200,7 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"): ###################### # State of Charge ###################### - I = param.current_with_time + I = self.param.current_with_time # The `rhs` dictionary contains differential equations, with the key being the # variable in the d/dt self.rhs[Q] = I / 3600 @@ -210,9 +213,9 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"): # The div and grad operators will be converted to the appropriate matrix # multiplication at the discretisation stage - N_s_n_p1 = -param.n.prim.D(c_s_n_p1, T) * pybamm.grad(c_s_n_p1) - N_s_n_p2 = -param.n.sec.D(c_s_n_p2, T) * pybamm.grad(c_s_n_p2) - N_s_p = -param.p.prim.D(c_s_p, T) * pybamm.grad(c_s_p) + N_s_n_p1 = -self.param.n.prim.D(c_s_n_p1, T) * pybamm.grad(c_s_n_p1) + N_s_n_p2 = -self.param.n.sec.D(c_s_n_p2, T) * pybamm.grad(c_s_n_p2) + N_s_p = -self.param.p.prim.D(c_s_p, T) * pybamm.grad(c_s_p) self.rhs[c_s_n_p1] = -pybamm.div(N_s_n_p1) self.rhs[c_s_n_p2] = -pybamm.div(N_s_n_p2) self.rhs[c_s_p] = -pybamm.div(N_s_p) @@ -220,27 +223,27 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"): self.boundary_conditions[c_s_n_p1] = { "left": (pybamm.Scalar(0), "Neumann"), "right": ( - -j_n_p1 / param.F / pybamm.surf(param.n.prim.D(c_s_n_p1, T)), + -j_n_p1 / self.param.F / pybamm.surf(self.param.n.prim.D(c_s_n_p1, T)), "Neumann", ), } self.boundary_conditions[c_s_n_p2] = { "left": (pybamm.Scalar(0), "Neumann"), "right": ( - -j_n_p2 / param.F / pybamm.surf(param.n.sec.D(c_s_n_p2, T)), + -j_n_p2 / self.param.F / pybamm.surf(self.param.n.sec.D(c_s_n_p2, T)), "Neumann", ), } self.boundary_conditions[c_s_p] = { "left": (pybamm.Scalar(0), "Neumann"), "right": ( - -j_p / param.F / pybamm.surf(param.p.prim.D(c_s_p, T)), + -j_p / self.param.F / pybamm.surf(self.param.p.prim.D(c_s_p, T)), "Neumann", ), } - self.initial_conditions[c_s_n_p1] = param.n.prim.c_init - self.initial_conditions[c_s_n_p2] = param.n.sec.c_init - self.initial_conditions[c_s_p] = param.p.prim.c_init + self.initial_conditions[c_s_n_p1] = self.param.n.prim.c_init + self.initial_conditions[c_s_n_p2] = self.param.n.sec.c_init + self.initial_conditions[c_s_p] = self.param.p.prim.c_init # Events specify points at which a solution should terminate tolerance = 0.0000001 self.events += [ @@ -272,15 +275,15 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"): ###################### # Current in the solid ###################### - sigma_eff_n = param.n.sigma(T) * eps_s_n**param.n.b_s + sigma_eff_n = self.param.n.sigma(T) * eps_s_n**self.param.n.b_s i_s_n = -sigma_eff_n * pybamm.grad(phi_s_n) - sigma_eff_p = param.p.sigma(T) * eps_s_p**param.p.b_s + sigma_eff_p = self.param.p.sigma(T) * eps_s_p**self.param.p.b_s i_s_p = -sigma_eff_p * pybamm.grad(phi_s_p) # The `algebraic` dictionary contains differential equations, with the key being # the main scalar variable of interest in the equation # multiply by Lx**2 to improve conditioning - self.algebraic[phi_s_n] = param.L_x**2 * (pybamm.div(i_s_n) + a_j_n) - self.algebraic[phi_s_p] = param.L_x**2 * (pybamm.div(i_s_p) + a_j_p) + self.algebraic[phi_s_n] = self.param.L_x**2 * (pybamm.div(i_s_n) + a_j_n) + self.algebraic[phi_s_p] = self.param.L_x**2 * (pybamm.div(i_s_p) + a_j_p) self.boundary_conditions[phi_s_n] = { "left": (pybamm.Scalar(0), "Dirichlet"), "right": (pybamm.Scalar(0), "Neumann"), @@ -295,34 +298,34 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"): # We evaluate c_n_init at x=0 and c_p_init at x=1 (this is just an initial # guess so actual value is not too important) self.initial_conditions[phi_s_n] = pybamm.Scalar(0) - self.initial_conditions[phi_s_p] = param.ocv_init + self.initial_conditions[phi_s_p] = self.param.ocv_init ###################### # Current in the electrolyte ###################### - i_e = (param.kappa_e(c_e, T) * tor) * ( - param.chiRT_over_Fc(c_e, T) * pybamm.grad(c_e) - pybamm.grad(phi_e) + i_e = (self.param.kappa_e(c_e, T) * tor) * ( + self.param.chiRT_over_Fc(c_e, T) * pybamm.grad(c_e) - pybamm.grad(phi_e) ) # multiply by Lx**2 to improve conditioning - self.algebraic[phi_e] = param.L_x**2 * (pybamm.div(i_e) - a_j) + self.algebraic[phi_e] = self.param.L_x**2 * (pybamm.div(i_e) - a_j) self.boundary_conditions[phi_e] = { "left": (pybamm.Scalar(0), "Neumann"), "right": (pybamm.Scalar(0), "Neumann"), } - self.initial_conditions[phi_e] = -param.n.prim.U_init + self.initial_conditions[phi_e] = -self.param.n.prim.U_init ###################### # Electrolyte concentration ###################### - N_e = -tor * param.D_e(c_e, T) * pybamm.grad(c_e) + N_e = -tor * self.param.D_e(c_e, T) * pybamm.grad(c_e) self.rhs[c_e] = (1 / eps) * ( - -pybamm.div(N_e) + (1 - param.t_plus(c_e, T)) * a_j / param.F + -pybamm.div(N_e) + (1 - self.param.t_plus(c_e, T)) * a_j / self.param.F ) self.boundary_conditions[c_e] = { "left": (pybamm.Scalar(0), "Neumann"), "right": (pybamm.Scalar(0), "Neumann"), } - self.initial_conditions[c_e] = param.c_e_init + self.initial_conditions[c_e] = self.param.c_e_init ###################### # (Some) variables @@ -400,8 +403,8 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"): } # Events specify points at which a solution should terminate self.events += [ - pybamm.Event("Minimum voltage [V]", voltage - param.voltage_low_cut), - pybamm.Event("Maximum voltage [V]", param.voltage_high_cut - voltage), + pybamm.Event("Minimum voltage [V]", voltage - self.param.voltage_low_cut), + pybamm.Event("Maximum voltage [V]", self.param.voltage_high_cut - voltage), ] @property diff --git a/src/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 index bc1eba3a83..b23b9dba0f 100644 --- a/src/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 @@ -36,7 +36,6 @@ def __init__(self, options=None, name="Doyle-Fuller-Newman half cell model"): # `param` is a class containing all the relevant parameters and functions for # this model. These are purely symbolic at this stage, and will be set by the # `ParameterValues` class when the model is processed. - param = self.param ###################### # Variables @@ -69,14 +68,14 @@ def __init__(self, options=None, name="Doyle-Fuller-Newman half cell model"): phi_e = pybamm.concatenation(phi_e_s, phi_e_w) # Constant temperature - T = param.T_init + T = self.param.T_init ###################### # Other set-up ###################### # Current density - i_cell = param.current_density_with_time + i_cell = self.param.current_density_with_time # Define particle surface concentration # Surf takes the surface value of a variable, i.e. its boundary value on the @@ -94,39 +93,39 @@ def __init__(self, options=None, name="Doyle-Fuller-Newman half cell model"): eps_w = pybamm.PrimaryBroadcast( pybamm.Parameter("Positive electrode porosity"), "positive electrode" ) - b_e_s = param.s.b_e - b_e_w = param.p.b_e + b_e_s = self.param.s.b_e + b_e_w = self.param.p.b_e # Interfacial reactions - j0_w = param.p.prim.j0(c_e_w, c_s_surf_w, T) - U_w = param.p.prim.U - ne_w = param.p.prim.ne + j0_w = self.param.p.prim.j0(c_e_w, c_s_surf_w, T) + U_w = self.param.p.prim.U + ne_w = self.param.p.prim.ne # Particle diffusion parameters - D_w = param.p.prim.D - c_w_init = param.p.prim.c_init + D_w = self.param.p.prim.D + c_w_init = self.param.p.prim.c_init # Electrode equation parameters eps_s_w = pybamm.Parameter("Positive electrode active material volume fraction") - b_s_w = param.p.b_s - sigma_w = param.p.sigma + b_s_w = self.param.p.b_s + sigma_w = self.param.p.sigma # Other parameters (for outputs) - c_w_max = param.p.prim.c_max - L_w = param.p.L + c_w_max = self.param.p.prim.c_max + L_w = self.param.p.L eps = pybamm.concatenation(eps_s, eps_w) tor = pybamm.concatenation(eps_s**b_e_s, eps_w**b_e_w) - F_RT = param.F / (param.R * T) - RT_F = param.R * T / param.F + F_RT = self.param.F / (self.param.R * T) + RT_F = self.param.R * T / self.param.F sto_surf_w = c_s_surf_w / c_w_max j_w = ( 2 * j0_w * pybamm.sinh(ne_w / 2 * F_RT * (phi_s_w - phi_e_w - U_w(sto_surf_w, T))) ) - R_w = param.p.prim.R_typ + R_w = self.param.p.prim.R_typ a_w = 3 * eps_s_w / R_w a_j_w = a_w * j_w a_j_s = pybamm.PrimaryBroadcast(0, "separator") @@ -135,7 +134,7 @@ def __init__(self, options=None, name="Doyle-Fuller-Newman half cell model"): ###################### # State of Charge ###################### - I = param.current_with_time + I = self.param.current_with_time # The `rhs` dictionary contains differential equations, with the key being the # variable in the d/dt self.rhs[Q] = I / 3600 @@ -154,7 +153,7 @@ def __init__(self, options=None, name="Doyle-Fuller-Newman half cell model"): # derivatives self.boundary_conditions[c_s_w] = { "left": (pybamm.Scalar(0), "Neumann"), - "right": (-j_w / pybamm.surf(D_w(c_s_w, T)) / param.F, "Neumann"), + "right": (-j_w / pybamm.surf(D_w(c_s_w, T)) / self.param.F, "Neumann"), } self.initial_conditions[c_s_w] = c_w_init @@ -183,21 +182,23 @@ def __init__(self, options=None, name="Doyle-Fuller-Newman half cell model"): ), } # multiply by Lx**2 to improve conditioning - self.algebraic[phi_s_w] = param.L_x**2 * (pybamm.div(i_s_w) + a_j_w) + self.algebraic[phi_s_w] = self.param.L_x**2 * (pybamm.div(i_s_w) + a_j_w) # Initial conditions must also be provided for algebraic equations, as an # initial guess for a root-finding algorithm which calculates consistent # initial conditions - self.initial_conditions[phi_s_w] = param.p.prim.U_init + self.initial_conditions[phi_s_w] = self.param.p.prim.U_init ###################### # Electrolyte concentration ###################### - N_e = -tor * param.D_e(c_e, T) * pybamm.grad(c_e) + N_e = -tor * self.param.D_e(c_e, T) * pybamm.grad(c_e) self.rhs[c_e] = (1 / eps) * ( - -pybamm.div(N_e) + (1 - param.t_plus(c_e, T)) * a_j / param.F + -pybamm.div(N_e) + (1 - self.param.t_plus(c_e, T)) * a_j / self.param.F ) dce_dx = ( - -(1 - param.t_plus(c_e, T)) * i_cell / (tor * param.F * param.D_e(c_e, T)) + -(1 - self.param.t_plus(c_e, T)) + * i_cell + / (tor * self.param.F * self.param.D_e(c_e, T)) ) self.boundary_conditions[c_e] = { @@ -205,7 +206,7 @@ def __init__(self, options=None, name="Doyle-Fuller-Newman half cell model"): "right": (pybamm.Scalar(0), "Neumann"), } - self.initial_conditions[c_e] = param.c_e_init + self.initial_conditions[c_e] = self.param.c_e_init self.events.append( pybamm.Event( "Zero electrolyte concentration cut-off", pybamm.min(c_e) - 0.002 @@ -215,16 +216,16 @@ def __init__(self, options=None, name="Doyle-Fuller-Newman half cell model"): ###################### # Current in the electrolyte ###################### - i_e = (param.kappa_e(c_e, T) * tor) * ( - param.chiRT_over_Fc(c_e, T) * pybamm.grad(c_e) - pybamm.grad(phi_e) + i_e = (self.param.kappa_e(c_e, T) * tor) * ( + self.param.chiRT_over_Fc(c_e, T) * pybamm.grad(c_e) - pybamm.grad(phi_e) ) # multiply by Lx**2 to improve conditioning - self.algebraic[phi_e] = param.L_x**2 * (pybamm.div(i_e) - a_j) + self.algebraic[phi_e] = self.param.L_x**2 * (pybamm.div(i_e) - a_j) # reference potential - L_Li = param.n.L - sigma_Li = param.n.sigma - j_Li = param.j0_Li_metal(pybamm.boundary_value(c_e, "left"), c_w_max, T) + L_Li = self.param.n.L + sigma_Li = self.param.n.sigma + j_Li = self.param.j0_Li_metal(pybamm.boundary_value(c_e, "left"), c_w_max, T) eta_Li = 2 * RT_F * pybamm.arcsinh(i_cell / (2 * j_Li)) phi_s_cn = 0 @@ -237,7 +238,7 @@ def __init__(self, options=None, name="Doyle-Fuller-Newman half cell model"): "right": (pybamm.Scalar(0), "Neumann"), } - self.initial_conditions[phi_e] = -param.n.prim.U_init + self.initial_conditions[phi_e] = -self.param.n.prim.U_init ###################### # (Some) variables @@ -290,11 +291,15 @@ def __init__(self, options=None, name="Doyle-Fuller-Newman half cell model"): "X-averaged positive particle surface concentration " "[mol.m-3]": c_s_surf_w_av, "Positive particle concentration [mol.m-3]": c_s_w, - "Total lithium in positive electrode [mol]": c_s_vol_av * L_w * param.A_cc, + "Total lithium in positive electrode [mol]": c_s_vol_av + * L_w + * self.param.A_cc, "Electrolyte concentration [mol.m-3]": c_e, "Separator electrolyte concentration [mol.m-3]": c_e_s, "Positive electrolyte concentration [mol.m-3]": c_e_w, - "Total lithium in electrolyte [mol]": c_e_total * param.L_x * param.A_cc, + "Total lithium in electrolyte [mol]": c_e_total + * self.param.L_x + * self.param.A_cc, "Current [A]": I, "Current variable [A]": I, # for compatibility with pybamm.Experiment "Current density [A.m-2]": i_cell, diff --git a/src/pybamm/models/full_battery_models/lithium_ion/basic_spm.py b/src/pybamm/models/full_battery_models/lithium_ion/basic_spm.py index 6bd93f3b27..cd1b968017 100644 --- a/src/pybamm/models/full_battery_models/lithium_ion/basic_spm.py +++ b/src/pybamm/models/full_battery_models/lithium_ion/basic_spm.py @@ -26,7 +26,6 @@ def __init__(self, name="Single Particle Model"): # `param` is a class containing all the relevant parameters and functions for # this model. These are purely symbolic at this stage, and will be set by the # `ParameterValues` class when the model is processed. - param = self.param ###################### # Variables @@ -44,23 +43,23 @@ def __init__(self, name="Single Particle Model"): ) # Constant temperature - T = param.T_init + T = self.param.T_init ###################### # Other set-up ###################### # Current density - i_cell = param.current_density_with_time - a_n = 3 * param.n.prim.epsilon_s_av / param.n.prim.R_typ - a_p = 3 * param.p.prim.epsilon_s_av / param.p.prim.R_typ - j_n = i_cell / (param.n.L * a_n) - j_p = -i_cell / (param.p.L * a_p) + i_cell = self.param.current_density_with_time + a_n = 3 * self.param.n.prim.epsilon_s_av / self.param.n.prim.R_typ + a_p = 3 * self.param.p.prim.epsilon_s_av / self.param.p.prim.R_typ + j_n = i_cell / (self.param.n.L * a_n) + j_p = -i_cell / (self.param.p.L * a_p) ###################### # State of Charge ###################### - I = param.current_with_time + I = self.param.current_with_time # The `rhs` dictionary contains differential equations, with the key being the # variable in the d/dt self.rhs[Q] = I / 3600 @@ -73,8 +72,8 @@ def __init__(self, name="Single Particle Model"): # The div and grad operators will be converted to the appropriate matrix # multiplication at the discretisation stage - N_s_n = -param.n.prim.D(c_s_n, T) * pybamm.grad(c_s_n) - N_s_p = -param.p.prim.D(c_s_p, T) * pybamm.grad(c_s_p) + N_s_n = -self.param.n.prim.D(c_s_n, T) * pybamm.grad(c_s_n) + N_s_p = -self.param.p.prim.D(c_s_p, T) * pybamm.grad(c_s_p) self.rhs[c_s_n] = -pybamm.div(N_s_n) self.rhs[c_s_p] = -pybamm.div(N_s_p) # Surf takes the surface value of a variable, i.e. its boundary value on the @@ -86,24 +85,24 @@ def __init__(self, name="Single Particle Model"): self.boundary_conditions[c_s_n] = { "left": (pybamm.Scalar(0), "Neumann"), "right": ( - -j_n / (param.F * pybamm.surf(param.n.prim.D(c_s_n, T))), + -j_n / (self.param.F * pybamm.surf(self.param.n.prim.D(c_s_n, T))), "Neumann", ), } self.boundary_conditions[c_s_p] = { "left": (pybamm.Scalar(0), "Neumann"), "right": ( - -j_p / (param.F * pybamm.surf(param.p.prim.D(c_s_p, T))), + -j_p / (self.param.F * pybamm.surf(self.param.p.prim.D(c_s_p, T))), "Neumann", ), } # c_n_init and c_p_init are functions of r and x, but for the SPM we # take the x-averaged value since there is no x-dependence in the particles - self.initial_conditions[c_s_n] = pybamm.x_average(param.n.prim.c_init) - self.initial_conditions[c_s_p] = pybamm.x_average(param.p.prim.c_init) + self.initial_conditions[c_s_n] = pybamm.x_average(self.param.n.prim.c_init) + self.initial_conditions[c_s_p] = pybamm.x_average(self.param.p.prim.c_init) # Events specify points at which a solution should terminate - sto_surf_n = c_s_surf_n / param.n.prim.c_max - sto_surf_p = c_s_surf_p / param.p.prim.c_max + sto_surf_n = c_s_surf_n / self.param.n.prim.c_max + sto_surf_p = c_s_surf_p / self.param.p.prim.c_max self.events += [ pybamm.Event( "Minimum negative particle surface stoichiometry", @@ -130,14 +129,14 @@ def __init__(self, name="Single Particle Model"): # (Some) variables ###################### # Interfacial reactions - RT_F = param.R * T / param.F - j0_n = param.n.prim.j0(param.c_e_init_av, c_s_surf_n, T) - j0_p = param.p.prim.j0(param.c_e_init_av, c_s_surf_p, T) - eta_n = (2 / param.n.prim.ne) * RT_F * pybamm.arcsinh(j_n / (2 * j0_n)) - eta_p = (2 / param.p.prim.ne) * RT_F * pybamm.arcsinh(j_p / (2 * j0_p)) + RT_F = self.param.R * T / self.param.F + j0_n = self.param.n.prim.j0(self.param.c_e_init_av, c_s_surf_n, T) + j0_p = self.param.p.prim.j0(self.param.c_e_init_av, c_s_surf_p, T) + eta_n = (2 / self.param.n.prim.ne) * RT_F * pybamm.arcsinh(j_n / (2 * j0_n)) + eta_p = (2 / self.param.p.prim.ne) * RT_F * pybamm.arcsinh(j_p / (2 * j0_p)) phi_s_n = 0 - phi_e = -eta_n - param.n.prim.U(sto_surf_n, T) - phi_s_p = eta_p + phi_e + param.p.prim.U(sto_surf_p, T) + phi_e = -eta_n - self.param.n.prim.U(sto_surf_n, T) + phi_s_p = eta_p + phi_e + self.param.p.prim.U(sto_surf_p, T) V = phi_s_p num_cells = pybamm.Parameter( "Number of cells connected in series to make a battery" @@ -157,7 +156,7 @@ def __init__(self, name="Single Particle Model"): c_s_surf_n, "negative electrode" ), "Electrolyte concentration [mol.m-3]": pybamm.PrimaryBroadcast( - param.c_e_init_av, whole_cell + self.param.c_e_init_av, whole_cell ), "X-averaged positive particle concentration [mol.m-3]": c_s_p, "Positive particle surface " @@ -178,6 +177,6 @@ def __init__(self, name="Single Particle Model"): } # Events specify points at which a solution should terminate self.events += [ - pybamm.Event("Minimum voltage [V]", V - param.voltage_low_cut), - pybamm.Event("Maximum voltage [V]", param.voltage_high_cut - V), + pybamm.Event("Minimum voltage [V]", V - self.param.voltage_low_cut), + pybamm.Event("Maximum voltage [V]", self.param.voltage_high_cut - V), ] diff --git a/src/pybamm/models/full_battery_models/lithium_ion/electrode_soh.py b/src/pybamm/models/full_battery_models/lithium_ion/electrode_soh.py index c5b6a9b911..d40a74b93e 100644 --- a/src/pybamm/models/full_battery_models/lithium_ion/electrode_soh.py +++ b/src/pybamm/models/full_battery_models/lithium_ion/electrode_soh.py @@ -666,13 +666,12 @@ def get_initial_stoichiometries(self, initial_value, tol=1e-6, inputs=None): The initial stoichiometries that give the desired initial state of charge """ parameter_values = self.parameter_values - param = self.param x_0, x_100, y_100, y_0 = self.get_min_max_stoichiometries(inputs=inputs) if isinstance(initial_value, str) and initial_value.endswith("V"): V_init = float(initial_value[:-1]) - V_min = parameter_values.evaluate(param.ocp_soc_0) - V_max = parameter_values.evaluate(param.ocp_soc_100) + V_min = parameter_values.evaluate(self.param.ocp_soc_0) + V_max = parameter_values.evaluate(self.param.ocp_soc_100) if not V_min <= V_init <= V_max: raise ValueError( @@ -687,8 +686,8 @@ def get_initial_stoichiometries(self, initial_value, tol=1e-6, inputs=None): y = y_0 - soc * (y_0 - y_100) T_ref = parameter_values["Reference temperature [K]"] if self.options["open-circuit potential"] == "MSMR": - xn = param.n.prim.x - xp = param.p.prim.x + xn = self.param.n.prim.x + xp = self.param.p.prim.x Up = pybamm.Variable("Up") Un = pybamm.Variable("Un") soc_model.algebraic[Up] = x - xn(Un, T_ref) @@ -697,8 +696,8 @@ def get_initial_stoichiometries(self, initial_value, tol=1e-6, inputs=None): soc_model.initial_conditions[Up] = V_max soc_model.algebraic[soc] = Up - Un - V_init else: - Up = param.p.prim.U - Un = param.n.prim.U + Up = self.param.p.prim.U + Un = self.param.n.prim.U soc_model.algebraic[soc] = Up(y, T_ref) - Un(x, T_ref) - V_init # initial guess for soc linearly interpolates between 0 and 1 # based on V linearly interpolating between V_max and V_min @@ -741,17 +740,18 @@ def get_min_max_stoichiometries(self, inputs=None): """ inputs = inputs or {} parameter_values = self.parameter_values - param = self.param - Q_n = parameter_values.evaluate(param.n.Q_init, inputs=inputs) - Q_p = parameter_values.evaluate(param.p.Q_init, inputs=inputs) + Q_n = parameter_values.evaluate(self.param.n.Q_init, inputs=inputs) + Q_p = parameter_values.evaluate(self.param.p.Q_init, inputs=inputs) if self.known_value == "cyclable lithium capacity": - Q_Li = parameter_values.evaluate(param.Q_Li_particles_init, inputs=inputs) + Q_Li = parameter_values.evaluate( + self.param.Q_Li_particles_init, inputs=inputs + ) all_inputs = {**inputs, "Q_n": Q_n, "Q_p": Q_p, "Q_Li": Q_Li} elif self.known_value == "cell capacity": Q = parameter_values.evaluate( - param.Q / param.n_electrodes_parallel, inputs=inputs + self.param.Q / self.param.n_electrodes_parallel, inputs=inputs ) all_inputs = {**inputs, "Q_n": Q_n, "Q_p": Q_p, "Q": Q} # Solve the model and check outputs @@ -782,7 +782,6 @@ def get_initial_ocps(self, initial_value, tol=1e-6, inputs=None): The initial open-circuit potentials at the desired initial state of charge """ parameter_values = self.parameter_values - param = self.param x, y = self.get_initial_stoichiometries(initial_value, tol, inputs=inputs) if self.options["open-circuit potential"] == "MSMR": msmr_pot_model = _get_msmr_potential_model( @@ -795,8 +794,8 @@ def get_initial_ocps(self, initial_value, tol=1e-6, inputs=None): Up = sol["Up"].data[0] else: T_ref = parameter_values["Reference temperature [K]"] - Un = parameter_values.evaluate(param.n.prim.U(x, T_ref), inputs=inputs) - Up = parameter_values.evaluate(param.p.prim.U(y, T_ref), inputs=inputs) + Un = parameter_values.evaluate(self.param.n.prim.U(x, T_ref), inputs=inputs) + Up = parameter_values.evaluate(self.param.p.prim.U(y, T_ref), inputs=inputs) return Un, Up def get_min_max_ocps(self): @@ -810,16 +809,17 @@ def get_min_max_ocps(self): The min/max ocps """ parameter_values = self.parameter_values - param = self.param - Q_n = parameter_values.evaluate(param.n.Q_init) - Q_p = parameter_values.evaluate(param.p.Q_init) + Q_n = parameter_values.evaluate(self.param.n.Q_init) + Q_p = parameter_values.evaluate(self.param.p.Q_init) if self.known_value == "cyclable lithium capacity": - Q_Li = parameter_values.evaluate(param.Q_Li_particles_init) + Q_Li = parameter_values.evaluate(self.param.Q_Li_particles_init) inputs = {"Q_n": Q_n, "Q_p": Q_p, "Q_Li": Q_Li} elif self.known_value == "cell capacity": - Q = parameter_values.evaluate(param.Q / param.n_electrodes_parallel) + Q = parameter_values.evaluate( + self.param.Q / self.param.n_electrodes_parallel + ) inputs = {"Q_n": Q_n, "Q_p": Q_p, "Q": Q} # Solve the model and check outputs sol = self.solve(inputs) @@ -834,10 +834,10 @@ def theoretical_energy_integral(self, inputs, points=1000): x_vals = np.linspace(x_100, x_0, num=points) y_vals = np.linspace(y_100, y_0, num=points) # Calculate OCV at each stoichiometry - param = self.param - T = param.T_amb_av(0) + T = self.param.T_amb_av(0) Vs = self.parameter_values.evaluate( - param.p.prim.U(y_vals, T) - param.n.prim.U(x_vals, T), inputs=inputs + self.param.p.prim.U(y_vals, T) - self.param.n.prim.U(x_vals, T), + inputs=inputs, ).flatten() # Calculate dQ Q = Q_p * (y_0 - y_100) diff --git a/src/pybamm/models/submodels/active_material/base_active_material.py b/src/pybamm/models/submodels/active_material/base_active_material.py index ba39adf852..ea0e826e09 100644 --- a/src/pybamm/models/submodels/active_material/base_active_material.py +++ b/src/pybamm/models/submodels/active_material/base_active_material.py @@ -23,7 +23,6 @@ def __init__(self, param, domain, options, phase="primary"): super().__init__(param, domain, options=options, phase=phase) def _get_standard_active_material_variables(self, eps_solid): - param = self.param phase_name = self.phase_name domain, Domain = self.domain_Domain @@ -61,9 +60,9 @@ def _get_standard_active_material_variables(self, eps_solid): C = ( pybamm.yz_average(eps_solid_av) * L - * param.A_cc + * self.param.A_cc * c_s_max - * param.F + * self.param.F / 3600 ) if phase_name == "": diff --git a/src/pybamm/models/submodels/convection/through_cell/explicit_convection.py b/src/pybamm/models/submodels/convection/through_cell/explicit_convection.py index 33b58e2b23..c0423bfc41 100644 --- a/src/pybamm/models/submodels/convection/through_cell/explicit_convection.py +++ b/src/pybamm/models/submodels/convection/through_cell/explicit_convection.py @@ -19,7 +19,6 @@ def __init__(self, param): def get_coupled_variables(self, variables): # Set up - param = self.param p_s = variables["X-averaged separator pressure [Pa]"] for domain in self.options.whole_cell_domains: if domain == "separator": @@ -29,22 +28,30 @@ def get_coupled_variables(self, variables): ] if domain == "negative electrode": x_n = pybamm.standard_spatial_vars.x_n - DeltaV_k = param.n.DeltaV + DeltaV_k = self.param.n.DeltaV p_k = ( - -DeltaV_k * a_j_k_av / param.F * (-(x_n**2) + param.n.L**2) / 2 + -DeltaV_k + * a_j_k_av + / self.param.F + * (-(x_n**2) + self.param.n.L**2) + / 2 + p_s ) - v_box_k = -DeltaV_k * a_j_k_av / param.F * x_n + v_box_k = -DeltaV_k * a_j_k_av / self.param.F * x_n elif domain == "positive electrode": x_p = pybamm.standard_spatial_vars.x_p - DeltaV_k = param.p.DeltaV + DeltaV_k = self.param.p.DeltaV p_k = ( - -DeltaV_k * a_j_k_av / param.F * ((x_p - 1) ** 2 - param.p.L**2) / 2 + -DeltaV_k + * a_j_k_av + / self.param.F + * ((x_p - 1) ** 2 - self.param.p.L**2) + / 2 + p_s ) - v_box_k = -DeltaV_k * a_j_k_av / param.F * (x_p - param.L_x) + v_box_k = -DeltaV_k * a_j_k_av / self.param.F * (x_p - self.param.L_x) div_v_box_k = pybamm.PrimaryBroadcast( - -DeltaV_k * a_j_k_av / param.F, domain + -DeltaV_k * a_j_k_av / self.param.F, domain ) variables.update( @@ -58,13 +65,13 @@ def get_coupled_variables(self, variables): "X-averaged separator transverse volume-averaged acceleration [m.s-2]" ] i_boundary_cc = variables["Current collector current density [A.m-2]"] - v_box_n_right = -param.n.DeltaV * i_boundary_cc / param.F + v_box_n_right = -self.param.n.DeltaV * i_boundary_cc / self.param.F div_v_box_s_av = -div_Vbox_s div_v_box_s = pybamm.PrimaryBroadcast(div_v_box_s_av, "separator") # Simple formula for velocity in the separator x_s = pybamm.standard_spatial_vars.x_s - v_box_s = div_v_box_s_av * (x_s - param.n.L) + v_box_n_right + v_box_s = div_v_box_s_av * (x_s - self.param.n.L) + v_box_n_right variables.update( self._get_standard_sep_velocity_variables(v_box_s, div_v_box_s) diff --git a/src/pybamm/models/submodels/convection/through_cell/full_convection.py b/src/pybamm/models/submodels/convection/through_cell/full_convection.py index 0fdc089de7..07241bb236 100644 --- a/src/pybamm/models/submodels/convection/through_cell/full_convection.py +++ b/src/pybamm/models/submodels/convection/through_cell/full_convection.py @@ -43,8 +43,7 @@ def get_fundamental_variables(self): def get_coupled_variables(self, variables): # Set up - param = self.param - L_n = param.n.L + L_n = self.param.n.L x_s = pybamm.standard_spatial_vars.x_s # Transverse velocity in the separator determines through-cell velocity @@ -52,7 +51,7 @@ def get_coupled_variables(self, variables): "X-averaged separator transverse volume-averaged acceleration [m.s-2]" ] i_boundary_cc = variables["Current collector current density [A.m-2]"] - v_box_n_right = -param.n.DeltaV * i_boundary_cc / self.param.F + v_box_n_right = -self.param.n.DeltaV * i_boundary_cc / self.param.F div_v_box_s_av = -div_Vbox_s div_v_box_s = pybamm.PrimaryBroadcast(div_v_box_s_av, "separator") diff --git a/src/pybamm/models/submodels/convection/transverse/full_convection.py b/src/pybamm/models/submodels/convection/transverse/full_convection.py index 16da47ae47..0a6367fec1 100644 --- a/src/pybamm/models/submodels/convection/transverse/full_convection.py +++ b/src/pybamm/models/submodels/convection/transverse/full_convection.py @@ -37,15 +37,13 @@ def get_fundamental_variables(self): return variables def set_algebraic(self, variables): - param = self.param - p_s = variables["X-averaged separator pressure [Pa]"] # Difference in negative and positive electrode velocities determines the # velocity in the separator i_boundary_cc = variables["Current collector current density [A.m-2]"] - v_box_n_right = -param.n.DeltaV * i_boundary_cc / self.param.F - v_box_p_left = -param.p.DeltaV * i_boundary_cc / self.param.F - d_vbox_s_dx = (v_box_p_left - v_box_n_right) / param.s.L + v_box_n_right = -self.param.n.DeltaV * i_boundary_cc / self.param.F + v_box_p_left = -self.param.p.DeltaV * i_boundary_cc / self.param.F + d_vbox_s_dx = (v_box_p_left - v_box_n_right) / self.param.s.L # Simple formula for velocity in the separator div_Vbox_s = -d_vbox_s_dx diff --git a/src/pybamm/models/submodels/convection/transverse/uniform_convection.py b/src/pybamm/models/submodels/convection/transverse/uniform_convection.py index 15a073c148..a4b05f1ad5 100644 --- a/src/pybamm/models/submodels/convection/transverse/uniform_convection.py +++ b/src/pybamm/models/submodels/convection/transverse/uniform_convection.py @@ -26,15 +26,14 @@ def get_fundamental_variables(self): def get_coupled_variables(self, variables): # Set up - param = self.param z = pybamm.standard_spatial_vars.z # Difference in negative and positive electrode velocities determines the # velocity in the separator i_boundary_cc = variables["Current collector current density [A.m-2]"] - v_box_n_right = -param.n.DeltaV * i_boundary_cc / param.F - v_box_p_left = -param.p.DeltaV * i_boundary_cc / param.F - d_vbox_s_dx = (v_box_p_left - v_box_n_right) / param.s.L + v_box_n_right = -self.param.n.DeltaV * i_boundary_cc / self.param.F + v_box_p_left = -self.param.p.DeltaV * i_boundary_cc / self.param.F + d_vbox_s_dx = (v_box_p_left - v_box_n_right) / self.param.s.L # Simple formula for velocity in the separator div_Vbox_s = -d_vbox_s_dx diff --git a/src/pybamm/models/submodels/current_collector/effective_resistance_current_collector.py b/src/pybamm/models/submodels/current_collector/effective_resistance_current_collector.py index 23001b9d02..808e0a34a3 100644 --- a/src/pybamm/models/submodels/current_collector/effective_resistance_current_collector.py +++ b/src/pybamm/models/submodels/current_collector/effective_resistance_current_collector.py @@ -12,29 +12,28 @@ def default_parameter_values(self): @property def default_geometry(self): geometry = {} - param = self.param if self.options["dimensionality"] == 1: geometry["current collector"] = { - "z": {"min": 0, "max": param.L_z}, + "z": {"min": 0, "max": self.param.L_z}, "tabs": { - "negative": {"z_centre": param.n.centre_z_tab}, - "positive": {"z_centre": param.p.centre_z_tab}, + "negative": {"z_centre": self.param.n.centre_z_tab}, + "positive": {"z_centre": self.param.p.centre_z_tab}, }, } elif self.options["dimensionality"] == 2: geometry["current collector"] = { - "y": {"min": 0, "max": param.L_y}, - "z": {"min": 0, "max": param.L_z}, + "y": {"min": 0, "max": self.param.L_y}, + "z": {"min": 0, "max": self.param.L_z}, "tabs": { "negative": { - "y_centre": param.n.centre_y_tab, - "z_centre": param.n.centre_z_tab, - "width": param.n.L_tab, + "y_centre": self.param.n.centre_y_tab, + "z_centre": self.param.n.centre_z_tab, + "width": self.param.n.L_tab, }, "positive": { - "y_centre": param.p.centre_y_tab, - "z_centre": param.p.centre_z_tab, - "width": param.p.L_tab, + "y_centre": self.param.p.centre_y_tab, + "z_centre": self.param.p.centre_z_tab, + "width": self.param.p.L_tab, }, }, } @@ -131,11 +130,10 @@ def __init__( def get_fundamental_variables(self): # Get necessary parameters - param = self.param - L_cn = param.n.L_cc - L_cp = param.p.L_cc - sigma_cn = param.n.sigma_cc - sigma_cp = param.p.sigma_cc + L_cn = self.param.n.L_cc + L_cp = self.param.p.L_cc + sigma_cn = self.param.n.sigma_cc + sigma_cp = self.param.p.sigma_cc # Set model variables: Note: we solve using a scaled version that is # better conditioned @@ -273,13 +271,12 @@ def __init__(self): self.param = pybamm.LithiumIonParameters() # Get necessary parameters - param = self.param - L_cn = param.n.L_cc - L_cp = param.p.L_cc - L_tab_p = param.p.L_tab + L_cn = self.param.n.L_cc + L_cp = self.param.p.L_cc + L_tab_p = self.param.p.L_tab A_tab_p = L_cp * L_tab_p - sigma_cn = param.n.sigma_cc - sigma_cp = param.p.sigma_cc + sigma_cn = self.param.n.sigma_cc + sigma_cp = self.param.p.sigma_cc # Set model variables -- we solve a auxilliary problem in each current collector # then relate this to the potentials and resistances later @@ -347,11 +344,10 @@ def post_process(self, solution, param_values, V_av, I_av): processed potentials. """ # Get evaluated parameters - param = self.param - L_cn = param_values.evaluate(param.n.L_cc) - L_cp = param_values.evaluate(param.p.L_cc) - sigma_cn = param_values.evaluate(param.n.sigma_cc) - sigma_cp = param_values.evaluate(param.p.sigma_cc) + L_cn = param_values.evaluate(self.param.n.L_cc) + L_cp = param_values.evaluate(self.param.p.L_cc) + sigma_cn = param_values.evaluate(self.param.n.sigma_cc) + sigma_cp = param_values.evaluate(self.param.p.sigma_cc) # Process unit solutions f_n = solution["Unit solution in negative current collector"] diff --git a/src/pybamm/models/submodels/current_collector/potential_pair.py b/src/pybamm/models/submodels/current_collector/potential_pair.py index 68a9066da3..c2a197ae64 100644 --- a/src/pybamm/models/submodels/current_collector/potential_pair.py +++ b/src/pybamm/models/submodels/current_collector/potential_pair.py @@ -23,7 +23,6 @@ def __init__(self, param): pybamm.citations.register("Timms2021") def get_fundamental_variables(self): - param = self.param phi_s_cn = pybamm.Variable( "Negative current collector potential [V]", domain="current collector" ) @@ -35,7 +34,7 @@ def get_fundamental_variables(self): i_boundary_cc = pybamm.Variable( "Current collector current density [A.m-2]", domain="current collector", - scale=param.Q / (param.A_cc * param.n_electrodes_parallel), + scale=self.param.Q / (self.param.A_cc * self.param.n_electrodes_parallel), ) variables.update(self._get_standard_current_variables(i_cc, i_boundary_cc)) @@ -43,16 +42,15 @@ def get_fundamental_variables(self): return variables def set_algebraic(self, variables): - param = self.param - phi_s_cn = variables["Negative current collector potential [V]"] phi_s_cp = variables["Positive current collector potential [V]"] i_boundary_cc = variables["Current collector current density [A.m-2]"] self.algebraic = { - phi_s_cn: (param.n.sigma_cc * param.n.L_cc) * pybamm.laplacian(phi_s_cn) + phi_s_cn: (self.param.n.sigma_cc * self.param.n.L_cc) + * pybamm.laplacian(phi_s_cn) - pybamm.source(i_boundary_cc, phi_s_cn), - i_boundary_cc: (param.p.sigma_cc * param.p.L_cc) + i_boundary_cc: (self.param.p.sigma_cc * self.param.p.L_cc) * pybamm.laplacian(phi_s_cp) + pybamm.source(i_boundary_cc, phi_s_cp), } @@ -77,15 +75,14 @@ def set_boundary_conditions(self, variables): phi_s_cn = variables["Negative current collector potential [V]"] phi_s_cp = variables["Positive current collector potential [V]"] - param = self.param applied_current_density = variables["Total current density [A.m-2]"] - total_current = applied_current_density * param.A_cc + total_current = applied_current_density * self.param.A_cc # In the 1+1D model, the behaviour is averaged over the y-direction, so the # effective tab area is the cell width multiplied by the current collector # thickness - positive_tab_area = param.L_y * param.p.L_cc - pos_tab_bc = -total_current / (param.p.sigma_cc * positive_tab_area) + positive_tab_area = self.param.L_y * self.param.p.L_cc + pos_tab_bc = -total_current / (self.param.p.sigma_cc * positive_tab_area) # Boundary condition needs to be on the variables that go into the Laplacian, # even though phi_s_cp isn't a pybamm.Variable object @@ -111,20 +108,19 @@ def set_boundary_conditions(self, variables): phi_s_cn = variables["Negative current collector potential [V]"] phi_s_cp = variables["Positive current collector potential [V]"] - param = self.param applied_current_density = variables["Total current density [A.m-2]"] - total_current = applied_current_density * param.A_cc + total_current = applied_current_density * self.param.A_cc # Note: we divide by the *numerical* tab area so that the correct total # current is applied. That is, numerically integrating the current density # around the boundary gives the applied current exactly. positive_tab_area = pybamm.BoundaryIntegral( - pybamm.PrimaryBroadcast(param.p.L_cc, "current collector"), + pybamm.PrimaryBroadcast(self.param.p.L_cc, "current collector"), region="positive tab", ) # cc_area appears here due to choice of non-dimensionalisation - pos_tab_bc = -total_current / (param.p.sigma_cc * positive_tab_area) + pos_tab_bc = -total_current / (self.param.p.sigma_cc * positive_tab_area) # Boundary condition needs to be on the variables that go into the Laplacian, # even though phi_s_cp isn't a pybamm.Variable object diff --git a/src/pybamm/models/submodels/electrode/base_electrode.py b/src/pybamm/models/submodels/electrode/base_electrode.py index 4248131a75..3abe563c77 100644 --- a/src/pybamm/models/submodels/electrode/base_electrode.py +++ b/src/pybamm/models/submodels/electrode/base_electrode.py @@ -170,9 +170,8 @@ def _get_standard_whole_cell_variables(self, variables): phi_s_p = variables["Positive electrode potential [V]"] phi_s_cp = pybamm.boundary_value(phi_s_p, "right") if self.options["contact resistance"] == "true": - param = self.param I = variables["Current [A]"] - delta_phi_contact = I * param.R_contact + delta_phi_contact = I * self.param.R_contact else: delta_phi_contact = pybamm.Scalar(0) variables.update( diff --git a/src/pybamm/models/submodels/electrode/ohm/composite_ohm.py b/src/pybamm/models/submodels/electrode/ohm/composite_ohm.py index 4845ea9fb2..7c4d8d62b8 100644 --- a/src/pybamm/models/submodels/electrode/ohm/composite_ohm.py +++ b/src/pybamm/models/submodels/electrode/ohm/composite_ohm.py @@ -26,14 +26,13 @@ def __init__(self, param, domain, options=None): def get_coupled_variables(self, variables): domain = self.domain - param = self.param i_boundary_cc = variables["Current collector current density [A.m-2]"] # import parameters and spatial variables - L_n = param.n.L - L_p = param.p.L - L_x = param.L_x + L_n = self.param.n.L + L_p = self.param.p.L + L_x = self.param.L_x x_n = pybamm.standard_spatial_vars.x_n x_p = pybamm.standard_spatial_vars.x_p diff --git a/src/pybamm/models/submodels/electrode/ohm/leading_ohm.py b/src/pybamm/models/submodels/electrode/ohm/leading_ohm.py index 7e414f94c9..8385a31fc1 100644 --- a/src/pybamm/models/submodels/electrode/ohm/leading_ohm.py +++ b/src/pybamm/models/submodels/electrode/ohm/leading_ohm.py @@ -35,15 +35,14 @@ def get_coupled_variables(self, variables): """ Returns variables which are derived from the fundamental variables in the model. """ - param = self.param i_boundary_cc = variables["Current collector current density [A.m-2]"] phi_s_cn = variables["Negative current collector potential [V]"] # import parameters and spatial variables - L_n = param.n.L - L_p = param.p.L - L_x = param.L_x + L_n = self.param.n.L + L_p = self.param.p.L + L_x = self.param.L_x x_n = pybamm.standard_spatial_vars.x_n x_p = pybamm.standard_spatial_vars.x_p diff --git a/src/pybamm/models/submodels/electrolyte_conductivity/base_electrolyte_conductivity.py b/src/pybamm/models/submodels/electrolyte_conductivity/base_electrolyte_conductivity.py index d1178c8cc2..5a7d3163c2 100644 --- a/src/pybamm/models/submodels/electrolyte_conductivity/base_electrolyte_conductivity.py +++ b/src/pybamm/models/submodels/electrolyte_conductivity/base_electrolyte_conductivity.py @@ -217,7 +217,6 @@ def _get_electrolyte_overpotentials(self, variables): The variables including the whole-cell electrolyte potentials and currents. """ - param = self.param if self.options.electrode_types["negative"] == "planar": # No concentration overpotential in the counter electrode @@ -229,7 +228,7 @@ def _get_electrolyte_overpotentials(self, variables): c_e_n = variables["Negative electrolyte concentration [mol.m-3]"] T_n = variables["Negative electrode temperature [K]"] indef_integral_n = pybamm.IndefiniteIntegral( - param.chiRT_over_Fc(c_e_n, T_n) * pybamm.grad(c_e_n), + self.param.chiRT_over_Fc(c_e_n, T_n) * pybamm.grad(c_e_n), pybamm.standard_spatial_vars.x_n, ) @@ -243,11 +242,11 @@ def _get_electrolyte_overpotentials(self, variables): # concentration overpotential indef_integral_s = pybamm.IndefiniteIntegral( - param.chiRT_over_Fc(c_e_s, T_s) * pybamm.grad(c_e_s), + self.param.chiRT_over_Fc(c_e_s, T_s) * pybamm.grad(c_e_s), pybamm.standard_spatial_vars.x_s, ) indef_integral_p = pybamm.IndefiniteIntegral( - param.chiRT_over_Fc(c_e_p, T_p) * pybamm.grad(c_e_p), + self.param.chiRT_over_Fc(c_e_p, T_p) * pybamm.grad(c_e_p), pybamm.standard_spatial_vars.x_p, ) diff --git a/src/pybamm/models/submodels/electrolyte_conductivity/composite_conductivity.py b/src/pybamm/models/submodels/electrolyte_conductivity/composite_conductivity.py index 475d1a4232..d6c7ea6473 100644 --- a/src/pybamm/models/submodels/electrolyte_conductivity/composite_conductivity.py +++ b/src/pybamm/models/submodels/electrolyte_conductivity/composite_conductivity.py @@ -52,23 +52,22 @@ def get_coupled_variables(self, variables): T_av_s = pybamm.PrimaryBroadcast(T_av, "separator") T_av_p = pybamm.PrimaryBroadcast(T_av, "positive electrode") - param = self.param - RT_F_av = param.R * T_av / param.F - RT_F_av_s = param.R * T_av_s / param.F - RT_F_av_p = param.R * T_av_p / param.F - - L_n = param.n.L - L_s = param.s.L - L_p = param.p.L - L_x = param.L_x + RT_F_av = self.param.R * T_av / self.param.F + RT_F_av_s = self.param.R * T_av_s / self.param.F + RT_F_av_p = self.param.R * T_av_p / self.param.F + + L_n = self.param.n.L + L_s = self.param.s.L + L_p = self.param.p.L + L_x = self.param.L_x x_s = pybamm.standard_spatial_vars.x_s x_p = pybamm.standard_spatial_vars.x_p # bulk conductivities - kappa_s_av = param.kappa_e(c_e_av, T_av) * tor_s_av - kappa_p_av = param.kappa_e(c_e_av, T_av) * tor_p_av + kappa_s_av = self.param.kappa_e(c_e_av, T_av) * tor_s_av + kappa_p_av = self.param.kappa_e(c_e_av, T_av) * tor_p_av - chi_av = param.chi(c_e_av, T_av) + chi_av = self.param.chi(c_e_av, T_av) chi_av_s = pybamm.PrimaryBroadcast(chi_av, "separator") chi_av_p = pybamm.PrimaryBroadcast(chi_av, "positive electrode") @@ -79,8 +78,8 @@ def get_coupled_variables(self, variables): x_n = pybamm.standard_spatial_vars.x_n chi_av_n = pybamm.PrimaryBroadcast(chi_av, "negative electrode") T_av_n = pybamm.PrimaryBroadcast(T_av, "negative electrode") - RT_F_av_n = param.R * T_av_n / param.F - kappa_n_av = param.kappa_e(c_e_av, T_av) * tor_n_av + RT_F_av_n = self.param.R * T_av_n / self.param.F + kappa_n_av = self.param.kappa_e(c_e_av, T_av) * tor_n_av i_e_n = i_boundary_cc * x_n / L_n i_e_s = pybamm.PrimaryBroadcast(i_boundary_cc, "separator") i_e_p = i_boundary_cc * (L_x - x_p) / L_p diff --git a/src/pybamm/models/submodels/electrolyte_conductivity/full_conductivity.py b/src/pybamm/models/submodels/electrolyte_conductivity/full_conductivity.py index 5acb7d2434..a688209441 100644 --- a/src/pybamm/models/submodels/electrolyte_conductivity/full_conductivity.py +++ b/src/pybamm/models/submodels/electrolyte_conductivity/full_conductivity.py @@ -46,14 +46,13 @@ def get_fundamental_variables(self): return variables def get_coupled_variables(self, variables): - param = self.param T = variables["Cell temperature [K]"] tor = variables["Electrolyte transport efficiency"] c_e = variables["Electrolyte concentration [mol.m-3]"] phi_e = variables["Electrolyte potential [V]"] - i_e = (param.kappa_e(c_e, T) * tor) * ( - param.chiRT_over_Fc(c_e, T) * pybamm.grad(c_e) - pybamm.grad(phi_e) + i_e = (self.param.kappa_e(c_e, T) * tor) * ( + self.param.chiRT_over_Fc(c_e, T) * pybamm.grad(c_e) - pybamm.grad(phi_e) ) # Override print_name diff --git a/src/pybamm/models/submodels/electrolyte_conductivity/integrated_conductivity.py b/src/pybamm/models/submodels/electrolyte_conductivity/integrated_conductivity.py index cb9979c6bb..2250d99f6d 100644 --- a/src/pybamm/models/submodels/electrolyte_conductivity/integrated_conductivity.py +++ b/src/pybamm/models/submodels/electrolyte_conductivity/integrated_conductivity.py @@ -32,7 +32,6 @@ def _higher_order_macinnes_function(self, x): return pybamm.log(x) def get_coupled_variables(self, variables): - param = self.param c_e_av = variables["X-averaged electrolyte concentration [mol.m-3]"] i_boundary_cc = variables["Current collector current density [A.m-2]"] @@ -55,22 +54,21 @@ def get_coupled_variables(self, variables): T_av_s = pybamm.PrimaryBroadcast(T_av, "separator") T_av_p = pybamm.PrimaryBroadcast(T_av, "positive electrode") - RT_F_av = param.R * T_av / param.F - RT_F_av_n = param.R * T_av_n / param.F - RT_F_av_s = param.R * T_av_s / param.F - RT_F_av_p = param.R * T_av_p / param.F + RT_F_av = self.param.R * T_av / self.param.F + RT_F_av_n = self.param.R * T_av_n / self.param.F + RT_F_av_s = self.param.R * T_av_s / self.param.F + RT_F_av_p = self.param.R * T_av_p / self.param.F - param = self.param - L_n = param.n.L - L_p = param.p.L - L_x = param.L_x + L_n = self.param.n.L + L_p = self.param.p.L + L_x = self.param.L_x x_n = pybamm.standard_spatial_vars.x_n x_s = pybamm.standard_spatial_vars.x_s x_p = pybamm.standard_spatial_vars.x_p x_n_edge = pybamm.standard_spatial_vars.x_n_edge x_p_edge = pybamm.standard_spatial_vars.x_p_edge - chi_av = param.chi(c_e_av, T_av) + chi_av = self.param.chi(c_e_av, T_av) chi_av_n = pybamm.PrimaryBroadcast(chi_av, "negative electrode") chi_av_s = pybamm.PrimaryBroadcast(chi_av, "separator") chi_av_p = pybamm.PrimaryBroadcast(chi_av, "positive electrode") @@ -87,13 +85,13 @@ def get_coupled_variables(self, variables): # electrolyte potential indef_integral_n = pybamm.IndefiniteIntegral( - i_e_n_edge / (param.kappa_e(c_e_n, T_av_n) * tor_n), x_n + i_e_n_edge / (self.param.kappa_e(c_e_n, T_av_n) * tor_n), x_n ) indef_integral_s = pybamm.IndefiniteIntegral( - i_e_s_edge / (param.kappa_e(c_e_s, T_av_s) * tor_s), x_s + i_e_s_edge / (self.param.kappa_e(c_e_s, T_av_s) * tor_s), x_s ) indef_integral_p = pybamm.IndefiniteIntegral( - i_e_p_edge / (param.kappa_e(c_e_p, T_av_p) * tor_p), x_p + i_e_p_edge / (self.param.kappa_e(c_e_p, T_av_p) * tor_p), x_p ) integral_n = indef_integral_n diff --git a/src/pybamm/models/submodels/electrolyte_conductivity/leading_order_conductivity.py b/src/pybamm/models/submodels/electrolyte_conductivity/leading_order_conductivity.py index ad2a5b6486..42c7770c54 100644 --- a/src/pybamm/models/submodels/electrolyte_conductivity/leading_order_conductivity.py +++ b/src/pybamm/models/submodels/electrolyte_conductivity/leading_order_conductivity.py @@ -36,10 +36,9 @@ def get_coupled_variables(self, variables): i_boundary_cc = variables["Current collector current density [A.m-2]"] - param = self.param - L_n = param.n.L - L_p = param.p.L - L_x = param.L_x + L_n = self.param.n.L + L_p = self.param.p.L + L_x = self.param.L_x x_n = pybamm.standard_spatial_vars.x_n x_p = pybamm.standard_spatial_vars.x_p diff --git a/src/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 index cceb88f83e..fd32e6a83c 100644 --- a/src/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 @@ -47,7 +47,6 @@ def get_fundamental_variables(self): def get_coupled_variables(self, variables): Domain = self.domain.capitalize() - param = self.param if self.domain in ["negative", "positive"]: conductivity, sigma_eff = self._get_conductivities(variables) @@ -59,7 +58,7 @@ def get_coupled_variables(self, variables): T = variables[f"{Domain} electrode temperature [K]"] i_e = conductivity * ( - param.chiRT_over_Fc(c_e, T) * pybamm.grad(c_e) + self.param.chiRT_over_Fc(c_e, T) * pybamm.grad(c_e) + pybamm.grad(delta_phi) + i_boundary_cc / sigma_eff ) @@ -83,8 +82,8 @@ def get_coupled_variables(self, variables): tor_s = variables["Separator electrolyte transport efficiency"] T = variables["Separator temperature [K]"] - chiRT_over_Fc_e_s = param.chiRT_over_Fc(c_e_s, T) - kappa_s_eff = param.kappa_e(c_e_s, T) * tor_s + chiRT_over_Fc_e_s = self.param.chiRT_over_Fc(c_e_s, T) + kappa_s_eff = self.param.kappa_e(c_e_s, T) * tor_s phi_e = phi_e_n_s + pybamm.IndefiniteIntegral( chiRT_over_Fc_e_s * pybamm.grad(c_e_s) - i_boundary_cc / kappa_s_eff, @@ -124,7 +123,8 @@ def get_coupled_variables(self, variables): grad_left = -i_boundary_cc * pybamm.boundary_value(1 / sigma_eff, "left") grad_right = ( (i_boundary_cc / pybamm.boundary_value(conductivity, "right")) - - pybamm.boundary_value(param.chiRT_over_Fc(c_e, T), "right") * grad_c_e + - pybamm.boundary_value(self.param.chiRT_over_Fc(c_e, T), "right") + * grad_c_e - i_boundary_cc * pybamm.boundary_value(1 / sigma_eff, "right") ) @@ -132,7 +132,8 @@ def get_coupled_variables(self, variables): grad_c_e = pybamm.boundary_gradient(c_e, "left") grad_left = ( (i_boundary_cc / pybamm.boundary_value(conductivity, "left")) - - pybamm.boundary_value(param.chiRT_over_Fc(c_e, T), "left") * grad_c_e + - pybamm.boundary_value(self.param.chiRT_over_Fc(c_e, T), "left") + * grad_c_e - i_boundary_cc * pybamm.boundary_value(1 / sigma_eff, "left") ) grad_right = -i_boundary_cc * pybamm.boundary_value(1 / sigma_eff, "right") @@ -150,14 +151,13 @@ def get_coupled_variables(self, variables): def _get_conductivities(self, variables): Domain = self.domain.capitalize() - param = self.param tor_e = variables[f"{Domain} electrolyte transport efficiency"] tor_s = variables[f"{Domain} electrode transport efficiency"] c_e = variables[f"{Domain} electrolyte concentration [mol.m-3]"] T = variables[f"{Domain} electrode temperature [K]"] sigma = self.domain_param.sigma(T) - kappa_eff = param.kappa_e(c_e, T) * tor_e + kappa_eff = self.param.kappa_e(c_e, T) * tor_e sigma_eff = sigma * tor_s conductivity = kappa_eff / (1 + kappa_eff / sigma_eff) diff --git a/src/pybamm/models/submodels/electrolyte_diffusion/full_diffusion.py b/src/pybamm/models/submodels/electrolyte_diffusion/full_diffusion.py index 2fdd937966..06f95bc2f1 100644 --- a/src/pybamm/models/submodels/electrolyte_diffusion/full_diffusion.py +++ b/src/pybamm/models/submodels/electrolyte_diffusion/full_diffusion.py @@ -67,10 +67,8 @@ def get_coupled_variables(self, variables): v_box = variables["Volume-averaged velocity [m.s-1]"] T = variables["Cell temperature [K]"] - param = self.param - - N_e_diffusion = -tor * param.D_e(c_e, T) * pybamm.grad(c_e) - N_e_migration = param.t_plus(c_e, T) * i_e / param.F + N_e_diffusion = -tor * self.param.D_e(c_e, T) * pybamm.grad(c_e) + N_e_migration = self.param.t_plus(c_e, T) * i_e / self.param.F N_e_convection = c_e * v_box N_e = N_e_diffusion + N_e_migration + N_e_convection @@ -106,7 +104,6 @@ def set_initial_conditions(self, variables): } def set_boundary_conditions(self, variables): - param = self.param c_e = variables["Electrolyte concentration [mol.m-3]"] c_e_conc = variables["Electrolyte concentration concatenation [mol.m-3]"] T = variables["Cell temperature [K]"] @@ -118,7 +115,8 @@ def flux_bc(side): # assuming v_box = 0 for now return ( pybamm.boundary_value( - -(1 - param.t_plus(c_e, T)) / (tor * param.D_e(c_e, T) * param.F), + -(1 - self.param.t_plus(c_e, T)) + / (tor * self.param.D_e(c_e, T) * self.param.F), side, ) * i_boundary_cc diff --git a/src/pybamm/models/submodels/electrolyte_diffusion/leading_order_diffusion.py b/src/pybamm/models/submodels/electrolyte_diffusion/leading_order_diffusion.py index 8dedc28cf5..104b12e34e 100644 --- a/src/pybamm/models/submodels/electrolyte_diffusion/leading_order_diffusion.py +++ b/src/pybamm/models/submodels/electrolyte_diffusion/leading_order_diffusion.py @@ -52,8 +52,6 @@ def get_coupled_variables(self, variables): return variables def set_rhs(self, variables): - param = self.param - c_e_av = variables["X-averaged electrolyte concentration [mol.m-3]"] T_av = variables["X-averaged cell temperature [K]"] @@ -86,17 +84,24 @@ def set_rhs(self, variables): "reaction source terms [A.m-3]" ] source_terms = ( - param.n.L * (sum_s_j_n_0 - param.t_plus(c_e_av, T_av) * sum_a_j_n_0) - + param.p.L * (sum_s_j_p_0 - param.t_plus(c_e_av, T_av) * sum_a_j_p_0) - ) / param.F + self.param.n.L + * (sum_s_j_n_0 - self.param.t_plus(c_e_av, T_av) * sum_a_j_n_0) + + self.param.p.L + * (sum_s_j_p_0 - self.param.t_plus(c_e_av, T_av) * sum_a_j_p_0) + ) / self.param.F self.rhs = { c_e_av: 1 - / (param.n.L * eps_n_av + param.s.L * eps_s_av + param.p.L * eps_p_av) + / ( + self.param.n.L * eps_n_av + + self.param.s.L * eps_s_av + + self.param.p.L * eps_p_av + ) * ( source_terms - - c_e_av * (param.n.L * deps_n_dt_av + param.p.L * deps_p_dt_av) - - c_e_av * param.s.L * div_Vbox_s_av + - c_e_av + * (self.param.n.L * deps_n_dt_av + self.param.p.L * deps_p_dt_av) + - c_e_av * self.param.s.L * div_Vbox_s_av ) } diff --git a/src/pybamm/models/submodels/external_circuit/explicit_control_external_circuit.py b/src/pybamm/models/submodels/external_circuit/explicit_control_external_circuit.py index 760e9e2b20..6d1845c3b0 100644 --- a/src/pybamm/models/submodels/external_circuit/explicit_control_external_circuit.py +++ b/src/pybamm/models/submodels/external_circuit/explicit_control_external_circuit.py @@ -27,20 +27,18 @@ class ExplicitPowerControl(BaseModel): """External circuit with current set explicitly to hit target power.""" def get_coupled_variables(self, variables): - param = self.param - # Current is given as applied power divided by voltage V = variables["Voltage [V]"] P = pybamm.FunctionParameter("Power function [W]", {"Time [s]": pybamm.t}) I = P / V # Update derived variables - i_cell = I / (param.n_electrodes_parallel * param.A_cc) + i_cell = I / (self.param.n_electrodes_parallel * self.param.A_cc) variables = { "Total current density [A.m-2]": i_cell, "Current [A]": I, - "C-rate": I / param.Q, + "C-rate": I / self.param.Q, } return variables @@ -50,8 +48,6 @@ class ExplicitResistanceControl(BaseModel): """External circuit with current set explicitly to hit target resistance.""" def get_coupled_variables(self, variables): - param = self.param - # Current is given as applied voltage divided by resistance V = variables["Voltage [V]"] R = pybamm.FunctionParameter( @@ -60,12 +56,12 @@ def get_coupled_variables(self, variables): I = V / R # Update derived variables - i_cell = I / (param.n_electrodes_parallel * param.A_cc) + i_cell = I / (self.param.n_electrodes_parallel * self.param.A_cc) variables = { "Total current density [A.m-2]": i_cell, "Current [A]": I, - "C-rate": I / param.Q, + "C-rate": I / self.param.Q, } return variables diff --git a/src/pybamm/models/submodels/external_circuit/function_control_external_circuit.py b/src/pybamm/models/submodels/external_circuit/function_control_external_circuit.py index 60d6fb0e40..fcb18086da 100644 --- a/src/pybamm/models/submodels/external_circuit/function_control_external_circuit.py +++ b/src/pybamm/models/submodels/external_circuit/function_control_external_circuit.py @@ -29,9 +29,8 @@ def __init__(self, param, external_circuit_function, options, control="algebraic self.control = control def get_fundamental_variables(self): - param = self.param # Current is a variable - i_var = pybamm.Variable("Current variable [A]", scale=param.Q) + i_var = pybamm.Variable("Current variable [A]", scale=self.param.Q) if self.control in ["algebraic", "differential"]: I = i_var elif self.control == "differential with max": @@ -41,13 +40,13 @@ def get_fundamental_variables(self): I = pybamm.maximum(i_var, i_input) # Update derived variables - i_cell = I / (param.n_electrodes_parallel * param.A_cc) + i_cell = I / (self.param.n_electrodes_parallel * self.param.A_cc) variables = { "Current variable [A]": i_var, "Total current density [A.m-2]": i_cell, "Current [A]": I, - "C-rate": I / param.Q, + "C-rate": I / self.param.Q, } return variables diff --git a/src/pybamm/models/submodels/interface/base_interface.py b/src/pybamm/models/submodels/interface/base_interface.py index ab9b80eae0..0ad08d5454 100644 --- a/src/pybamm/models/submodels/interface/base_interface.py +++ b/src/pybamm/models/submodels/interface/base_interface.py @@ -61,7 +61,6 @@ def _get_exchange_current_density(self, variables): j0 : :class: `pybamm.Symbol` The exchange current density. """ - param = self.param phase_param = self.phase_param domain, Domain = self.domain_Domain phase_name = self.phase_name @@ -132,8 +131,8 @@ def _get_exchange_current_density(self, variables): elif self.reaction == "lithium metal plating": # compute T on the surface of the anode (interface with separator) T = pybamm.boundary_value(T, "right") - c_Li_metal = 1 / param.V_bar_Li - j0 = param.j0_Li_metal(c_e, c_Li_metal, T) + c_Li_metal = 1 / self.param.V_bar_Li + j0 = self.param.j0_Li_metal(c_e, c_Li_metal, T) elif self.reaction == "lead-acid main": # If variable was broadcast, take only the orphan @@ -150,7 +149,7 @@ def _get_exchange_current_density(self, variables): if self.domain == "negative": j0 = pybamm.Scalar(0) elif self.domain == "positive": - j0 = param.p.prim.j0_Ox(c_e, T) + j0 = self.param.p.prim.j0_Ox(c_e, T) return j0 diff --git a/src/pybamm/models/submodels/interface/kinetics/diffusion_limited.py b/src/pybamm/models/submodels/interface/kinetics/diffusion_limited.py index 08c2db2175..19b8dbea97 100644 --- a/src/pybamm/models/submodels/interface/kinetics/diffusion_limited.py +++ b/src/pybamm/models/submodels/interface/kinetics/diffusion_limited.py @@ -68,7 +68,6 @@ def get_coupled_variables(self, variables): return variables def _get_diffusion_limited_current_density(self, variables): - param = self.param if self.domain == "negative": if self.order == "leading": j_p = variables[ @@ -81,10 +80,10 @@ def _get_diffusion_limited_current_density(self, variables): c_ox_s = variables["Separator oxygen concentration [mol.m-3]"] N_ox_neg_sep_interface = ( -pybamm.boundary_value(tor_s, "left") - * param.D_ox + * self.param.D_ox * pybamm.boundary_gradient(c_ox_s, "left") ) - j = -N_ox_neg_sep_interface / -param.s_ox_Ox / param.n.L + j = -N_ox_neg_sep_interface / -self.param.s_ox_Ox / self.param.n.L return j diff --git a/src/pybamm/models/submodels/interface/kinetics/inverse_kinetics/inverse_butler_volmer.py b/src/pybamm/models/submodels/interface/kinetics/inverse_kinetics/inverse_butler_volmer.py index 959cb027c1..b49993afd8 100644 --- a/src/pybamm/models/submodels/interface/kinetics/inverse_kinetics/inverse_butler_volmer.py +++ b/src/pybamm/models/submodels/interface/kinetics/inverse_kinetics/inverse_butler_volmer.py @@ -93,8 +93,9 @@ def get_coupled_variables(self, variables): return variables def _get_overpotential(self, j, j0, ne, T, u): - param = self.param - return (2 * (param.R * T) / param.F / ne) * pybamm.arcsinh(j / (2 * j0 * u)) + return (2 * (self.param.R * T) / self.param.F / ne) * pybamm.arcsinh( + j / (2 * j0 * u) + ) class CurrentForInverseButlerVolmer(BaseInterface): diff --git a/src/pybamm/models/submodels/interface/sei/sei_growth.py b/src/pybamm/models/submodels/interface/sei/sei_growth.py index bed4b04952..2f506323ce 100644 --- a/src/pybamm/models/submodels/interface/sei/sei_growth.py +++ b/src/pybamm/models/submodels/interface/sei/sei_growth.py @@ -80,7 +80,6 @@ def get_fundamental_variables(self): return variables def get_coupled_variables(self, variables): - param = self.param phase_param = self.phase_param domain, Domain = self.domain_Domain SEI_option = getattr(getattr(self.options, domain), self.phase)["SEI"] @@ -118,7 +117,7 @@ def get_coupled_variables(self, variables): R_sei = phase_param.R_sei eta_SEI = delta_phi - phase_param.U_sei - j * L_sei * R_sei # Thermal prefactor for reaction, interstitial and EC models - F_RT = param.F / (param.R * T) + F_RT = self.param.F / (self.param.R * T) # Define alpha_SEI depending on whether it is symmetric or asymmetric. This # applies to "reaction limited" and "EC reaction limited" @@ -138,12 +137,12 @@ def get_coupled_variables(self, variables): elif SEI_option == "interstitial-diffusion limited": # Scott Marquis thesis (eq. 5.96) j_sei = -( - phase_param.D_li * phase_param.c_li_0 * param.F / L_sei_outer + phase_param.D_li * phase_param.c_li_0 * self.param.F / L_sei_outer ) * pybamm.exp(-F_RT * delta_phi) elif SEI_option == "solvent-diffusion limited": # Scott Marquis thesis (eq. 5.91) - j_sei = -phase_param.D_sol * phase_param.c_sol * param.F / L_sei_outer + j_sei = -phase_param.D_sol * phase_param.c_sol * self.param.F / L_sei_outer elif SEI_option.startswith("ec reaction limited"): # we have a linear system for j and c @@ -159,7 +158,7 @@ def get_coupled_variables(self, variables): k_exp = phase_param.k_sei * pybamm.exp(-alpha_SEI * F_RT * eta_SEI) L_over_D = L_sei / phase_param.D_ec c_0 = phase_param.c_ec_0 - j_sei = -param.F * c_0 * k_exp / (1 + L_over_D * k_exp) + j_sei = -self.param.F * c_0 * k_exp / (1 + L_over_D * k_exp) c_ec = c_0 / (1 + L_over_D * k_exp) # Get variables related to the concentration @@ -177,7 +176,9 @@ def get_coupled_variables(self, variables): inner_sei_proportion = phase_param.inner_sei_proportion # All SEI growth mechanisms assumed to have Arrhenius dependence - Arrhenius = pybamm.exp(phase_param.E_sei / param.R * (1 / param.T_ref - 1 / T)) + Arrhenius = pybamm.exp( + phase_param.E_sei / self.param.R * (1 / self.param.T_ref - 1 / T) + ) j_inner = inner_sei_proportion * Arrhenius * j_sei j_outer = (1 - inner_sei_proportion) * Arrhenius * j_sei @@ -192,7 +193,6 @@ def get_coupled_variables(self, variables): def set_rhs(self, variables): phase_param = self.phase_param - param = self.param domain, Domain = self.domain_Domain if self.reaction_loc == "x-average": @@ -249,10 +249,10 @@ def set_rhs(self, variables): # V_bar / a converts from SEI moles to SEI thickness # V_bar * j_sei / (F * z_sei) is the rate of SEI thickness change dLdt_SEI_inner = ( - phase_param.V_bar_inner * j_inner / (param.F * phase_param.z_sei) + phase_param.V_bar_inner * j_inner / (self.param.F * phase_param.z_sei) ) dLdt_SEI_outer = ( - phase_param.V_bar_outer * j_outer / (param.F * phase_param.z_sei) + phase_param.V_bar_outer * j_outer / (self.param.F * phase_param.z_sei) ) # we have to add the spreading rate to account for cracking diff --git a/src/pybamm/models/submodels/oxygen_diffusion/full_oxygen_diffusion.py b/src/pybamm/models/submodels/oxygen_diffusion/full_oxygen_diffusion.py index c69312e342..7ecad6fa4c 100644 --- a/src/pybamm/models/submodels/oxygen_diffusion/full_oxygen_diffusion.py +++ b/src/pybamm/models/submodels/oxygen_diffusion/full_oxygen_diffusion.py @@ -58,9 +58,7 @@ def get_coupled_variables(self, variables): # TODO: allow charge and convection? v_box = pybamm.Scalar(0) - param = self.param - - N_ox_diffusion = -tor * param.D_ox * pybamm.grad(c_ox) + N_ox_diffusion = -tor * self.param.D_ox * pybamm.grad(c_ox) N_ox = N_ox_diffusion + c_ox * v_box # Flux in the negative electrode is zero @@ -73,8 +71,6 @@ def get_coupled_variables(self, variables): return variables def set_rhs(self, variables): - param = self.param - eps_s = variables["Separator porosity"] eps_p = variables["Positive electrode porosity"] eps = pybamm.concatenation(eps_s, eps_p) @@ -93,12 +89,12 @@ def set_rhs(self, variables): ] source_terms = pybamm.concatenation( pybamm.FullBroadcast(0, "separator", "current collector"), - param.s_ox_Ox * a_j_ox, + self.param.s_ox_Ox * a_j_ox, ) self.rhs = { c_ox: (1 / eps) - * (-pybamm.div(N_ox) + source_terms / param.F - c_ox * deps_dt) + * (-pybamm.div(N_ox) + source_terms / self.param.F - c_ox * deps_dt) } def set_boundary_conditions(self, variables): diff --git a/src/pybamm/models/submodels/oxygen_diffusion/leading_oxygen_diffusion.py b/src/pybamm/models/submodels/oxygen_diffusion/leading_oxygen_diffusion.py index 056c7f6715..bdc064c340 100644 --- a/src/pybamm/models/submodels/oxygen_diffusion/leading_oxygen_diffusion.py +++ b/src/pybamm/models/submodels/oxygen_diffusion/leading_oxygen_diffusion.py @@ -41,8 +41,6 @@ def get_coupled_variables(self, variables): return variables def set_rhs(self, variables): - param = self.param - c_ox_av = variables["X-averaged oxygen concentration [mol.m-3]"] eps_n_av = variables["X-averaged negative electrode porosity"] @@ -62,16 +60,21 @@ def set_rhs(self, variables): ] source_terms = ( - param.n.L * param.s_ox_Ox * a_j_ox_n_av - + param.p.L * param.s_ox_Ox * a_j_ox_p_av + self.param.n.L * self.param.s_ox_Ox * a_j_ox_n_av + + self.param.p.L * self.param.s_ox_Ox * a_j_ox_p_av ) self.rhs = { c_ox_av: 1 - / (param.n.L * eps_n_av + param.s.L * eps_s_av + param.p.L * eps_p_av) + / ( + self.param.n.L * eps_n_av + + self.param.s.L * eps_s_av + + self.param.p.L * eps_p_av + ) * ( - source_terms / param.F - - c_ox_av * (param.n.L * deps_n_dt_av + param.p.L * deps_p_dt_av) + source_terms / self.param.F + - c_ox_av + * (self.param.n.L * deps_n_dt_av + self.param.p.L * deps_p_dt_av) ) } diff --git a/src/pybamm/models/submodels/particle/base_particle.py b/src/pybamm/models/submodels/particle/base_particle.py index fe37d2ff2e..b774e58a0c 100644 --- a/src/pybamm/models/submodels/particle/base_particle.py +++ b/src/pybamm/models/submodels/particle/base_particle.py @@ -28,7 +28,6 @@ def __init__(self, param, domain, options, phase="primary"): self.size_distribution = domain_options["particle size"] == "distribution" def _get_effective_diffusivity(self, c, T, current): - param = self.param domain, Domain = self.domain_Domain domain_param = self.domain_param phase_param = self.phase_param @@ -60,7 +59,7 @@ def _get_effective_diffusivity(self, c, T, current): Omega = pybamm.r_average(phase_param.Omega(sto, T)) E = pybamm.r_average(phase_param.E(sto, T)) nu = phase_param.nu - theta_M = Omega / (param.R * T) * (2 * Omega * E) / (9 * (1 - nu)) + theta_M = Omega / (self.param.R * T) * (2 * Omega * E) / (9 * (1 - nu)) stress_factor = 1 + theta_M * (c - domain_param.c_0) else: stress_factor = 1 diff --git a/src/pybamm/models/submodels/particle/fickian_diffusion.py b/src/pybamm/models/submodels/particle/fickian_diffusion.py index 31c5e6be6c..634e2ce730 100644 --- a/src/pybamm/models/submodels/particle/fickian_diffusion.py +++ b/src/pybamm/models/submodels/particle/fickian_diffusion.py @@ -130,7 +130,6 @@ def get_fundamental_variables(self): def get_coupled_variables(self, variables): domain, Domain = self.domain_Domain phase_name = self.phase_name - param = self.param if self.size_distribution is False: if self.x_average is False: @@ -208,7 +207,7 @@ def get_coupled_variables(self, variables): * pybamm.div(N_s), f"{Domain} {phase_name}particle bc [mol.m-4]": -j * R_nondim - / param.F + / self.param.F / pybamm.surf(D_eff), } ) diff --git a/src/pybamm/models/submodels/particle/msmr_diffusion.py b/src/pybamm/models/submodels/particle/msmr_diffusion.py index fb712dcdef..8967116ce9 100644 --- a/src/pybamm/models/submodels/particle/msmr_diffusion.py +++ b/src/pybamm/models/submodels/particle/msmr_diffusion.py @@ -136,7 +136,6 @@ def get_fundamental_variables(self): def get_coupled_variables(self, variables): domain, Domain = self.domain_Domain phase_name = self.phase_name - param = self.param if self.size_distribution is False: if self.x_average is False: @@ -236,7 +235,7 @@ def get_coupled_variables(self, variables): / dxdU, f"{Domain} {phase_name}particle bc [V.m-1]": j * R_nondim - / param.F + / self.param.F / pybamm.surf(c_max * x * (1 - x) * f * D_eff), } ) diff --git a/src/pybamm/models/submodels/particle/x_averaged_polynomial_profile.py b/src/pybamm/models/submodels/particle/x_averaged_polynomial_profile.py index 8b4b7ffe7c..9dccc0a6c4 100644 --- a/src/pybamm/models/submodels/particle/x_averaged_polynomial_profile.py +++ b/src/pybamm/models/submodels/particle/x_averaged_polynomial_profile.py @@ -97,7 +97,6 @@ def get_fundamental_variables(self): def get_coupled_variables(self, variables): domain = self.domain - param = self.param c_s_av = variables[f"Average {domain} particle concentration [mol.m-3]"] T_av = variables[f"X-averaged {domain} electrode temperature [K]"] @@ -135,7 +134,7 @@ def get_coupled_variables(self, variables): # an extra algebraic equation to solve. For now, using the average c is an # ok approximation and means the SPM(e) still gives a system of ODEs rather # than DAEs. - c_s_surf_xav = c_s_av - (j_xav * R / 5 / param.F / D_eff_av) + c_s_surf_xav = c_s_av - (j_xav * R / 5 / self.param.F / D_eff_av) elif self.name == "quartic profile": # The surface concentration is computed from the average concentration, # the average concentration gradient and the boundary flux (see notes @@ -144,7 +143,9 @@ def get_coupled_variables(self, variables): q_s_av = variables[ f"Average {domain} particle concentration gradient [mol.m-4]" ] - c_s_surf_xav = c_s_av + R / 35 * (8 * q_s_av - (j_xav / param.F / D_eff_av)) + c_s_surf_xav = c_s_av + R / 35 * ( + 8 * q_s_av - (j_xav / self.param.F / D_eff_av) + ) # Set concentration depending on polynomial order # Since c_s_xav doesn't depend on x, we need to define a spatial @@ -223,7 +224,6 @@ def set_rhs(self, variables): # using this model with 2D current collectors with the finite element # method (see #1399) domain = self.domain - param = self.param if self.size_distribution is False: c_s_av = variables[f"Average {domain} particle concentration [mol.m-3]"] @@ -243,7 +243,7 @@ def set_rhs(self, variables): # eq 15 of Subramanian2005 # equivalent to dcdt = -i_cc / (eps * F * L) - dcdt = -3 * j_xav / param.F / R + dcdt = -3 * j_xav / self.param.F / R if self.size_distribution is False: self.rhs = {c_s_av: pybamm.source(dcdt, c_s_av)} @@ -262,7 +262,7 @@ def set_rhs(self, variables): # eq 30 of Subramanian2005 dqdt = ( -30 * pybamm.surf(D_eff_xav) * q_s_av / R**2 - - 45 / 2 * j_xav / param.F / R**2 + - 45 / 2 * j_xav / self.param.F / R**2 ) self.rhs[q_s_av] = pybamm.source(dqdt, q_s_av) diff --git a/src/pybamm/models/submodels/porosity/reaction_driven_porosity_ode.py b/src/pybamm/models/submodels/porosity/reaction_driven_porosity_ode.py index f82a73ae68..8675842ebe 100644 --- a/src/pybamm/models/submodels/porosity/reaction_driven_porosity_ode.py +++ b/src/pybamm/models/submodels/porosity/reaction_driven_porosity_ode.py @@ -46,8 +46,6 @@ def get_fundamental_variables(self): return variables def get_coupled_variables(self, variables): - param = self.param - depsdt_dict = {} for domain in self.options.whole_cell_domains: domain_param = self.param.domain_params[domain.split()[0]] @@ -59,14 +57,14 @@ def get_coupled_variables(self, variables): f"X-averaged {domain} volumetric " "interfacial current density [A.m-3]" ] - depsdt_k_av = domain_param.DeltaVsurf * a_j_k_av / param.F + depsdt_k_av = domain_param.DeltaVsurf * a_j_k_av / self.param.F depsdt_k = pybamm.PrimaryBroadcast(depsdt_k_av, domain) else: Domain = domain.capitalize() a_j_k = variables[ f"{Domain} volumetric interfacial current density [A.m-3]" ] - depsdt_k = domain_param.DeltaVsurf * a_j_k / param.F + depsdt_k = domain_param.DeltaVsurf * a_j_k / self.param.F depsdt_dict[domain] = depsdt_k variables.update(self._get_standard_porosity_change_variables(depsdt_dict)) diff --git a/src/pybamm/models/submodels/thermal/base_thermal.py b/src/pybamm/models/submodels/thermal/base_thermal.py index c5ebbc7dbd..42d90f1bcf 100644 --- a/src/pybamm/models/submodels/thermal/base_thermal.py +++ b/src/pybamm/models/submodels/thermal/base_thermal.py @@ -34,7 +34,6 @@ def _get_standard_fundamental_variables(self, T_dict): For more information about this method in general, see :meth:`pybamm.base_submodel._get_standard_fundamental_variables` """ - param = self.param # The variable T is the concatenation of the temperature in the middle domains # (e.g. negative electrode, separator and positive electrode for a full cell), @@ -46,7 +45,7 @@ def _get_standard_fundamental_variables(self, T_dict): # (y, z) only and time y = pybamm.standard_spatial_vars.y z = pybamm.standard_spatial_vars.z - T_amb = param.T_amb(y, z, pybamm.t) + T_amb = self.param.T_amb(y, z, pybamm.t) T_amb_av = self._yz_average(T_amb) variables = { @@ -69,8 +68,6 @@ def _get_standard_fundamental_variables(self, T_dict): return variables def _get_standard_coupled_variables(self, variables): - param = self.param - # Ohmic heating in solid i_s_p = variables["Positive electrode current density [A.m-2]"] phi_s_p = variables["Positive electrode potential [V]"] @@ -78,7 +75,7 @@ def _get_standard_coupled_variables(self, variables): if self.options.electrode_types["negative"] == "planar": i_boundary_cc = variables["Current collector current density [A.m-2]"] T_n = variables["Negative electrode temperature [K]"] - Q_ohm_s_n = i_boundary_cc**2 / param.n.sigma(T_n) + Q_ohm_s_n = i_boundary_cc**2 / self.param.n.sigma(T_n) else: i_s_n = variables["Negative electrode current density [A.m-2]"] phi_s_n = variables["Negative electrode potential [V]"] @@ -199,11 +196,11 @@ def _get_standard_coupled_variables(self, variables): # Compute the integrated heat source per unit simulated electrode-pair area # in W.m-2. Note: this can still be a function of y and z for # higher-dimensional pouch cell models - Q_ohm_Wm2 = Q_ohm_av * param.L - Q_rxn_Wm2 = Q_rxn_av * param.L - Q_rev_Wm2 = Q_rev_av * param.L - Q_mix_Wm2 = Q_mix_av * param.L - Q_Wm2 = Q_av * param.L + Q_ohm_Wm2 = Q_ohm_av * self.param.L + Q_rxn_Wm2 = Q_rxn_av * self.param.L + Q_rev_Wm2 = Q_rev_av * self.param.L + Q_mix_Wm2 = Q_mix_av * self.param.L + Q_Wm2 = Q_av * self.param.L # Now average over the electrode height and width Q_ohm_Wm2_av = self._yz_average(Q_ohm_Wm2) @@ -216,8 +213,8 @@ def _get_standard_coupled_variables(self, variables): # the product of electrode height * electrode width * electrode stack thickness # Note: we multiply by the number of electrode pairs, since the Q_xx_Wm2_av # variables are per electrode pair - n_elec = param.n_electrodes_parallel - A = param.L_y * param.L_z # *modelled* electrode area + n_elec = self.param.n_electrodes_parallel + A = self.param.L_y * self.param.L_z # *modelled* electrode area Q_ohm_W = Q_ohm_Wm2_av * n_elec * A Q_rxn_W = Q_rxn_Wm2_av * n_elec * A Q_rev_W = Q_rev_Wm2_av * n_elec * A @@ -226,7 +223,7 @@ def _get_standard_coupled_variables(self, variables): # Compute volume-averaged heat source terms over the *entire cell volume*, not # the product of electrode height * electrode width * electrode stack thickness - V = param.V_cell # *actual* cell volume + V = self.param.V_cell # *actual* cell volume Q_ohm_vol_av = Q_ohm_W / V Q_rxn_vol_av = Q_rxn_W / V Q_rev_vol_av = Q_rev_W / V @@ -235,7 +232,7 @@ def _get_standard_coupled_variables(self, variables): # Effective heat capacity T_vol_av = variables["Volume-averaged cell temperature [K]"] - rho_c_p_eff_av = param.rho_c_p_eff(T_vol_av) + rho_c_p_eff_av = self.param.rho_c_p_eff(T_vol_av) variables.update( { @@ -314,7 +311,6 @@ def _current_collector_heating(self, variables): def _heat_of_mixing(self, variables): """Compute heat of mixing source terms.""" - param = self.param if self.options["heat of mixing"] == "true": F = pybamm.constants.F.value @@ -339,8 +335,10 @@ def _heat_of_mixing(self, variables): T_n = variables["Negative electrode temperature [K]"] T_n_part = pybamm.PrimaryBroadcast(T_n, ["negative particle"]) dc_n_dr2 = pybamm.inner(pybamm.grad(c_n), pybamm.grad(c_n)) - D_n = param.n.prim.D(c_n, T_n_part) - dUeq_n = param.n.prim.U(c_n / param.n.prim.c_max, T_n_part).diff(c_n) + D_n = self.param.n.prim.D(c_n, T_n_part) + dUeq_n = self.param.n.prim.U( + c_n / self.param.n.prim.c_max, T_n_part + ).diff(c_n) integrand_r_n = D_n * dc_n_dr2 * dUeq_n integration_variable_r_n = [ pybamm.SpatialVariable("r", domain=integrand_r_n.domain) @@ -360,8 +358,10 @@ def _heat_of_mixing(self, variables): T_p = variables["Positive electrode temperature [K]"] T_p_part = pybamm.PrimaryBroadcast(T_p, ["positive particle"]) dc_p_dr2 = pybamm.inner(pybamm.grad(c_p), pybamm.grad(c_p)) - D_p = param.p.prim.D(c_p, T_p_part) - dUeq_p = param.p.prim.U(c_p / param.p.prim.c_max, T_p_part).diff(c_p) + D_p = self.param.p.prim.D(c_p, T_p_part) + dUeq_p = self.param.p.prim.U(c_p / self.param.p.prim.c_max, T_p_part).diff( + c_p + ) integrand_r_p = D_p * dc_p_dr2 * dUeq_p integration_variable_r_p = [ pybamm.SpatialVariable("r", domain=integrand_r_p.domain) 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 fb026a9a0a..5c29ef0a9f 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 @@ -86,24 +86,23 @@ def set_rhs(self, variables): } def set_boundary_conditions(self, variables): - param = self.param T_surf = variables["Surface temperature [K]"] T_av = variables["X-averaged cell temperature [K]"] # Find tab locations (top vs bottom) - L_y = param.L_y - L_z = param.L_z - neg_tab_z = param.n.centre_z_tab - pos_tab_z = param.p.centre_z_tab + L_y = self.param.L_y + L_z = self.param.L_z + neg_tab_z = self.param.n.centre_z_tab + pos_tab_z = self.param.p.centre_z_tab neg_tab_top_bool = pybamm.Equality(neg_tab_z, L_z) neg_tab_bottom_bool = pybamm.Equality(neg_tab_z, 0) pos_tab_top_bool = pybamm.Equality(pos_tab_z, L_z) pos_tab_bottom_bool = pybamm.Equality(pos_tab_z, 0) # Calculate tab vs non-tab area on top and bottom - neg_tab_area = param.n.L_tab * param.n.L_cc - pos_tab_area = param.p.L_tab * param.p.L_cc - total_area = param.L * param.L_y + neg_tab_area = self.param.n.L_tab * self.param.n.L_cc + pos_tab_area = self.param.p.L_tab * self.param.p.L_cc + total_area = self.param.L * self.param.L_y non_tab_top_area = ( total_area - neg_tab_area * neg_tab_top_bool @@ -118,10 +117,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_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_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_top = -self.param.h_edge(L_y / 2, L_z) * (T_av - T_surf) + q_edge_bottom = -self.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 @@ -136,7 +135,7 @@ def set_boundary_conditions(self, variables): # just use left and right for clarity # left = bottom of cell (z=0) # right = top of cell (z=L_z) - lambda_eff = param.lambda_eff(T_av) + lambda_eff = self.param.lambda_eff(T_av) self.boundary_conditions = { T_av: { "left": ( From ab0020ae5bfb70bb24ae369eb8f1d207923cfd66 Mon Sep 17 00:00:00 2001 From: Marc Berliner <34451391+MarcBerliner@users.noreply.github.com> Date: Thu, 10 Oct 2024 13:37:21 -0400 Subject: [PATCH 13/24] More accurate `QuickPlot`s with Hermite interpolation (#4483) * Update CHANGELOG.md accurate quickplots * evenly sample sub-solutions * lowercase variable * move `_solver_args` inside class --------- Co-authored-by: Eric G. Kratz --- CHANGELOG.md | 1 + src/pybamm/plotting/quick_plot.py | 22 +++++++++++++++ tests/unit/test_plotting/test_quick_plot.py | 30 +++++++++++++-------- 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84f01d3797..a03ec39715 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Features +- Improved `QuickPlot` accuracy for simulations with Hermite interpolation. ([#4483](https://github.com/pybamm-team/PyBaMM/pull/4483)) - Added Hermite interpolation to the (`IDAKLUSolver`) that improves the accuracy and performance of post-processing variables. ([#4464](https://github.com/pybamm-team/PyBaMM/pull/4464)) - Added `BasicDFN` model for sodium-ion batteries ([#4451](https://github.com/pybamm-team/PyBaMM/pull/4451)) - Added sensitivity calculation support for `pybamm.Simulation` and `pybamm.Experiment` ([#4415](https://github.com/pybamm-team/PyBaMM/pull/4415)) diff --git a/src/pybamm/plotting/quick_plot.py b/src/pybamm/plotting/quick_plot.py index cddce58d77..babfd2e761 100644 --- a/src/pybamm/plotting/quick_plot.py +++ b/src/pybamm/plotting/quick_plot.py @@ -84,6 +84,9 @@ class QuickPlot: variable_limits : str or dict of str, optional How to set the axis limits (for 0D or 1D variables) or colorbar limits (for 2D variables). Options are: + n_t_linear: int, optional + The number of linearly spaced time points added to the t axis for each sub-solution. + Note: this is only used if the solution has hermite interpolation enabled. - "fixed" (default): keep all axes fixes so that all data is visible - "tight": make axes tight to plot at each time @@ -105,6 +108,7 @@ def __init__( time_unit=None, spatial_unit="um", variable_limits="fixed", + n_t_linear=100, ): solutions = self.preprocess_solutions(solutions) @@ -169,6 +173,24 @@ def __init__( min_t = np.min([t[0] for t in self.ts_seconds]) max_t = np.max([t[-1] for t in self.ts_seconds]) + hermite_interp = all(sol.hermite_interpolation for sol in solutions) + + def t_sample(sol): + if hermite_interp and n_t_linear > 2: + # Linearly spaced time points + t_linspace = np.linspace(sol.t[0], sol.t[-1], n_t_linear + 2)[1:-1] + t_plot = np.union1d(sol.t, t_linspace) + else: + t_plot = sol.t + return t_plot + + ts_seconds = [] + for sol in solutions: + # Sample time points for each sub-solution + t_sol = [t_sample(sub_sol) for sub_sol in sol.sub_solutions] + ts_seconds.append(np.concatenate(t_sol)) + self.ts_seconds = ts_seconds + # Set timescale if time_unit is None: # defaults depend on how long the simulation is diff --git a/tests/unit/test_plotting/test_quick_plot.py b/tests/unit/test_plotting/test_quick_plot.py index d5d994117d..188a725680 100644 --- a/tests/unit/test_plotting/test_quick_plot.py +++ b/tests/unit/test_plotting/test_quick_plot.py @@ -7,7 +7,12 @@ class TestQuickPlot: - def test_simple_ode_model(self): + _solver_args = [pybamm.CasadiSolver()] + if pybamm.has_idaklu(): + _solver_args.append(pybamm.IDAKLUSolver()) + + @pytest.mark.parametrize("solver", _solver_args) + def test_simple_ode_model(self, solver): model = pybamm.lithium_ion.BaseModel(name="Simple ODE Model") whole_cell = ["negative electrode", "separator", "positive electrode"] @@ -48,9 +53,6 @@ def test_simple_ode_model(self): "NaN variable": pybamm.Scalar(np.nan), } - # ODEs only (don't use Jacobian) - model.use_jacobian = False - # Process and solve geometry = model.default_geometry param = model.default_parameter_values @@ -59,7 +61,6 @@ def test_simple_ode_model(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) - solver = model.default_solver t_eval = np.linspace(0, 2, 100) solution = solver.solve(model, t_eval) quick_plot = pybamm.QuickPlot( @@ -142,6 +143,13 @@ def test_simple_ode_model(self): assert quick_plot.n_rows == 2 assert quick_plot.n_cols == 1 + if solution.hermite_interpolation: + t_plot = np.union1d( + solution.t, np.linspace(solution.t[0], solution.t[-1], 100 + 2)[1:-1] + ) + else: + t_plot = t_eval + # Test different time units quick_plot = pybamm.QuickPlot(solution, ["a"]) assert quick_plot.time_scaling_factor == 1 @@ -149,28 +157,28 @@ def test_simple_ode_model(self): quick_plot.plot(0) assert quick_plot.time_scaling_factor == 1 np.testing.assert_array_almost_equal( - quick_plot.plots[("a",)][0][0].get_xdata(), t_eval + quick_plot.plots[("a",)][0][0].get_xdata(), t_plot ) np.testing.assert_array_almost_equal( - quick_plot.plots[("a",)][0][0].get_ydata(), 0.2 * t_eval + quick_plot.plots[("a",)][0][0].get_ydata(), 0.2 * t_plot ) quick_plot = pybamm.QuickPlot(solution, ["a"], time_unit="minutes") quick_plot.plot(0) assert quick_plot.time_scaling_factor == 60 np.testing.assert_array_almost_equal( - quick_plot.plots[("a",)][0][0].get_xdata(), t_eval / 60 + quick_plot.plots[("a",)][0][0].get_xdata(), t_plot / 60 ) np.testing.assert_array_almost_equal( - quick_plot.plots[("a",)][0][0].get_ydata(), 0.2 * t_eval + quick_plot.plots[("a",)][0][0].get_ydata(), 0.2 * t_plot ) quick_plot = pybamm.QuickPlot(solution, ["a"], time_unit="hours") quick_plot.plot(0) assert quick_plot.time_scaling_factor == 3600 np.testing.assert_array_almost_equal( - quick_plot.plots[("a",)][0][0].get_xdata(), t_eval / 3600 + quick_plot.plots[("a",)][0][0].get_xdata(), t_plot / 3600 ) np.testing.assert_array_almost_equal( - quick_plot.plots[("a",)][0][0].get_ydata(), 0.2 * t_eval + quick_plot.plots[("a",)][0][0].get_ydata(), 0.2 * t_plot ) with pytest.raises(ValueError, match="time unit"): pybamm.QuickPlot(solution, ["a"], time_unit="bad unit") From 7ea74f366279b2d9d8511e57e7bdc58b6b2e67a6 Mon Sep 17 00:00:00 2001 From: Medha Bhardwaj <143182673+medha-14@users.noreply.github.com> Date: Thu, 10 Oct 2024 23:40:00 +0530 Subject: [PATCH 14/24] Adds doc strings for attributes in `base_model` and `base_submodel` (#4480) * added docstring for some attributes * documented all public attribtes in base_model * documented all public attributes in base_submodel * minor changes in base_submodel * doc test fixes * handled warnings --------- Co-authored-by: Eric G. Kratz --- src/pybamm/models/base_model.py | 73 ++++++++++++++------ src/pybamm/models/submodels/base_submodel.py | 31 ++++++--- 2 files changed, 73 insertions(+), 31 deletions(-) diff --git a/src/pybamm/models/base_model.py b/src/pybamm/models/base_model.py index 989465e375..f6f47acc55 100644 --- a/src/pybamm/models/base_model.py +++ b/src/pybamm/models/base_model.py @@ -19,29 +19,27 @@ class BaseModel: Attributes ---------- name: str - A string giving the name of the model. + A string representing the name of the model. submodels: dict A dictionary of submodels that the model is composed of. - boundary_conditions: dict - A dictionary that maps expressions (variables) to expressions that represent - the boundary conditions. - variables: dict - A dictionary that maps strings to expressions that represent - the useful variables. - use_jacobian : bool + use_jacobian: bool Whether to use the Jacobian when solving the model (default is True). - convert_to_format : str - Whether to convert the expression trees representing the rhs and - algebraic equations, Jacobain (if using) and events into a different format: + convert_to_format: str + Specifies the format to convert the expression trees representing the RHS, + algebraic equations, Jacobian, and events. + Options are: - - None: keep PyBaMM expression tree structure. - - "python": convert into pure python code that will calculate the result of \ - 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 + - None: retain PyBaMM expression tree structure. + - "python": convert to Python code for evaluating `evaluate(t, y)` on expressions. + - "casadi": convert to CasADi expression tree for Jacobian calculation. + - "jax": convert to JAX expression tree. Default is "casadi". + is_discretised: bool + Indicates whether the model has been discretised (default is False). + y_slices: None or list + Slices of the concatenated state vector after discretisation, used to track + different submodels in the full concatenated solution vector. """ def __init__(self, name="Unnamed model"): @@ -144,6 +142,8 @@ def name(self, value): @property def rhs(self): + """Returns a dictionary mapping expressions (variables) to expressions that represent + the right-hand side (RHS) of the model's differential equations.""" return self._rhs @rhs.setter @@ -152,6 +152,8 @@ def rhs(self, rhs): @property def algebraic(self): + """Returns a dictionary mapping expressions (variables) to expressions that represent + the algebraic equations of the model.""" return self._algebraic @algebraic.setter @@ -160,6 +162,8 @@ def algebraic(self, algebraic): @property def initial_conditions(self): + """Returns a dictionary mapping expressions (variables) to expressions that represent + the initial conditions for the state variables.""" return self._initial_conditions @initial_conditions.setter @@ -170,6 +174,8 @@ def initial_conditions(self, initial_conditions): @property def boundary_conditions(self): + """Returns a dictionary mapping expressions (variables) to expressions representing + the boundary conditions of the model.""" return self._boundary_conditions @boundary_conditions.setter @@ -178,6 +184,7 @@ def boundary_conditions(self, boundary_conditions): @property def variables(self): + """Returns a dictionary mapping strings to expressions representing the model's useful variables.""" return self._variables @variables.setter @@ -200,9 +207,7 @@ def variable_names(self): @property def variables_and_events(self): - """ - Returns variables and events in a single dictionary - """ + """Returns a dictionary containing both models variables and events.""" try: return self._variables_and_events except AttributeError: @@ -214,6 +219,8 @@ def variables_and_events(self): @property def events(self): + """Returns a dictionary mapping expressions (variables) to expressions that represent + the initial conditions for the state variables.""" return self._events @events.setter @@ -222,6 +229,7 @@ def events(self, events): @property def concatenated_rhs(self): + """Returns the concatenated right-hand side (RHS) expressions for the model after discretisation.""" return self._concatenated_rhs @concatenated_rhs.setter @@ -230,6 +238,7 @@ def concatenated_rhs(self, concatenated_rhs): @property def concatenated_algebraic(self): + """Returns the concatenated algebraic equations for the model after discretisation.""" return self._concatenated_algebraic @concatenated_algebraic.setter @@ -238,6 +247,8 @@ def concatenated_algebraic(self, concatenated_algebraic): @property def concatenated_initial_conditions(self): + """Returns the initial conditions for all variables after discretization, providing the + initial values for the state variables.""" return self._concatenated_initial_conditions @concatenated_initial_conditions.setter @@ -246,6 +257,7 @@ def concatenated_initial_conditions(self, concatenated_initial_conditions): @property def mass_matrix(self): + """Returns the mass matrix for the system of differential equations after discretisation.""" return self._mass_matrix @mass_matrix.setter @@ -254,6 +266,7 @@ def mass_matrix(self, mass_matrix): @property def mass_matrix_inv(self): + """Returns the inverse of the mass matrix for the differential equations after discretisation.""" return self._mass_matrix_inv @mass_matrix_inv.setter @@ -262,6 +275,7 @@ def mass_matrix_inv(self, mass_matrix_inv): @property def jacobian(self): + """Returns the Jacobian matrix for the model, computed automatically if `use_jacobian` is True.""" return self._jacobian @jacobian.setter @@ -270,6 +284,8 @@ def jacobian(self, jacobian): @property def jacobian_rhs(self): + """Returns the Jacobian matrix for the right-hand side (RHS) part of the model, computed + if `use_jacobian` is True.""" return self._jacobian_rhs @jacobian_rhs.setter @@ -278,6 +294,8 @@ def jacobian_rhs(self, jacobian_rhs): @property def jacobian_algebraic(self): + """Returns the Jacobian matrix for the algebraic part of the model, computed automatically + during solver setup if `use_jacobian` is True.""" return self._jacobian_algebraic @jacobian_algebraic.setter @@ -286,6 +304,7 @@ def jacobian_algebraic(self, jacobian_algebraic): @property def param(self): + """Returns a dictionary to store parameter values for the model.""" return self._param @param.setter @@ -294,6 +313,7 @@ def param(self, values): @property def options(self): + """Returns the model options dictionary that allows customization of the model's behavior.""" return self._options @options.setter @@ -326,27 +346,32 @@ def length_scales(self, values): @property def geometry(self): + """Returns the geometry of the model.""" return self._geometry @property def default_var_pts(self): + """Returns a dictionary of the default variable points for the model, which is empty by default.""" return {} @property def default_geometry(self): + """Returns a dictionary of the default geometry for the model, which is empty by default.""" return {} @property def default_submesh_types(self): + """Returns a dictionary of the default submesh types for the model, which is empty by default.""" return {} @property def default_spatial_methods(self): + """Returns a dictionary of the default spatial methods for the model, which is empty by default.""" return {} @property def default_solver(self): - """Return default solver based on whether model is ODE/DAE or algebraic""" + """Returns the default solver for the model, based on whether it is an ODE/DAE or algebraic model.""" if len(self.rhs) == 0 and len(self.algebraic) != 0: return pybamm.CasadiAlgebraicSolver() else: @@ -354,15 +379,17 @@ def default_solver(self): @property def default_quick_plot_variables(self): + """Returns the default variables for quick plotting (None by default).""" return None @property def default_parameter_values(self): + """Returns the default parameter values for the model (an empty set of parameters by default).""" return pybamm.ParameterValues({}) @property def parameters(self): - """Returns all the parameters in the model""" + """Returns a list of all parameter symbols used in the model.""" self._parameters = self._find_symbols( (pybamm.Parameter, pybamm.InputParameter, pybamm.FunctionParameter) ) @@ -370,7 +397,7 @@ def parameters(self): @property def input_parameters(self): - """Returns all the input parameters in the model""" + """Returns a list of all input parameter symbols used in the model.""" if self._input_parameters is None: self._input_parameters = self._find_symbols(pybamm.InputParameter) return self._input_parameters diff --git a/src/pybamm/models/submodels/base_submodel.py b/src/pybamm/models/submodels/base_submodel.py index d5e313e153..6b83d1f292 100644 --- a/src/pybamm/models/submodels/base_submodel.py +++ b/src/pybamm/models/submodels/base_submodel.py @@ -28,14 +28,28 @@ class BaseSubModel(pybamm.BaseModel): Attributes ---------- - param: parameter class - The model parameter symbols - boundary_conditions: dict - A dictionary that maps expressions (variables) to expressions that represent - the boundary conditions - variables: dict - A dictionary that maps strings to expressions that represent - the useful variables + param : parameter class + The model parameter symbols. + domain : str + The domain of the submodel, could be either 'Negative', 'Positive', 'Separator', or None. + name : str + The name of the submodel. + external : bool + A boolean flag indicating whether the variables defined by the submodel will be + provided externally by the user. Set to False by default. + options : dict or pybamm.BatteryModelOptions + A dictionary or an instance of `pybamm.BatteryModelOptions` that stores configuration + options for the submodel. + phase_name : str + A string representing the phase of the submodel, which could be "primary", + "secondary", or an empty string if there is only one phase. + phase : str or None + The current phase of the submodel, which could be "primary", "secondary", or None. + boundary_conditions : dict + A dictionary mapping variables to their respective boundary conditions. + variables : dict + A dictionary mapping variable names (strings) to expressions or objects that + represent the useful variables for the submodel. """ def __init__( @@ -112,6 +126,7 @@ def domain(self, domain): @property def domain_Domain(self): + """Returns a tuple containing the current domain and its capitalized form.""" return self._domain, self._Domain def get_parameter_info(self, by_submodel=False): From 41ca5f63db41a8dd701b306037236ce729e3a034 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Fri, 11 Oct 2024 17:01:35 +0100 Subject: [PATCH 15/24] docs: add MarcBerliner as a contributor for code, doc, and infra (#4505) * 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 | 11 +++++++++++ README.md | 2 +- all_contributors.md | 3 +++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index caadc74b78..2d796ffbea 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -970,6 +970,17 @@ "contributions": [ "code" ] + }, + { + "login": "MarcBerliner", + "name": "Marc Berliner", + "avatar_url": "https://avatars.githubusercontent.com/u/34451391?v=4", + "profile": "http://marcberliner.com", + "contributions": [ + "code", + "doc", + "infra" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 611d8adcfe..8f52b98ed8 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-91-orange.svg)](#-contributors) +[![All Contributors](https://img.shields.io/badge/all_contributors-92-orange.svg)](#-contributors) diff --git a/all_contributors.md b/all_contributors.md index b701ef2a13..38931202d8 100644 --- a/all_contributors.md +++ b/all_contributors.md @@ -122,6 +122,9 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Pip Liggins
Pip Liggins

💻 ⚠️ Medha Bhardwaj
Medha Bhardwaj

💻 + + Marc Berliner
Marc Berliner

💻 📖 🚇 + From 974b10a34c064bce6a4261120f7cf0e22e748acf Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Fri, 11 Oct 2024 17:24:12 +0100 Subject: [PATCH 16/24] docs: add MarcBerliner as a contributor for maintenance (#4506) * 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 | 3 ++- all_contributors.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 2d796ffbea..ae78b29401 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -979,7 +979,8 @@ "contributions": [ "code", "doc", - "infra" + "infra", + "maintenance" ] } ], diff --git a/all_contributors.md b/all_contributors.md index 38931202d8..9c00e118e3 100644 --- a/all_contributors.md +++ b/all_contributors.md @@ -123,7 +123,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Medha Bhardwaj
Medha Bhardwaj

💻 - Marc Berliner
Marc Berliner

💻 📖 🚇 + Marc Berliner
Marc Berliner

💻 📖 🚇 🚧 From 3bf3ea8a2b9870d60820284cdfa05ea901ec9e9a Mon Sep 17 00:00:00 2001 From: "Eric G. Kratz" Date: Fri, 11 Oct 2024 12:40:45 -0400 Subject: [PATCH 17/24] Fix Jax links (#4504) * Fix lychee * Fix other jax links --- .gitignore | 1 + src/pybamm/CITATIONS.bib | 2 +- src/pybamm/solvers/jax_bdf_solver.py | 4 ++-- src/pybamm/solvers/jax_solver.py | 6 +++--- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 42c76b7c55..a3bd3b502c 100644 --- a/.gitignore +++ b/.gitignore @@ -65,6 +65,7 @@ coverage.xml htmlcov/ # virtual environment +.venv env/ venv/ venv3.5/ diff --git a/src/pybamm/CITATIONS.bib b/src/pybamm/CITATIONS.bib index 26919f7c66..62b4b1003e 100644 --- a/src/pybamm/CITATIONS.bib +++ b/src/pybamm/CITATIONS.bib @@ -291,7 +291,7 @@ @article{Hindmarsh2005 @misc{jax2018, author = {James Bradbury and Roy Frostig and Peter Hawkins and Matthew James Johnson and Chris Leary and Dougal Maclaurin and Skye Wanderman-Milne}, title = {{JAX: composable transformations of Python+NumPy programs}}, - url = {http://github.com/google/jax}, + url = {http://github.com/jax-ml/jax}, version = {0.2.5}, year = {2018}, } diff --git a/src/pybamm/solvers/jax_bdf_solver.py b/src/pybamm/solvers/jax_bdf_solver.py index a07ad8505b..3fd3f2384e 100644 --- a/src/pybamm/solvers/jax_bdf_solver.py +++ b/src/pybamm/solvers/jax_bdf_solver.py @@ -28,7 +28,7 @@ MIN_FACTOR = 0.2 MAX_FACTOR = 10 - # https://github.com/google/jax/issues/4572#issuecomment-709809897 + # https://github.com/jax-ml/jax/issues/4572#issuecomment-709809897 def some_hash_function(x): return hash(str(x)) @@ -711,7 +711,7 @@ def block_fun(i, j, Ai, Aj): return onp.block(blocks) # NOTE: the code below (except the docstring on jax_bdf_integrate and other minor - # edits), has been modified from the JAX library at https://github.com/google/jax. + # edits), has been modified from the JAX library at https://github.com/jax-ml/jax. # The main difference is the addition of support for semi-explicit dae index 1 # problems via the addition of a mass matrix. # This is under an Apache license, a short form of which is given here: diff --git a/src/pybamm/solvers/jax_solver.py b/src/pybamm/solvers/jax_solver.py index a1f1733ed6..c4f5a60a64 100644 --- a/src/pybamm/solvers/jax_solver.py +++ b/src/pybamm/solvers/jax_solver.py @@ -46,7 +46,7 @@ class JaxSolver(pybamm.BaseSolver): extra_options : dict, optional Any options to pass to the solver. Please consult `JAX documentation - `_ + `_ for details. """ @@ -263,8 +263,8 @@ async def solve_model_async(inputs_v): # sparse matrix support in JAX resulting in high memory usage, or a bug # in the BDF solver. # - # This issue on guthub appears related: - # https://github.com/google/jax/discussions/13930 + # This issue on GitHub appears related: + # https://github.com/jax-ml/jax/discussions/13930 # # # Split input list based on the number of available xla devices # device_count = jax.local_device_count() From 3ee7dd03d58713ff9136327e024f8a014454466a Mon Sep 17 00:00:00 2001 From: Aswinr24 <135364633+Aswinr24@users.noreply.github.com> Date: Mon, 14 Oct 2024 01:33:57 +0530 Subject: [PATCH 18/24] refactor: use pytest fixtures to reduce repetition in tests (#4509) * refactor: use pytest fixtures to reduce repetition in tests * style: pre-commit fixes * refactor: use pytest fixtures to reduce repetition in tests(v2) * refactor: use pytest fixtures to reduce repetition in tests(v2) * refactor: use pytest fixtures to reduce repetition in tests(v2) * style: pre-commit fixes --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../test_process_parameter_data.py | 64 ++++++++----------- 1 file changed, 28 insertions(+), 36 deletions(-) diff --git a/tests/unit/test_parameters/test_process_parameter_data.py b/tests/unit/test_parameters/test_process_parameter_data.py index 9352894c5c..dc363b862b 100644 --- a/tests/unit/test_parameters/test_process_parameter_data.py +++ b/tests/unit/test_parameters/test_process_parameter_data.py @@ -2,56 +2,48 @@ # Tests for the parameter processing functions # - -import os import numpy as np import pybamm - import pytest +from pathlib import Path -class TestProcessParameterData: - def test_process_1D_data(self): - name = "lico2_ocv_example" - path = os.path.abspath(os.path.dirname(__file__)) - processed = pybamm.parameters.process_1D_data(name, path) - assert processed[0] == name - assert isinstance(processed[1], tuple) - assert isinstance(processed[1][0][0], np.ndarray) - assert isinstance(processed[1][1], np.ndarray) +@pytest.fixture +def parameters_path(): + return Path(__file__).parent.resolve() - def test_process_2D_data(self): - name = "lico2_diffusivity_Dualfoil1998_2D" - path = os.path.abspath(os.path.dirname(__file__)) - processed = pybamm.parameters.process_2D_data(name, path) - assert processed[0] == name - assert isinstance(processed[1], tuple) - assert isinstance(processed[1][0][0], np.ndarray) - assert isinstance(processed[1][0][1], np.ndarray) - assert isinstance(processed[1][1], np.ndarray) - def test_process_2D_data_csv(self): - name = "data_for_testing_2D" - path = os.path.abspath(os.path.dirname(__file__)) - processed = pybamm.parameters.process_2D_data_csv(name, path) +@pytest.fixture( + params=[ + ("lico2_ocv_example", pybamm.parameters.process_1D_data), + ("lico2_diffusivity_Dualfoil1998_2D", pybamm.parameters.process_2D_data), + ("data_for_testing_2D", pybamm.parameters.process_2D_data_csv), + ("data_for_testing_3D", pybamm.parameters.process_3D_data_csv), + ] +) +def parameter_data(request, parameters_path): + name, processing_function = request.param + processed = processing_function(name, parameters_path) + return name, processed + +class TestProcessParameterData: + def test_processed_name(self, parameter_data): + name, processed = parameter_data assert processed[0] == name + + def test_processed_structure(self, parameter_data): + name, processed = parameter_data assert isinstance(processed[1], tuple) assert isinstance(processed[1][0][0], np.ndarray) - assert isinstance(processed[1][0][1], np.ndarray) assert isinstance(processed[1][1], np.ndarray) - def test_process_3D_data_csv(self): - name = "data_for_testing_3D" - path = os.path.abspath(os.path.dirname(__file__)) - processed = pybamm.parameters.process_3D_data_csv(name, path) + if len(processed[1][0]) > 1: + assert isinstance(processed[1][0][1], np.ndarray) - assert processed[0] == name - assert isinstance(processed[1], tuple) - assert isinstance(processed[1][0][0], np.ndarray) - assert isinstance(processed[1][0][1], np.ndarray) - assert isinstance(processed[1][0][2], np.ndarray) - assert isinstance(processed[1][1], np.ndarray) + elif len(processed[1]) == 3: + assert isinstance(processed[1][0][1], np.ndarray) + assert isinstance(processed[1][0][2], np.ndarray) def test_error(self): with pytest.raises(FileNotFoundError, match="Could not find file"): From 96d52e62818be72d74c5dd6232f5be8603de3e25 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 03:52:49 +0100 Subject: [PATCH 19/24] docs: add Aswinr24 as a contributor for test (#4511) * 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> Co-authored-by: Arjun Verma --- .all-contributorsrc | 9 +++++++++ README.md | 2 +- all_contributors.md | 1 + 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index ae78b29401..dcde101e80 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -982,6 +982,15 @@ "infra", "maintenance" ] + }, + { + "login": "Aswinr24", + "name": "Aswinr24", + "avatar_url": "https://avatars.githubusercontent.com/u/135364633?v=4", + "profile": "https://github.com/Aswinr24", + "contributions": [ + "test" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 8f52b98ed8..2b5250d856 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-92-orange.svg)](#-contributors) +[![All Contributors](https://img.shields.io/badge/all_contributors-93-orange.svg)](#-contributors) diff --git a/all_contributors.md b/all_contributors.md index 9c00e118e3..d4a41ba8e1 100644 --- a/all_contributors.md +++ b/all_contributors.md @@ -124,6 +124,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Marc Berliner
Marc Berliner

💻 📖 🚇 🚧 + Aswinr24
Aswinr24

⚠️ From f14ebed70fbc18a80592db1f1f2c85d667222c52 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Oct 2024 02:34:21 +0530 Subject: [PATCH 20/24] Build(deps): bump the actions group with 3 updates (#4513) Bumps the actions group with 3 updates: [lycheeverse/lychee-action](https://github.com/lycheeverse/lychee-action), [actions/upload-artifact](https://github.com/actions/upload-artifact) and [github/codeql-action](https://github.com/github/codeql-action). Updates `lycheeverse/lychee-action` from 1.10.0 to 2.0.2 - [Release notes](https://github.com/lycheeverse/lychee-action/releases) - [Commits](https://github.com/lycheeverse/lychee-action/compare/v1.10.0...v2.0.2) Updates `actions/upload-artifact` from 4.4.1 to 4.4.3 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4.4.1...v4.4.3) Updates `github/codeql-action` from 3.26.12 to 3.26.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/c36620d31ac7c881962c3d9dd939c40ec9434f2b...f779452ac5af1c261dce0346a8f964149f49322b) --- updated-dependencies: - dependency-name: lycheeverse/lychee-action dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - 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-patch dependency-group: actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/lychee_url_checker.yml | 2 +- .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 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/lychee_url_checker.yml b/.github/workflows/lychee_url_checker.yml index 9a636fda8a..b93743619b 100644 --- a/.github/workflows/lychee_url_checker.yml +++ b/.github/workflows/lychee_url_checker.yml @@ -28,7 +28,7 @@ jobs: # use stable version for now to avoid breaking changes - name: Lychee URL checker - uses: lycheeverse/lychee-action@v1.10.0 + uses: lycheeverse/lychee-action@v2.0.2 with: # arguments with file types to check args: >- diff --git a/.github/workflows/periodic_benchmarks.yml b/.github/workflows/periodic_benchmarks.yml index ef66cec238..14f97c55d8 100644 --- a/.github/workflows/periodic_benchmarks.yml +++ b/.github/workflows/periodic_benchmarks.yml @@ -51,7 +51,7 @@ jobs: LD_LIBRARY_PATH: $HOME/.local/lib - name: Upload results as artifact - uses: actions/upload-artifact@v4.4.1 + uses: actions/upload-artifact@v4.4.3 with: name: asv_periodic_results path: results diff --git a/.github/workflows/publish_pypi.yml b/.github/workflows/publish_pypi.yml index 27e6d1162f..64c8a3a777 100644 --- a/.github/workflows/publish_pypi.yml +++ b/.github/workflows/publish_pypi.yml @@ -92,7 +92,7 @@ jobs: python -c "import pybamm; print(pybamm.IDAKLUSolver())" python -m pytest -m cibw {project}/tests/unit - name: Upload Windows wheels - uses: actions/upload-artifact@v4.4.1 + uses: actions/upload-artifact@v4.4.3 with: name: wheels_windows path: ./wheelhouse/*.whl @@ -129,7 +129,7 @@ jobs: python -m pytest -m cibw {project}/tests/unit - name: Upload wheels for Linux - uses: actions/upload-artifact@v4.4.1 + uses: actions/upload-artifact@v4.4.3 with: name: wheels_manylinux path: ./wheelhouse/*.whl @@ -261,7 +261,7 @@ jobs: python -m pytest -m cibw {project}/tests/unit - name: Upload wheels for macOS (amd64, arm64) - uses: actions/upload-artifact@v4.4.1 + uses: actions/upload-artifact@v4.4.3 with: name: wheels_${{ matrix.os }} path: ./wheelhouse/*.whl @@ -281,7 +281,7 @@ jobs: run: pipx run build --sdist - name: Upload SDist - uses: actions/upload-artifact@v4.4.1 + uses: actions/upload-artifact@v4.4.3 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 ce8eb72ce0..d66704a635 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.4.1 + uses: actions/upload-artifact@v4.4.3 with: name: asv_over_history_results path: results diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index beb849e9fc..cb7e552857 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@604373da6381bf24206979c74d06a550515601b9 # v4.4.1 + uses: actions/upload-artifact@184d73b71b93c222403b2e7f1ffebe4508014249 # v4.4.1 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@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12 + uses: github/codeql-action/upload-sarif@f779452ac5af1c261dce0346a8f964149f49322b # v3.26.13 with: sarif_file: results.sarif From e5be8a54cf3ec49da8d6def269f60627704654e7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 17:34:50 -0400 Subject: [PATCH 21/24] chore: update pre-commit hooks (#4515) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/adamchainz/blacken-docs: 1.18.0 → 1.19.0](https://github.com/adamchainz/blacken-docs/compare/1.18.0...1.19.0) 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 b66b38b4db..2fc6b76539 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,7 +13,7 @@ repos: types_or: [python, pyi, jupyter] - repo: https://github.com/adamchainz/blacken-docs - rev: "1.18.0" + rev: "1.19.0" hooks: - id: blacken-docs additional_dependencies: [black==23.*] From 9eb4bc7d304dbb6c61b8242231d2e5a65832cd52 Mon Sep 17 00:00:00 2001 From: Pradyot Ranjan <99216956+prady0t@users.noreply.github.com> Date: Tue, 15 Oct 2024 15:43:15 +0530 Subject: [PATCH 22/24] Replacing setUp fixture with setup_method (#4514) 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> --- .../test_full_battery_models/test_lithium_ion/test_dfn.py | 3 +-- .../test_lithium_ion/test_dfn_half_cell.py | 4 +--- .../test_lithium_ion/test_newman_tobias.py | 3 +-- .../test_full_battery_models/test_lithium_ion/test_spm.py | 3 +-- .../test_lithium_ion/test_spm_half_cell.py | 4 +--- .../test_full_battery_models/test_lithium_ion/test_spme.py | 3 +-- .../test_lithium_ion/test_spme_half_cell.py | 4 +--- tests/unit/test_parameters/test_bpx.py | 3 +-- 8 files changed, 8 insertions(+), 19 deletions(-) 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 cddd59c352..973a0f348b 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 @@ -7,8 +7,7 @@ class TestDFN(BaseUnitTestLithiumIon): - @pytest.fixture(autouse=True) - def setUp(self): + def setup_method(self): self.model = pybamm.lithium_ion.DFN def test_electrolyte_options(self): 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 389fcf9429..395c6f54b9 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 @@ -4,10 +4,8 @@ import pybamm from tests import BaseUnitTestLithiumIonHalfCell -import pytest class TestDFNHalfCell(BaseUnitTestLithiumIonHalfCell): - @pytest.fixture(autouse=True) - def setUp(self): + def setup_method(self): self.model = pybamm.lithium_ion.DFN 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 ea641ee7cc..58149a69de 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 @@ -7,8 +7,7 @@ class TestNewmanTobias(BaseUnitTestLithiumIon): - @pytest.fixture(autouse=True) - def setUp(self): + def setup_method(self): self.model = pybamm.lithium_ion.NewmanTobias def test_electrolyte_options(self): 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 99affc7ddd..8551967dad 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 @@ -7,8 +7,7 @@ class TestSPM(BaseUnitTestLithiumIon): - @pytest.fixture(autouse=True) - def setUp(self): + def setup_method(self): self.model = pybamm.lithium_ion.SPM def test_electrolyte_options(self): 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 c1b6b34745..8ca0c7a7b9 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 @@ -3,10 +3,8 @@ # import pybamm from tests import BaseUnitTestLithiumIonHalfCell -import pytest class TestSPMHalfCell(BaseUnitTestLithiumIonHalfCell): - @pytest.fixture(autouse=True) - def setUp(self): + def setup_method(self): self.model = pybamm.lithium_ion.SPM 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 b0d38fa9c7..ecab4384fc 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 @@ -7,8 +7,7 @@ class TestSPMe(BaseUnitTestLithiumIon): - @pytest.fixture(autouse=True) - def setUp(self): + def setup_method(self): self.model = pybamm.lithium_ion.SPMe # def test_external_variables(self): 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 2a814c113e..f09f42a5a6 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 @@ -4,10 +4,8 @@ # import pybamm from tests import BaseUnitTestLithiumIonHalfCell -import pytest class TestSPMeHalfCell(BaseUnitTestLithiumIonHalfCell): - @pytest.fixture(autouse=True) - def setUp(self): + def setup_method(self): self.model = pybamm.lithium_ion.SPMe diff --git a/tests/unit/test_parameters/test_bpx.py b/tests/unit/test_parameters/test_bpx.py index aeb249cbc0..3e0e32d1fd 100644 --- a/tests/unit/test_parameters/test_bpx.py +++ b/tests/unit/test_parameters/test_bpx.py @@ -7,8 +7,7 @@ class TestBPX: - @pytest.fixture(autouse=True) - def setUp(self): + def setup_method(self): self.base = { "Header": { "BPX": 1.0, From 47c165d68ed86f623436cb5d243af667ff4dbdb1 Mon Sep 17 00:00:00 2001 From: Robert Timms <43040151+rtimms@users.noreply.github.com> Date: Tue, 15 Oct 2024 15:37:37 +0100 Subject: [PATCH 23/24] Add "voltage as a state" option (#4507) * add voltage as a state option * improve coverage * update CHANGELOG --------- Co-authored-by: Valentin Sulzer --- CHANGELOG.md | 1 + .../full_battery_models/base_battery_model.py | 4 ++++ .../models/submodels/electrode/base_electrode.py | 6 ++++-- .../explicit_control_external_circuit.py | 14 ++++++++++++++ .../function_control_external_circuit.py | 10 ++++++++++ .../test_base_battery_model.py | 12 ++++++++++++ 6 files changed, 45 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a03ec39715..762a23d019 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Features +- Adds an option "voltage as a state" that can be "false" (default) or "true". If "true" adds an explicit algebraic equation for the voltage. ([#4507](https://github.com/pybamm-team/PyBaMM/pull/4507)) - Improved `QuickPlot` accuracy for simulations with Hermite interpolation. ([#4483](https://github.com/pybamm-team/PyBaMM/pull/4483)) - Added Hermite interpolation to the (`IDAKLUSolver`) that improves the accuracy and performance of post-processing variables. ([#4464](https://github.com/pybamm-team/PyBaMM/pull/4464)) - Added `BasicDFN` model for sodium-ion batteries ([#4451](https://github.com/pybamm-team/PyBaMM/pull/4451)) 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 5340d685e3..a445f47d19 100644 --- a/src/pybamm/models/full_battery_models/base_battery_model.py +++ b/src/pybamm/models/full_battery_models/base_battery_model.py @@ -210,6 +210,9 @@ class BatteryModelOptions(pybamm.FuzzyDict): solve an algebraic equation for it. Default is "false", unless "SEI film resistance" is distributed in which case it is automatically set to "true". + * "voltage as a state" : str + Whether to make a state for the voltage and solve an algebraic equation + for it. Default is "false". * "working electrode" : str Can be "both" (default) for a standard battery or "positive" for a half-cell where the negative electrode is replaced with a lithium metal @@ -321,6 +324,7 @@ def __init__(self, extra_options): "heterogeneous catalyst", "cation-exchange membrane", ], + "voltage as a state": ["false", "true"], "working electrode": ["both", "positive"], "x-average side reactions": ["false", "true"], } diff --git a/src/pybamm/models/submodels/electrode/base_electrode.py b/src/pybamm/models/submodels/electrode/base_electrode.py index 3abe563c77..2b37ceb0d3 100644 --- a/src/pybamm/models/submodels/electrode/base_electrode.py +++ b/src/pybamm/models/submodels/electrode/base_electrode.py @@ -119,7 +119,7 @@ def _get_standard_current_collector_potential_variables( V_cc = phi_s_cp - phi_s_cn # Voltage - # Note phi_s_cn is always zero at the negative tab + # Note phi_s_cn is always zero at the negative tab by definition V = pybamm.boundary_value(phi_s_cp, "positive tab") # Voltage is local current collector potential difference at the tabs, in 1D @@ -128,10 +128,12 @@ def _get_standard_current_collector_potential_variables( "Negative current collector potential [V]": phi_s_cn, "Positive current collector potential [V]": phi_s_cp, "Local voltage [V]": V_cc, + "Voltage expression [V]": V - delta_phi_contact, "Terminal voltage [V]": V - delta_phi_contact, - "Voltage [V]": V - delta_phi_contact, "Contact overpotential [V]": delta_phi_contact, } + if self.options["voltage as a state"] == "false": + variables.update({"Voltage [V]": V - delta_phi_contact}) return variables diff --git a/src/pybamm/models/submodels/external_circuit/explicit_control_external_circuit.py b/src/pybamm/models/submodels/external_circuit/explicit_control_external_circuit.py index 6d1845c3b0..6dcd9a4541 100644 --- a/src/pybamm/models/submodels/external_circuit/explicit_control_external_circuit.py +++ b/src/pybamm/models/submodels/external_circuit/explicit_control_external_circuit.py @@ -19,9 +19,23 @@ def get_fundamental_variables(self): "Current [A]": I, "C-rate": I / self.param.Q, } + if self.options.get("voltage as a state") == "true": + V = pybamm.Variable("Voltage [V]") + variables.update({"Voltage [V]": V}) return variables + def set_initial_conditions(self, variables): + if self.options.get("voltage as a state") == "true": + V = variables["Voltage [V]"] + self.initial_conditions[V] = self.param.ocv_init + + def set_algebraic(self, variables): + if self.options.get("voltage as a state") == "true": + V = variables["Voltage [V]"] + V_expression = variables["Voltage expression [V]"] + self.algebraic[V] = V - V_expression + class ExplicitPowerControl(BaseModel): """External circuit with current set explicitly to hit target power.""" diff --git a/src/pybamm/models/submodels/external_circuit/function_control_external_circuit.py b/src/pybamm/models/submodels/external_circuit/function_control_external_circuit.py index fcb18086da..274d35954a 100644 --- a/src/pybamm/models/submodels/external_circuit/function_control_external_circuit.py +++ b/src/pybamm/models/submodels/external_circuit/function_control_external_circuit.py @@ -48,6 +48,9 @@ def get_fundamental_variables(self): "Current [A]": I, "C-rate": I / self.param.Q, } + if self.options.get("voltage as a state") == "true": + V = pybamm.Variable("Voltage [V]") + variables.update({"Voltage [V]": V}) return variables @@ -55,6 +58,9 @@ def set_initial_conditions(self, variables): # Initial condition as a guess for consistent initial conditions i_cell = variables["Current variable [A]"] self.initial_conditions[i_cell] = self.param.Q + if self.options.get("voltage as a state") == "true": + V = variables["Voltage [V]"] + self.initial_conditions[V] = self.param.ocv_init def set_rhs(self, variables): # External circuit submodels are always equations on the current @@ -71,6 +77,10 @@ def set_algebraic(self, variables): if self.control == "algebraic": i_cell = variables["Current variable [A]"] self.algebraic[i_cell] = self.external_circuit_function(variables) + if self.options.get("voltage as a state") == "true": + V = variables["Voltage [V]"] + V_expression = variables["Voltage expression [V]"] + self.algebraic[V] = V - V_expression class VoltageFunctionControl(FunctionControl): 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 95d0b53a64..7dcfccdb66 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 @@ -51,6 +51,7 @@ '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']) +'voltage as a state': 'false' (possible: ['false', 'true']) 'working electrode': 'both' (possible: ['both', 'positive']) 'x-average side reactions': 'false' (possible: ['false', 'true']) """ @@ -472,6 +473,17 @@ def test_save_load_model(self): os.remove("test_base_battery_model.json") + def test_voltage_as_state(self): + model = pybamm.lithium_ion.SPM({"voltage as a state": "true"}) + assert model.options["voltage as a state"] == "true" + assert isinstance(model.variables["Voltage [V]"], pybamm.Variable) + + model = pybamm.lithium_ion.SPM( + {"voltage as a state": "true", "operating mode": "voltage"} + ) + assert model.options["voltage as a state"] == "true" + assert isinstance(model.variables["Voltage [V]"], pybamm.Variable) + class TestOptions: def test_print_options(self): From fb81f216064d7e29f144cdd3d7603ab0885b682d Mon Sep 17 00:00:00 2001 From: Pip Liggins Date: Tue, 15 Oct 2024 16:33:38 +0100 Subject: [PATCH 24/24] Add _from_json functionality for pybamm.sign (#4517) * Add _from_json functionality for Sign + test * Update changelog --------- Co-authored-by: Eric G. Kratz --- CHANGELOG.md | 1 + src/pybamm/expression_tree/unary_operators.py | 3 ++- .../test_unary_operators.py | 17 ++++++++++++++--- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 762a23d019..12a7d921d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ ## Bug Fixes +- Added `_from_json()` functionality to `Sign` which was erroneously omitted previously. ([#4517](https://github.com/pybamm-team/PyBaMM/pull/4517)) - Fixed bug where IDAKLU solver failed when `output variables` were specified and an extrapolation event is present. ([#4440](https://github.com/pybamm-team/PyBaMM/pull/4440)) ## Breaking changes diff --git a/src/pybamm/expression_tree/unary_operators.py b/src/pybamm/expression_tree/unary_operators.py index ace1cd9942..aa90fd6f4c 100644 --- a/src/pybamm/expression_tree/unary_operators.py +++ b/src/pybamm/expression_tree/unary_operators.py @@ -212,7 +212,8 @@ def __init__(self, child): @classmethod def _from_json(cls, snippet: dict): - raise NotImplementedError() + """See :meth:`pybamm.UnaryOperator._from_json()`.""" + return cls(snippet["children"][0]) def diff(self, variable): """See :meth:`pybamm.Symbol.diff()`.""" diff --git a/tests/unit/test_expression_tree/test_unary_operators.py b/tests/unit/test_expression_tree/test_unary_operators.py index 0dbafa38c5..d7544763fe 100644 --- a/tests/unit/test_expression_tree/test_unary_operators.py +++ b/tests/unit/test_expression_tree/test_unary_operators.py @@ -138,9 +138,20 @@ def test_sign(self): ) # Test from_json - with pytest.raises(NotImplementedError): - # signs are always scalar/array types in a discretised model - pybamm.Sign._from_json({}) + c = pybamm.Multiplication(pybamm.Variable("a"), pybamm.Scalar(3)) + sign_json = { + "name": "sign", + "id": 5341515228900508018, + "domains": { + "primary": [], + "secondary": [], + "tertiary": [], + "quaternary": [], + }, + "children": [c], + } + + assert pybamm.sign(c) == pybamm.Sign._from_json(sign_json) def test_floor(self, mocker): a = pybamm.Symbol("a")