Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validation based on models #34

74 changes: 63 additions & 11 deletions bpx/schema.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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,
Expand All @@ -176,7 +179,7 @@ class Contact(ExtraBaseModel):
)


class Electrode(Contact):
class Particle(ExtraBaseModel):
minimum_stoichiometry: float = Field(
alias="Minimum stoichiometry",
example=0.1,
Expand Down Expand Up @@ -216,11 +219,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]},
Expand Down Expand Up @@ -248,6 +246,30 @@ class Electrode(Contact):
)


class Electrode(Contact):
conductivity: float = Field(
alias="Conductivity [S.m-1]",
example=0.18,
description=("Electrolyte conductivity (constant)"),
)


class ElectrodeSingle(Electrode, Particle):
pass


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]",
Expand Down Expand Up @@ -279,20 +301,50 @@ class Parameterisation(ExtraBaseModel):
electrolyte: Electrolyte = Field(
alias="Electrolyte",
)
negative_electrode: Electrode = Field(
negative_electrode: Union[ElectrodeSingle, ElectrodeBlended] = Field(
alias="Negative electrode",
)
positive_electrode: Electrode = Field(
positive_electrode: Union[ElectrodeSingle, ElectrodeBlended] = Field(
alias="Positive electrode",
)
separator: Contact = Field(
alias="Separator",
)


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(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we raise a warning instead of an error? think error might be aggressive and will end up being a barrier to #40

Copy link
Collaborator Author

@ikorotkin ikorotkin Sep 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any native way to raise a warning in pydantic?
It looks like it hasn't been implemented (see this issue).

EDIT: I'll do it using warnings.warn()

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed it to a warning. Updated tests and CHANGELOG.
Please feel free to suggest a better warning message if it sounds awkward :)

f"The model type {model} does not correspond to the parameter set"
)
130 changes: 122 additions & 8 deletions tests/test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,32 @@ 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,
"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,
},
},
},
"Separator": {
"Thickness [m]": 1.2e-5,
Expand All @@ -69,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]"] = {
Expand Down