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

Adds EIS prediction methods #405

Merged
merged 39 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
8d606fd
Initial eis classes
BradyPlanden Jul 7, 2024
b364d86
feat: Added EISProblem, refactored dataset.check for varying data dom…
BradyPlanden Jul 7, 2024
280ed7e
Merge branch '312-bug-synchronise-pybop-with-pybammdevelop' into 269-…
BradyPlanden Jul 10, 2024
c8ebf13
refactor: integration of pbeis functionality
BradyPlanden Jul 10, 2024
12cdfde
feat: integrate eis predictions into PyBOP model building, switch lin…
BradyPlanden Jul 11, 2024
3c9ce9a
refactor: methods for SimulateEIS, updt. fitting_example
BradyPlanden Jul 11, 2024
521f1ea
fix: add model build for eis predictions if not built
BradyPlanden Jul 22, 2024
02176ec
Merge branch 'develop' into 269-add-eis-parameter-identification-methods
BradyPlanden Jul 22, 2024
c77f82a
feat: adds support for param identification for EIS models, updts 'ti…
BradyPlanden Jul 22, 2024
57c1d4f
feat: adds support for param identification for EIS models, updts 'ti…
BradyPlanden Jul 22, 2024
1711679
feat: improved nyquist plot
BradyPlanden Jul 22, 2024
01a0f2c
Equal axis scaling for Nyquist plots
NicolaCourtier Jul 23, 2024
d6e0eef
Merge branch 'develop' into 269-add-eis-parameter-identification-methods
NicolaCourtier Jul 23, 2024
8b51fbc
style: pre-commit fixes
pre-commit-ci[bot] Jul 23, 2024
f8bfd04
Merge branch 'develop' into 269-add-eis-parameter-identification-methods
NicolaCourtier Aug 6, 2024
5b36c03
Align EISProblem.evaluate
NicolaCourtier Aug 6, 2024
1ffe866
Enable contact resistance
NicolaCourtier Aug 7, 2024
2d213ff
Formatting
NicolaCourtier Aug 7, 2024
294ae36
Merge branch 'develop' into 269-add-eis-parameter-identification-methods
NicolaCourtier Aug 7, 2024
0698db1
Merge branch 'refs/heads/develop' into 269-add-eis-parameter-identifi…
BradyPlanden Aug 14, 2024
bd6a820
fix: post merge additions
BradyPlanden Aug 14, 2024
6203281
feat: removes EISProblem, updates FittingProblem
BradyPlanden Aug 14, 2024
958dfa0
tests: adds eis unit tests, refactors
BradyPlanden Aug 14, 2024
b6375f9
refactor: SymbolReplacer for clarity, adds pre-commit skips for req. …
BradyPlanden Aug 14, 2024
e571b8d
tests: Adds nyquist plot tests
BradyPlanden Aug 15, 2024
563737e
tests: adds integration tests for eis fitting, update problem.evaluat…
BradyPlanden Aug 16, 2024
d8f0be9
lychee - add exclusion link for w3.org, increase n_frequency for eis …
BradyPlanden Aug 19, 2024
000ba88
lychee fix, increase increase eis iterations arg, remove try-except
BradyPlanden Aug 19, 2024
bab82a6
Merge branch 'refs/heads/develop' into 269-add-eis-parameter-identifi…
BradyPlanden Aug 19, 2024
823cc4b
tests: increase coverage, try-except, infeasible parameter values
BradyPlanden Aug 19, 2024
9b84c1e
tests: adds direct try-except coverage, w/ verbose
BradyPlanden Aug 19, 2024
100d65e
tests: adds tests for frequency dataset.check(), nyquist w/o inputs
BradyPlanden Aug 19, 2024
beb28d1
add changelog entry, remove redundant builds()
BradyPlanden Aug 20, 2024
4bf4e76
Merge branch 'develop' into 269-add-eis-parameter-identification-methods
BradyPlanden Aug 20, 2024
d66e206
Merge branch 'refs/heads/develop' into 269-add-eis-parameter-identifi…
BradyPlanden Aug 21, 2024
b53a95b
fix: missed merge items, update docstrings
BradyPlanden Aug 21, 2024
904fd77
adds Thevenin EIS args, small cleanup
BradyPlanden Aug 21, 2024
f6530d5
fix: extends eis integration parameter bounds, clips ground truth sam…
BradyPlanden Aug 21, 2024
6a6b7dd
tests: removes sigma assert for GaussianLogLikehood w/ eis parameteri…
BradyPlanden Aug 21, 2024
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
1 change: 1 addition & 0 deletions .github/workflows/lychee_links.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ jobs:
--exclude "https://a.tile.openstreetmap.org/*"
--exclude "https://openstreetmap.org/*|https://www.openstreetmap.org/*"
--exclude "https://cdn.plot.ly/*"
--exclude "http://www.w3.org/*|https://www.w3.org/*"
--exclude "https://doi.org/*"
--exclude-path ./CHANGELOG.md
--exclude-path asv.conf.json
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Features

- [#405](https://github.com/pybop-team/PyBOP/pull/405) - Adds frequency-domain based EIS prediction methods via `model.simulateEIS` and updates to `problem.evaluate` with examples and tests.
- [#460](https://github.com/pybop-team/PyBOP/pull/460) - Notebook example files added for ECM and folder structure updated.
- [#450](https://github.com/pybop-team/PyBOP/pull/450) - Adds support for IDAKLU with output variables, and corresponding examples, tests.
- [#364](https://github.com/pybop-team/PyBOP/pull/364) - Adds the MultiFittingProblem class and the multi_fitting example script.
Expand Down
82 changes: 82 additions & 0 deletions examples/scripts/eis_fitting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import numpy as np

import pybop

# Define model
parameter_set = pybop.ParameterSet.pybamm("Chen2020")
parameter_set["Contact resistance [Ohm]"] = 0.0
initial_state = {"Initial SoC": 0.5}
n_frequency = 20
sigma0 = 1e-4
f_eval = np.logspace(-4, 5, n_frequency)
model = pybop.lithium_ion.SPM(
parameter_set=parameter_set,
eis=True,
options={"surface form": "differential", "contact resistance": "true"},
)

# Create synthetic data for parameter inference
sim = model.simulateEIS(
inputs={
"Negative electrode active material volume fraction": 0.531,
"Positive electrode active material volume fraction": 0.732,
},
f_eval=f_eval,
initial_state=initial_state,
)

# Fitting parameters
parameters = pybop.Parameters(
pybop.Parameter(
"Negative electrode active material volume fraction",
prior=pybop.Uniform(0.4, 0.75),
bounds=[0.375, 0.75],
),
pybop.Parameter(
"Positive electrode active material volume fraction",
prior=pybop.Uniform(0.4, 0.75),
bounds=[0.375, 0.75],
),
)


def noise(sigma, values):
# Generate real part noise
real_noise = np.random.normal(0, sigma, values)

# Generate imaginary part noise
imag_noise = np.random.normal(0, sigma, values)

# Combine them into a complex noise
return real_noise + 1j * imag_noise


# Form dataset
dataset = pybop.Dataset(
{
"Frequency [Hz]": f_eval,
"Current function [A]": np.ones(n_frequency) * 0.0,
"Impedance": sim["Impedance"] + noise(sigma0, len(sim["Impedance"])),
}
)

signal = ["Impedance"]
# Generate problem, cost function, and optimisation class
problem = pybop.FittingProblem(model, parameters, dataset, signal=signal)
cost = pybop.GaussianLogLikelihoodKnownSigma(problem, sigma0=sigma0)
optim = pybop.CMAES(cost, max_iterations=100, sigma0=0.25, max_unchanged_iterations=30)

x, final_cost = optim.run()
print("Estimated parameters:", x)

# Plot the nyquist
pybop.nyquist(problem, problem_inputs=x, title="Optimised Comparison")

# Plot convergence
pybop.plot_convergence(optim)

# Plot the parameter traces
pybop.plot_parameters(optim)

# Plot 2d landscape
pybop.plot2d(optim, steps=10)
2 changes: 1 addition & 1 deletion examples/scripts/exp_UKF.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
# Verification step: make another prediction using the Observer class
model.build(parameters=parameters)
simulator = pybop.Observer(parameters, model, signal=["2y"])
simulator.time_data = t_eval
simulator.domain_data = t_eval
measurements = simulator.evaluate(true_inputs)

# Verification step: Compare by plotting
Expand Down
2 changes: 1 addition & 1 deletion examples/scripts/spm_IRPropMin.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@

# Generate problem, cost function, and optimisation class
problem = pybop.FittingProblem(model, parameters, dataset)
cost = pybop.SumSquaredError(problem)
cost = pybop.Minkowski(problem, p=2)
optim = pybop.IRPropMin(cost, max_iterations=100)

x, final_cost = optim.run()
Expand Down
18 changes: 9 additions & 9 deletions examples/standalone/problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def __init__(
check_model=True,
signal=None,
additional_variables=None,
init_soc=None,
initial_state=None,
):
super().__init__(parameters, model, check_model, signal, additional_variables)
self._dataset = dataset.data
Expand All @@ -26,15 +26,15 @@ def __init__(
if name not in self._dataset:
raise ValueError(f"expected {name} in list of dataset")

self._time_data = self._dataset["Time [s]"]
self.n_time_data = len(self._time_data)
if np.any(self._time_data < 0):
self._domain_data = self._dataset[self.domain]
self.n_data = len(self._domain_data)
if np.any(self._domain_data < 0):
raise ValueError("Times can not be negative.")
if np.any(self._time_data[:-1] >= self._time_data[1:]):
if np.any(self._domain_data[:-1] >= self._domain_data[1:]):
raise ValueError("Times must be increasing.")

for signal in self.signal:
if len(self._dataset[signal]) != self.n_time_data:
if len(self._dataset[signal]) != self.n_data:
raise ValueError(
f"Time data and {signal} data must be the same length."
)
Expand All @@ -56,7 +56,7 @@ def evaluate(self, inputs, **kwargs):
"""

return {
signal: inputs["Gradient"] * self._time_data + inputs["Intercept"]
signal: inputs["Gradient"] * self._domain_data + inputs["Intercept"]
for signal in self.signal
}

Expand All @@ -78,7 +78,7 @@ def evaluateS1(self, inputs):

y = self.evaluate(inputs)

dy = np.zeros((self.n_time_data, self.n_outputs, self.n_parameters))
dy[:, 0, 0] = self._time_data
dy = np.zeros((self.n_data, self.n_outputs, self.n_parameters))
dy[:, 0, 0] = self._domain_data

return (y, dy)
3 changes: 2 additions & 1 deletion pybop/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
#
# Utilities
#
from ._utils import is_numeric
from ._utils import is_numeric, SymbolReplacer

#
# Experiment class
Expand Down Expand Up @@ -157,6 +157,7 @@
from .plotting.plot_convergence import plot_convergence
from .plotting.plot_parameters import plot_parameters
from .plotting.plot_problem import quick_plot
from .plotting.nyquist import nyquist

#
# Remove any imported modules, so we don't expose them as part of pybop
Expand Down
73 changes: 51 additions & 22 deletions pybop/_dataset.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Union

import numpy as np
from pybamm import solvers

Expand Down Expand Up @@ -76,41 +78,68 @@ def __getitem__(self, key):

return self.data[key]

def check(self, signal=None):
def check(self, domain: str = None, signal: Union[str, list[str]] = None) -> bool:
"""
Check the consistency of a PyBOP Dataset against the expected format.

Parameters
----------
domain : str, optional
The domain of the dataset. Defaults to "Time [s]".
signal : str or List[str], optional
The signal(s) to check. Defaults to ["Voltage [V]"].

Returns
-------
bool
If True, the dataset has the expected attributes.
True if the dataset has the expected attributes.

Raises
------
ValueError
If the time series and the data series are not consistent.
"""
if signal is None:
signal = ["Voltage [V]"]
if isinstance(signal, str):
signal = [signal]

# Check that the dataset contains time and chosen signal
for name in ["Time [s]", *signal]:
if name not in self.names:
raise ValueError(f"expected {name} in list of dataset")

# Check for increasing times
time_data = self.data["Time [s]"]
self.domain = domain or "Time [s]"
signals = [signal] if isinstance(signal, str) else (signal or ["Voltage [V]"])

# Check that the dataset contains domain and chosen signals
missing_attributes = set([self.domain, *signals]) - set(self.names)
if missing_attributes:
raise ValueError(
f"Expected {', '.join(missing_attributes)} in list of dataset"
)

domain_data = self.data[self.domain]

# Check domain-specific constraints
if self.domain == "Time [s]":
self._check_time_constraints(domain_data)
elif self.domain == "Frequency [Hz]":
self._check_frequency_constraints(domain_data)

# Check for consistent data length
self._check_data_consistency(domain_data, signals)

return True

@staticmethod
def _check_time_constraints(time_data: np.ndarray) -> None:
if np.any(time_data < 0):
raise ValueError("Times can not be negative.")
raise ValueError("Times cannot be negative.")
if np.any(time_data[:-1] >= time_data[1:]):
raise ValueError("Times must be increasing.")

# Check for consistent data
n_time_data = len(time_data)
for s in signal:
if len(self.data[s]) != n_time_data:
raise ValueError(f"Time data and {s} data must be the same length.")

return True
@staticmethod
def _check_frequency_constraints(freq_data: np.ndarray) -> None:
if np.any(freq_data < 0):
raise ValueError("Frequencies cannot be negative.")

def _check_data_consistency(
self, domain_data: np.ndarray, signals: list[str]
) -> None:
n_domain_data = len(domain_data)
for s in signals:
if len(self.data[s]) != n_domain_data:
raise ValueError(
f"{self.domain} data and {s} data must be the same length."
)
Loading
Loading