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

Validate the sto limits subbed into the OCPs give the correct voltage limits #32

Merged
merged 19 commits into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 39 additions & 2 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 @@ -189,7 +189,7 @@ class Electrode(Contact):
)
maximum_concentration: float = Field(
alias="Maximum concentration [mol.m-3]",
example=631040,
example=63104.0,
description="Maximum concentration of lithium ions in particles",
)
particle_radius: float = Field(
Expand Down Expand Up @@ -289,6 +289,43 @@ class Parameterisation(ExtraBaseModel):
alias="Separator",
)

# Validates that the STO limits subbed into the OCPs give the correct voltage limits.
# Works if both OCPs are defined as functions.
# https://docs.pydantic.dev/latest/usage/validators/#root-validators
@root_validator(skip_on_failure=True)
def check_sto_limits(cls, values):
try:
ocp_n = values.get("negative_electrode").ocp.to_python_function()
ocp_p = values.get("positive_electrode").ocp.to_python_function()
except AttributeError:
# OCPs defined as interpolated tables; do nothing
return values

sto_n_min = values.get("negative_electrode").minimum_stoichiometry
sto_n_max = values.get("negative_electrode").maximum_stoichiometry
sto_p_min = values.get("positive_electrode").minimum_stoichiometry
sto_p_max = values.get("positive_electrode").maximum_stoichiometry
V_min = values.get("cell").lower_voltage_cutoff
V_max = values.get("cell").upper_voltage_cutoff

# Checks the maximum voltage estimated from STO
V_max_sto = ocp_p(sto_p_min) - ocp_n(sto_n_max)
if V_max_sto > V_max:
raise ValueError(
f"The maximum voltage computed from the STO limits ({V_max_sto} V) "
f"is higher than the maximum allowed voltage ({V_max} V)"
)

# Checks the minimum voltage estimated from STO
V_min_sto = ocp_p(sto_p_max) - ocp_n(sto_n_min)
if V_min_sto < V_min:
raise ValueError(
f"The minimum voltage computed from the STO limits ({V_min_sto} V) "
f"is lower than the minimum allowed voltage ({V_min} V)"
)

return values


class BPX(ExtraBaseModel):
header: Header = Field(
Expand Down
3 changes: 3 additions & 0 deletions bpx/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ def get_electrode_concentrations(target_soc, bpx):
c_n, c_p
The electrode concentrations that give the target state of charge
"""
if target_soc < 0 or target_soc > 1:
raise ValueError("Target SOC should be between 0 and 1")

c_n_max = bpx.parameterisation.negative_electrode.maximum_concentration
c_p_max = bpx.parameterisation.positive_electrode.maximum_concentration

Expand Down
43 changes: 36 additions & 7 deletions tests/test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def setUp(self):
"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,
"Upper voltage cut-off [V]": 4.5,
},
"Electrolyte": {
"Initial concentration [mol.m-3]": 1000,
Expand All @@ -37,29 +37,40 @@ def setUp(self):
"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]},
"OCP [V]": (
"9.47057878e-01 * exp(-1.59418743e+02 * x) - 3.50928033e+04 + "
"1.64230269e-01 * tanh(-4.55509094e+01 * (x - 3.24116012e-02 )) + "
"3.69968491e-02 * tanh(-1.96718868e+01 * (x - 1.68334476e-01)) + "
"1.91517003e+04 * tanh(3.19648312e+00 * (x - 1.85139824e+00)) + "
"5.42448511e+04 * tanh(-3.19009848e+00 * (x - 2.01660395e+00))"
),
"Conductivity [S.m-1]": 215.0,
"Surface area per unit volume [m-1]": 383959,
"Porosity": 0.25,
"Transport efficiency": 0.125,
"Reaction rate constant [mol.m-2.s-1]": 1e-10,
"Maximum concentration [mol.m-3]": 33133,
"Minimum stoichiometry": 0.01,
"Maximum stoichiometry": 0.99,
"Minimum stoichiometry": 0.005504,
"Maximum stoichiometry": 0.75668,
},
"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]},
"OCP [V]": (
"-3.04420906 * x + 10.04892207 - "
"0.65637536 * tanh(-4.02134095 * (x - 0.80063948)) + "
"4.24678547 * tanh(12.17805062 * (x - 7.57659337)) - "
"0.3757068 * tanh(59.33067782 * (x - 0.99784492))"
),
"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,
"Minimum stoichiometry": 0.42424,
"Maximum stoichiometry": 0.96210,
},
"Separator": {
"Thickness [m]": 1.2e-5,
Expand Down Expand Up @@ -144,6 +155,24 @@ def test_validation_data(self):
},
}

def test_check_sto_limits_validator(self):
test = copy.copy(self.base)
test["Parameterisation"]["Cell"]["Upper voltage cut-off [V]"] = 4.3
test["Parameterisation"]["Cell"]["Lower voltage cut-off [V]"] = 2.5
parse_obj_as(BPX, test)

def test_check_sto_limits_validator_high_voltage(self):
test = copy.copy(self.base)
test["Parameterisation"]["Cell"]["Upper voltage cut-off [V]"] = 4.0
with self.assertRaises(ValidationError):
parse_obj_as(BPX, test)

def test_check_sto_limits_validator_low_voltage(self):
test = copy.copy(self.base)
test["Parameterisation"]["Cell"]["Lower voltage cut-off [V]"] = 3.0
with self.assertRaises(ValidationError):
parse_obj_as(BPX, test)


if __name__ == "__main__":
unittest.main()
36 changes: 36 additions & 0 deletions tests/test_utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,42 @@ def test_get_init_conc(self):
self.assertAlmostEqual(x, 23060.568)
self.assertAlmostEqual(y, 21455.36)

def test_get_init_sto_negative_target_soc(self):
test = copy.copy(self.base)
obj = parse_obj_as(BPX, test)
with self.assertRaisesRegex(
ValueError,
"Target SOC should be between 0 and 1",
):
get_electrode_stoichiometries(-0.1, obj)

def test_get_init_sto_bad_target_soc(self):
test = copy.copy(self.base)
obj = parse_obj_as(BPX, test)
with self.assertRaisesRegex(
ValueError,
"Target SOC should be between 0 and 1",
):
get_electrode_stoichiometries(1.1, obj)

def test_get_init_conc_negative_target_soc(self):
test = copy.copy(self.base)
obj = parse_obj_as(BPX, test)
with self.assertRaisesRegex(
ValueError,
"Target SOC should be between 0 and 1",
):
get_electrode_concentrations(-0.5, obj)

def test_get_init_conc_bad_target_soc(self):
test = copy.copy(self.base)
obj = parse_obj_as(BPX, test)
with self.assertRaisesRegex(
ValueError,
"Target SOC should be between 0 and 1",
):
get_electrode_concentrations(1.05, obj)


if __name__ == "__main__":
unittest.main()