Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# [Unreleased](https://github.com/FaradayInstitution/BPX)

- Added `State` section to the schema and moved temperature & concentration parameters
from `Parameterisation` in. ([#113](https://github.com/FaradayInstitution/BPX/pull/113))

# [v0.5.0](https://github.com/FaradayInstitution/BPX/releases/tag/v0.5.0)

- Bug fixes for Pydantic ([#81](https://github.com/FaradayInstitution/BPX/pull/81))
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This repository features a Pydantic-based parser for JSON files in the BPX forma

To support the new open standard, [About:Energy](https://www.aboutenergy.io/) have supplied two parameter sets for an NMC and LFP cell. The BPX files and associated examples and information can be found on the [A:E BPX Parameterisation repository](https://github.com/About-Energy-OpenSource/About-Energy-BPX-Parameterisation/).

To see how to use BPX with [PyBaMM](https://www.pybamm.org/), check out the [BPX example notebook]([https://github.com/pybamm-team/bpx-example](https://github.com/pybamm-team/PyBaMM/blob/1ead6c512a6cff3effaa35f47efb354ec4a3c8c8/docs/source/examples/notebooks/parameterization/bpx.ipynb)).
To see how to use BPX with [PyBaMM](https://www.pybamm.org/), check out the [BPX example notebook](https://github.com/pybamm-team/PyBaMM/blob/develop/docs/source/examples/notebooks/parameterization/bpx.ipynb).

## 🚀 Installation
The BPX package can be installed using pip
Expand All @@ -33,17 +33,17 @@ import bpx
filename = 'path/to/my/file.json'
my_params = bpx.parse_bpx_file(filename)
```
`my_params` will now be of type `BPX`, which acts like a python dataclass with the same attributes as the BPX format. To obtain example files, see the `examples` folder, the [A:E BPX Parameterisation repository](https://github.com/About-Energy-OpenSource/About-Energy-BPX-Parameterisation/), or the [BPX example repository](https://github.com/pybamm-team/bpx-example).
`my_params` will now be of type `BPX`, which acts like a python dataclass with the same attributes as the BPX format. To obtain example files, see the `examples` folder, or the [A:E BPX Parameterisation repository](https://github.com/About-Energy-OpenSource/About-Energy-BPX-Parameterisation/).

Attributes of the class can be printed out using the standard Python dot notation, for example, you can print out the initial temperature of the cell using
Attributes of the class can be printed out using the standard Python dot notation, for example, you can print out the reference temperature of the cell using
```python
print('Initial temperature of cell:', my_params.parameterisation.cell.initial_temperature)
print('Reference temperature of cell:', my_params.parameterisation.cell.reference_temperature)
```

Alternatively, you can export the `BPX` object as a dictionary and use the string names (aliases) of the parameters from the standard
```python
my_params_dict = my_params.model_dump(by_alias=True)
print('Initial temperature of cell:', my_params_dict["Parameterisation"]["Cell"]["Initial temperature [K]"])
print('Reference temperature of cell:', my_params_dict["Parameterisation"]["Cell"]["Reference temperature [K]"])
```

The entire BPX object can be pretty-printed using the `devtools` package
Expand Down
178 changes: 146 additions & 32 deletions bpx/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from bpx import Function, InterpolatedTable

from .base_extra_model import ExtraBaseModel
from .schema_utils import get_materials_in_electrode, validate_section_against_electrodes
from .validators import check_sto_limits

FloatFunctionTable = Union[float, int, Function, InterpolatedTable]
Expand Down Expand Up @@ -117,20 +118,9 @@ class Cell(ExtraBaseModel):
)
nominal_cell_capacity: FloatInt = Field(
alias="Nominal cell capacity [A.h]",
description=(
"Nominal cell capacity. Used to convert between current and C-rate."
),
description=("Nominal cell capacity. Used to convert between current and C-rate."),
examples=[5.0],
)
ambient_temperature: FloatInt = Field(
alias="Ambient temperature [K]",
examples=[298.15],
)
initial_temperature: FloatInt = Field(
None,
alias="Initial temperature [K]",
examples=[298.15],
)
reference_temperature: FloatInt = Field(
None,
alias="Reference temperature [K]",
Expand Down Expand Up @@ -165,17 +155,30 @@ def _validate_thermal_conductivity(cls, data: dict) -> dict:
raise ValueError(error_message)
return data

@model_validator(mode="before")
@classmethod
def _validate_temperatures_not_in_cell(cls, data: dict) -> dict:
"""
Validates that initial/ambient temperatures are not provided in the Cell section.
If provided, raises an error directing users to the State section.
"""
if isinstance(data, dict) and any(
field in data for field in ["Initial temperature [K]", "Ambient temperature [K]"]
):
error_message = (
"The 'Initial temperature [K]' and 'Ambient temperature [K]' fields have been moved. "
"Please provide these parameters in 'State.InitialConditions' and "
"'State.ThermalState' sections respectively."
)
raise ValueError(error_message)
return data


class Electrolyte(ExtraBaseModel):
"""
Electrolyte parameters.
"""

initial_concentration: FloatInt = Field(
alias="Initial concentration [mol.m-3]",
examples=[1000],
description=("Initial / rest lithium ion concentration in the electrolyte"),
)
cation_transference_number: FloatInt = Field(
alias="Cation transference number",
examples=[0.259],
Expand All @@ -184,9 +187,7 @@ class Electrolyte(ExtraBaseModel):
diffusivity: FloatFunctionTable = Field(
alias="Diffusivity [m2.s-1]",
examples=["8.794e-7 * x * x - 3.972e-6 * x + 4.862e-6"],
description=(
"Lithium ion diffusivity in electrolyte (constant or function of concentration)"
),
description=("Lithium ion diffusivity in electrolyte (constant or function of concentration)"),
)
diffusivity_activation_energy: FloatInt = Field(
None,
Expand All @@ -197,9 +198,7 @@ class Electrolyte(ExtraBaseModel):
conductivity: FloatFunctionTable = Field(
alias="Conductivity [S.m-1]",
examples=[1.0],
description=(
"Electrolyte conductivity (constant or function of concentration)"
),
description=("Electrolyte conductivity (constant or function of concentration)"),
)
conductivity_activation_energy: FloatInt = Field(
None,
Expand All @@ -208,6 +207,22 @@ class Electrolyte(ExtraBaseModel):
description="Activation energy for conductivity in electrolyte",
)

@model_validator(mode="before")
@classmethod
def _validate_initial_concentration(cls, data: dict) -> dict:
"""
Validates that initial concentration is not provided in the Electrolyte section.
If provided, raises an error directing users to the State section.
"""
if isinstance(data, dict) and "Initial concentration [mol.m-3]" in data:
error_message = (
"'Initial concentration [mol.m-3]' has been renamed and moved. "
"Please provide this parameter as 'Initial electrolyte concentration [mol.m-3]'"
" in the 'State.InitialConditions' section instead."
)
raise ValueError(error_message)
return data


class ContactBase(ExtraBaseModel):
"""
Expand Down Expand Up @@ -271,9 +286,7 @@ class Particle(ExtraBaseModel):
diffusivity: FloatFunctionTable = Field(
alias="Diffusivity [m2.s-1]",
examples=["3.3e-14"],
description=(
"Lithium ion diffusivity in particle (constant or function of stoichiometry)"
),
description=("Lithium ion diffusivity in particle (constant or function of stoichiometry)"),
)
diffusivity_activation_energy: FloatInt = Field(
None,
Expand All @@ -284,9 +297,7 @@ class Particle(ExtraBaseModel):
ocp: FloatFunctionTable = Field(
alias="OCP [V]",
examples=[{"x": [0, 0.1, 1], "y": [1.72, 1.2, 0.06]}],
description=(
"Open-circuit potential (OCP) at the reference temperature, function of particle stoichiometry"
),
description=("Open-circuit potential (OCP) at the reference temperature, function of particle stoichiometry"),
)
ocp_delith: FloatFunctionTable = Field(
None,
Expand Down Expand Up @@ -339,9 +350,7 @@ class Electrode(Contact):
conductivity: FloatInt = Field(
alias="Conductivity [S.m-1]",
examples=[0.18],
description=(
"Effective electronic conductivity of the porous electrode matrix (constant)"
),
description=("Effective electronic conductivity of the porous electrode matrix (constant)"),
)


Expand Down Expand Up @@ -509,6 +518,82 @@ def _sto_limit_validation(self) -> ParameterisationSPM:
return check_sto_limits(self)


class InitialConditions(ExtraBaseModel):
initial_soc: FloatInt = Field(
alias="Initial state-of-charge",
examples=[0.5],
description=("Initial state of charge of the battery (between 0 and 1)"),
)

initial_temperature: FloatInt = Field(
alias="Initial temperature [K]",
examples=[298.15],
)

initial_electrolyte_concentration: FloatInt = Field(
alias="Initial electrolyte concentration [mol.m-3]",
examples=[1000],
description=("Initial / rest lithium ion concentration in the electrolyte"),
)

initial_hysteresis_state_positive: FloatInt | dict[str, FloatInt] = Field(
alias="Initial hysteresis state: Positive electrode",
examples=[1.0, {"Primary": 1.0, "Secondary": 5.0}],
description=("Initial hysteresis state for positive electrode"),
json_schema_extra={"material_check": "positive_electrode"},
)

initial_hysteresis_state_negative: FloatInt | dict[str, FloatInt] = Field(
alias="Initial hysteresis state: Negative electrode",
examples=[1.0, {"Primary": 1.0, "Secondary": 5.0}],
description=("Initial hysteresis state for negative electrode"),
json_schema_extra={"material_check": "negative_electrode"},
)


class ThermalState(ExtraBaseModel):
ambient_temperature: FloatInt = Field(
alias="Ambient temperature [K]",
examples=[298.15],
)

heat_transfer_coefficient: FloatInt = Field(
alias="Heat transfer coefficient [W.m-2.K-1]",
examples=[10.0],
)


class Degradation(ExtraBaseModel):
lli: FloatInt = Field(
alias="LLI",
)

lam_positive: FloatInt | dict[str, FloatInt] = Field(
alias="LAM: Positive electrode",
json_schema_extra={"material_check": "positive_electrode"},
)

lam_negative: FloatInt | dict[str, FloatInt] = Field(
alias="LAM: Negative electrode",
json_schema_extra={"material_check": "negative_electrode"},
)


class State(ExtraBaseModel):
initial_conditions: InitialConditions = Field(
alias="Initial conditions",
)

thermal_state: ThermalState = Field(
alias="Thermal state",
)

degradation: Degradation = Field(
None,
alias="Degradation",
)


class BPX(ExtraBaseModel):
"""
A class to store a BPX model. Consists of a header, parameterisation, and optional
Expand All @@ -521,6 +606,7 @@ class BPX(ExtraBaseModel):
parameterisation: Union[ParameterisationSPM, Parameterisation] = Field(
alias="Parameterisation",
)
state: State = Field(alias="State")
validation: dict[str, Experiment] = Field(None, alias="Validation")

@model_validator(mode="before")
Expand Down Expand Up @@ -552,3 +638,31 @@ def _dispatch_param_subclasses(cls, data: Any) -> Any: # noqa:ANN401
data["Header"] = header
data["Parameterisation"] = parameterisation
return data

@model_validator(mode="after")
def _check_state_against_blended_electrodes(self) -> BPX:
"""
Check that if blended electrodes are used, values which require per-material
values in State are provided as such (and vice versa).
"""

param = self.parameterisation

electrode_materials = {
"negative_electrode": get_materials_in_electrode(param.negative_electrode),
"positive_electrode": get_materials_in_electrode(param.positive_electrode),
}

validate_section_against_electrodes(
self.state.initial_conditions,
"Initial conditions",
electrode_materials,
)
if self.state.degradation is not None:
validate_section_against_electrodes(
self.state.degradation,
"Degradation",
electrode_materials,
)

return self
Loading