Skip to content

Commit 95669e4

Browse files
committed
Merge branch 'release/3.0.1'
2 parents 4365c56 + f24ffc7 commit 95669e4

File tree

11 files changed

+5757
-5720
lines changed

11 files changed

+5757
-5720
lines changed

README.md

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# PyBatteryID
22

3+
<div>
4+
5+
[![release](https://img.shields.io/github/v/release/muizabdul29/PyBatteryID)](https://github.com/muizabdul29/PyBatteryID/releases)
6+
[![DOI](https://zenodo.org/badge/704093134.svg)](https://doi.org/10.5281/zenodo.15437221)
7+
8+
</div>
9+
310
**PyBatteryID** — a shorthand for **Py**thon **Battery** Model **ID**entification, is an open-source library for data-driven battery model identification in the linear parameter-varying (LPV) framework.
411

512
> Briefly, the LPV framework can be considered as an upgrade to the linear time-invariant (LTI) framework for systems whose dynamics cannot be considered time-invariant — for instance, batteries exhibit varying dynamics as the battery state-of-charge (SOC), temperature, current magnitude, and current direction vary. Refer to the book [Modeling and Identification of Linear Parameter-Varying Systems](https://link.springer.com/book/10.1007/978-3-642-13812-6) for a formal introduction to the LPV systems.
@@ -12,10 +19,20 @@ Use the package manager [pip](https://pip.pypa.io/en/stable/) to install PyBatte
1219
pip install pybatteryid
1320
```
1421

22+
## Regarding the required experiments
23+
24+
There are a few experiments that the user may need to perform before using PyBatteryID, namely,
25+
26+
1. **Battery electromotive force (EMF) data**: Also referred to as the open-circuit voltage (OCV). The user needs to provide the (SOC, EMF) points that cover the SOC range expected in the test application. PyBatteryID performs interpolation using the given points to determine an EMF value for a certain SOC value. Note that by design, PyBatteryID DOES NOT perform extrapolation outside the given SOC range, because this can lead to poor modelling results.
27+
28+
2. **Current-voltage data**: The user must provide a sufficiently informative identification dataset, comprising current-voltage data (and optionally, temperature data if temperature-dependent models are desired). Please DO NOT expect a low-quality dataset, such as an HPPC test or a random drive cycle, to give you good models. In our paper [1], we have emphasised as much as possible that an informative dataset is crucial for obtaining high-quality (accurate, sparse, etc.) models, while presenting a current profile design that can lead to suitable identification datasets (see [examples/4_input_design.ipynb](/examples/4_input_design.ipynb)). Also note that this requirement holds for *any* modelling activity, and not specific to PyBatteryID or batteries, namely, it is a general principle of the **[System Identification](https://en.wikipedia.org/wiki/System_identification)** to use informative identification datasets.
29+
1530
## Basic usage
1631

1732
In the following, an example usage of PyBatteryID has been demonstrated for modelling the battery overpotential using the LPV framework while assuming the battery electromotive force (EMF) to be known *a priori* via appropriate experiments, such as GITT, or low-current cycling. In effect, the battery voltage output at a given time instant can then be calculated by using the EMF value at that instant and evaluating the overpotential using the identified LPV model.
1833

34+
> It is recommended that the user follows the International System of Units (SI) while using PyBatteryID. For example, the battery capacity should be specified in Coulombs, time in seconds, current in amperes, and voltage in volts. For the temperature, both Celsius or Kelvin can be used as long as the user stays consistent and adjusts the temperature-related basis functions accordingly. Note that the temperature is in Celsius in the [example](/examples/3_1_nmc_with_temperature_identification.ipynb) provided with the package.
35+
1936
#### 1. Initialize model structure
2037

2138
The very first step is to create an instance of the `ModelStructure` class which requires the capacity of the studied battery and the model sampling time.
@@ -97,13 +114,16 @@ voltage_simulated = simulate_model(model, dataset)
97114

98115
## Relevant publications
99116

100-
<a id="1">[1]</a> A.M.A. Sheikh, M.C.F. Donkers, and H.J. Bergveld, “A comprehensive approach to sparse identification of linear parameter-varying models for lithium-ion batteries using improved experimental design,” *Journal of Energy Storage, 2024*.
117+
<a id="1">[1]</a> A.M.A. Sheikh, M.C.F. Donkers, and H.J. Bergveld, “A comprehensive approach to sparse identification of linear parameter-varying models for lithium-ion batteries using improved experimental design,” *Journal of Energy Storage, 2024*. https://doi.org/10.1016/j.est.2024.112581
101118

102-
<a id="2">[2]</a> A.M.A. Sheikh, M.C.F. Donkers, and H.J. Bergveld, “Investigating Identification Input Designs for Modelling Lithium-ion Batteries with Hysteresis using LPV Framework,” *2024 American Control Conference (ACC)*.
119+
<a id="2">[2]</a> A.M.A. Sheikh, M.C.F. Donkers, and H.J. Bergveld, “Investigating Identification Input Designs for Modelling Lithium-ion Batteries with Hysteresis using LPV Framework,” *2024 American Control Conference (ACC)*. https://doi.org/10.23919/ACC60939.2024.10644893
103120

104121
<a id="3">[3]</a> A.M.A. Sheikh, M.C.F. Donkers, and H.J. Bergveld, “Towards temperature-dependent linear
105-
parameter-varying models for lithium-ion batteries
106-
using novel experimental design,” *Submitted to Journal*.
122+
parameter-varying models for lithium-ion batteries using novel experimental design,” *Journal of Energy Storage, 2025*. https://doi.org/10.1016/j.est.2025.116311
123+
124+
## Get in touch
125+
126+
For general inquiries about using the package, you can either [start a discussion](https://github.com/muizabdul29/PyBatteryID/discussions) or email at [a.m.a.sheikh@tue.nl](mailto:a.m.a.sheikh@tue.nl) (Muiz Sheikh).
107127

108128
## License
109129
PyBatteryID is an open-source library licensed under the BSD-3-Clause license. For more information, see [LICENSE](LICENSE.txt).

examples/1_1_nmc_identification.ipynb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"cells": [
33
{
44
"cell_type": "code",
5-
"execution_count": 1,
5+
"execution_count": null,
66
"metadata": {
77
"jupyter": {
88
"is_executing": true
@@ -12,7 +12,7 @@
1212
"source": [
1313
"from pybatteryid import ModelStructure\n",
1414
"from pybatteryid.identification import identify_model\n",
15-
"from pybatteryid.utilities import print_model_details, save_model_to_file, invert_voltage_function, analyze_dataset\n",
15+
"from pybatteryid.utilities import print_model_details, save_model_to_file, analyze_dataset\n",
1616
"from pybatteryid.plotter import plot_time_vs_current\n",
1717
"\n",
1818
"from data import helper"
@@ -7337,7 +7337,7 @@
73377337
"name": "python",
73387338
"nbconvert_exporter": "python",
73397339
"pygments_lexer": "ipython3",
7340-
"version": "3.12.4"
7340+
"version": "3.13.5"
73417341
},
73427342
"orig_nbformat": 4
73437343
},

examples/1_2_nmc_validation.ipynb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
"cells": [
33
{
44
"cell_type": "code",
5-
"execution_count": 36,
5+
"execution_count": null,
66
"metadata": {},
77
"outputs": [],
88
"source": [
99
"from sklearn.metrics import root_mean_squared_error, mean_absolute_error\n",
1010
"\n",
1111
"from pybatteryid.simulation import simulate_model\n",
12-
"from pybatteryid.utilities import load_model_from_file, invert_voltage_function\n",
12+
"from pybatteryid.utilities import load_model_from_file\n",
1313
"from pybatteryid.plotter import plot_time_vs_current, plot_time_vs_voltage\n",
1414
"\n",
1515
"from data import helper"

examples/3_1_nmc_with_temperature_identification.ipynb

Lines changed: 534 additions & 534 deletions
Large diffs are not rendered by default.

examples/3_2_nmc_with_temperature_validation.ipynb

Lines changed: 5163 additions & 5157 deletions
Large diffs are not rendered by default.

examples/4_input_design.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2557,7 +2557,7 @@
25572557
"name": "python",
25582558
"nbconvert_exporter": "python",
25592559
"pygments_lexer": "ipython3",
2560-
"version": "3.12.4"
2560+
"version": "3.13.5"
25612561
},
25622562
"orig_nbformat": 4
25632563
},
-67 Bytes
Binary file not shown.

pybatteryid/basisfunctions.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def extract_basis_functions(basis_function_strings: list[str]) -> list[BasisFunc
4242
r"(([0-9]*[.])?[0-9]+)\]$", [0, 1, 3], Operation.LOWPASS)
4343
]
4444
#
45-
for i, function_string in enumerate(basis_function_strings):
45+
for function_string in basis_function_strings:
4646
for identifier, indices, operation in identifiers:
4747
#
4848
result = re.findall(identifier, function_string.strip())
@@ -52,7 +52,7 @@ def extract_basis_functions(basis_function_strings: list[str]) -> list[BasisFunc
5252
basis_functions.append(BasisFunction(result[indices[0]], operation, args,
5353
function_string))
5454
break
55-
if len(basis_functions) == i:
55+
else:
5656
raise ValueError(f"Could not recognize expression: {function_string}")
5757
return basis_functions
5858

@@ -222,9 +222,10 @@ def generate_signal_trajectories(signals: Tuple[list[Signal], ...], model_order:
222222
#
223223
io_signals, basis_function_signals, hysteresis_basis_function_signals = signals
224224
#
225-
time_delays = [np.arange(1, model_order + 1, 1).tolist(), # voltage
226-
np.arange(0, model_order + 1, 1).tolist(), # current
227-
[0]] # hysteresis input
225+
time_delays = [
226+
[0] if signal.symbol == 'h'
227+
else np.arange(signal.symbol == 'v', model_order + 1).tolist() for signal in io_signals
228+
]
228229
#
229230
io_trajectories_dict = generate_io_trajectories(io_signals,
230231
time_delays, no_of_initial_values)

pybatteryid/identification.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@
1313
generate_signal_trajectories
1414

1515

16-
# pylint: disable=R0914
16+
# pylint: disable=R0914,W0102
1717
def setup_regression_problems(datasets: list[CurrentVoltageData],
1818
model_structure: ModelStructure,
19-
model_order: int, nonlinearity_order: int):
19+
model_order: int, nonlinearity_order: int,
20+
input_symbols: list[str] = ['i']):
2021
"""Generate regression matrices for the given dataset(s)."""
2122
# Shorthand for convenience
2223
ms = model_structure
@@ -28,11 +29,10 @@ def setup_regression_problems(datasets: list[CurrentVoltageData],
2829
#
2930
bf_signal_vector = generate_basis_function_signals(ms.basis_functions, signals)
3031
hysteresis_bf_signal_vector = generate_basis_function_signals(ms.hysteresis_basis_functions,
31-
signals)
32+
signals)
3233
#
33-
io_signals = [ signals.find('v'), signals.find('i') ]
34-
if ms.hysteresis_function is not None:
35-
io_signals.append( signals.find('h') )
34+
io_signals = [signals.find(s)
35+
for s in ['v'] + input_symbols + (['h'] if ms.hysteresis_function else [])]
3636
#
3737
signal_tuple = io_signals, bf_signal_vector.signals, hysteresis_bf_signal_vector.signals
3838
signal_trajectories = generate_signal_trajectories(signal_tuple,
@@ -66,8 +66,8 @@ def identify_model(datasets: list[CurrentVoltageData] | CurrentVoltageData,
6666
# Shorthand for convenience
6767
ms = model_structure
6868
#
69-
if isinstance(datasets, dict):
70-
datasets = [ datasets ]
69+
if not isinstance(datasets, list):
70+
datasets = [datasets]
7171
regression_problems, regressor_labels = setup_regression_problems(datasets,
7272
model_structure,
7373
model_order,

pybatteryid/utilities.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
from .typeddicts import CurrentVoltageData
1717
from .dataclasses import VoltageFunction, Model, BasisFunction
18-
from .basisfunctions import generate_signals
18+
from .basisfunctions import Operation, generate_signals
1919
from .plotter import plot_custom
2020

2121

@@ -110,7 +110,12 @@ def save_model_to_file(model: Model, path_to_directory: str, description: str):
110110
file_name = f'{description}_n,l={model.model_order},{model.nonlinearity_order}'
111111
file_path = f'{path_to_directory}/{file_name}'
112112

113-
np.save(file_path, asdict(model)) # type: ignore
113+
model_as_dict = asdict(model)
114+
# Change enum to its value
115+
for bf in model_as_dict['basis_functions']:
116+
bf['operation'] = bf['operation'].value
117+
#
118+
np.save(file_path, model_as_dict) # type: ignore
114119

115120

116121
def load_model_from_file(file_path: str):
@@ -120,9 +125,14 @@ def load_model_from_file(file_path: str):
120125
try:
121126
model = Model(**model_as_dict)
122127
# Load basis functions
123-
model.basis_functions = [BasisFunction(**bf) for bf in model_as_dict['basis_functions']]
124-
model.hysteresis_basis_functions = [BasisFunction(**hbf)
125-
for hbf in model_as_dict['hysteresis_basis_functions']]
128+
model.basis_functions = [
129+
BasisFunction(**{**bf, 'operation': Operation(bf['operation'])})
130+
for bf in model_as_dict['basis_functions']
131+
]
132+
model.hysteresis_basis_functions = [
133+
BasisFunction(**{**hbf, 'operation': Operation(hbf['operation'])})
134+
for hbf in model_as_dict['hysteresis_basis_functions']
135+
]
126136
# Load voltage functions
127137
model.emf_function = VoltageFunction(**model_as_dict['emf_function'])
128138
if model_as_dict['hysteresis_function'] is not None:

0 commit comments

Comments
 (0)