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

Add design optimisation example #149

Merged
merged 25 commits into from
Jan 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
18c77d2
Lower parameter range
NicolaCourtier Dec 14, 2023
1cd6732
Lower test parameters
NicolaCourtier Dec 14, 2023
22526a9
Add model-specific parameter checks
NicolaCourtier Dec 14, 2023
e55eb81
Wrap cost functions for error checking
NicolaCourtier Dec 14, 2023
985fc19
Add electrode SOH functionality to echem
NicolaCourtier Dec 15, 2023
40ec760
Use predict for evaluating a DesignProblem
NicolaCourtier Dec 15, 2023
d45463c
Update plotting bounds
NicolaCourtier Dec 15, 2023
5e5c85e
Add design optimisation example
NicolaCourtier Dec 15, 2023
30520f9
Merge branch 'develop' into 38b-design-example
NicolaCourtier Dec 15, 2023
c444690
style: pre-commit fixes
pre-commit-ci[bot] Dec 15, 2023
b7d3125
Set SciPy pop_size and relax two_signal assertion
NicolaCourtier Dec 15, 2023
85ec240
Remove unused dependency
NicolaCourtier Dec 15, 2023
546a1be
Merge branch '38b-design-example' of https://github.com/pybop-team/Py…
NicolaCourtier Dec 15, 2023
9384bc7
Switch SciPy test
NicolaCourtier Dec 15, 2023
97606f0
Reduce SciPy pop size
NicolaCourtier Dec 15, 2023
7c9bb44
Scale cost for SciPyMinimize
NicolaCourtier Dec 15, 2023
5d0c60f
Switch Adam to more resilient IRPropMin
NicolaCourtier Dec 15, 2023
77a66d9
Change areal mass to area density
NicolaCourtier Dec 15, 2023
f1ec4bc
style: pre-commit fixes
pre-commit-ci[bot] Dec 15, 2023
5b0335b
Update ps to parameter_set
NicolaCourtier Dec 15, 2023
9247fa1
Merge branch '38b-design-example' of https://github.com/pybop-team/Py…
NicolaCourtier Dec 15, 2023
a53b3e1
Add Experiment class and unit test
NicolaCourtier Dec 15, 2023
b900110
style: pre-commit fixes
pre-commit-ci[bot] Dec 15, 2023
2578c36
Update comment
NicolaCourtier Dec 15, 2023
112139d
Add calc nom. voltage from half-cell ocps, np.trapz dx variable, prin…
BradyPlanden Dec 19, 2023
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- [#114](https://github.com/pybop-team/PyBOP/issues/114) - Adds a SciPy minimize example and logging for non-Pints optimisers
- [#116](https://github.com/pybop-team/PyBOP/issues/116) - Adds PSO, SNES, XNES, ADAM, and IPropMin optimisers to PintsOptimisers() class
- [#38](https://github.com/pybop-team/PyBOP/issues/38) - Restructures the Problem classes ahead of adding a design optimisation example
- [#38](https://github.com/pybop-team/PyBOP/issues/38) - Updates tests and adds a design optimisation example script `spme_max_energy`
- [#120](https://github.com/pybop-team/PyBOP/issues/120) - Updates the parameterisation test settings including the number of iterations
- [#145](https://github.com/pybop-team/PyBOP/issues/145) - Reformats Dataset to contain a dictionary and signal into a list of strings

Expand Down
10 changes: 5 additions & 5 deletions examples/scripts/spm_CMAES.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@
parameters = [
pybop.Parameter(
"Negative electrode active material volume fraction",
prior=pybop.Gaussian(0.7, 0.05),
bounds=[0.6, 0.9],
prior=pybop.Gaussian(0.6, 0.05),
bounds=[0.5, 0.8],
),
pybop.Parameter(
"Positive electrode active material volume fraction",
prior=pybop.Gaussian(0.58, 0.05),
bounds=[0.5, 0.8],
prior=pybop.Gaussian(0.48, 0.05),
bounds=[0.4, 0.7],
),
]

Expand Down Expand Up @@ -57,5 +57,5 @@
pybop.plot_cost2d(cost, steps=15)

# Plot the cost landscape with optimisation path and updated bounds
bounds = np.array([[0.6, 0.9], [0.5, 0.8]])
bounds = np.array([[0.55, 0.75], [0.48, 0.68]])
pybop.plot_cost2d(cost, optim=optim, bounds=bounds, steps=15)
8 changes: 4 additions & 4 deletions examples/scripts/spm_IRPropMin.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@
parameters = [
pybop.Parameter(
"Negative electrode active material volume fraction",
prior=pybop.Gaussian(0.7, 0.05),
bounds=[0.6, 0.9],
prior=pybop.Gaussian(0.6, 0.05),
bounds=[0.5, 0.8],
),
pybop.Parameter(
"Positive electrode active material volume fraction",
prior=pybop.Gaussian(0.58, 0.05),
bounds=[0.5, 0.8],
prior=pybop.Gaussian(0.48, 0.05),
bounds=[0.4, 0.7],
),
]

Expand Down
8 changes: 4 additions & 4 deletions examples/scripts/spm_SNES.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@
parameters = [
pybop.Parameter(
"Negative electrode active material volume fraction",
prior=pybop.Gaussian(0.7, 0.05),
bounds=[0.6, 0.9],
prior=pybop.Gaussian(0.6, 0.05),
bounds=[0.5, 0.8],
),
pybop.Parameter(
"Positive electrode active material volume fraction",
prior=pybop.Gaussian(0.58, 0.05),
bounds=[0.5, 0.8],
prior=pybop.Gaussian(0.48, 0.05),
bounds=[0.4, 0.7],
),
]

Expand Down
8 changes: 4 additions & 4 deletions examples/scripts/spm_XNES.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@
parameters = [
pybop.Parameter(
"Negative electrode active material volume fraction",
prior=pybop.Gaussian(0.7, 0.05),
bounds=[0.6, 0.9],
prior=pybop.Gaussian(0.6, 0.05),
bounds=[0.5, 0.8],
),
pybop.Parameter(
"Positive electrode active material volume fraction",
prior=pybop.Gaussian(0.58, 0.05),
bounds=[0.5, 0.8],
prior=pybop.Gaussian(0.48, 0.05),
bounds=[0.4, 0.7],
),
]

Expand Down
8 changes: 4 additions & 4 deletions examples/scripts/spm_adam.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@
parameters = [
pybop.Parameter(
"Negative electrode active material volume fraction",
prior=pybop.Gaussian(0.7, 0.05),
bounds=[0.6, 0.9],
prior=pybop.Gaussian(0.6, 0.05),
bounds=[0.5, 0.8],
),
pybop.Parameter(
"Positive electrode active material volume fraction",
prior=pybop.Gaussian(0.58, 0.05),
bounds=[0.5, 0.8],
prior=pybop.Gaussian(0.48, 0.05),
bounds=[0.4, 0.7],
),
]

Expand Down
8 changes: 4 additions & 4 deletions examples/scripts/spm_descent.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@
parameters = [
pybop.Parameter(
"Negative electrode active material volume fraction",
prior=pybop.Gaussian(0.7, 0.05),
bounds=[0.6, 0.9],
prior=pybop.Gaussian(0.6, 0.05),
bounds=[0.5, 0.8],
),
pybop.Parameter(
"Positive electrode active material volume fraction",
prior=pybop.Gaussian(0.58, 0.05),
bounds=[0.5, 0.8],
prior=pybop.Gaussian(0.48, 0.05),
bounds=[0.4, 0.7],
),
]

Expand Down
8 changes: 4 additions & 4 deletions examples/scripts/spm_nlopt.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@
parameters = [
pybop.Parameter(
"Negative electrode active material volume fraction",
prior=pybop.Gaussian(0.75, 0.05),
bounds=[0.6, 0.9],
prior=pybop.Gaussian(0.6, 0.05),
bounds=[0.5, 0.8],
),
pybop.Parameter(
"Positive electrode active material volume fraction",
prior=pybop.Gaussian(0.65, 0.05),
bounds=[0.5, 0.8],
prior=pybop.Gaussian(0.48, 0.05),
bounds=[0.4, 0.7],
),
]

Expand Down
8 changes: 4 additions & 4 deletions examples/scripts/spm_pso.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@
parameters = [
pybop.Parameter(
"Negative electrode active material volume fraction",
prior=pybop.Gaussian(0.7, 0.05),
bounds=[0.6, 0.9],
prior=pybop.Gaussian(0.6, 0.05),
bounds=[0.5, 0.8],
),
pybop.Parameter(
"Positive electrode active material volume fraction",
prior=pybop.Gaussian(0.58, 0.05),
bounds=[0.5, 0.8],
prior=pybop.Gaussian(0.48, 0.05),
bounds=[0.4, 0.7],
),
]

Expand Down
8 changes: 4 additions & 4 deletions examples/scripts/spm_scipymin.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@
parameters = [
pybop.Parameter(
"Negative electrode active material volume fraction",
prior=pybop.Gaussian(0.75, 0.05),
bounds=[0.6, 0.9],
prior=pybop.Gaussian(0.6, 0.05),
bounds=[0.5, 0.8],
),
pybop.Parameter(
"Positive electrode active material volume fraction",
prior=pybop.Gaussian(0.65, 0.05),
bounds=[0.5, 0.8],
prior=pybop.Gaussian(0.48, 0.05),
bounds=[0.4, 0.7],
),
]

Expand Down
194 changes: 194 additions & 0 deletions examples/scripts/spme_max_energy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import pybop
import numpy as np
import warnings

## NOTE: This is a brittle example, the classes and methods below will be
## integrated into pybop in a future release.

# A design optimisation example loosely based on work by L.D. Couto
# available at https://doi.org/10.1016/j.energy.2022.125966.

# The target is to maximise the gravimetric energy density over a
# range of possible design parameter values, including for example:
# cross-sectional area = height x width (only need change one)
# electrode widths, particle radii, volume fractions and
# separator width.

# Define parameter set and model
parameter_set = pybop.ParameterSet.pybamm("Chen2020")
model = pybop.lithium_ion.SPMe(parameter_set=parameter_set)

# Fitting parameters
parameters = [
pybop.Parameter(
"Positive electrode thickness [m]",
prior=pybop.Gaussian(7.56e-05, 0.05e-05),
bounds=[65e-06, 10e-05],
),
pybop.Parameter(
"Positive particle radius [m]",
prior=pybop.Gaussian(5.22e-06, 0.05e-06),
bounds=[2e-06, 9e-06],
),
]
# Define stoichiometries
sto = model._electrode_soh.get_min_max_stoichiometries(parameter_set)
mean_sto_neg = np.mean(sto[0:2])
mean_sto_pos = np.mean(sto[2:4])


# Define functions
def nominal_capacity(
x, model
): # > 50% of the simulation time is spent in this function (~0.7 sec per iteration vs ~1.1 for the forward simulation)
"""
Update the nominal capacity based on the theoretical energy density and an
average voltage.
"""
inputs = {
key: x[i] for i, key in enumerate([param.name for param in model.parameters])
}
model._parameter_set.update(inputs)

theoretical_energy = model._electrode_soh.calculate_theoretical_energy( # All of the computational time is in this line (~0.7)
model._parameter_set
)
average_voltage = model._parameter_set["Positive electrode OCP [V]"](
mean_sto_pos
) - model._parameter_set["Negative electrode OCP [V]"](mean_sto_neg)

theoretical_capacity = theoretical_energy / average_voltage
model._parameter_set.update({"Nominal cell capacity [A.h]": theoretical_capacity})


def cell_mass(parameter_set): # This is very low compute time
"""
Compute the total cell mass [kg] for the current parameter set.
"""

# Approximations due to SPM(e) parameter set limitations
electrolyte_density = parameter_set["Separator density [kg.m-3]"]

# Electrode mass densities [kg.m-3]
positive_mass_density = (
parameter_set["Positive electrode active material volume fraction"]
* parameter_set["Positive electrode density [kg.m-3]"]
)
+(parameter_set["Positive electrode porosity"] * electrolyte_density)
negative_mass_density = (
parameter_set["Negative electrode active material volume fraction"]
* parameter_set["Negative electrode density [kg.m-3]"]
)
+(parameter_set["Negative electrode porosity"] * electrolyte_density)

# Area densities [kg.m-2]
positive_area_density = (
parameter_set["Positive electrode thickness [m]"] * positive_mass_density
)
negative_area_density = (
parameter_set["Negative electrode thickness [m]"] * negative_mass_density
)
separator_area_density = (
parameter_set["Separator thickness [m]"]
* parameter_set["Separator porosity"]
* parameter_set["Separator density [kg.m-3]"]
)
positive_current_collector_area_density = (
parameter_set["Positive current collector thickness [m]"]
* parameter_set["Positive current collector density [kg.m-3]"]
)
negative_current_collector_area_density = (
parameter_set["Negative current collector thickness [m]"]
* parameter_set["Negative current collector density [kg.m-3]"]
)

# Cross-sectional area [m2]
cross_sectional_area = (
parameter_set["Electrode height [m]"] * parameter_set["Electrode width [m]"]
)

return cross_sectional_area * (
positive_area_density
+ separator_area_density
+ negative_area_density
+ positive_current_collector_area_density
+ negative_current_collector_area_density
)


# Define test protocol
experiment = pybop.Experiment(
["Discharge at 1C until 2.5 V (5 seconds period)"],
)
init_soc = 1 # start from full charge
signal = ["Voltage [V]", "Current [A]"]

# Generate problem
problem = pybop.DesignProblem(
model, parameters, experiment, signal=signal, init_soc=init_soc
)

# Update the C-rate and the example dataset
nominal_capacity(problem.x0, model)
sol = problem.evaluate(problem.x0)
problem._time_data = sol[:, -1]
BradyPlanden marked this conversation as resolved.
Show resolved Hide resolved
problem._target = sol[:, 0:-1]
dt = sol[1, -1] - sol[0, -1]


# Define cost function as a subclass
class GravimetricEnergyDensity(pybop.BaseCost):
Copy link
Member

Choose a reason for hiding this comment

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

Can this be within _costs.py?

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think it should be taken out of context at this stage.

"""
Defines the (negative*) gravimetric energy density corresponding to a
normalised 1C discharge from upper to lower voltage limits.
*The energy density is maximised by minimising the negative energy density.
"""

def __init__(self, problem):
super().__init__(problem)

def _evaluate(self, x, grad=None):
"""
Compute the cost
"""
with warnings.catch_warnings(record=True) as w:
# Update the C-rate and run the simulation
nominal_capacity(x, self.problem._model)
sol = self.problem.evaluate(x)

if any(w) and issubclass(w[-1].category, UserWarning):
# Catch infeasible parameter combinations e.g. E_Li > Q_p
print(f"ignoring this sample due to: {w[-1].message}")
return np.inf

else:
voltage = sol[:, 0]
current = sol[:, 1]
gravimetric_energy_density = -np.trapz(
voltage * current, dx=dt
) / ( # trapz over-estimates compares to pybamm (~0.5%)
3600 * cell_mass(self.problem._model._parameter_set)
)
# Return the negative energy density, as the optimiser minimises
# this function, to carry out maximisation of the energy density
return gravimetric_energy_density


# Generate cost function and optimisation class
cost = GravimetricEnergyDensity(problem)
optim = pybop.Optimisation(cost, optimiser=pybop.PSO, verbose=True)
NicolaCourtier marked this conversation as resolved.
Show resolved Hide resolved
optim.set_max_iterations(5)

# Run optimisation
x, final_cost = optim.run()
print("Estimated parameters:", x)
print(f"Initial gravimetric energy density: {-cost(cost.x0):.2f} Wh.kg-1")
print(f"Optimised gravimetric energy density: {-final_cost:.2f} Wh.kg-1")

# Plot the timeseries output
nominal_capacity(x, cost.problem._model)
pybop.quick_plot(x, cost, title="Optimised Comparison")

# Plot the cost landscape with optimisation path
if len(x) == 2:
pybop.plot_cost2d(cost, optim=optim, steps=3)
5 changes: 5 additions & 0 deletions pybop/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@
from .models import lithium_ion
from .models import empirical

#
# Experiment class
#
from ._experiment import Experiment

#
# Main optimisation class
#
Expand Down
Loading
Loading