From 3a848d759e5e4c0ebbe8a21281a21b107db02579 Mon Sep 17 00:00:00 2001 From: Ivan Korotkin Date: Tue, 20 Jun 2023 09:57:20 +0100 Subject: [PATCH 1/8] Added dict of electrodes for well-mixed blended electrode support --- bpx/schema.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bpx/schema.py b/bpx/schema.py index 07c76f6..4c83e83 100644 --- a/bpx/schema.py +++ b/bpx/schema.py @@ -279,10 +279,10 @@ class Parameterisation(ExtraBaseModel): electrolyte: Electrolyte = Field( alias="Electrolyte", ) - negative_electrode: Electrode = Field( + negative_electrode: Union[Electrode, Dict[str, Electrode]] = Field( alias="Negative electrode", ) - positive_electrode: Electrode = Field( + positive_electrode: Union[Electrode, Dict[str, Electrode]] = Field( alias="Positive electrode", ) separator: Contact = Field( From d03c39ae87dd001910639aeb377a834259060cf5 Mon Sep 17 00:00:00 2001 From: Ivan Korotkin Date: Tue, 20 Jun 2023 11:01:33 +0100 Subject: [PATCH 2/8] Added blended electrode example to the test --- tests/test_schema.py | 40 ++++++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/tests/test_schema.py b/tests/test_schema.py index 797910e..551e586 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -48,18 +48,34 @@ def setUp(self): "Maximum stoichiometry": 0.99, }, "Positive electrode": { - "Particle radius [m]": 5.22e-6, - "Thickness [m]": 75.6e-6, - "Diffusivity [m2.s-1]": 4.0e-15, - "OCP [V]": {"x": [0, 0.1, 1], "y": [1.72, 1.2, 0.06]}, - "Conductivity [S.m-1]": 0.18, - "Surface area per unit volume [m-1]": 382184, - "Porosity": 0.335, - "Transport efficiency": 0.1939, - "Reaction rate constant [mol.m-2.s-1]": 1e-10, - "Maximum concentration [mol.m-3]": 63104.0, - "Minimum stoichiometry": 0.1, - "Maximum stoichiometry": 0.9, + "Primary": { + "Particle radius [m]": 5.22e-6, + "Thickness [m]": 75.6e-6, + "Diffusivity [m2.s-1]": 4.0e-15, + "OCP [V]": {"x": [0, 0.1, 1], "y": [1.72, 1.2, 0.06]}, + "Conductivity [S.m-1]": 0.18, + "Surface area per unit volume [m-1]": 382184, + "Porosity": 0.335, + "Transport efficiency": 0.1939, + "Reaction rate constant [mol.m-2.s-1]": 1e-10, + "Maximum concentration [mol.m-3]": 63104.0, + "Minimum stoichiometry": 0.1, + "Maximum stoichiometry": 0.9, + }, + "Secondary": { + "Particle radius [m]": 5.22e-6, + "Thickness [m]": 50.0e-6, + "Diffusivity [m2.s-1]": 4.0e-15, + "OCP [V]": {"x": [0, 0.1, 1], "y": [1.72, 1.2, 0.06]}, + "Conductivity [S.m-1]": 0.18, + "Surface area per unit volume [m-1]": 382184, + "Porosity": 0.335, + "Transport efficiency": 0.1939, + "Reaction rate constant [mol.m-2.s-1]": 1e-10, + "Maximum concentration [mol.m-3]": 63104.0, + "Minimum stoichiometry": 0.1, + "Maximum stoichiometry": 0.9, + }, }, "Separator": { "Thickness [m]": 1.2e-5, From 8dc406bb6df6c63e0c7079d0300f2a30998b3d61 Mon Sep 17 00:00:00 2001 From: Ivan Korotkin Date: Tue, 20 Jun 2023 15:22:57 +0100 Subject: [PATCH 3/8] Split electrode properties into chemistry and other, update test --- bpx/schema.py | 27 ++++++++++++++++------- tests/test_schema.py | 52 +++++++++++++++++++++----------------------- 2 files changed, 44 insertions(+), 35 deletions(-) diff --git a/bpx/schema.py b/bpx/schema.py index 4c83e83..7b1719d 100644 --- a/bpx/schema.py +++ b/bpx/schema.py @@ -176,7 +176,7 @@ class Contact(ExtraBaseModel): ) -class Electrode(Contact): +class ElectrodeChemistry(ExtraBaseModel): minimum_stoichiometry: float = Field( alias="Minimum stoichiometry", example=0.1, @@ -216,11 +216,6 @@ class Electrode(Contact): example=17800, description="Activation energy for diffusivity in particles", ) - conductivity: float = Field( - alias="Conductivity [S.m-1]", - example=0.18, - description=("Electrolyte conductivity (constant)"), - ) ocp: FloatFunctionTable = Field( alias="OCP [V]", example={"x": [0, 0.1, 1], "y": [1.72, 1.2, 0.06]}, @@ -248,6 +243,22 @@ class Electrode(Contact): ) +class ElectrodeOther(Contact): + conductivity: float = Field( + alias="Conductivity [S.m-1]", + example=0.18, + description=("Electrolyte conductivity (constant)"), + ) + + +class Electrode(ElectrodeOther, ElectrodeChemistry): + pass + + +class ElectrodeBlended(ElectrodeOther): + chemistry: Dict[str, ElectrodeChemistry] = Field(alias="Chemistry") + + class Experiment(ExtraBaseModel): time: List[float] = Field( alias="Time [s]", @@ -279,10 +290,10 @@ class Parameterisation(ExtraBaseModel): electrolyte: Electrolyte = Field( alias="Electrolyte", ) - negative_electrode: Union[Electrode, Dict[str, Electrode]] = Field( + negative_electrode: Union[Electrode, ElectrodeBlended] = Field( alias="Negative electrode", ) - positive_electrode: Union[Electrode, Dict[str, Electrode]] = Field( + positive_electrode: Union[Electrode, ElectrodeBlended] = Field( alias="Positive electrode", ) separator: Contact = Field( diff --git a/tests/test_schema.py b/tests/test_schema.py index 551e586..63275f6 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -48,33 +48,31 @@ def setUp(self): "Maximum stoichiometry": 0.99, }, "Positive electrode": { - "Primary": { - "Particle radius [m]": 5.22e-6, - "Thickness [m]": 75.6e-6, - "Diffusivity [m2.s-1]": 4.0e-15, - "OCP [V]": {"x": [0, 0.1, 1], "y": [1.72, 1.2, 0.06]}, - "Conductivity [S.m-1]": 0.18, - "Surface area per unit volume [m-1]": 382184, - "Porosity": 0.335, - "Transport efficiency": 0.1939, - "Reaction rate constant [mol.m-2.s-1]": 1e-10, - "Maximum concentration [mol.m-3]": 63104.0, - "Minimum stoichiometry": 0.1, - "Maximum stoichiometry": 0.9, - }, - "Secondary": { - "Particle radius [m]": 5.22e-6, - "Thickness [m]": 50.0e-6, - "Diffusivity [m2.s-1]": 4.0e-15, - "OCP [V]": {"x": [0, 0.1, 1], "y": [1.72, 1.2, 0.06]}, - "Conductivity [S.m-1]": 0.18, - "Surface area per unit volume [m-1]": 382184, - "Porosity": 0.335, - "Transport efficiency": 0.1939, - "Reaction rate constant [mol.m-2.s-1]": 1e-10, - "Maximum concentration [mol.m-3]": 63104.0, - "Minimum stoichiometry": 0.1, - "Maximum stoichiometry": 0.9, + "Thickness [m]": 75.6e-6, + "Conductivity [S.m-1]": 0.18, + "Porosity": 0.335, + "Transport efficiency": 0.1939, + "Chemistry": { + "Primary": { + "Particle radius [m]": 5.22e-6, + "Diffusivity [m2.s-1]": 4.0e-15, + "OCP [V]": {"x": [0, 0.1, 1], "y": [1.72, 1.2, 0.06]}, + "Surface area per unit volume [m-1]": 382184, + "Reaction rate constant [mol.m-2.s-1]": 1e-10, + "Maximum concentration [mol.m-3]": 63104.0, + "Minimum stoichiometry": 0.1, + "Maximum stoichiometry": 0.9, + }, + "Secondary": { + "Particle radius [m]": 10.0e-6, + "Diffusivity [m2.s-1]": 4.0e-15, + "OCP [V]": {"x": [0, 0.1, 1], "y": [1.72, 1.2, 0.06]}, + "Surface area per unit volume [m-1]": 382184, + "Reaction rate constant [mol.m-2.s-1]": 1e-10, + "Maximum concentration [mol.m-3]": 63104.0, + "Minimum stoichiometry": 0.1, + "Maximum stoichiometry": 0.9, + }, }, }, "Separator": { From 4a36ec61b98456064d08e33661a59297d41d6ab7 Mon Sep 17 00:00:00 2001 From: Ivan Korotkin Date: Wed, 21 Jun 2023 10:21:57 +0100 Subject: [PATCH 4/8] Renamed classes ElectrodeChemistry and ElectrodeOther to Particle and Electrode, renamed Chemistry field to Particle --- bpx/schema.py | 14 +++++++------- tests/test_schema.py | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/bpx/schema.py b/bpx/schema.py index 7b1719d..9225087 100644 --- a/bpx/schema.py +++ b/bpx/schema.py @@ -176,7 +176,7 @@ class Contact(ExtraBaseModel): ) -class ElectrodeChemistry(ExtraBaseModel): +class Particle(ExtraBaseModel): minimum_stoichiometry: float = Field( alias="Minimum stoichiometry", example=0.1, @@ -243,7 +243,7 @@ class ElectrodeChemistry(ExtraBaseModel): ) -class ElectrodeOther(Contact): +class Electrode(Contact): conductivity: float = Field( alias="Conductivity [S.m-1]", example=0.18, @@ -251,12 +251,12 @@ class ElectrodeOther(Contact): ) -class Electrode(ElectrodeOther, ElectrodeChemistry): +class ElectrodeSingle(Electrode, Particle): pass -class ElectrodeBlended(ElectrodeOther): - chemistry: Dict[str, ElectrodeChemistry] = Field(alias="Chemistry") +class ElectrodeBlended(Electrode): + particle: Dict[str, Particle] = Field(alias="Particle") class Experiment(ExtraBaseModel): @@ -290,10 +290,10 @@ class Parameterisation(ExtraBaseModel): electrolyte: Electrolyte = Field( alias="Electrolyte", ) - negative_electrode: Union[Electrode, ElectrodeBlended] = Field( + negative_electrode: Union[ElectrodeSingle, ElectrodeBlended] = Field( alias="Negative electrode", ) - positive_electrode: Union[Electrode, ElectrodeBlended] = Field( + positive_electrode: Union[ElectrodeSingle, ElectrodeBlended] = Field( alias="Positive electrode", ) separator: Contact = Field( diff --git a/tests/test_schema.py b/tests/test_schema.py index 63275f6..5fae2cf 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -52,7 +52,7 @@ def setUp(self): "Conductivity [S.m-1]": 0.18, "Porosity": 0.335, "Transport efficiency": 0.1939, - "Chemistry": { + "Particle": { "Primary": { "Particle radius [m]": 5.22e-6, "Diffusivity [m2.s-1]": 4.0e-15, From bed16f895ff879b29db3b6cd72b24a5e34f5037d Mon Sep 17 00:00:00 2001 From: Ivan Korotkin Date: Wed, 21 Jun 2023 17:30:26 +0100 Subject: [PATCH 5/8] First attempt to add validation based on models according to issue #29 --- bpx/schema.py | 47 ++++++++++++++++++-- tests/test_schema.py | 100 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+), 3 deletions(-) diff --git a/bpx/schema.py b/bpx/schema.py index 9225087..b0e0e15 100644 --- a/bpx/schema.py +++ b/bpx/schema.py @@ -1,6 +1,6 @@ from typing import List, Literal, Union, Dict -from pydantic import BaseModel, Field, Extra +from pydantic import BaseModel, Field, Extra, root_validator from bpx import Function, InterpolatedTable @@ -158,12 +158,15 @@ class Electrolyte(ExtraBaseModel): ) -class Contact(ExtraBaseModel): +class ContactBase(ExtraBaseModel): thickness: float = Field( alias="Thickness [m]", example=85.2e-6, description="Contact thickness", ) + + +class Contact(ContactBase): porosity: float = Field( alias="Porosity", example=0.47, @@ -259,6 +262,14 @@ class ElectrodeBlended(Electrode): particle: Dict[str, Particle] = Field(alias="Particle") +class ElectrodeSingleSPM(ContactBase, Particle): + pass + + +class ElectrodeBlendedSPM(ContactBase): + particle: Dict[str, Particle] = Field(alias="Particle") + + class Experiment(ExtraBaseModel): time: List[float] = Field( alias="Time [s]", @@ -301,9 +312,39 @@ class Parameterisation(ExtraBaseModel): ) +class ParameterisationSPM(ExtraBaseModel): + cell: Cell = Field( + alias="Cell", + ) + negative_electrode: Union[ElectrodeSingleSPM, ElectrodeBlendedSPM] = Field( + alias="Negative electrode", + ) + positive_electrode: Union[ElectrodeSingleSPM, ElectrodeBlendedSPM] = Field( + alias="Positive electrode", + ) + + class BPX(ExtraBaseModel): header: Header = Field( alias="Header", ) - parameterisation: Parameterisation = Field(alias="Parameterisation") + parameterisation: Union[ParameterisationSPM, Parameterisation] = Field( + alias="Parameterisation" + ) validation: Dict[str, Experiment] = Field(None, alias="Validation") + + @root_validator(skip_on_failure=True) + def model_based_validation(cls, values): + model = values.get("header").model + parameter_class_name = values.get("parameterisation").__class__.__name__ + allowed_combinations = [ + ("Parameterisation", "DFN"), + ("Parameterisation", "SPMe"), + ("ParameterisationSPM", "SPM"), + ] + if (parameter_class_name, model) in allowed_combinations: + return values + else: + raise ValueError( + f"The model type {model} does not correspond to the parameter set" + ) diff --git a/tests/test_schema.py b/tests/test_schema.py index 5fae2cf..034e412 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -83,10 +83,110 @@ def setUp(self): }, } + # SPM parameter set + self.base_spm = { + "Header": { + "BPX": 1.0, + "Model": "SPM", + }, + "Parameterisation": { + "Cell": { + "Ambient temperature [K]": 299.0, + "Initial temperature [K]": 299.0, + "Reference temperature [K]": 299.0, + "Electrode area [m2]": 2.0, + "External surface area [m2]": 2.2, + "Volume [m3]": 1.0, + "Number of electrode pairs connected in parallel to make a cell": 1, + "Nominal cell capacity [A.h]": 5.0, + "Lower voltage cut-off [V]": 2.0, + "Upper voltage cut-off [V]": 4.0, + }, + "Negative electrode": { + "Particle radius [m]": 5.86e-6, + "Thickness [m]": 85.2e-6, + "Diffusivity [m2.s-1]": 3.3e-14, + "OCP [V]": {"x": [0, 0.1, 1], "y": [1.72, 1.2, 0.06]}, + "Surface area per unit volume [m-1]": 383959, + "Reaction rate constant [mol.m-2.s-1]": 1e-10, + "Maximum concentration [mol.m-3]": 33133, + "Minimum stoichiometry": 0.01, + "Maximum stoichiometry": 0.99, + }, + "Positive electrode": { + "Thickness [m]": 75.6e-6, + "Particle": { + "Primary": { + "Particle radius [m]": 5.22e-6, + "Diffusivity [m2.s-1]": 4.0e-15, + "OCP [V]": {"x": [0, 0.1, 1], "y": [1.72, 1.2, 0.06]}, + "Surface area per unit volume [m-1]": 382184, + "Reaction rate constant [mol.m-2.s-1]": 1e-10, + "Maximum concentration [mol.m-3]": 63104.0, + "Minimum stoichiometry": 0.1, + "Maximum stoichiometry": 0.9, + }, + "Secondary": { + "Particle radius [m]": 10.0e-6, + "Diffusivity [m2.s-1]": 4.0e-15, + "OCP [V]": {"x": [0, 0.1, 1], "y": [1.72, 1.2, 0.06]}, + "Surface area per unit volume [m-1]": 382184, + "Reaction rate constant [mol.m-2.s-1]": 1e-10, + "Maximum concentration [mol.m-3]": 63104.0, + "Minimum stoichiometry": 0.1, + "Maximum stoichiometry": 0.9, + }, + }, + }, + }, + } + def test_simple(self): test = copy.copy(self.base) parse_obj_as(BPX, test) + def test_simple_spme(self): + test = copy.copy(self.base) + test["Header"]["Model"] = "SPMe" + parse_obj_as(BPX, test) + + def test_simple_spm(self): + test = copy.copy(self.base_spm) + parse_obj_as(BPX, test) + + def test_bad_model(self): + test = copy.copy(self.base) + test["Header"]["Model"] = "Wrong model type" + with self.assertRaises(ValidationError): + parse_obj_as(BPX, test) + + def test_bad_dfn(self): + test = copy.copy(self.base_spm) + test["Header"]["Model"] = "DFN" + with self.assertRaisesRegex( + ValidationError, + "The model type DFN does not correspond to the parameter set", + ): + parse_obj_as(BPX, test) + + def test_bad_spme(self): + test = copy.copy(self.base_spm) + test["Header"]["Model"] = "SPMe" + with self.assertRaisesRegex( + ValidationError, + "The model type SPMe does not correspond to the parameter set", + ): + parse_obj_as(BPX, test) + + def test_bad_spm(self): + test = copy.copy(self.base) + test["Header"]["Model"] = "SPM" + with self.assertRaisesRegex( + ValidationError, + "The model type SPM does not correspond to the parameter set", + ): + parse_obj_as(BPX, test) + def test_table(self): test = copy.copy(self.base) test["Parameterisation"]["Electrolyte"]["Conductivity [S.m-1]"] = { From e286492c21568166a789dfacd073fda80c13cbe2 Mon Sep 17 00:00:00 2001 From: Ivan Korotkin Date: Mon, 4 Sep 2023 20:53:52 +0100 Subject: [PATCH 6/8] Updated CHANGELOG --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31b32cf..056d034 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ +# Unreleased + +- Added validation based on models: SPM, SPMe, DFN ([#34](https://github.com/pybamm-team/BPX/pull/34)) + # [v0.3.0](https://github.com/pybamm-team/BPX/releases/tag/v0.3.1) + - Temporarily pin Pydantic version ([#35](https://github.com/pybamm-team/BPX/pull/35)) + # [v0.3.0](https://github.com/pybamm-team/BPX/releases/tag/v0.3.0) - Added a missing factor of 2 in the definition of the interfacial current, see the Butler-Volmer equation (2a) in the associated BPX standard document. The interfacial current is now given by $j=2j_0\sinh(F\eta/2/R/T)$ instead of $j=j_0\sinh(F\eta/2/R/T)$. From f88c6f4f55b922acdc1ece909933c23c62ee9c22 Mon Sep 17 00:00:00 2001 From: Ivan Korotkin Date: Wed, 6 Sep 2023 16:04:45 +0100 Subject: [PATCH 7/8] Raise a warning instead of an error --- bpx/schema.py | 11 +++++------ tests/test_schema.py | 12 ++++++------ 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/bpx/schema.py b/bpx/schema.py index b0e0e15..cc16ce6 100644 --- a/bpx/schema.py +++ b/bpx/schema.py @@ -4,6 +4,8 @@ from bpx import Function, InterpolatedTable +from warnings import warn + FloatFunctionTable = Union[float, Function, InterpolatedTable] @@ -342,9 +344,6 @@ def model_based_validation(cls, values): ("Parameterisation", "SPMe"), ("ParameterisationSPM", "SPM"), ] - if (parameter_class_name, model) in allowed_combinations: - return values - else: - raise ValueError( - f"The model type {model} does not correspond to the parameter set" - ) + if (parameter_class_name, model) not in allowed_combinations: + warn(f"The model type {model} does not correspond to the parameter set") + return values diff --git a/tests/test_schema.py b/tests/test_schema.py index 034e412..f412ceb 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -163,8 +163,8 @@ def test_bad_model(self): def test_bad_dfn(self): test = copy.copy(self.base_spm) test["Header"]["Model"] = "DFN" - with self.assertRaisesRegex( - ValidationError, + with self.assertWarnsRegex( + UserWarning, "The model type DFN does not correspond to the parameter set", ): parse_obj_as(BPX, test) @@ -172,8 +172,8 @@ def test_bad_dfn(self): def test_bad_spme(self): test = copy.copy(self.base_spm) test["Header"]["Model"] = "SPMe" - with self.assertRaisesRegex( - ValidationError, + with self.assertWarnsRegex( + UserWarning, "The model type SPMe does not correspond to the parameter set", ): parse_obj_as(BPX, test) @@ -181,8 +181,8 @@ def test_bad_spme(self): def test_bad_spm(self): test = copy.copy(self.base) test["Header"]["Model"] = "SPM" - with self.assertRaisesRegex( - ValidationError, + with self.assertWarnsRegex( + UserWarning, "The model type SPM does not correspond to the parameter set", ): parse_obj_as(BPX, test) From bca53cec569fada712cf578fd5055e67e90064dc Mon Sep 17 00:00:00 2001 From: Ivan Korotkin Date: Wed, 6 Sep 2023 16:09:56 +0100 Subject: [PATCH 8/8] Updated CHANGELOG --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 056d034..650bfef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Unreleased -- Added validation based on models: SPM, SPMe, DFN ([#34](https://github.com/pybamm-team/BPX/pull/34)) +- Added validation based on models: SPM, SPMe, DFN ([#34](https://github.com/pybamm-team/BPX/pull/34)). A warning will be produced if the user-defined model type does not match the parameter set (e.g., if the model is `SPM`, but the full DFN model parameters are provided). -# [v0.3.0](https://github.com/pybamm-team/BPX/releases/tag/v0.3.1) +# [v0.3.1](https://github.com/pybamm-team/BPX/releases/tag/v0.3.1) - Temporarily pin Pydantic version ([#35](https://github.com/pybamm-team/BPX/pull/35))