Repository: pybamm-team/PyBaMM
Pull request #4330 (2024-08-09 20:43:25)

ECM with split OCV
# Description

This creates a simple equivalent circuit model with split OCV for the two electrodes. The OCV functions are from the lithium ion parameters, and this model is housed underneath full lithium ion models.

## Type of change

Please add a line in the relevant section of [CHANGELOG.md](https://github.com/pybamm-team/PyBaMM/blob/develop/CHANGELOG.md) to document the change (include PR #) - note reverse order of PR #s. If necessary, also add to the list of breaking changes.

- [x] New feature (non-breaking change which adds functionality)
- [ ] Optimization (back-end change that speeds up the code)
- [ ] Bug fix (non-breaking change which fixes an issue)

# Key checklist:

- [x] No style issues: `$ pre-commit run` (or `$ nox -s pre-commit`) (see [CONTRIBUTING.md](https://github.com/pybamm-team/PyBaMM/blob/develop/CONTRIBUTING.md#installing-and-using-pre-commit) for how to set this up to run automatically when committing locally, in just two lines of code)
- [ ] All tests pass: `$ python run-tests.py --all` (or `$ nox -s tests`)
- [ ] The documentation builds: `$ python run-tests.py --doctest` (or `$ nox -s doctests`)

You can run integration tests, unit tests, and doctests together at once, using `$ python run-tests.py --quick` (or `$ nox -s quick`).

## Further checks:

- [ ] Code is commented, particularly in hard-to-understand areas
- [ ] Tests added that prove fix is effective or that feature works



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b65d1342f..ebe4abdd6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,10 +1,13 @@
 # [Unreleased](https://github.com/pybamm-team/PyBaMM/)
 
 ## Features
+
 - 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 a lithium ion equivalent circuit model with split open circuit voltages for each electrode (`SplitOCVR`). ([#4330](https://github.com/pybamm-team/PyBaMM/pull/4330))
 
 ## Optimizations
+
 - Removed the `start_step_offset` setting and disabled minimum `dt` warnings for drive cycles with the (`IDAKLUSolver`). ([#4416](https://github.com/pybamm-team/PyBaMM/pull/4416))
 
 ## Features
@@ -12,10 +15,10 @@
 - Added phase-dependent particle options to LAM #4369
 
 ## Breaking changes
+
 - 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))
 - Renamed `set_events` function to `add_events_from` to better reflect its purpose. ([#4421](https://github.com/pybamm-team/PyBaMM/pull/4421))
 
-
 # [v24.9.0](https://github.com/pybamm-team/PyBaMM/tree/v24.9.0) - 2024-09-03
 
 ## Features
diff --git a/docs/source/api/models/lithium_ion/ecm_split_ocv.rst b/docs/source/api/models/lithium_ion/ecm_split_ocv.rst
new file mode 100644
index 000000000..a7d833cf5
--- /dev/null
+++ b/docs/source/api/models/lithium_ion/ecm_split_ocv.rst
@@ -0,0 +1,7 @@
+Equivalent Circuit Model with Split OCV (SplitOCVR)
+=====================================================
+
+.. autoclass:: pybamm.lithium_ion.SplitOCVR
+    :members:
+
+.. footbibliography::
diff --git a/docs/source/api/models/lithium_ion/index.rst b/docs/source/api/models/lithium_ion/index.rst
index 1a72c3c66..52efe44d6 100644
--- a/docs/source/api/models/lithium_ion/index.rst
+++ b/docs/source/api/models/lithium_ion/index.rst
@@ -12,3 +12,4 @@ Lithium-ion Models
   msmr
   yang2017
   electrode_soh
+  ecm_split_ocv
diff --git a/src/pybamm/models/full_battery_models/lithium_ion/__init__.py b/src/pybamm/models/full_battery_models/lithium_ion/__init__.py
index b02868dbe..556e8de31 100644
--- a/src/pybamm/models/full_battery_models/lithium_ion/__init__.py
+++ b/src/pybamm/models/full_battery_models/lithium_ion/__init__.py
@@ -24,8 +24,9 @@ from .basic_dfn_composite import BasicDFNComposite
 from .Yang2017 import Yang2017
 from .mpm import MPM
 from .msmr import MSMR
+from .basic_splitOCVR import SplitOCVR
 
 __all__ = ['Yang2017', 'base_lithium_ion_model', 'basic_dfn',
            'basic_dfn_composite', 'basic_dfn_half_cell', 'basic_spm', 'dfn',
            'electrode_soh', 'electrode_soh_half_cell', 'mpm', 'msmr',
-           'newman_tobias', 'spm', 'spme']
+           'newman_tobias', 'spm', 'spme', 'basic_splitOCVR']
diff --git a/src/pybamm/models/full_battery_models/lithium_ion/basic_splitOCVR.py b/src/pybamm/models/full_battery_models/lithium_ion/basic_splitOCVR.py
new file mode 100644
index 000000000..386a5c08f
--- /dev/null
+++ b/src/pybamm/models/full_battery_models/lithium_ion/basic_splitOCVR.py
@@ -0,0 +1,100 @@
+#
+# Equivalent Circuit Model with split OCV
+#
+import pybamm
+
+
+class SplitOCVR(pybamm.BaseModel):
+    """Basic Equivalent Circuit Model that uses two OCV functions
+    for each electrode. This model is easily parameterizable with minimal parameters.
+    This class differs from the :class: pybamm.equivalent_circuit.Thevenin() due
+    to dual OCV functions to make up the voltage from each electrode.
+
+    Parameters
+    ----------
+    name: str, optional
+        The name of the model.
+    """
+
+    def __init__(self, name="ECM with split OCV"):
+        super().__init__(name)
+
+        ######################
+        # Variables
+        ######################
+        # All variables are only time-dependent
+        # No domain definition needed
+
+        theta_n = pybamm.Variable("Negative particle stoichiometry")
+        theta_p = pybamm.Variable("Positive particle stoichiometry")
+        Q = pybamm.Variable("Discharge capacity [A.h]")
+        V = pybamm.Variable("Voltage [V]")
+
+        # model is isothermal
+        I = pybamm.FunctionParameter("Current function [A]", {"Time [s]": pybamm.t})
+
+        # Capacity equation
+        self.rhs[Q] = I / 3600
+        self.initial_conditions[Q] = pybamm.Scalar(0)
+
+        # Capacity in each electrode
+        Q_n = pybamm.Parameter("Negative electrode capacity [A.h]")
+        Q_p = pybamm.Parameter("Positive electrode capacity [A.h]")
+
+        # State of charge electrode equations
+        theta_n_0 = pybamm.Parameter("Negative electrode initial stoichiometry")
+        theta_p_0 = pybamm.Parameter("Positive electrode initial stoichiometry")
+        self.rhs[theta_n] = -I / Q_n / 3600
+        self.rhs[theta_p] = I / Q_p / 3600
+        self.initial_conditions[theta_n] = theta_n_0
+        self.initial_conditions[theta_p] = theta_p_0
+
+        # Resistance for IR expression
+        R = pybamm.Parameter("Ohmic resistance [Ohm]")
+
+        # Open-circuit potential for each electrode
+        Un = pybamm.FunctionParameter(
+            "Negative electrode OCP [V]", {"Negative particle stoichiometry": theta_n}
+        )
+        Up = pybamm.FunctionParameter(
+            "Positive electrode OCP [V]", {"Positive particle stoichiometry": theta_p}
+        )
+
+        # Voltage expression
+        V = Up - Un - I * R
+
+        # Parameters for Voltage cutoff
+        voltage_high_cut = pybamm.Parameter("Upper voltage cut-off [V]")
+        voltage_low_cut = pybamm.Parameter("Lower voltage cut-off [V]")
+
+        self.variables = {
+            "Negative particle stoichiometry": theta_n,
+            "Positive particle stoichiometry": theta_p,
+            "Current [A]": I,
+            "Discharge capacity [A.h]": Q,
+            "Voltage [V]": V,
+            "Times [s]": pybamm.t,
+            "Positive electrode OCP [V]": Up,
+            "Negative electrode OCP [V]": Un,
+            "Current function [A]": I,
+        }
+
+        # Events specify points at which a solution should terminate
+        self.events += [
+            pybamm.Event("Minimum voltage [V]", V - voltage_low_cut),
+            pybamm.Event("Maximum voltage [V]", voltage_high_cut - V),
+            pybamm.Event("Maximum Negative Electrode stoichiometry", 0.999 - theta_n),
+            pybamm.Event("Maximum Positive Electrode stoichiometry", 0.999 - theta_p),
+            pybamm.Event("Minimum Negative Electrode stoichiometry", theta_n - 0.0001),
+            pybamm.Event("Minimum Positive Electrode stoichiometry", theta_p - 0.0001),
+        ]
+
+    @property
+    def default_quick_plot_variables(self):
+        return [
+            "Voltage [V]",
+            ["Negative particle stoichiometry", "Positive particle stoichiometry"],
+            "Negative electrode OCP [V]",
+            "Positive electrode OCP [V]",
+            "Current [A]",
+        ]


Test patch:
diff --git a/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_splitOCVR.py b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_splitOCVR.py
new file mode 100644
index 000000000..ef4391acd
--- /dev/null
+++ b/tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_splitOCVR.py
@@ -0,0 +1,56 @@
+#
+# Test that the model works with an example parameter set
+#
+import pybamm
+import numpy as np
+import tests
+
+
+class TestSplitOCVR:
+    def test_basic_processing(self):
+        # example parameters
+        qp0 = 8.73231852
+        qn0 = 5.82761507
+        theta0_n = 0.9013973983641687 * 0.9
+        theta0_p = 0.5142305254580026 * 0.83
+
+        # OCV functions
+        def Un(theta_n):
+            Un = (
+                0.1493
+                + 0.8493 * np.exp(-61.79 * theta_n)
+                + 0.3824 * np.exp(-665.8 * theta_n)
+                - np.exp(39.42 * theta_n - 41.92)
+                - 0.03131 * np.arctan(25.59 * theta_n - 4.099)
+                - 0.009434 * np.arctan(32.49 * theta_n - 15.74)
+            )
+            return Un
+
+        def Up(theta_p):
+            Up = (
+                -10.72 * theta_p**4
+                + 23.88 * theta_p**3
+                - 16.77 * theta_p**2
+                + 2.595 * theta_p
+                + 4.563
+            )
+            return Up
+
+        pars = pybamm.ParameterValues(
+            {
+                "Positive electrode capacity [A.h]": qp0,
+                "Ohmic resistance [Ohm]": 0.001,
+                "Negative electrode initial stoichiometry": theta0_n,
+                "Lower voltage cut-off [V]": 2.8,
+                "Positive electrode initial stoichiometry": theta0_p,
+                "Upper voltage cut-off [V]": 4.2,
+                "Negative electrode capacity [A.h]": qn0,
+                "Current function [A]": 5,
+                "Positive electrode OCP [V]": Up,
+                "Negative electrode OCP [V]": Un,
+                "Nominal cell capacity [A.h]": 5,
+            }
+        )
+        model = pybamm.lithium_ion.SplitOCVR()
+        modeltest = tests.StandardModelTest(model)
+        modeltest.test_all(param=pars)
diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_splitOCVR.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_splitOCVR.py
new file mode 100644
index 000000000..e807ec160
--- /dev/null
+++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_splitOCVR.py
@@ -0,0 +1,15 @@
+#
+# Test for the ecm split-OCV model
+#
+import pybamm
+
+
+class TestSplitOCVR:
+    def test_ecmsplitocv_well_posed(self):
+        model = pybamm.lithium_ion.SplitOCVR()
+        model.check_well_posedness()
+
+    def test_get_default_quick_plot_variables(self):
+        model = pybamm.lithium_ion.SplitOCVR()
+        variables = model.default_quick_plot_variables
+        assert "Current [A]" in variables