From b13240c5baeae256f6c34d2bc4d0fdb63659bef3 Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Tue, 28 Nov 2023 12:07:28 +0000 Subject: [PATCH 1/7] #2188 implement evaluate at in 1D --- examples/scripts/3E_cell.py | 53 ++++++++ pybamm/__init__.py | 1 + pybamm/discretisations/discretisation.py | 4 + pybamm/expression_tree/unary_operators.py | 119 +++++++++++++++--- pybamm/spatial_methods/finite_volume.py | 44 +++++++ pybamm/spatial_methods/spatial_method.py | 22 +++- .../test_unary_operators.py | 10 +- .../test_base_spatial_method.py | 2 + .../test_finite_volume/test_finite_volume.py | 35 ++++++ 9 files changed, 269 insertions(+), 21 deletions(-) create mode 100644 examples/scripts/3E_cell.py diff --git a/examples/scripts/3E_cell.py b/examples/scripts/3E_cell.py new file mode 100644 index 0000000000..625e25a0a7 --- /dev/null +++ b/examples/scripts/3E_cell.py @@ -0,0 +1,53 @@ +# +# Simulate insertion of a reference electrode in the middle of the cell +# +import pybamm + +# load model +model = pybamm.lithium_ion.SPM() + +# load parameters and evaluate the mid-point of the cell +parameter_values = pybamm.ParameterValues("Chen2020") +L_n = model.param.n.L +L_s = model.param.s.L +L_mid = parameter_values.evaluate(L_n + L_s / 2) + +# extract the potential in the negative and positive electrode at the electrode/current +# collector interfaces +phi_n = pybamm.boundary_value( + model.variables["Negative electrode potential [V]"], "left" +) +phi_p = pybamm.boundary_value( + model.variables["Positive electrode potential [V]"], "right" +) + +# evaluate the electrolyte potential at the mid-point of the cell +phi_e_mid = pybamm.EvaluateAt(model.variables["Electrolyte potential [V]"], L_mid) + +# add the new variables to the model +model.variables.update( + { + "Negative electrode 3E potential [V]": phi_n - phi_e_mid, + "Positive electrode 3E potential [V]": phi_p - phi_e_mid, + } +) + +# solve +sim = pybamm.Simulation(model) +sim.solve([0, 3600]) + +# plot a comparison of the 3E potential and the potential difference between the solid +# and electrolyte phases at the electrode/separator interfaces +sim.plot( + [ + [ + "Negative electrode surface potential difference at separator interface [V]", + "Negative electrode 3E potential [V]", + ], + [ + "Positive electrode surface potential difference at separator interface [V]", + "Positive electrode 3E potential [V]", + ], + "Voltage [V]", + ] +) diff --git a/pybamm/__init__.py b/pybamm/__init__.py index 07d8a1c0ea..084128b6ff 100644 --- a/pybamm/__init__.py +++ b/pybamm/__init__.py @@ -54,6 +54,7 @@ from .logger import logger, set_logging_level, get_new_logger from .settings import settings from .citations import Citations, citations, print_citations + # # Classes for the Expression Tree # diff --git a/pybamm/discretisations/discretisation.py b/pybamm/discretisations/discretisation.py index bb6e678f4c..09f0e37496 100644 --- a/pybamm/discretisations/discretisation.py +++ b/pybamm/discretisations/discretisation.py @@ -865,6 +865,10 @@ def _process_symbol(self, symbol): return child_spatial_method.boundary_value_or_flux( symbol, disc_child, self.bcs ) + elif isinstance(symbol, pybamm.EvaluateAt): + return child_spatial_method.evaluate_at( + symbol, disc_child, symbol.value + ) elif isinstance(symbol, pybamm.UpwindDownwind): direction = symbol.name # upwind or downwind return spatial_method.upwind_or_downwind( diff --git a/pybamm/expression_tree/unary_operators.py b/pybamm/expression_tree/unary_operators.py index 95306ebad5..5490e06718 100644 --- a/pybamm/expression_tree/unary_operators.py +++ b/pybamm/expression_tree/unary_operators.py @@ -287,7 +287,14 @@ def _unary_jac(self, child_jac): def set_id(self): """See :meth:`pybamm.Symbol.set_id()`""" self._id = hash( - (self.__class__, self.name, self.slice.start, self.slice.stop, self.children[0].id, *tuple(self.domain)) + ( + self.__class__, + self.name, + self.slice.start, + self.slice.stop, + self.children[0].id, + *tuple(self.domain), + ) ) def _unary_evaluate(self, child): @@ -396,7 +403,9 @@ def _unary_new_copy(self, child): def _sympy_operator(self, child): """Override :meth:`pybamm.UnaryOperator._sympy_operator`""" - sympy_Divergence = have_optional_dependency("sympy.vector.operators", "Divergence") + sympy_Divergence = have_optional_dependency( + "sympy.vector.operators", "Divergence" + ) return sympy_Divergence(child) @@ -547,7 +556,18 @@ def integration_variable(self): def set_id(self): """See :meth:`pybamm.Symbol.set_id()`""" self._id = hash( - (self.__class__, self.name, *tuple([integration_variable.id for integration_variable in self.integration_variable]), self.children[0].id, *tuple(self.domain)) + ( + self.__class__, + self.name, + *tuple( + [ + integration_variable.id + for integration_variable in self.integration_variable + ] + ), + self.children[0].id, + *tuple(self.domain), + ) ) def _unary_new_copy(self, child): @@ -687,7 +707,13 @@ def __init__(self, child, vector_type="row"): def set_id(self): """See :meth:`pybamm.Symbol.set_id()`""" self._id = hash( - (self.__class__, self.name, self.vector_type, self.children[0].id, *tuple(self.domain)) + ( + self.__class__, + self.name, + self.vector_type, + self.children[0].id, + *tuple(self.domain), + ) ) def _unary_new_copy(self, child): @@ -757,6 +783,18 @@ def _evaluates_on_edges(self, dimension): return False +class ExplicitTimeIntegral(UnaryOperator): + def __init__(self, children, initial_condition): + super().__init__("explicit time integral", children) + self.initial_condition = initial_condition + + def _unary_new_copy(self, child): + return self.__class__(child, self.initial_condition) + + def is_constant(self): + return False + + class DeltaFunction(SpatialOperator): """ Delta function. Currently can only be implemented at the edge of a domain. @@ -781,7 +819,13 @@ def __init__(self, child, side, domain): def set_id(self): """See :meth:`pybamm.Symbol.set_id()`""" self._id = hash( - (self.__class__, self.name, self.side, self.children[0].id, *tuple([(k, tuple(v)) for k, v in self.domains.items()])) + ( + self.__class__, + self.name, + self.side, + self.children[0].id, + *tuple([(k, tuple(v)) for k, v in self.domains.items()]), + ) ) def _evaluates_on_edges(self, dimension): @@ -839,7 +883,13 @@ def __init__(self, name, child, side): def set_id(self): """See :meth:`pybamm.Symbol.set_id()`""" self._id = hash( - (self.__class__, self.name, self.side, self.children[0].id, *tuple([(k, tuple(v)) for k, v in self.domains.items()])) + ( + self.__class__, + self.name, + self.side, + self.children[0].id, + *tuple([(k, tuple(v)) for k, v in self.domains.items()]), + ) ) def _unary_new_copy(self, child): @@ -892,18 +942,6 @@ def _sympy_operator(self, child): return sympy.Symbol(latex_child) -class ExplicitTimeIntegral(UnaryOperator): - def __init__(self, children, initial_condition): - super().__init__("explicit time integral", children) - self.initial_condition = initial_condition - - def _unary_new_copy(self, child): - return self.__class__(child, self.initial_condition) - - def is_constant(self): - return False - - class BoundaryGradient(BoundaryOperator): """ A node in the expression tree which gets the boundary flux of a variable. @@ -920,6 +958,51 @@ def __init__(self, child, side): super().__init__("boundary flux", child, side) +class EvaluateAt(SpatialOperator): + """ + A node in the expression tree which evaluates a symbol at a given position. Only + implemented for variables that depend on a single spatial dimension. + + Parameters + ---------- + child : :class:`pybamm.Symbol` + The variable whose boundary value to take + value : float + The point in one-dimensional space at which to evaluate the symbol. + """ + + def __init__(self, child, value): + self.value = value + + super().__init__("evaluate", child) + + # evaluating removes the domain + self.clear_domains() + + def set_id(self): + """See :meth:`pybamm.Symbol.set_id()`""" + self._id = hash( + ( + self.__class__, + self.name, + self.value, + self.children[0].id, + ) + ) + + def _unary_jac(self, child_jac): + """See :meth:`pybamm.UnaryOperator._unary_jac()`.""" + return pybamm.Scalar(0) + + def _unary_new_copy(self, child): + """See :meth:`UnaryOperator._unary_new_copy()`.""" + return self.__class__(child, self.value) + + def _evaluate_for_shape(self): + """See :meth:`pybamm.Symbol.evaluate_for_shape_using_domain()`""" + return pybamm.evaluate_for_shape_using_domain(self.domains) + + class UpwindDownwind(SpatialOperator): """ A node in the expression tree representing an upwinding or downwinding operator. diff --git a/pybamm/spatial_methods/finite_volume.py b/pybamm/spatial_methods/finite_volume.py index 5c32e5a2c0..0e25a7b3fb 100644 --- a/pybamm/spatial_methods/finite_volume.py +++ b/pybamm/spatial_methods/finite_volume.py @@ -1023,6 +1023,50 @@ def boundary_value_or_flux(self, symbol, discretised_child, bcs=None): return boundary_value + def evaluate_at(self, symbol, discretised_child, value): + """ + Returns the symbol evaluated at a given position in space. In the Finite + Volume method, the symbol is evaluated at the nearest node to the given value. + + Parameters + ---------- + symbol: :class:`pybamm.Symbol` + The boundary value or flux symbol + discretised_child : :class:`pybamm.StateVector` + The discretised variable from which to calculate the boundary value + value : float + The point in one-dimensional space at which to evaluate the symbol. + + Returns + ------- + :class:`pybamm.MatrixMultiplication` + The variable representing the value at the given point. + """ + # Check dimension + if self._get_auxiliary_domain_repeats(discretised_child.domains) > 1: + raise NotImplementedError( + "'EvaluateAt' is only implemented for 1D variables." + ) + + # Get mesh nodes + domain = discretised_child.domain + mesh = self.mesh[domain] + nodes = mesh.nodes + + # Find the index of the node closest to the value + index = np.argmin(np.abs(nodes - value)) + + # Create a sparse matrix with a 1 at the index + matrix = csr_matrix(([1], ([0], [index])), shape=(1, mesh.npts)) + + # Index into the discretised child + out = pybamm.Matrix(matrix) @ discretised_child + + # `EvaluateAt` removes domain + out.clear_domains() + + return out + def process_binary_operators(self, bin_op, left, right, disc_left, disc_right): """Discretise binary operators in model equations. Performs appropriate averaging of diffusivities if one of the children is a gradient operator, so diff --git a/pybamm/spatial_methods/spatial_method.py b/pybamm/spatial_methods/spatial_method.py index acb0227bc2..4945c7e1bb 100644 --- a/pybamm/spatial_methods/spatial_method.py +++ b/pybamm/spatial_methods/spatial_method.py @@ -331,7 +331,7 @@ def internal_neumann_condition( def boundary_value_or_flux(self, symbol, discretised_child, bcs=None): """ - Returns the boundary value or flux using the approriate expression for the + Returns the boundary value or flux using the appropriate expression for the spatial method. To do this, we create a sparse vector 'bv_vector' that extracts either the first (for side="left") or last (for side="right") point from 'discretised_child'. @@ -377,6 +377,26 @@ def boundary_value_or_flux(self, symbol, discretised_child, bcs=None): out.clear_domains() return out + def evaluate_at(self, symbol, discretised_child, value): + """ + Returns the symbol evaluated at a given position in space. + + Parameters + ---------- + symbol: :class:`pybamm.Symbol` + The boundary value or flux symbol + discretised_child : :class:`pybamm.StateVector` + The discretised variable from which to calculate the boundary value + value : float + The point in one-dimensional space at which to evaluate the symbol. + + Returns + ------- + :class:`pybamm.MatrixMultiplication` + The variable representing the value at the given point. + """ + raise NotImplementedError + def mass_matrix(self, symbol, boundary_conditions): """ Calculates the mass matrix for a spatial method. diff --git a/tests/unit/test_expression_tree/test_unary_operators.py b/tests/unit/test_expression_tree/test_unary_operators.py index fc845cb574..51ceed8495 100644 --- a/tests/unit/test_expression_tree/test_unary_operators.py +++ b/tests/unit/test_expression_tree/test_unary_operators.py @@ -392,6 +392,11 @@ def test_index(self): pybamm.Index(vec, 5) pybamm.settings.debug_mode = debug_mode + def test_evaluate_at(self): + a = pybamm.Symbol("a", domain=["negative electrode"]) + f = pybamm.EvaluateAt(a, 1) + self.assertEqual(f.value, 1) + def test_upwind_downwind(self): # upwind of scalar symbol should fail a = pybamm.Symbol("a") @@ -611,9 +616,10 @@ def test_not_constant(self): self.assertFalse((2 * a).is_constant()) def test_to_equation(self): - sympy = have_optional_dependency("sympy") - sympy_Divergence = have_optional_dependency("sympy.vector.operators", "Divergence") + sympy_Divergence = have_optional_dependency( + "sympy.vector.operators", "Divergence" + ) sympy_Gradient = have_optional_dependency("sympy.vector.operators", "Gradient") a = pybamm.Symbol("a", domain="negative particle") diff --git a/tests/unit/test_spatial_methods/test_base_spatial_method.py b/tests/unit/test_spatial_methods/test_base_spatial_method.py index 37b4eb6a0b..d48ea69a7b 100644 --- a/tests/unit/test_spatial_methods/test_base_spatial_method.py +++ b/tests/unit/test_spatial_methods/test_base_spatial_method.py @@ -36,6 +36,8 @@ def test_basics(self): spatial_method.delta_function(None, None) with self.assertRaises(NotImplementedError): spatial_method.internal_neumann_condition(None, None, None, None) + with self.assertRaises(NotImplementedError): + spatial_method.evaluate_at(None, None, None) def test_get_auxiliary_domain_repeats(self): # Test the method to read number of repeats from auxiliary domains diff --git a/tests/unit/test_spatial_methods/test_finite_volume/test_finite_volume.py b/tests/unit/test_spatial_methods/test_finite_volume/test_finite_volume.py index 91a5b70044..b98cfa2abe 100644 --- a/tests/unit/test_spatial_methods/test_finite_volume/test_finite_volume.py +++ b/tests/unit/test_spatial_methods/test_finite_volume/test_finite_volume.py @@ -551,6 +551,41 @@ def test_full_broadcast_domains(self): disc = pybamm.Discretisation(mesh, spatial_methods) disc.process_model(model) + def test_evaluate_at(self): + mesh = get_p2d_mesh_for_testing() + spatial_methods = { + "macroscale": pybamm.FiniteVolume(), + "negative particle": pybamm.FiniteVolume(), + "positive particle": pybamm.FiniteVolume(), + } + disc = pybamm.Discretisation(mesh, spatial_methods) + + n = mesh["negative electrode"].npts + var = pybamm.StateVector(slice(0, n), domain="negative electrode") + + idx = 3 + value = mesh["negative electrode"].nodes[idx] + evaluate_at = pybamm.EvaluateAt(var, value) + evaluate_at_disc = disc.process_symbol(evaluate_at) + + self.assertIsInstance(evaluate_at_disc, pybamm.MatrixMultiplication) + self.assertIsInstance(evaluate_at_disc.left, pybamm.Matrix) + self.assertIsInstance(evaluate_at_disc.right, pybamm.StateVector) + + y = np.arange(n)[:, np.newaxis] + self.assertEqual(evaluate_at_disc.evaluate(y=y), y[idx]) + + # test fail if not 1D + var = pybamm.Variable( + "var", + domain=["negative particle"], + auxiliary_domains={"secondary": "negative electrode"}, + ) + disc.set_variable_slices([var]) + evaluate_at = pybamm.EvaluateAt(var, value) + with self.assertRaisesRegex(NotImplementedError, "'EvaluateAt' is only"): + disc.process_symbol(evaluate_at) + if __name__ == "__main__": print("Add -v for more debug output") From 1b973d3c291467cf51698084f95b0bd6cdc078ac Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Tue, 28 Nov 2023 13:57:59 +0000 Subject: [PATCH 2/7] #2188 changelog and coverage --- CHANGELOG.md | 3 +++ .../unit/test_expression_tree/test_operations/test_copy.py | 1 + tests/unit/test_expression_tree/test_operations/test_jac.py | 6 ++++++ 3 files changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12c6ad8c84..7dd12bc30f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # [Unreleased](https://github.com/pybamm-team/PyBaMM/) +## Features + +- Added a new unary operator, `EvaluateAt`, that evaluates a spatial variable at a given position ([#3573](https://github.com/pybamm-team/PyBaMM/pull/3573)) ## Bug fixes - Fixed a bug where simulations using the CasADi-based solvers would fail randomly with the half-cell model ([#3494](https://github.com/pybamm-team/PyBaMM/pull/3494)) diff --git a/tests/unit/test_expression_tree/test_operations/test_copy.py b/tests/unit/test_expression_tree/test_operations/test_copy.py index 0340e56bb1..6800f9092f 100644 --- a/tests/unit/test_expression_tree/test_operations/test_copy.py +++ b/tests/unit/test_expression_tree/test_operations/test_copy.py @@ -60,6 +60,7 @@ def test_symbol_new_copy(self): pybamm.maximum(a, b), pybamm.SparseStack(mat, mat), pybamm.Equality(a, b), + pybamm.EvaluateAt(a, 0), ]: self.assertEqual(symbol, symbol.new_copy()) diff --git a/tests/unit/test_expression_tree/test_operations/test_jac.py b/tests/unit/test_expression_tree/test_operations/test_jac.py index c6e04d331f..0271adbfad 100644 --- a/tests/unit/test_expression_tree/test_operations/test_jac.py +++ b/tests/unit/test_expression_tree/test_operations/test_jac.py @@ -236,6 +236,12 @@ def test_index(self): jac = ind.jac(vec).evaluate(y=np.linspace(0, 2, 5)).toarray() np.testing.assert_array_equal(jac, np.array([[0, 0, 0, 0, 0]])) + def test_evluate_at(self): + y = pybamm.StateVector(slice(0, 4)) + expr = pybamm.EvaluateAt(y, 2) + jac = expr.jac(y).evaluate(y=np.linspace(0, 2, 4)) + np.testing.assert_array_equal(jac, 0) + def test_jac_of_number(self): """Jacobian of a number should be zero""" a = pybamm.Scalar(1) From 23d6e9ae30c07ba73f43a0d54c114cdf3900fc5b Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Wed, 29 Nov 2023 11:22:35 +0000 Subject: [PATCH 3/7] #2188 valentin comments --- .../api/expression_tree/unary_operator.rst | 6 + .../notebooks/models/simulate-3E-cell.ipynb | 153 ++++++++++++++++++ ...simulating-ORegan-2022-parameter-set.ipynb | 6 +- examples/scripts/3E_cell.py | 53 ------ pybamm/discretisations/discretisation.py | 2 +- pybamm/expression_tree/unary_operators.py | 28 ++-- .../lithium_ion/base_lithium_ion_model.py | 48 ++++++ pybamm/parameters/parameter_values.py | 9 ++ pybamm/spatial_methods/finite_volume.py | 20 +-- pybamm/spatial_methods/spatial_method.py | 4 +- .../test_operations/test_jac.py | 2 +- .../test_unary_operators.py | 2 +- .../test_base_lithium_ion_model.py | 19 +++ .../test_finite_volume/test_finite_volume.py | 15 +- 14 files changed, 269 insertions(+), 98 deletions(-) create mode 100644 docs/source/examples/notebooks/models/simulate-3E-cell.ipynb delete mode 100644 examples/scripts/3E_cell.py diff --git a/docs/source/api/expression_tree/unary_operator.rst b/docs/source/api/expression_tree/unary_operator.rst index ad5bb0a48f..e6a3cbe554 100644 --- a/docs/source/api/expression_tree/unary_operator.rst +++ b/docs/source/api/expression_tree/unary_operator.rst @@ -34,6 +34,9 @@ Unary Operators .. autoclass:: pybamm.Mass :members: +.. autoclass:: pybamm.BoundaryMass + :members: + .. autoclass:: pybamm.Integral :members: @@ -58,6 +61,9 @@ Unary Operators .. autoclass:: pybamm.BoundaryGradient :members: +.. autoclass:: pybamm.EvaluateAt + :members: + .. autoclass:: pybamm.UpwindDownwind :members: diff --git a/docs/source/examples/notebooks/models/simulate-3E-cell.ipynb b/docs/source/examples/notebooks/models/simulate-3E-cell.ipynb new file mode 100644 index 0000000000..501c54265d --- /dev/null +++ b/docs/source/examples/notebooks/models/simulate-3E-cell.ipynb @@ -0,0 +1,153 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Simulating a 3E cell\n", + "\n", + "In this notebook we show how to insert a reference electrode to mimic a 3E cell." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%pip install \"pybamm[plot,cite]\" -q # install PyBaMM if it is not installed\n", + "import pybamm" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We first load a model" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "model = pybamm.lithium_ion.DFN()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next we use the helper function `insert_reference_electrode` to insert a reference electrode into the model. This function takes the position of the reference electrode as an optional argument. If no position is given, the reference electrode is inserted at the midpoint of the separator. The helper function adds the new variables \"Reference electrode potential [V]\", \"Negative electrode 3E potential [V]\" and \"Positive electrode 3E potential [V]\" to the model.\n", + "\n", + "In this example we will explicitly pass a position to show how it is done" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "L_n = model.param.n.L # Negative electrode thickness [m]\n", + "L_s = model.param.s.L # Separator thickness [m]\n", + "L_ref = L_n + L_s / 2 # Reference electrode position [m]\n", + "\n", + "model.insert_reference_electrode(L_ref)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next we can set up a simulation and solve the model as usual" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sim = pybamm.Simulation(model)\n", + "sim.solve([0, 3600])" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's plot a comparison of the 3E potentials and the potential difference between the solid and electrolyte phases at the electrode/separator interfaces" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sim.plot(\n", + " [\n", + " [\n", + " \"Negative electrode surface potential difference at separator interface [V]\",\n", + " \"Negative electrode 3E potential [V]\",\n", + " ],\n", + " [\n", + " \"Positive electrode surface potential difference at separator interface [V]\",\n", + " \"Positive electrode 3E potential [V]\",\n", + " ],\n", + " \"Voltage [V]\",\n", + " ]\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pybamm.print_citations()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "dev", + "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.9.16" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "bca2b99bfac80e18288b793d52fa0653ab9b5fe5d22e7b211c44eb982a41c00c" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/source/examples/notebooks/models/simulating-ORegan-2022-parameter-set.ipynb b/docs/source/examples/notebooks/models/simulating-ORegan-2022-parameter-set.ipynb index 7eb647fc97..f20f385601 100644 --- a/docs/source/examples/notebooks/models/simulating-ORegan-2022-parameter-set.ipynb +++ b/docs/source/examples/notebooks/models/simulating-ORegan-2022-parameter-set.ipynb @@ -163,7 +163,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3.7.4 ('dev': venv)", + "display_name": "dev", "language": "python", "name": "python3" }, @@ -177,7 +177,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.4" + "version": "3.9.16" }, "toc": { "base_numbering": 1, @@ -194,7 +194,7 @@ }, "vscode": { "interpreter": { - "hash": "0f0e5a277ebcf03e91e138edc3d4774b5dee64e7d6640c0d876f99a9f6b2a4dc" + "hash": "bca2b99bfac80e18288b793d52fa0653ab9b5fe5d22e7b211c44eb982a41c00c" } } }, diff --git a/examples/scripts/3E_cell.py b/examples/scripts/3E_cell.py deleted file mode 100644 index 625e25a0a7..0000000000 --- a/examples/scripts/3E_cell.py +++ /dev/null @@ -1,53 +0,0 @@ -# -# Simulate insertion of a reference electrode in the middle of the cell -# -import pybamm - -# load model -model = pybamm.lithium_ion.SPM() - -# load parameters and evaluate the mid-point of the cell -parameter_values = pybamm.ParameterValues("Chen2020") -L_n = model.param.n.L -L_s = model.param.s.L -L_mid = parameter_values.evaluate(L_n + L_s / 2) - -# extract the potential in the negative and positive electrode at the electrode/current -# collector interfaces -phi_n = pybamm.boundary_value( - model.variables["Negative electrode potential [V]"], "left" -) -phi_p = pybamm.boundary_value( - model.variables["Positive electrode potential [V]"], "right" -) - -# evaluate the electrolyte potential at the mid-point of the cell -phi_e_mid = pybamm.EvaluateAt(model.variables["Electrolyte potential [V]"], L_mid) - -# add the new variables to the model -model.variables.update( - { - "Negative electrode 3E potential [V]": phi_n - phi_e_mid, - "Positive electrode 3E potential [V]": phi_p - phi_e_mid, - } -) - -# solve -sim = pybamm.Simulation(model) -sim.solve([0, 3600]) - -# plot a comparison of the 3E potential and the potential difference between the solid -# and electrolyte phases at the electrode/separator interfaces -sim.plot( - [ - [ - "Negative electrode surface potential difference at separator interface [V]", - "Negative electrode 3E potential [V]", - ], - [ - "Positive electrode surface potential difference at separator interface [V]", - "Positive electrode 3E potential [V]", - ], - "Voltage [V]", - ] -) diff --git a/pybamm/discretisations/discretisation.py b/pybamm/discretisations/discretisation.py index 09f0e37496..62110b1676 100644 --- a/pybamm/discretisations/discretisation.py +++ b/pybamm/discretisations/discretisation.py @@ -867,7 +867,7 @@ def _process_symbol(self, symbol): ) elif isinstance(symbol, pybamm.EvaluateAt): return child_spatial_method.evaluate_at( - symbol, disc_child, symbol.value + symbol, disc_child, symbol.position ) elif isinstance(symbol, pybamm.UpwindDownwind): direction = symbol.name # upwind or downwind diff --git a/pybamm/expression_tree/unary_operators.py b/pybamm/expression_tree/unary_operators.py index ce2e8c6245..608cf070a2 100644 --- a/pybamm/expression_tree/unary_operators.py +++ b/pybamm/expression_tree/unary_operators.py @@ -904,7 +904,8 @@ def evaluate_for_shape(self): class BoundaryOperator(SpatialOperator): """ - A node in the expression tree which gets the boundary value of a variable. + A node in the expression tree which gets the boundary value of a variable on its + primary domain. Parameters ---------- @@ -961,7 +962,8 @@ def _evaluate_for_shape(self): class BoundaryValue(BoundaryOperator): """ - A node in the expression tree which gets the boundary value of a variable. + A node in the expression tree which gets the boundary value of a variable on its + primary domain. Parameters ---------- @@ -1036,7 +1038,8 @@ def to_json(self): class BoundaryGradient(BoundaryOperator): """ - A node in the expression tree which gets the boundary flux of a variable. + A node in the expression tree which gets the boundary flux of a variable on its + primary domain. Parameters ---------- @@ -1052,19 +1055,20 @@ def __init__(self, child, side): class EvaluateAt(SpatialOperator): """ - A node in the expression tree which evaluates a symbol at a given position. Only - implemented for variables that depend on a single spatial dimension. + A node in the expression tree which evaluates a symbol at a given position in space + in its primary domain. Currently this is only implemented for 1D primary domains. Parameters ---------- child : :class:`pybamm.Symbol` - The variable whose boundary value to take - value : float - The point in one-dimensional space at which to evaluate the symbol. + The variable to evaluate + position : :class:`pybamm.Symbol` + The position in space on the symbol's primary domain at which to evaluate + the symbol. """ - def __init__(self, child, value): - self.value = value + def __init__(self, child, position): + self.position = position super().__init__("evaluate", child) @@ -1077,7 +1081,7 @@ def set_id(self): ( self.__class__, self.name, - self.value, + self.position, self.children[0].id, ) ) @@ -1088,7 +1092,7 @@ def _unary_jac(self, child_jac): def _unary_new_copy(self, child): """See :meth:`UnaryOperator._unary_new_copy()`.""" - return self.__class__(child, self.value) + return self.__class__(child, self.position) def _evaluate_for_shape(self): """See :meth:`pybamm.Symbol.evaluate_for_shape_using_domain()`""" diff --git a/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py b/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py index cc736d6d04..fbe19b0d42 100644 --- a/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py +++ b/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py @@ -462,3 +462,51 @@ def set_convection_submodel(self): self.submodels[ "through-cell convection" ] = pybamm.convection.through_cell.NoConvection(self.param, self.options) + + def insert_reference_electrode(self, position=None): + """ + Insert a reference electrode to measure the electrolyte potential at a given + position in space. Adds model variables for the electrolyte potential at the + reference electrode and for the potential difference between the electrode + potentials measured at the electrode/current collector interface and the + reference electrode. Only implemented for 1D models (i.e. where the + 'dimensionality' option is 0). + + Parameters + ---------- + position : :class:`pybamm.Symbol`, optional + The position in space at which to measure the electrolyte potential. If + None, defaults to the mid-point of the separator. + """ + if self.options["dimensionality"] != 0: + raise NotImplementedError( + "Reference electrode can only be inserted for models where " + "'dimensionality' is 0. For other models, please add a reference " + "electrode manually." + ) + + param = self.param + if position is None: + position = param.n.L + param.s.L / 2 + + phi_e_ref = pybamm.EvaluateAt( + self.variables["Electrolyte potential [V]"], position + ) + phi_p = pybamm.boundary_value( + self.variables["Positive electrode potential [V]"], "right" + ) + variables = { + "Positive electrode 3E potential [V]": phi_p - phi_e_ref, + "Reference electrode potential [V]": phi_e_ref, + } + + if self.options["working electrode"] == "both": + phi_n = pybamm.boundary_value( + self.variables["Negative electrode potential [V]"], "left" + ) + variables.update( + { + "Negative electrode 3E potential [V]": phi_n - phi_e_ref, + } + ) + self.variables.update(variables) diff --git a/pybamm/parameters/parameter_values.py b/pybamm/parameters/parameter_values.py index d5f12f362f..049910ae9e 100644 --- a/pybamm/parameters/parameter_values.py +++ b/pybamm/parameters/parameter_values.py @@ -721,6 +721,15 @@ def _process_symbol(self, symbol): # f_a_dist in the size average needs to be processed if isinstance(new_symbol, pybamm.SizeAverage): new_symbol.f_a_dist = self.process_symbol(new_symbol.f_a_dist) + # position in evaluate at needs to be processed, and should be a Scalar + if isinstance(new_symbol, pybamm.EvaluateAt): + new_symbol_position = self.process_symbol(new_symbol.position) + if not isinstance(new_symbol_position, pybamm.Scalar): + raise ValueError( + "'position' in 'EvaluateAt' must evaluate to a scalar" + ) + else: + new_symbol.position = new_symbol_position return new_symbol # Functions diff --git a/pybamm/spatial_methods/finite_volume.py b/pybamm/spatial_methods/finite_volume.py index 0e25a7b3fb..636243f829 100644 --- a/pybamm/spatial_methods/finite_volume.py +++ b/pybamm/spatial_methods/finite_volume.py @@ -1023,10 +1023,9 @@ def boundary_value_or_flux(self, symbol, discretised_child, bcs=None): return boundary_value - def evaluate_at(self, symbol, discretised_child, value): + def evaluate_at(self, symbol, discretised_child, position): """ - Returns the symbol evaluated at a given position in space. In the Finite - Volume method, the symbol is evaluated at the nearest node to the given value. + Returns the symbol evaluated at a given position in space. Parameters ---------- @@ -1034,7 +1033,7 @@ def evaluate_at(self, symbol, discretised_child, value): The boundary value or flux symbol discretised_child : :class:`pybamm.StateVector` The discretised variable from which to calculate the boundary value - value : float + position : :class:`pybamm.Scalar` The point in one-dimensional space at which to evaluate the symbol. Returns @@ -1042,22 +1041,19 @@ def evaluate_at(self, symbol, discretised_child, value): :class:`pybamm.MatrixMultiplication` The variable representing the value at the given point. """ - # Check dimension - if self._get_auxiliary_domain_repeats(discretised_child.domains) > 1: - raise NotImplementedError( - "'EvaluateAt' is only implemented for 1D variables." - ) - # Get mesh nodes domain = discretised_child.domain mesh = self.mesh[domain] nodes = mesh.nodes + repeats = self._get_auxiliary_domain_repeats(discretised_child.domains) # Find the index of the node closest to the value - index = np.argmin(np.abs(nodes - value)) + index = np.argmin(np.abs(nodes - position.value)) # Create a sparse matrix with a 1 at the index - matrix = csr_matrix(([1], ([0], [index])), shape=(1, mesh.npts)) + sub_matrix = csr_matrix(([1], ([0], [index])), shape=(1, mesh.npts)) + # repeat across auxiliary domains + matrix = csr_matrix(kron(eye(repeats), sub_matrix)) # Index into the discretised child out = pybamm.Matrix(matrix) @ discretised_child diff --git a/pybamm/spatial_methods/spatial_method.py b/pybamm/spatial_methods/spatial_method.py index 4945c7e1bb..a461d6c150 100644 --- a/pybamm/spatial_methods/spatial_method.py +++ b/pybamm/spatial_methods/spatial_method.py @@ -377,7 +377,7 @@ def boundary_value_or_flux(self, symbol, discretised_child, bcs=None): out.clear_domains() return out - def evaluate_at(self, symbol, discretised_child, value): + def evaluate_at(self, symbol, discretised_child, position): """ Returns the symbol evaluated at a given position in space. @@ -387,7 +387,7 @@ def evaluate_at(self, symbol, discretised_child, value): The boundary value or flux symbol discretised_child : :class:`pybamm.StateVector` The discretised variable from which to calculate the boundary value - value : float + position : :class:`pybamm.Scalar` The point in one-dimensional space at which to evaluate the symbol. Returns diff --git a/tests/unit/test_expression_tree/test_operations/test_jac.py b/tests/unit/test_expression_tree/test_operations/test_jac.py index 0271adbfad..503e7321ea 100644 --- a/tests/unit/test_expression_tree/test_operations/test_jac.py +++ b/tests/unit/test_expression_tree/test_operations/test_jac.py @@ -236,7 +236,7 @@ def test_index(self): jac = ind.jac(vec).evaluate(y=np.linspace(0, 2, 5)).toarray() np.testing.assert_array_equal(jac, np.array([[0, 0, 0, 0, 0]])) - def test_evluate_at(self): + def test_evaluate_at(self): y = pybamm.StateVector(slice(0, 4)) expr = pybamm.EvaluateAt(y, 2) jac = expr.jac(y).evaluate(y=np.linspace(0, 2, 4)) diff --git a/tests/unit/test_expression_tree/test_unary_operators.py b/tests/unit/test_expression_tree/test_unary_operators.py index f39dba335e..6ae6b62d05 100644 --- a/tests/unit/test_expression_tree/test_unary_operators.py +++ b/tests/unit/test_expression_tree/test_unary_operators.py @@ -457,7 +457,7 @@ def test_index(self): def test_evaluate_at(self): a = pybamm.Symbol("a", domain=["negative electrode"]) f = pybamm.EvaluateAt(a, 1) - self.assertEqual(f.value, 1) + self.assertEqual(f.position, 1) def test_upwind_downwind(self): # upwind of scalar symbol should fail diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_base_lithium_ion_model.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_base_lithium_ion_model.py index 315896b29f..fbc916d4a5 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_base_lithium_ion_model.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_base_lithium_ion_model.py @@ -29,6 +29,25 @@ def test_default_parameters(self): ) os.chdir(cwd) + def test_insert_reference_electrode(self): + model = pybamm.lithium_ion.SPM() + model.insert_reference_electrode() + self.assertIn("Negative electrode 3E potential [V]", model.variables) + self.assertIn("Positive electrode 3E potential [V]", model.variables) + self.assertIn("Reference electrode potential [V]", model.variables) + + model = pybamm.lithium_ion.SPM({"working electrode": "positive"}) + model.insert_reference_electrode() + self.assertNotIn("Negative electrode potential [V]", model.variables) + self.assertIn("Positive electrode 3E potential [V]", model.variables) + self.assertIn("Reference electrode potential [V]", model.variables) + + model = pybamm.lithium_ion.SPM({"dimensionality": 2}) + with self.assertRaisesRegex( + NotImplementedError, "Reference electrode can only be" + ): + model.insert_reference_electrode() + if __name__ == "__main__": print("Add -v for more debug output") diff --git a/tests/unit/test_spatial_methods/test_finite_volume/test_finite_volume.py b/tests/unit/test_spatial_methods/test_finite_volume/test_finite_volume.py index b98cfa2abe..16a3bbde2c 100644 --- a/tests/unit/test_spatial_methods/test_finite_volume/test_finite_volume.py +++ b/tests/unit/test_spatial_methods/test_finite_volume/test_finite_volume.py @@ -564,8 +564,8 @@ def test_evaluate_at(self): var = pybamm.StateVector(slice(0, n), domain="negative electrode") idx = 3 - value = mesh["negative electrode"].nodes[idx] - evaluate_at = pybamm.EvaluateAt(var, value) + position = pybamm.Scalar(mesh["negative electrode"].nodes[idx]) + evaluate_at = pybamm.EvaluateAt(var, position) evaluate_at_disc = disc.process_symbol(evaluate_at) self.assertIsInstance(evaluate_at_disc, pybamm.MatrixMultiplication) @@ -575,17 +575,6 @@ def test_evaluate_at(self): y = np.arange(n)[:, np.newaxis] self.assertEqual(evaluate_at_disc.evaluate(y=y), y[idx]) - # test fail if not 1D - var = pybamm.Variable( - "var", - domain=["negative particle"], - auxiliary_domains={"secondary": "negative electrode"}, - ) - disc.set_variable_slices([var]) - evaluate_at = pybamm.EvaluateAt(var, value) - with self.assertRaisesRegex(NotImplementedError, "'EvaluateAt' is only"): - disc.process_symbol(evaluate_at) - if __name__ == "__main__": print("Add -v for more debug output") From 6120caf744f6e8988029c352bc555b6f324cf69a Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Wed, 29 Nov 2023 14:30:03 +0000 Subject: [PATCH 4/7] #2188 debug domains --- .../notebooks/models/simulate-3E-cell.ipynb | 83 ++++++++++++++++--- pybamm/expression_tree/unary_operators.py | 13 ++- 2 files changed, 80 insertions(+), 16 deletions(-) diff --git a/docs/source/examples/notebooks/models/simulate-3E-cell.ipynb b/docs/source/examples/notebooks/models/simulate-3E-cell.ipynb index 501c54265d..c92cb53465 100644 --- a/docs/source/examples/notebooks/models/simulate-3E-cell.ipynb +++ b/docs/source/examples/notebooks/models/simulate-3E-cell.ipynb @@ -12,9 +12,17 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], "source": [ "%pip install \"pybamm[plot,cite]\" -q # install PyBaMM if it is not installed\n", "import pybamm" @@ -30,7 +38,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -49,7 +57,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -69,9 +77,20 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "sim = pybamm.Simulation(model)\n", "sim.solve([0, 3600])" @@ -87,9 +106,34 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "d1f4e1ed03764660b87ee56b135b24a7", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "interactive(children=(FloatSlider(value=0.0, description='t', max=1.0, step=0.01), Output()), _dom_classes=('w…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "sim.plot(\n", " [\n", @@ -108,9 +152,22 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": {}, - "outputs": [], + "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] Marc Doyle, Thomas F. Fuller, and John Newman. Modeling of galvanostatic charge and discharge of the lithium/polymer/insertion cell. Journal of the Electrochemical society, 140(6):1526–1533, 1993. doi:10.1149/1.2221597.\n", + "[3] Charles R. Harris, K. Jarrod Millman, Stéfan J. van der Walt, Ralf Gommers, Pauli Virtanen, David Cournapeau, Eric Wieser, Julian Taylor, Sebastian Berg, Nathaniel J. Smith, and others. Array programming with NumPy. Nature, 585(7825):357–362, 2020. doi:10.1038/s41586-020-2649-2.\n", + "[4] Scott G. Marquis, Valentin Sulzer, Robert Timms, Colin P. Please, and S. Jon Chapman. An asymptotic derivation of a single particle model with electrolyte. Journal of The Electrochemical Society, 166(15):A3693–A3706, 2019. doi:10.1149/2.0341915jes.\n", + "[5] 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()" ] @@ -125,7 +182,7 @@ ], "metadata": { "kernelspec": { - "display_name": "dev", + "display_name": "venv", "language": "python", "name": "python3" }, @@ -139,12 +196,12 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.16" + "version": "3.11.6" }, "orig_nbformat": 4, "vscode": { "interpreter": { - "hash": "bca2b99bfac80e18288b793d52fa0653ab9b5fe5d22e7b211c44eb982a41c00c" + "hash": "9ff3d0c7e37de5f5aa47f4f719e4c84fc6cba7b39c571a05173422444e82fa58" } } }, diff --git a/pybamm/expression_tree/unary_operators.py b/pybamm/expression_tree/unary_operators.py index 608cf070a2..319429183c 100644 --- a/pybamm/expression_tree/unary_operators.py +++ b/pybamm/expression_tree/unary_operators.py @@ -1070,10 +1070,16 @@ class EvaluateAt(SpatialOperator): def __init__(self, child, position): self.position = position - super().__init__("evaluate", child) + # "evaluate at" of a child takes the primary domain from secondary domain + # of the child + # tertiary auxiliary domain shift down to secondary, quarternary to tertiary + domains = { + "primary": child.domains["secondary"], + "secondary": child.domains["tertiary"], + "tertiary": child.domains["quaternary"], + } - # evaluating removes the domain - self.clear_domains() + super().__init__("evaluate", child, domains) def set_id(self): """See :meth:`pybamm.Symbol.set_id()`""" @@ -1083,6 +1089,7 @@ def set_id(self): self.name, self.position, self.children[0].id, + *tuple([(k, tuple(v)) for k, v in self.domains.items()]), ) ) From d4211624cdd87cdcc5adb3e09e1c5a4ee53a0de2 Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Wed, 29 Nov 2023 14:35:15 +0000 Subject: [PATCH 5/7] #2188 add 3E notebook to toctree --- docs/source/examples/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/examples/index.rst b/docs/source/examples/index.rst index 4afaa6eeeb..e025ea71b4 100644 --- a/docs/source/examples/index.rst +++ b/docs/source/examples/index.rst @@ -65,6 +65,7 @@ The notebooks are organised into subfolders, and can be viewed in the galleries notebooks/models/rate-capability.ipynb notebooks/models/saving_models.ipynb notebooks/models/SEI-on-cracks.ipynb + notebooks/models/simulate-3E-cell.ipynb notebooks/models/simulating-ORegan-2022-parameter-set.ipynb notebooks/models/SPM.ipynb notebooks/models/SPMe.ipynb From a1ae912c873a2f9c3eeec1fc2cf6e30efe47261b Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Wed, 29 Nov 2023 15:18:53 +0000 Subject: [PATCH 6/7] #2188 coverage --- tests/unit/test_parameters/test_parameter_values.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/unit/test_parameters/test_parameter_values.py b/tests/unit/test_parameters/test_parameter_values.py index 37ec89068f..40964e8d6d 100644 --- a/tests/unit/test_parameters/test_parameter_values.py +++ b/tests/unit/test_parameters/test_parameter_values.py @@ -267,6 +267,15 @@ def test_process_symbol(self): self.assertEqual(processed_a.value, 4) self.assertEqual(processed_x, x) + # process EvaluateAt + evaluate_at = pybamm.EvaluateAt(x, aa) + processed_evaluate_at = parameter_values.process_symbol(evaluate_at) + self.assertIsInstance(processed_evaluate_at, pybamm.EvaluateAt) + self.assertEqual(processed_evaluate_at.children[0], x) + self.assertEqual(processed_evaluate_at.position, 4) + with self.assertRaisesRegex(ValueError, "'position' in 'EvaluateAt'"): + parameter_values.process_symbol(pybamm.EvaluateAt(x, x)) + # process broadcast whole_cell = ["negative electrode", "separator", "positive electrode"] broad = pybamm.PrimaryBroadcast(a, whole_cell) From b2848f51eb4dbdf99806ccbf7b7199472ed9fafb Mon Sep 17 00:00:00 2001 From: Robert Timms Date: Tue, 5 Dec 2023 14:42:37 +0000 Subject: [PATCH 7/7] #2188 changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cdd99125fd..a7bd875f63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Features - Added a new unary operator, `EvaluateAt`, that evaluates a spatial variable at a given position ([#3573](https://github.com/pybamm-team/PyBaMM/pull/3573)) +- Added a method, `insert_reference_electrode`, to `pybamm.lithium_ion.BaseModel` that insert a reference electrode to measure the electrolyte potential at a given position in space and adds new variables that mimic a 3E cell setup. ([#3573](https://github.com/pybamm-team/PyBaMM/pull/3573)) - Serialisation added so models can be written to/read from JSON ([#3397](https://github.com/pybamm-team/PyBaMM/pull/3397)) ## Bug fixes