`_ for guidelines.
59 |
--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
1 | # Examples
2 |
3 | This directory contains example notebooks and scripts demonstrating how to use PyBOP.
4 |
5 | ## Directory Structure
6 |
7 | - `notebooks/`: Jupyter notebooks of example functionality with explanations
8 | - `scripts/`: Python scripts for quick reference and command-line usage
9 | - `standalone/`: Example scripts for using standalone classes
10 |
11 | ## Notebooks
12 |
13 | The `notebooks/` directory contains Jupyter notebooks that provide detailed, interactive examples of various features and use cases. These notebooks include explanations, code snippets, and visualisations.
14 |
15 | To view the notebooks with interactive figures without downloading the repository, please use nbviewer:
16 |
17 |
18 |
19 | [Notebooks on nbviewer](https://nbviewer.org/github/pybop-team/PyBOP/tree/develop/examples/notebooks/)
20 |
21 |
22 |
23 | ## Scripts
24 |
25 | The `scripts/` directory contains standalone Python scripts that demonstrate specific tasks or workflows. These scripts are designed for quick reference and can be run directly from the command line.
26 |
27 | ## Getting Started
28 |
29 | 1. Clone the repository: `git clone https://github.com/pybop-team/pybop.git`
30 | 2. Navigate to the examples directory: `cd pybop/examples`
31 | 3. Explore the notebooks and scripts in their respective directories.
32 | 4. To run the Jupyter notebooks locally:
33 | - Install Jupyter: `pip install jupyter`
34 | - Start Jupyter Notebook: `jupyter notebook`
35 | - Navigate to the `notebooks/` directory and open the desired notebook
36 |
37 | 5. To run the Python scripts:
38 | - Install PyBOP: `pip install pybop`
39 | - Run a script using Python: `python scripts/script_name.py`
40 |
41 | ## Contributing
42 |
43 | If you have additional examples or improvements to existing ones, please feel free to submit a pull request. We appreciate your contributions!
44 |
--------------------------------------------------------------------------------
/examples/data/LG_M50_ECM/data/LGM50_5Ah_OCV.mat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/examples/data/LG_M50_ECM/data/LGM50_5Ah_OCV.mat
--------------------------------------------------------------------------------
/examples/data/LG_M50_ECM/data/LGM50_5Ah_Pulse.mat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/examples/data/LG_M50_ECM/data/LGM50_5Ah_Pulse.mat
--------------------------------------------------------------------------------
/examples/data/LG_M50_ECM/data/LGM50_5Ah_RateTest.mat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/examples/data/LG_M50_ECM/data/LGM50_5Ah_RateTest.mat
--------------------------------------------------------------------------------
/examples/data/README.md:
--------------------------------------------------------------------------------
1 | ## Data directory
2 | This directory contains both the experimental and synthetic data used in the examples.
3 |
--------------------------------------------------------------------------------
/examples/data/Samsung_INR21700/multipulse_hppc.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/examples/data/Samsung_INR21700/multipulse_hppc.xlsx
--------------------------------------------------------------------------------
/examples/data/Samsung_INR21700/sample_drive_cycle.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/examples/data/Samsung_INR21700/sample_drive_cycle.xlsx
--------------------------------------------------------------------------------
/examples/data/Samsung_INR21700/sample_hppc_pulse.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/examples/data/Samsung_INR21700/sample_hppc_pulse.xlsx
--------------------------------------------------------------------------------
/examples/data/Tesla_4680/README.md:
--------------------------------------------------------------------------------
1 | # Tesla 4680 Dataset
2 |
3 | This repository contains a subset of the data from the supplementary material of the research paper:
4 | **M. Ank et al., "Lithium-Ion Cells in Automotive Applications: Tesla 4680 Cylindrical Cell Teardown and Characterization,"** *Journal of the Electrochemical Society*, vol. 170, no. 12, p. 120536, December 2023. [DOI: 10.1149/1945-7111/ad14d0](https://doi.org/10.1149/1945-7111/ad14d0)
5 |
6 | ## Accessing the Supplemental Material
7 |
8 | The supplemental material can be accessed through the following link:
9 | [Supplemental Material on MediaTUM](https://mediatum.ub.tum.de/1725661)
10 |
11 |
12 | ## Citation
13 |
14 | If you use this dataset in your work, please cite the original publication as follows:
15 |
16 | ```plaintext
17 | M. Ank et al., "Lithium-Ion Cells in Automotive Applications: Tesla 4680 Cylindrical Cell Teardown and Characterization," Journal of the Electrochemical Society, vol. 170, no. 12, p. 120536, Dec. 2023, doi: 10.1149/1945-7111/ad14d0.
18 |
--------------------------------------------------------------------------------
/examples/data/synthetic/README.md:
--------------------------------------------------------------------------------
1 | # Data description
2 | The data files in this directory have the following metadata
3 |
4 | `{model}_charge_discharge_{soc}.csv`
5 | - Time, current, terminal voltage, and open-circuit voltage data for a Chen2020 parameters at the corresponding SOC. 0.5mV of noise is applied to both voltage signals.
6 | - See `discharge_charge_data_gen.py` for reference
7 |
8 | `{model}_pulse_{soc}.csv`
9 | - Time, current, terminal voltage, and open-circuit voltage data for a Chen2020 parameters at the corresponding SOC. 0.5mV of noise is applied to both voltage signals.
10 | - See `pulse_data_gen.py` for reference
11 |
--------------------------------------------------------------------------------
/examples/data/synthetic/discharge_charge_data_gen.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import pandas as pd
3 | import pybamm
4 |
5 | import pybop
6 |
7 | # Define model and use high-performant solver for sensitivities
8 | solver = pybamm.CasadiSolver(atol=1e-7, rtol=1e-7)
9 | parameter_set = pybop.ParameterSet("Chen2020")
10 | models = [
11 | (pybop.lithium_ion.DFN(parameter_set=parameter_set, solver=solver), "dfn"),
12 | (pybop.lithium_ion.SPMe(parameter_set=parameter_set, solver=solver), "spme"),
13 | (pybop.lithium_ion.SPM(parameter_set=parameter_set, solver=solver), "spm"),
14 | ]
15 |
16 | # Generate data
17 | sigma = 5e-4
18 | soc = 0.75
19 | experiment = pybop.Experiment(
20 | [
21 | "Rest for 2 seconds (1 second period)",
22 | "Discharge at 0.5C for 40 minutes (20 second period)",
23 | "Charge at 0.5C for 20 minutes (20 second period)",
24 | ]
25 | )
26 | for model, name in models:
27 | values = model.predict(
28 | initial_state={"Initial SoC": soc},
29 | experiment=experiment,
30 | parameter_set=parameter_set,
31 | )
32 |
33 | def noise(sigma, dict_obj):
34 | return np.random.normal(0, sigma, len(dict_obj["Voltage [V]"].data))
35 |
36 | pd.DataFrame(
37 | {
38 | "Time [s]": np.round(values["Time [s]"].data, decimals=5),
39 | "Current function [A]": values["Current [A]"].data,
40 | "Voltage [V]": values["Voltage [V]"].data + noise(sigma, values),
41 | "Bulk open-circuit voltage [V]": values[
42 | "Bulk open-circuit voltage [V]"
43 | ].data
44 | + noise(sigma, values),
45 | }
46 | ).drop_duplicates(subset=["Time [s]"]).to_csv(
47 | f"{name}_charge_discharge_{int(soc * 100)}.csv", index=False
48 | )
49 |
--------------------------------------------------------------------------------
/examples/data/synthetic/pulse_data_gen.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import pandas as pd
3 | import pybamm
4 |
5 | import pybop
6 |
7 | # Define model and use high-performant solver for sensitivities
8 | solver = pybamm.CasadiSolver(atol=1e-7, rtol=1e-7)
9 | parameter_set = pybop.ParameterSet("Chen2020")
10 | models = [
11 | (pybop.lithium_ion.DFN(parameter_set=parameter_set, solver=solver), "dfn"),
12 | (pybop.lithium_ion.SPMe(parameter_set=parameter_set, solver=solver), "spme"),
13 | (pybop.lithium_ion.SPM(parameter_set=parameter_set, solver=solver), "spm"),
14 | ]
15 |
16 | # Generate data
17 | sigma = 5e-4
18 | soc = [0.15, 0.5]
19 | experiment = pybop.Experiment(
20 | [
21 | "Rest for 2 seconds (1 second period)",
22 | "Discharge at 0.1C for 1 minute (2 second period)",
23 | "Rest for 10 minutes (8 second period)",
24 | "Charge at 0.1C for 1 minute (2 second period)",
25 | "Rest for 10 minutes (8 second period)",
26 | ]
27 | )
28 | for model, name in models:
29 | for s in soc:
30 | values = model.predict(
31 | initial_state={"Initial SoC": s},
32 | experiment=experiment,
33 | parameter_set=parameter_set,
34 | )
35 |
36 | def noise(sigma, dict_obj):
37 | return np.random.normal(0, sigma, len(dict_obj["Voltage [V]"].data))
38 |
39 | pd.DataFrame(
40 | {
41 | "Time [s]": np.round(values["Time [s]"].data, decimals=5),
42 | "Current function [A]": values["Current [A]"].data,
43 | "Voltage [V]": values["Voltage [V]"].data + noise(sigma, values),
44 | "Bulk open-circuit voltage [V]": values[
45 | "Bulk open-circuit voltage [V]"
46 | ].data
47 | + noise(sigma, values),
48 | }
49 | ).drop_duplicates(subset=["Time [s]"]).to_csv(
50 | f"{name}_pulse_{int(s * 100)}.csv", index=False
51 | )
52 |
--------------------------------------------------------------------------------
/examples/parameters/initial_ecm_parameters.json:
--------------------------------------------------------------------------------
1 | {
2 | "chemistry": "ecm",
3 | "Initial SoC": 0.5,
4 | "Initial temperature [K]": 298.15,
5 | "Cell capacity [A.h]": 5,
6 | "Nominal cell capacity [A.h]": 5,
7 | "Ambient temperature [K]": 298.15,
8 | "Current function [A]": 5,
9 | "Upper voltage cut-off [V]": 4.2,
10 | "Lower voltage cut-off [V]": 3.0,
11 | "Cell thermal mass [J/K]": 1000,
12 | "Cell-jig heat transfer coefficient [W/K]": 10,
13 | "Jig thermal mass [J/K]": 500,
14 | "Jig-air heat transfer coefficient [W/K]": 10,
15 | "R0 [Ohm]": 0.001,
16 | "Element-1 initial overpotential [V]": 0,
17 | "Element-2 initial overpotential [V]": 0,
18 | "R1 [Ohm]": 0.0002,
19 | "R2 [Ohm]": 0.0003,
20 | "C1 [F]": 10000,
21 | "C2 [F]": 5000,
22 | "Entropic change [V/K]": 0.0004
23 | }
24 |
--------------------------------------------------------------------------------
/examples/scripts/battery_parameterisation/full_cell_balancing.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 |
3 | import pybop
4 |
5 | # Parameter set definition
6 | parameter_set = pybop.ParameterSet("Chen2020")
7 | parameter_set["Lower voltage cut-off [V]"] = 2.3
8 | parameter_set["Upper voltage cut-off [V]"] = 4.4
9 |
10 | # Set initial state and unpack true values
11 | parameter_set.parameter_values.set_initial_stoichiometries(initial_value=1.0)
12 | cs_n_max = parameter_set["Maximum concentration in negative electrode [mol.m-3]"]
13 | cs_p_max = parameter_set["Maximum concentration in positive electrode [mol.m-3]"]
14 | cs_n_init = parameter_set["Initial concentration in negative electrode [mol.m-3]"]
15 | cs_p_init = parameter_set["Initial concentration in positive electrode [mol.m-3]"]
16 |
17 | # Model definition
18 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set)
19 |
20 | # Define fitting parameters for OCP balancing
21 | parameters = pybop.Parameters(
22 | pybop.Parameter(
23 | "Maximum concentration in negative electrode [mol.m-3]",
24 | prior=pybop.Gaussian(cs_n_max, 6e3),
25 | bounds=[cs_n_max * 0.75, cs_n_max * 1.25],
26 | true_value=cs_n_max,
27 | initial_value=cs_n_max * 0.8,
28 | ),
29 | pybop.Parameter(
30 | "Maximum concentration in positive electrode [mol.m-3]",
31 | prior=pybop.Gaussian(cs_p_max, 6e3),
32 | bounds=[cs_p_max * 0.75, cs_p_max * 1.25],
33 | true_value=cs_p_max,
34 | initial_value=cs_p_max * 0.8,
35 | ),
36 | pybop.Parameter(
37 | "Initial concentration in negative electrode [mol.m-3]",
38 | prior=pybop.Gaussian(cs_n_init, 6e3),
39 | bounds=[cs_n_max * 0.75, cs_n_max * 1.25],
40 | true_value=cs_n_init,
41 | initial_value=cs_n_max * 0.8,
42 | ),
43 | pybop.Parameter(
44 | "Initial concentration in positive electrode [mol.m-3]",
45 | prior=pybop.Gaussian(cs_p_init, 6e3),
46 | bounds=[0, cs_p_max * 0.5],
47 | true_value=cs_p_init,
48 | initial_value=cs_p_max * 0.2,
49 | ),
50 | )
51 |
52 | # Generate synthetic data
53 | sigma = 5e-4 # Volts
54 | experiment = pybop.Experiment(
55 | [
56 | "Discharge at 0.1C until 2.5V (3 min period)",
57 | "Charge at 0.1C until 4.2V (3 min period)",
58 | ]
59 | )
60 | values = model.predict(experiment=experiment)
61 |
62 |
63 | def noisy(data, sigma):
64 | return data + np.random.normal(0, sigma, len(data))
65 |
66 |
67 | # Form dataset
68 | dataset = pybop.Dataset(
69 | {
70 | "Time [s]": values["Time [s]"].data,
71 | "Current function [A]": values["Current [A]"].data,
72 | "Voltage [V]": noisy(values["Voltage [V]"].data, sigma),
73 | }
74 | )
75 |
76 | # Generate problem, cost function, and optimisation class
77 | problem = pybop.FittingProblem(model, parameters, dataset)
78 | cost = pybop.GaussianLogLikelihoodKnownSigma(problem, sigma0=sigma)
79 | optim = pybop.SciPyMinimize(cost, max_iterations=125)
80 |
81 | # Run optimisation for Maximum Likelihood Estimate (MLE)
82 | results = optim.run()
83 | print("True parameters:", parameters.true_value())
84 |
85 | # Plot the timeseries output
86 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison")
87 |
88 | # Plot convergence
89 | pybop.plot.convergence(optim)
90 |
91 | # Plot the parameter traces
92 | pybop.plot.parameters(optim)
93 |
94 | # Plot the cost landscape with optimisation path
95 | pybop.plot.contour(optim, steps=5)
96 |
--------------------------------------------------------------------------------
/examples/scripts/battery_parameterisation/gitt_pulse.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 |
3 | import pybop
4 |
5 | # Define model
6 | parameter_set = pybop.ParameterSet("Xu2019")
7 | model = pybop.lithium_ion.SPMe(
8 | parameter_set=parameter_set, options={"working electrode": "positive"}
9 | )
10 |
11 | # Generate data
12 | sigma = 1e-3
13 | initial_state = {"Initial SoC": 0.9}
14 | experiment = pybop.Experiment(
15 | [
16 | "Rest for 1 second",
17 | "Discharge at 1C for 10 minutes (10 second period)",
18 | "Rest for 20 minutes",
19 | ]
20 | )
21 | values = model.predict(initial_state=initial_state, experiment=experiment)
22 | corrupt_values = values["Voltage [V]"].data + np.random.normal(
23 | 0, sigma, len(values["Voltage [V]"].data)
24 | )
25 |
26 | # Form dataset
27 | dataset = pybop.Dataset(
28 | {
29 | "Time [s]": values["Time [s]"].data,
30 | "Current function [A]": values["Current [A]"].data,
31 | "Discharge capacity [A.h]": values["Discharge capacity [A.h]"].data,
32 | "Voltage [V]": corrupt_values,
33 | }
34 | )
35 |
36 | # Define parameter set
37 | parameter_set = pybop.lithium_ion.SPDiffusion.apply_parameter_grouping(
38 | model.parameter_set, electrode="positive"
39 | )
40 |
41 | # Fit the GITT pulse using the single particle diffusion model
42 | gitt_fit = pybop.GITTPulseFit(
43 | gitt_pulse=dataset,
44 | parameter_set=parameter_set,
45 | electrode="positive",
46 | )
47 | gitt_results = gitt_fit()
48 |
49 | # Plot the timeseries output
50 | pybop.plot.problem(
51 | gitt_fit.problem, problem_inputs=gitt_results.x, title="Optimised Comparison"
52 | )
53 |
54 | # Plot convergence
55 | pybop.plot.convergence(gitt_fit.optim)
56 |
57 | # Plot the parameter traces
58 | pybop.plot.parameters(gitt_fit.optim)
59 |
--------------------------------------------------------------------------------
/examples/scripts/battery_parameterisation/simple_ecm.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 |
3 | import pybop
4 |
5 | # Import the ECM parameter set from JSON
6 | parameter_set = pybop.ParameterSet(
7 | json_path="examples/parameters/initial_ecm_parameters.json"
8 | )
9 |
10 | # Alternatively, define the initial parameter set with a dictionary
11 | # Add definitions for R's, C's, and initial overpotentials for any additional RC elements
12 | # parameter_set = pybop.ParameterSet(
13 | # params_dict={
14 | # "chemistry": "ecm",
15 | # "Initial SoC": 0.5,
16 | # "Initial temperature [K]": 25 + 273.15,
17 | # "Cell capacity [A.h]": 5,
18 | # "Nominal cell capacity [A.h]": 5,
19 | # "Ambient temperature [K]": 25 + 273.15,
20 | # "Current function [A]": 5,
21 | # "Upper voltage cut-off [V]": 4.2,
22 | # "Lower voltage cut-off [V]": 3.0,
23 | # "Cell thermal mass [J/K]": 1000,
24 | # "Cell-jig heat transfer coefficient [W/K]": 10,
25 | # "Jig thermal mass [J/K]": 500,
26 | # "Jig-air heat transfer coefficient [W/K]": 10,
27 | # "Open-circuit voltage [V]": pybop.empirical.Thevenin().default_parameter_values[
28 | # "Open-circuit voltage [V]"
29 | # ],
30 | # "R0 [Ohm]": 0.001,
31 | # "Element-1 initial overpotential [V]": 0,
32 | # "Element-2 initial overpotential [V]": 0,
33 | # "R1 [Ohm]": 0.0002,
34 | # "R2 [Ohm]": 0.0003,
35 | # "C1 [F]": 10000,
36 | # "C2 [F]": 5000,
37 | # "Entropic change [V/K]": 0.0004,
38 | # }
39 | # )
40 |
41 | # Define the model
42 | model = pybop.empirical.Thevenin(
43 | parameter_set=parameter_set, options={"number of rc elements": 2}
44 | )
45 |
46 | # Fitting parameters
47 | parameters = pybop.Parameters(
48 | pybop.Parameter(
49 | "R0 [Ohm]",
50 | prior=pybop.Gaussian(0.0002, 0.0001),
51 | bounds=[1e-4, 1e-2],
52 | ),
53 | pybop.Parameter(
54 | "R1 [Ohm]",
55 | prior=pybop.Gaussian(0.0001, 0.0001),
56 | bounds=[1e-5, 1e-2],
57 | ),
58 | )
59 |
60 | sigma = 0.001
61 | t_eval = np.arange(0, 900, 3)
62 | values = model.predict(t_eval=t_eval)
63 | corrupt_values = values["Voltage [V]"].data + np.random.normal(0, sigma, len(t_eval))
64 |
65 | # Form dataset
66 | dataset = pybop.Dataset(
67 | {
68 | "Time [s]": t_eval,
69 | "Current function [A]": values["Current [A]"].data,
70 | "Voltage [V]": corrupt_values,
71 | }
72 | )
73 |
74 | # Generate problem, cost function, and optimisation class
75 | problem = pybop.FittingProblem(model, parameters, dataset)
76 | cost = pybop.SumSquaredError(problem)
77 | optim = pybop.CMAES(cost, max_iterations=100)
78 |
79 | results = optim.run()
80 |
81 | # Export the parameters to JSON
82 | parameter_set.export_parameters(
83 | "examples/parameters/fit_ecm_parameters.json", fit_params=parameters
84 | )
85 |
86 | # Plot the time series
87 | pybop.plot.dataset(dataset)
88 |
89 | # Plot the timeseries output
90 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison")
91 |
92 | # Plot convergence
93 | pybop.plot.convergence(optim)
94 |
95 | # Plot the parameter traces
96 | pybop.plot.parameters(optim)
97 |
98 | # Plot the cost landscape
99 | pybop.plot.surface(optim)
100 |
--------------------------------------------------------------------------------
/examples/scripts/battery_parameterisation/simple_eis.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 |
3 | import pybop
4 |
5 | # Define model
6 | parameter_set = pybop.ParameterSet("Chen2020")
7 | parameter_set["Contact resistance [Ohm]"] = 0.0
8 | initial_state = {"Initial SoC": 0.5}
9 | n_frequency = 20
10 | sigma0 = 1e-4
11 | f_eval = np.logspace(-4, 5, n_frequency)
12 | model = pybop.lithium_ion.SPM(
13 | parameter_set=parameter_set,
14 | eis=True,
15 | options={"surface form": "differential", "contact resistance": "true"},
16 | )
17 |
18 | # Create synthetic data for parameter inference
19 | sim = model.simulateEIS(
20 | inputs={
21 | "Negative electrode active material volume fraction": 0.531,
22 | "Positive electrode active material volume fraction": 0.732,
23 | },
24 | f_eval=f_eval,
25 | initial_state=initial_state,
26 | )
27 |
28 | # Fitting parameters
29 | parameters = pybop.Parameters(
30 | pybop.Parameter(
31 | "Negative electrode active material volume fraction",
32 | prior=pybop.Uniform(0.4, 0.75),
33 | bounds=[0.375, 0.75],
34 | ),
35 | pybop.Parameter(
36 | "Positive electrode active material volume fraction",
37 | prior=pybop.Uniform(0.4, 0.75),
38 | bounds=[0.375, 0.75],
39 | ),
40 | )
41 |
42 |
43 | def noisy(data, sigma):
44 | # Generate real part noise
45 | real_noise = np.random.normal(0, sigma, len(data))
46 |
47 | # Generate imaginary part noise
48 | imag_noise = np.random.normal(0, sigma, len(data))
49 |
50 | # Combine them into a complex noise
51 | return data + real_noise + 1j * imag_noise
52 |
53 |
54 | # Form dataset
55 | dataset = pybop.Dataset(
56 | {
57 | "Frequency [Hz]": f_eval,
58 | "Current function [A]": np.ones(n_frequency) * 0.0,
59 | "Impedance": noisy(sim["Impedance"], sigma0),
60 | }
61 | )
62 |
63 | signal = ["Impedance"]
64 | # Generate problem, cost function, and optimisation class
65 | problem = pybop.FittingProblem(model, parameters, dataset, signal=signal)
66 | cost = pybop.GaussianLogLikelihoodKnownSigma(problem, sigma0=sigma0)
67 | optim = pybop.CMAES(cost, max_iterations=100, sigma0=0.25, max_unchanged_iterations=30)
68 |
69 | results = optim.run()
70 |
71 | # Plot the nyquist
72 | pybop.plot.nyquist(problem, problem_inputs=results.x, title="Optimised Comparison")
73 |
74 | # Plot convergence
75 | pybop.plot.convergence(optim)
76 |
77 | # Plot the parameter traces
78 | pybop.plot.parameters(optim)
79 |
80 | # Plot 2d landscape
81 | pybop.plot.surface(optim)
82 |
--------------------------------------------------------------------------------
/examples/scripts/battery_parameterisation/simple_pulse_fit.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import numpy as np
4 |
5 | import pybop
6 |
7 | # This example introduces pulse fitting
8 | # within PyBOP. 5% SOC pulse data is loaded from a local `csv` file
9 | # and particle diffusivity identification for a SPMe model is performed.
10 | # Additionally, uncertainty metrics are computed.
11 |
12 | # Get the current directory location and convert to absolute path
13 | current_dir = os.path.dirname(os.path.abspath(__file__))
14 | dataset_path = os.path.join(current_dir, "../../data/synthetic/spme_pulse_15.csv")
15 |
16 | # Define model and use high-performant solver for sensitivities
17 | parameter_set = pybop.ParameterSet("Chen2020")
18 | model = pybop.lithium_ion.SPMe(
19 | parameter_set=parameter_set,
20 | )
21 |
22 | # Fitting parameters
23 | parameters = pybop.Parameters(
24 | pybop.Parameter(
25 | "Negative particle diffusivity [m2.s-1]",
26 | prior=pybop.Gaussian(4e-14, 1e-14),
27 | transformation=pybop.LogTransformation(),
28 | bounds=[1e-14, 1e-13],
29 | ),
30 | pybop.Parameter(
31 | "Positive particle diffusivity [m2.s-1]",
32 | prior=pybop.Gaussian(7e-15, 1e-15),
33 | transformation=pybop.LogTransformation(),
34 | bounds=[1e-15, 1e-14],
35 | ),
36 | )
37 |
38 | # Import the synthetic dataset
39 | csv_data = np.loadtxt(dataset_path, delimiter=",", skiprows=1)
40 |
41 |
42 | # Form dataset
43 | dataset = pybop.Dataset(
44 | {
45 | "Time [s]": csv_data[:, 0],
46 | "Current function [A]": csv_data[:, 1],
47 | "Voltage [V]": csv_data[:, 2],
48 | }
49 | )
50 |
51 | # Generate problem, cost function, and optimisation class
52 | # In this example, we initialise the SPMe at the first voltage
53 | # point in `csv_data`, an optimise without rebuilding the
54 | # model on every evaluation.
55 | initial_state = {"Initial open-circuit voltage [V]": csv_data[0, 2]}
56 | model.set_initial_state(initial_state=initial_state)
57 | problem = pybop.FittingProblem(
58 | model,
59 | parameters,
60 | dataset,
61 | )
62 |
63 | likelihood = pybop.SumSquaredError(problem)
64 | optim = pybop.CMAES(
65 | likelihood,
66 | verbose=True,
67 | sigma0=0.02,
68 | max_iterations=100,
69 | max_unchanged_iterations=30,
70 | # compute_sensitivities=True,
71 | # n_sensitivity_samples=64, # Decrease samples for CI (increase for higher accuracy)
72 | )
73 |
74 | # Slow the step-size shrinking (default is 0.5)
75 | optim.optimiser.eta_min = 0.7
76 |
77 | # Run optimisation
78 | results = optim.run()
79 |
80 | # Plot the timeseries output
81 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison")
82 |
83 | # Plot convergence
84 | pybop.plot.convergence(optim)
85 |
86 | # Plot the parameter traces
87 | pybop.plot.parameters(optim)
88 |
89 | # Plot the cost landscape with optimisation path
90 | pybop.plot.contour(optim)
91 |
--------------------------------------------------------------------------------
/examples/scripts/battery_parameterisation/stoichiometry_fitting.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 |
3 | import pybop
4 |
5 | # Generate some synthetic data for testing
6 | parameter_set = pybop.ParameterSet("Chen2020")
7 | ocv_function = parameter_set["Positive electrode OCP [V]"]
8 | nom_capacity = parameter_set["Nominal cell capacity [A.h]"]
9 |
10 |
11 | def noise(sigma):
12 | return np.random.normal(0, sigma, 91)
13 |
14 |
15 | sto = np.linspace(0, 0.9, 91)
16 | voltage = ocv_function(sto) + noise(2e-3)
17 |
18 | # Create the OCV dataset
19 | ocv_dataset = pybop.Dataset(
20 | {"Charge capacity [A.h]": (sto + 0.1) * nom_capacity, "Voltage [V]": voltage}
21 | )
22 |
23 | # Estimate the stoichiometry corresponding to the GITT-OCV
24 | ocv_fit = pybop.OCPCapacityToStoichiometry(ocv_dataset, ocv_function)
25 | fitted_dataset = ocv_fit()
26 |
27 | # Verify the method through plotting
28 | stoichiometry = np.linspace(0, 1, 101)
29 | fig = pybop.plot.trajectories(
30 | x=[stoichiometry, fitted_dataset["Stoichiometry"]],
31 | y=[
32 | parameter_set["Positive electrode OCP [V]"](stoichiometry),
33 | fitted_dataset["Voltage [V]"],
34 | ],
35 | trace_names=["Ground truth", "Data vs. stoichiometry"],
36 | )
37 |
--------------------------------------------------------------------------------
/examples/scripts/comparison_examples/adamw.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import numpy as np
4 |
5 | import pybop
6 |
7 | # Get the current directory location and convert to absolute path
8 | current_dir = os.path.dirname(os.path.abspath(__file__))
9 | dataset_path = os.path.join(
10 | current_dir, "../../data/synthetic/spm_charge_discharge_75.csv"
11 | )
12 |
13 | # Define model and use high-performant solver for sensitivities
14 | parameter_set = pybop.ParameterSet("Chen2020")
15 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set)
16 |
17 | # Fitting parameters
18 | parameters = pybop.Parameters(
19 | pybop.Parameter(
20 | "Negative electrode active material volume fraction",
21 | prior=pybop.Gaussian(0.68, 0.05),
22 | initial_value=0.45,
23 | bounds=[0.4, 0.9],
24 | ),
25 | pybop.Parameter(
26 | "Positive electrode active material volume fraction",
27 | prior=pybop.Gaussian(0.58, 0.05),
28 | initial_value=0.45,
29 | bounds=[0.4, 0.9],
30 | ),
31 | )
32 |
33 | # Import the synthetic dataset, set model initial state
34 | csv_data = np.loadtxt(dataset_path, delimiter=",", skiprows=1)
35 | initial_state = {"Initial open-circuit voltage [V]": csv_data[0, 2]}
36 | model.set_initial_state(initial_state=initial_state)
37 |
38 | # Form dataset
39 | dataset = pybop.Dataset(
40 | {
41 | "Time [s]": csv_data[:, 0],
42 | "Current function [A]": csv_data[:, 1],
43 | "Voltage [V]": csv_data[:, 2],
44 | "Bulk open-circuit voltage [V]": csv_data[:, 3],
45 | }
46 | )
47 |
48 | signal = ["Voltage [V]", "Bulk open-circuit voltage [V]"]
49 | # Generate problem, cost function, and optimisation class
50 | problem = pybop.FittingProblem(
51 | model,
52 | parameters,
53 | dataset,
54 | signal=signal,
55 | )
56 | cost = pybop.SumOfPower(problem, p=2.5)
57 |
58 | optim = pybop.AdamW(
59 | cost,
60 | verbose=True,
61 | verbose_print_rate=20,
62 | allow_infeasible_solutions=True,
63 | sigma0=0.02,
64 | max_iterations=100,
65 | max_unchanged_iterations=45,
66 | compute_sensitivities=True,
67 | n_sensitivity_samples=128,
68 | )
69 | # Reduce the momentum influence
70 | # for the reduced number of optimiser iterations
71 | optim.optimiser.b1 = 0.9
72 | optim.optimiser.b2 = 0.9
73 |
74 | # Run optimisation
75 | results = optim.run()
76 |
77 | # Plot the timeseries output
78 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison")
79 |
80 | # Plot convergence
81 | pybop.plot.convergence(optim)
82 |
83 | # Plot the parameter traces
84 | pybop.plot.parameters(optim)
85 |
86 | # Plot the cost landscape with optimisation path
87 | pybop.plot.surface(optim)
88 |
--------------------------------------------------------------------------------
/examples/scripts/comparison_examples/covariance_matrix_adaptation.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 |
3 | import pybop
4 |
5 | # Define model
6 | parameter_set = pybop.ParameterSet("Chen2020")
7 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set)
8 |
9 | # Fitting parameters
10 | parameters = pybop.Parameters(
11 | pybop.Parameter(
12 | "Negative particle radius [m]",
13 | prior=pybop.Gaussian(6e-06, 0.1e-6),
14 | bounds=[1e-6, 9e-6],
15 | true_value=parameter_set["Negative particle radius [m]"],
16 | transformation=pybop.LogTransformation(),
17 | ),
18 | pybop.Parameter(
19 | "Positive particle radius [m]",
20 | prior=pybop.Gaussian(4.5e-06, 0.1e-6),
21 | bounds=[1e-6, 9e-6],
22 | true_value=parameter_set["Positive particle radius [m]"],
23 | transformation=pybop.LogTransformation(),
24 | ),
25 | )
26 |
27 | # Generate data
28 | sigma = 0.001
29 | t_eval = np.arange(0, 900, 5)
30 | values = model.predict(t_eval=t_eval)
31 | corrupt_values = values["Voltage [V]"].data + np.random.normal(0, sigma, len(t_eval))
32 |
33 | # Form dataset
34 | dataset = pybop.Dataset(
35 | {
36 | "Time [s]": t_eval,
37 | "Current function [A]": values["Current [A]"].data,
38 | "Voltage [V]": corrupt_values,
39 | "Bulk open-circuit voltage [V]": values["Bulk open-circuit voltage [V]"].data,
40 | }
41 | )
42 |
43 | signal = ["Voltage [V]", "Bulk open-circuit voltage [V]"]
44 | # Generate problem, cost function, and optimisation class
45 | problem = pybop.FittingProblem(model, parameters, dataset, signal=signal)
46 | cost = pybop.SumSquaredError(problem)
47 | optim = pybop.CMAES(
48 | cost,
49 | sigma0=0.25,
50 | max_unchanged_iterations=10,
51 | verbose=True,
52 | max_iterations=40,
53 | multistart=2,
54 | )
55 |
56 | # Run the optimisation
57 | results = optim.run()
58 | print("True parameters:", parameters.true_value())
59 |
60 | # Plot the time series
61 | pybop.plot.dataset(dataset)
62 |
63 | # Plot the timeseries output
64 | pybop.plot.problem(problem, problem_inputs=results.x_best, title="Optimised Comparison")
65 |
66 | # Plot convergence
67 | pybop.plot.convergence(optim)
68 |
69 | # Plot the parameter traces
70 | pybop.plot.parameters(optim)
71 |
72 | # Plot the cost landscape
73 | pybop.plot.surface(optim)
74 |
--------------------------------------------------------------------------------
/examples/scripts/comparison_examples/cuckoo.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import numpy as np
4 |
5 | import pybop
6 |
7 | # Get the current directory location and convert to absolute path
8 | current_dir = os.path.dirname(os.path.abspath(__file__))
9 | dataset_path = os.path.join(
10 | current_dir, "../../data/synthetic/spm_charge_discharge_75.csv"
11 | )
12 |
13 | # Define model
14 | parameter_set = pybop.ParameterSet("Chen2020")
15 | parameter_set.update(
16 | {
17 | "Negative electrode active material volume fraction": 0.7,
18 | "Positive electrode active material volume fraction": 0.67,
19 | }
20 | )
21 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set)
22 |
23 | # Fitting parameters
24 | parameters = pybop.Parameters(
25 | pybop.Parameter(
26 | "Negative electrode active material volume fraction",
27 | prior=pybop.Gaussian(0.6, 0.05),
28 | bounds=[0.4, 0.75],
29 | initial_value=0.41,
30 | ),
31 | pybop.Parameter(
32 | "Positive electrode active material volume fraction",
33 | prior=pybop.Gaussian(0.48, 0.05),
34 | bounds=[0.4, 0.75],
35 | initial_value=0.41,
36 | ),
37 | )
38 |
39 | # Import the synthetic dataset, set model initial state
40 | csv_data = np.loadtxt(dataset_path, delimiter=",", skiprows=1)
41 | initial_state = {"Initial open-circuit voltage [V]": csv_data[0, 2]}
42 | model.set_initial_state(initial_state=initial_state)
43 |
44 | # Form dataset
45 | dataset = pybop.Dataset(
46 | {
47 | "Time [s]": csv_data[:, 0],
48 | "Current function [A]": csv_data[:, 1],
49 | "Voltage [V]": csv_data[:, 2],
50 | }
51 | )
52 |
53 | # Generate problem, cost function, and optimisation class
54 | problem = pybop.FittingProblem(
55 | model,
56 | parameters,
57 | dataset,
58 | )
59 | cost = pybop.GaussianLogLikelihood(problem, sigma0=8e-3)
60 | optim = pybop.Optimisation(
61 | cost,
62 | optimiser=pybop.CuckooSearch,
63 | max_iterations=100,
64 | )
65 |
66 | results = optim.run()
67 |
68 | # Plot the timeseries output
69 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison")
70 |
71 | # Plot convergence
72 | pybop.plot.convergence(optim)
73 |
74 | # Plot the parameter traces
75 | pybop.plot.parameters(optim)
76 |
77 | # Plot the cost landscape with optimisation path
78 | pybop.plot.contour(optim, steps=15)
79 |
--------------------------------------------------------------------------------
/examples/scripts/comparison_examples/exponential_decay.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import pybamm
3 |
4 | import pybop
5 |
6 | # Define model and use high-performant solver for sensitivities
7 | parameter_set = pybamm.ParameterValues({"k": 1, "y0": 0.5})
8 | model = pybop.ExponentialDecayModel(parameter_set=parameter_set, n_states=2)
9 |
10 | # Fitting parameters
11 | parameters = pybop.Parameters(
12 | pybop.Parameter(
13 | "k",
14 | prior=pybop.Gaussian(0.5, 0.05),
15 | ),
16 | pybop.Parameter(
17 | "y0",
18 | prior=pybop.Gaussian(0.2, 0.05),
19 | ),
20 | )
21 |
22 | # Generate data
23 | sigma = 0.003
24 | t_eval = np.linspace(0, 10, 100)
25 | values = model.predict(t_eval=t_eval)
26 |
27 |
28 | def noisy(data, sigma):
29 | return data + np.random.normal(0, sigma, len(data))
30 |
31 |
32 | # Form dataset
33 | dataset = pybop.Dataset(
34 | {
35 | "Time [s]": t_eval,
36 | "Current function [A]": 0 * t_eval,
37 | "y_0": noisy(values["y_0"].data, sigma),
38 | "y_1": noisy(values["y_1"].data, sigma),
39 | }
40 | )
41 |
42 | signal = ["y_0", "y_1"]
43 | # Generate problem, cost function, and optimisation class
44 | problem = pybop.FittingProblem(model, parameters, dataset, signal=signal)
45 | cost = pybop.Minkowski(problem, p=2)
46 | optim = pybop.AdamW(
47 | cost,
48 | verbose=True,
49 | allow_infeasible_solutions=True,
50 | sigma0=0.02,
51 | max_iterations=100,
52 | max_unchanged_iterations=20,
53 | )
54 |
55 | # Run optimisation
56 | results = optim.run()
57 |
58 | # Plot the timeseries output
59 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison")
60 |
61 | # Plot convergence
62 | pybop.plot.convergence(optim)
63 |
64 | # Plot the parameter traces
65 | pybop.plot.parameters(optim)
66 |
67 | # Plot the cost landscape with optimisation path
68 | pybop.plot.surface(optim)
69 |
--------------------------------------------------------------------------------
/examples/scripts/comparison_examples/exponential_natural_evolution.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 |
3 | import pybop
4 |
5 | # Define model
6 | parameter_set = pybop.ParameterSet("Chen2020")
7 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set)
8 |
9 | # Fitting parameters
10 | parameters = pybop.Parameters(
11 | pybop.Parameter(
12 | "Negative electrode active material volume fraction",
13 | prior=pybop.Gaussian(0.68, 0.05),
14 | bounds=[0.5, 0.8],
15 | ),
16 | pybop.Parameter(
17 | "Positive electrode active material volume fraction",
18 | prior=pybop.Gaussian(0.58, 0.05),
19 | bounds=[0.4, 0.7],
20 | ),
21 | )
22 |
23 | sigma = 0.001
24 | t_eval = np.arange(0, 900, 3)
25 | values = model.predict(t_eval=t_eval)
26 | corrupt_values = values["Voltage [V]"].data + np.random.normal(0, sigma, len(t_eval))
27 |
28 | # Form dataset
29 | dataset = pybop.Dataset(
30 | {
31 | "Time [s]": t_eval,
32 | "Current function [A]": values["Current [A]"].data,
33 | "Voltage [V]": corrupt_values,
34 | }
35 | )
36 |
37 | # Generate problem, cost function, and optimisation class
38 | problem = pybop.FittingProblem(model, parameters, dataset)
39 | cost = pybop.SumSquaredError(problem)
40 | optim = pybop.XNES(cost, max_iterations=100)
41 |
42 | results = optim.run()
43 |
44 | # Plot the timeseries output
45 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison")
46 |
47 | # Plot convergence
48 | pybop.plot.convergence(optim)
49 |
50 | # Plot the parameter traces
51 | pybop.plot.parameters(optim)
52 |
53 | # Plot the cost landscape with optimisation path
54 | pybop.plot.surface(optim)
55 |
56 | # Plot contour
57 | pybop.plot.contour(optim)
58 |
--------------------------------------------------------------------------------
/examples/scripts/comparison_examples/gradient_descent.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 |
3 | import pybop
4 |
5 | # Define model and use high-performant solver for sensitivities
6 | parameter_set = pybop.ParameterSet("Chen2020")
7 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set)
8 |
9 | # Fitting parameters
10 | parameters = pybop.Parameters(
11 | pybop.Parameter(
12 | "Negative electrode active material volume fraction",
13 | prior=pybop.Gaussian(0.6, 0.1),
14 | ),
15 | pybop.Parameter(
16 | "Positive electrode active material volume fraction",
17 | prior=pybop.Gaussian(0.6, 0.1),
18 | ),
19 | )
20 |
21 | # Generate data
22 | sigma = 0.001
23 | t_eval = np.arange(0, 900, 3)
24 | values = model.predict(t_eval=t_eval)
25 | corrupt_values = values["Voltage [V]"].data + np.random.normal(
26 | 0, sigma, len(values["Voltage [V]"].data)
27 | )
28 |
29 | # Form dataset
30 | dataset = pybop.Dataset(
31 | {
32 | "Time [s]": values["Time [s]"].data,
33 | "Current function [A]": values["Current [A]"].data,
34 | "Voltage [V]": corrupt_values,
35 | }
36 | )
37 |
38 | # Generate problem, cost function, and optimisation class
39 | problem = pybop.FittingProblem(model, parameters, dataset)
40 | cost = pybop.RootMeanSquaredError(problem)
41 | optim = pybop.GradientDescent(
42 | cost,
43 | sigma0=[0.6, 0.02],
44 | verbose=True,
45 | max_iterations=75,
46 | )
47 |
48 | # Run optimisation
49 | results = optim.run()
50 |
51 | # Plot the timeseries output
52 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison")
53 |
54 | # Plot convergence
55 | pybop.plot.convergence(optim)
56 |
57 | # Plot the parameter traces
58 | pybop.plot.parameters(optim)
59 |
60 | # Plot the cost landscape with optimisation path
61 | bounds = np.asarray([[0.5, 0.8], [0.4, 0.7]])
62 | pybop.plot.surface(optim, bounds=bounds)
63 |
--------------------------------------------------------------------------------
/examples/scripts/comparison_examples/irpropmin.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import numpy as np
4 |
5 | import pybop
6 |
7 | # Get the current directory location and convert to absolute path
8 | current_dir = os.path.dirname(os.path.abspath(__file__))
9 | dataset_path = os.path.join(
10 | current_dir, "../../data/synthetic/spm_charge_discharge_75.csv"
11 | )
12 |
13 | # Define model and use high-performant solver for sensitivities
14 | parameter_set = pybop.ParameterSet("Chen2020")
15 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set)
16 |
17 | # Construct the fitting parameters
18 | # with initial values sampled from a different distribution
19 | parameters = pybop.Parameters(
20 | pybop.Parameter(
21 | "Negative electrode active material volume fraction",
22 | prior=pybop.Uniform(0.3, 0.9),
23 | bounds=[0.3, 0.8],
24 | initial_value=pybop.Uniform(0.4, 0.75).rvs()[0],
25 | true_value=parameter_set["Negative electrode active material volume fraction"],
26 | ),
27 | pybop.Parameter(
28 | "Positive electrode active material volume fraction",
29 | prior=pybop.Uniform(0.3, 0.9),
30 | initial_value=pybop.Uniform(0.4, 0.75).rvs()[0],
31 | true_value=parameter_set["Positive electrode active material volume fraction"],
32 | # no bounds
33 | ),
34 | )
35 |
36 | # Import the synthetic dataset, set model initial state
37 | csv_data = np.loadtxt(dataset_path, delimiter=",", skiprows=1)
38 | initial_state = {"Initial open-circuit voltage [V]": csv_data[0, 2]}
39 | model.set_initial_state(initial_state=initial_state)
40 |
41 | # Form dataset
42 | dataset = pybop.Dataset(
43 | {
44 | "Time [s]": csv_data[:, 0],
45 | "Current function [A]": csv_data[:, 1],
46 | "Voltage [V]": csv_data[:, 2],
47 | }
48 | )
49 |
50 | # Generate problem, cost function, and optimisation class
51 | problem = pybop.FittingProblem(
52 | model,
53 | parameters,
54 | dataset,
55 | )
56 | likelihood = pybop.GaussianLogLikelihoodKnownSigma(problem, sigma0=2e-3 * 1.05)
57 | posterior = pybop.LogPosterior(likelihood)
58 | optim = pybop.IRPropMin(
59 | posterior, max_iterations=125, max_unchanged_iterations=60, sigma0=0.01
60 | )
61 |
62 | results = optim.run()
63 | print(parameters.true_value())
64 |
65 | # Plot the timeseries output
66 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison")
67 |
68 | # Plot convergence
69 | pybop.plot.convergence(optim)
70 |
71 | # Plot the parameter traces
72 | pybop.plot.parameters(optim)
73 |
74 | # Contour plot with optimisation path
75 | bounds = np.asarray([[0.5, 0.8], [0.4, 0.7]])
76 | pybop.plot.surface(optim, bounds=bounds)
77 |
--------------------------------------------------------------------------------
/examples/scripts/comparison_examples/maximum_a_posteriori.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import numpy as np
4 |
5 | import pybop
6 |
7 | # Get the current directory location and convert to absolute path
8 | current_dir = os.path.dirname(os.path.abspath(__file__))
9 | dataset_path = os.path.join(
10 | current_dir, "../../data/synthetic/spm_charge_discharge_75.csv"
11 | )
12 |
13 | # Construct and update initial parameter values
14 | parameter_set = pybop.ParameterSet("Chen2020")
15 | parameter_set.update(
16 | {
17 | "Negative electrode active material volume fraction": 0.43,
18 | "Positive electrode active material volume fraction": 0.56,
19 | }
20 | )
21 |
22 | # Define model
23 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set)
24 |
25 | # Fitting parameters
26 | parameters = pybop.Parameters(
27 | pybop.Parameter(
28 | "Negative electrode active material volume fraction",
29 | prior=pybop.Uniform(0.3, 0.8),
30 | bounds=[0.3, 0.8],
31 | initial_value=0.653,
32 | true_value=parameter_set["Negative electrode active material volume fraction"],
33 | transformation=pybop.LogTransformation(),
34 | ),
35 | pybop.Parameter(
36 | "Positive electrode active material volume fraction",
37 | prior=pybop.Uniform(0.3, 0.8),
38 | bounds=[0.4, 0.7],
39 | initial_value=0.657,
40 | true_value=parameter_set["Positive electrode active material volume fraction"],
41 | transformation=pybop.LogTransformation(),
42 | ),
43 | )
44 |
45 | # Import the synthetic dataset, set model initial state
46 | csv_data = np.loadtxt(dataset_path, delimiter=",", skiprows=1)
47 | initial_state = {"Initial open-circuit voltage [V]": csv_data[0, 2]}
48 | model.set_initial_state(initial_state=initial_state)
49 |
50 | # Form dataset
51 | dataset = pybop.Dataset(
52 | {
53 | "Time [s]": csv_data[:, 0],
54 | "Current function [A]": csv_data[:, 1],
55 | "Voltage [V]": csv_data[:, 2],
56 | "Bulk open-circuit voltage [V]": csv_data[:, 3],
57 | }
58 | )
59 |
60 | # Generate problem, cost function, and optimisation class
61 | problem = pybop.FittingProblem(
62 | model,
63 | parameters,
64 | dataset,
65 | )
66 | cost = pybop.LogPosterior(pybop.GaussianLogLikelihood(problem))
67 | optim = pybop.IRPropMin(
68 | cost,
69 | sigma0=0.05,
70 | max_unchanged_iterations=20,
71 | min_iterations=20,
72 | max_iterations=100,
73 | )
74 |
75 | # Run the optimisation
76 | results = optim.run()
77 | print("True parameters:", parameters.true_value())
78 |
79 | # Plot the timeseries output
80 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison")
81 |
82 | # Plot convergence
83 | pybop.plot.convergence(optim)
84 |
85 | # Plot the parameter traces
86 | pybop.plot.parameters(optim)
87 |
88 | # Plot the cost landscape
89 | pybop.plot.contour(cost, steps=15)
90 |
91 | # Plot the cost landscape with optimisation path
92 | bounds = np.asarray([[0.35, 0.7], [0.45, 0.625]])
93 | pybop.plot.contour(optim, bounds=bounds, steps=15)
94 |
--------------------------------------------------------------------------------
/examples/scripts/comparison_examples/maximum_likelihood.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import numpy as np
4 |
5 | import pybop
6 |
7 | # Get the current directory location and convert to absolute path
8 | current_dir = os.path.dirname(os.path.abspath(__file__))
9 | dataset_path = os.path.join(
10 | current_dir, "../../data/synthetic/spm_charge_discharge_75.csv"
11 | )
12 |
13 | # Define model and set initial parameter values
14 | parameter_set = pybop.ParameterSet("Chen2020")
15 | parameter_set.update(
16 | {
17 | "Negative electrode active material volume fraction": 0.63,
18 | "Positive electrode active material volume fraction": 0.51,
19 | }
20 | )
21 | model = pybop.lithium_ion.SPMe(parameter_set=parameter_set)
22 |
23 | # Fitting parameters
24 | parameters = pybop.Parameters(
25 | pybop.Parameter(
26 | "Negative electrode active material volume fraction",
27 | prior=pybop.Gaussian(0.6, 0.05),
28 | bounds=[0.5, 0.8],
29 | ),
30 | pybop.Parameter(
31 | "Positive electrode active material volume fraction",
32 | prior=pybop.Gaussian(0.48, 0.05),
33 | ),
34 | )
35 |
36 | # Import the synthetic dataset, set model initial state
37 | csv_data = np.loadtxt(dataset_path, delimiter=",", skiprows=1)
38 | initial_state = {"Initial open-circuit voltage [V]": csv_data[0, 2]}
39 | model.set_initial_state(initial_state=initial_state)
40 |
41 | # Form dataset
42 | dataset = pybop.Dataset(
43 | {
44 | "Time [s]": csv_data[:, 0],
45 | "Current function [A]": csv_data[:, 1],
46 | "Voltage [V]": csv_data[:, 2],
47 | "Bulk open-circuit voltage [V]": csv_data[:, 3],
48 | }
49 | )
50 |
51 | # Generate problem, cost function, and optimisation class
52 | problem = pybop.FittingProblem(
53 | model,
54 | parameters,
55 | dataset,
56 | )
57 | likelihood = pybop.GaussianLogLikelihood(problem, sigma0=8e-3)
58 | optim = pybop.IRPropMin(
59 | likelihood,
60 | max_unchanged_iterations=20,
61 | min_iterations=20,
62 | max_iterations=100,
63 | )
64 |
65 | # Run the optimisation
66 | results = optim.run()
67 |
68 | # Plot the timeseries output
69 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison")
70 |
71 | # Plot convergence
72 | pybop.plot.convergence(optim)
73 |
74 | # Plot the parameter traces
75 | pybop.plot.parameters(optim)
76 |
77 | # Plot the cost landscape with optimisation path
78 | bounds = np.asarray([[0.55, 0.77], [0.48, 0.68]])
79 | pybop.plot.contour(optim, bounds=bounds, steps=15)
80 |
--------------------------------------------------------------------------------
/examples/scripts/comparison_examples/nelder_mead.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 |
3 | import pybop
4 |
5 | # Parameter set and model definition
6 | parameter_set = pybop.ParameterSet("Chen2020")
7 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set)
8 |
9 | # Fitting parameters
10 | parameters = pybop.Parameters(
11 | pybop.Parameter(
12 | "Negative electrode active material volume fraction",
13 | prior=pybop.Gaussian(0.68, 0.05),
14 | ),
15 | pybop.Parameter(
16 | "Positive electrode active material volume fraction",
17 | prior=pybop.Gaussian(0.58, 0.05),
18 | ),
19 | )
20 |
21 | # Generate data
22 | sigma = 0.003
23 | experiment = pybop.Experiment(
24 | [
25 | (
26 | "Discharge at 0.5C for 3 minutes (3 second period)",
27 | "Charge at 0.5C for 3 minutes (3 second period)",
28 | ),
29 | ]
30 | * 2
31 | )
32 | values = model.predict(initial_state={"Initial SoC": 0.5}, experiment=experiment)
33 |
34 |
35 | def noisy(data, sigma):
36 | return data + np.random.normal(0, sigma, len(data))
37 |
38 |
39 | # Form dataset
40 | dataset = pybop.Dataset(
41 | {
42 | "Time [s]": values["Time [s]"].data,
43 | "Current function [A]": values["Current [A]"].data,
44 | "Voltage [V]": noisy(values["Voltage [V]"].data, sigma),
45 | "Bulk open-circuit voltage [V]": noisy(
46 | values["Bulk open-circuit voltage [V]"].data, sigma
47 | ),
48 | }
49 | )
50 |
51 | # Generate problem, cost function, and optimisation class
52 | signal = ["Voltage [V]", "Bulk open-circuit voltage [V]"]
53 | problem = pybop.FittingProblem(
54 | model,
55 | parameters,
56 | dataset,
57 | signal=signal,
58 | initial_state={"Initial open-circuit voltage [V]": dataset["Voltage [V]"][0]},
59 | )
60 | cost = pybop.RootMeanSquaredError(problem)
61 | optim = pybop.NelderMead(
62 | cost,
63 | verbose=True,
64 | allow_infeasible_solutions=True,
65 | sigma0=0.05,
66 | max_iterations=100,
67 | max_unchanged_iterations=20,
68 | )
69 |
70 | # Run optimisation
71 | results = optim.run()
72 |
73 | # Plot the timeseries output
74 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison")
75 |
76 | # Plot convergence
77 | pybop.plot.convergence(optim)
78 |
79 | # Plot the parameter traces
80 | pybop.plot.parameters(optim)
81 |
82 | # Plot the cost landscape with optimisation path
83 | pybop.plot.surface(optim)
84 |
--------------------------------------------------------------------------------
/examples/scripts/comparison_examples/particle_swarm.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 |
3 | import pybop
4 |
5 | # Define model
6 | parameter_set = pybop.ParameterSet("Chen2020")
7 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set)
8 |
9 | # Fitting parameters
10 | parameters = pybop.Parameters(
11 | pybop.Parameter(
12 | "Negative electrode active material volume fraction",
13 | prior=pybop.Gaussian(0.6, 0.05),
14 | bounds=[0.5, 0.8],
15 | ),
16 | pybop.Parameter(
17 | "Positive electrode active material volume fraction",
18 | prior=pybop.Gaussian(0.48, 0.05),
19 | bounds=[0.4, 0.7],
20 | ),
21 | )
22 |
23 | sigma = 0.002
24 | t_eval = np.arange(0, 900, 3)
25 | values = model.predict(t_eval=t_eval)
26 | corrupt_values = values["Voltage [V]"].data + np.random.normal(0, sigma, len(t_eval))
27 |
28 | # Form dataset
29 | dataset = pybop.Dataset(
30 | {
31 | "Time [s]": t_eval,
32 | "Current function [A]": values["Current [A]"].data,
33 | "Voltage [V]": corrupt_values,
34 | }
35 | )
36 |
37 | # Generate problem, cost function, and optimisation class
38 | problem = pybop.FittingProblem(model, parameters, dataset)
39 | cost = pybop.SumSquaredError(problem)
40 | optim = pybop.Optimisation(
41 | cost,
42 | optimiser=pybop.PSO,
43 | sigma0=0.05,
44 | max_unchanged_iterations=20,
45 | max_iterations=100,
46 | )
47 |
48 | results = optim.run()
49 |
50 | # Plot the timeseries output
51 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison")
52 |
53 | # Plot convergence
54 | pybop.plot.convergence(optim)
55 |
56 | # Plot the parameter traces
57 | pybop.plot.parameters(optim)
58 |
59 | # Plot the cost surface
60 | bounds = np.asarray([[0.5, 0.8], [0.4, 0.7]])
61 | pybop.plot.surface(optim, bounds)
62 |
63 | # Plot the cost landscape with optimisation path
64 | pybop.plot.contour(optim, steps=15, bounds=bounds)
65 |
--------------------------------------------------------------------------------
/examples/scripts/comparison_examples/random_search.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 |
3 | import pybop
4 |
5 | # Define model
6 | parameter_set = pybop.ParameterSet("Chen2020")
7 | parameter_set.update(
8 | {
9 | "Negative electrode active material volume fraction": 0.7,
10 | "Positive electrode active material volume fraction": 0.67,
11 | }
12 | )
13 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set)
14 |
15 | # Fitting parameters
16 | parameters = pybop.Parameters(
17 | pybop.Parameter(
18 | "Negative electrode active material volume fraction",
19 | bounds=[0.4, 0.75],
20 | initial_value=0.41,
21 | ),
22 | pybop.Parameter(
23 | "Positive electrode active material volume fraction",
24 | bounds=[0.4, 0.75],
25 | initial_value=0.41,
26 | ),
27 | )
28 | experiment = pybop.Experiment(
29 | [
30 | (
31 | "Discharge at 0.5C for 3 minutes (4 second period)",
32 | "Charge at 0.5C for 3 minutes (4 second period)",
33 | ),
34 | ]
35 | )
36 | values = model.predict(initial_state={"Initial SoC": 0.7}, experiment=experiment)
37 |
38 | sigma = 0.002
39 | corrupt_values = values["Voltage [V]"].data + np.random.normal(
40 | 0, sigma, len(values["Voltage [V]"].data)
41 | )
42 |
43 | # Form dataset
44 | dataset = pybop.Dataset(
45 | {
46 | "Time [s]": values["Time [s]"].data,
47 | "Current function [A]": values["Current [A]"].data,
48 | "Voltage [V]": corrupt_values,
49 | }
50 | )
51 |
52 | # Generate problem, cost function, and optimisation class
53 | problem = pybop.FittingProblem(model, parameters, dataset)
54 | cost = pybop.GaussianLogLikelihood(problem, sigma0=sigma * 4)
55 | optim = pybop.Optimisation(
56 | cost,
57 | optimiser=pybop.RandomSearch,
58 | max_iterations=100,
59 | )
60 |
61 | results = optim.run()
62 |
63 | # Plot the timeseries output
64 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison")
65 |
66 | # Plot convergence
67 | pybop.plot.convergence(optim)
68 |
69 | # Plot the parameter traces
70 | pybop.plot.parameters(optim)
71 |
72 | # Plot the cost landscape with optimisation path
73 | pybop.plot.contour(optim, steps=10)
74 |
--------------------------------------------------------------------------------
/examples/scripts/comparison_examples/scipy_minimize.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 |
3 | import pybop
4 |
5 | # Define model
6 | parameter_set = pybop.ParameterSet("Chen2020")
7 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set)
8 |
9 | # Fitting parameters
10 | parameters = pybop.Parameters(
11 | pybop.Parameter(
12 | "Negative electrode active material volume fraction",
13 | prior=pybop.Gaussian(0.6, 0.05),
14 | bounds=[0.5, 0.8],
15 | ),
16 | pybop.Parameter(
17 | "Positive electrode active material volume fraction",
18 | prior=pybop.Gaussian(0.48, 0.05),
19 | bounds=[0.4, 0.7],
20 | ),
21 | )
22 |
23 | sigma = 0.001
24 | t_eval = np.arange(0, 900, 3)
25 | values = model.predict(t_eval=t_eval)
26 | corrupt_values = values["Voltage [V]"].data + np.random.normal(
27 | 0, sigma, len(values["Voltage [V]"].data)
28 | )
29 |
30 | dataset = pybop.Dataset(
31 | {
32 | "Time [s]": values["Time [s]"].data,
33 | "Current function [A]": values["Current [A]"].data,
34 | "Voltage [V]": corrupt_values,
35 | }
36 | )
37 |
38 | # Generate problem, cost function, and optimisation class
39 | problem = pybop.FittingProblem(model, parameters, dataset)
40 | cost = pybop.SumSquaredError(problem)
41 | optim = pybop.SciPyMinimize(
42 | cost,
43 | max_iterations=100,
44 | multistart=1,
45 | method="L-BFGS-B",
46 | jac=True,
47 | n_sensitivity_samples=256,
48 | )
49 |
50 | results = optim.run()
51 |
52 | # Plot the timeseries output
53 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison")
54 |
55 | # Plot convergence
56 | pybop.plot.convergence(optim)
57 |
58 | # Plot the parameter traces
59 | pybop.plot.parameters(optim)
60 |
61 | # Plot the cost landscape with optimisation path
62 | pybop.plot.surface(optim)
63 |
--------------------------------------------------------------------------------
/examples/scripts/comparison_examples/selecting_a_solver.py:
--------------------------------------------------------------------------------
1 | import time
2 |
3 | import numpy as np
4 | import pybamm
5 |
6 | import pybop
7 |
8 | # Parameter set and model definition
9 | parameter_set = pybop.ParameterSet("Chen2020")
10 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set)
11 |
12 | solvers = [
13 | pybamm.IDAKLUSolver(atol=1e-6, rtol=1e-6),
14 | pybamm.CasadiSolver(mode="safe", atol=1e-6, rtol=1e-6),
15 | pybamm.CasadiSolver(mode="fast with events", atol=1e-6, rtol=1e-6),
16 | ]
17 |
18 | # Fitting parameters
19 | parameters = pybop.Parameters(
20 | pybop.Parameter(
21 | "Negative electrode active material volume fraction", initial_value=0.55
22 | ),
23 | pybop.Parameter(
24 | "Positive electrode active material volume fraction", initial_value=0.55
25 | ),
26 | )
27 |
28 | # Define test protocol and generate data
29 | experiment = pybop.Experiment([("Discharge at 0.5C for 10 minutes (3 second period)")])
30 | values = model.predict(
31 | initial_state={"Initial open-circuit voltage [V]": 4.2}, experiment=experiment
32 | )
33 |
34 | # Form dataset
35 | dataset = pybop.Dataset(
36 | {
37 | "Time [s]": values["Time [s]"].data,
38 | "Current function [A]": values["Current [A]"].data,
39 | "Voltage [V]": values["Voltage [V]"].data,
40 | }
41 | )
42 |
43 | # Create the list of input dicts
44 | n = 150 # Number of solves
45 | inputs = list(zip(np.linspace(0.45, 0.6, n), np.linspace(0.45, 0.6, n)))
46 |
47 | # Iterate over the solvers and print benchmarks
48 | for solver in solvers:
49 | model.solver = solver
50 | problem = pybop.FittingProblem(model, parameters, dataset)
51 |
52 | start_time = time.time()
53 | for input_values in inputs:
54 | problem.evaluate(inputs=input_values)
55 | print(f"Time Evaluate {solver.name}: {time.time() - start_time:.3f}")
56 |
57 | start_time = time.time()
58 | for input_values in inputs:
59 | problem.evaluateS1(inputs=input_values)
60 | print(f"Time EvaluateS1 {solver.name}: {time.time() - start_time:.3f}")
61 |
--------------------------------------------------------------------------------
/examples/scripts/comparison_examples/simulated_annealing.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 |
3 | import pybop
4 |
5 | # Define model and use high-performant solver for sensitivities
6 | parameter_set = pybop.ParameterSet.pybamm("Chen2020")
7 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set)
8 |
9 | # Fitting parameters
10 | parameters = pybop.Parameters(
11 | pybop.Parameter(
12 | "Negative electrode active material volume fraction",
13 | prior=pybop.Gaussian(0.6, 0.1),
14 | bounds=[0.4, 0.85],
15 | ),
16 | pybop.Parameter(
17 | "Positive electrode active material volume fraction",
18 | prior=pybop.Gaussian(0.6, 0.1),
19 | bounds=[0.4, 0.85],
20 | ),
21 | )
22 |
23 | # Generate data
24 | sigma = 0.001
25 | t_eval = np.arange(0, 900, 3)
26 | values = model.predict(t_eval=t_eval)
27 | corrupt_values = values["Voltage [V]"].data + np.random.normal(
28 | 0, sigma, len(values["Voltage [V]"].data)
29 | )
30 |
31 | # Form dataset
32 | dataset = pybop.Dataset(
33 | {
34 | "Time [s]": values["Time [s]"].data,
35 | "Current function [A]": values["Current [A]"].data,
36 | "Voltage [V]": corrupt_values,
37 | }
38 | )
39 |
40 | # Generate problem, cost function, and optimisation class
41 | problem = pybop.FittingProblem(model, parameters, dataset)
42 | cost = pybop.RootMeanSquaredError(problem)
43 | optim = pybop.SimulatedAnnealing(
44 | cost,
45 | max_iterations=120,
46 | max_unchanged_iterations=60,
47 | )
48 |
49 | # Update initial temperature and cooling rate
50 | # for the reduced number of iterations
51 | optim.optimiser.temperature = 0.9
52 | optim.optimiser.cooling_rate = 0.8
53 |
54 | # Run optimisation
55 | results = optim.run()
56 |
57 | # Plot the timeseries output
58 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison")
59 |
60 | # Plot convergence
61 | pybop.plot.convergence(optim)
62 |
63 | # Plot the parameter traces
64 | pybop.plot.parameters(optim)
65 |
66 | # Plot the cost landscape with optimisation path
67 | bounds = np.asarray([[0.5, 0.8], [0.4, 0.7]])
68 | pybop.plot.surface(optim, bounds=bounds)
69 |
--------------------------------------------------------------------------------
/examples/scripts/comparison_examples/stochastic_natural_evolution.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 |
3 | import pybop
4 |
5 | # Define model
6 | parameter_set = pybop.ParameterSet("Chen2020")
7 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set)
8 |
9 | # Fitting parameters
10 | parameters = pybop.Parameters(
11 | pybop.Parameter(
12 | "Negative electrode active material volume fraction",
13 | prior=pybop.Gaussian(0.6, 0.05),
14 | bounds=[0.5, 0.8],
15 | ),
16 | pybop.Parameter(
17 | "Positive electrode active material volume fraction",
18 | prior=pybop.Gaussian(0.48, 0.05),
19 | bounds=[0.4, 0.7],
20 | ),
21 | )
22 |
23 | sigma = 0.001
24 | t_eval = np.arange(0, 900, 3)
25 | values = model.predict(t_eval=t_eval)
26 | corrupt_values = values["Voltage [V]"].data + np.random.normal(0, sigma, len(t_eval))
27 |
28 | dataset = pybop.Dataset(
29 | {
30 | "Time [s]": t_eval,
31 | "Current function [A]": values["Current [A]"].data,
32 | "Voltage [V]": corrupt_values,
33 | }
34 | )
35 |
36 | # Generate problem, cost function, and optimisation class
37 | problem = pybop.FittingProblem(model, parameters, dataset)
38 | cost = pybop.SumOfPower(problem, p=2)
39 | optim = pybop.SNES(cost, max_iterations=100)
40 |
41 | results = optim.run()
42 |
43 | # Plot the timeseries output
44 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison")
45 |
46 | # Plot convergence
47 | pybop.plot.convergence(optim)
48 |
49 | # Plot the parameter traces
50 | pybop.plot.parameters(optim)
51 |
52 | # Plot the cost landscape with optimisation path
53 | pybop.plot.surface(optim)
54 |
--------------------------------------------------------------------------------
/examples/scripts/comparison_examples/unscented_kalman_filter.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from pybamm import CasadiSolver
3 |
4 | import pybop
5 |
6 | # Parameter set and model definition
7 | parameter_set = pybop.ParameterSet("Chen2020")
8 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set, solver=CasadiSolver())
9 |
10 | # Fitting parameters
11 | parameters = pybop.Parameters(
12 | pybop.Parameter(
13 | "Negative electrode active material volume fraction",
14 | prior=pybop.Gaussian(0.6, 0.05),
15 | bounds=[0.5, 0.8],
16 | ),
17 | pybop.Parameter(
18 | "Positive electrode active material volume fraction",
19 | prior=pybop.Gaussian(0.48, 0.05),
20 | bounds=[0.4, 0.7],
21 | ),
22 | )
23 |
24 | # Make a prediction with measurement noise
25 | sigma = 0.001
26 | t_eval = np.arange(0, 150, 0.5)
27 | values = model.predict(t_eval=t_eval)
28 | corrupt_values = values["Voltage [V]"].data + np.random.normal(0, sigma, len(t_eval))
29 |
30 | # Form dataset
31 | dataset = pybop.Dataset(
32 | {
33 | "Time [s]": t_eval,
34 | "Current function [A]": values["Current [A]"].data,
35 | "Voltage [V]": corrupt_values,
36 | }
37 | )
38 |
39 | # Build the model to get the number of states
40 | model.build(dataset=dataset, parameters=parameters)
41 |
42 | # Define the UKF observer, setting the particle boundaries as uncertain states
43 | covariance = np.diag([0] * 20 + [sigma**2] + [0] * 20 + [sigma**2])
44 | process_noise = np.diag([0] * 20 + [1e-6] + [0] * 20 + [1e-6])
45 | measurement_noise = np.diag([sigma**2])
46 | observer = pybop.UnscentedKalmanFilterObserver(
47 | parameters,
48 | model,
49 | covariance,
50 | process_noise,
51 | measurement_noise,
52 | dataset,
53 | )
54 |
55 | # Generate problem, cost function, and optimisation class
56 | cost = pybop.ObserverCost(observer)
57 | optim = pybop.NelderMead(cost, verbose=True)
58 |
59 | # Parameter identification using the current observer implementation is very slow
60 | # so let's restrict the number of iterations and reduce the number of plots
61 | optim.set_max_iterations(5)
62 |
63 | # Run optimisation
64 | results = optim.run()
65 |
66 | # Plot the timeseries output (requires model that returns Voltage)
67 | pybop.plot.problem(observer, problem_inputs=results.x, title="Optimised Comparison")
68 |
69 | # Plot convergence
70 | pybop.plot.convergence(optim)
71 |
72 | # Plot the parameter traces
73 | pybop.plot.parameters(optim)
74 |
75 | # Plot the cost landscape with optimisation path
76 | pybop.plot.surface(optim)
77 |
--------------------------------------------------------------------------------
/examples/scripts/design_optimisation/maximising_energy.py:
--------------------------------------------------------------------------------
1 | from pybamm import Parameter
2 |
3 | import pybop
4 |
5 | # A design optimisation example loosely based on work by L.D. Couto
6 | # available at https://doi.org/10.1016/j.energy.2022.125966.
7 |
8 | # The target is to maximise the energy density over a range of
9 | # possible design parameter values, including for example:
10 | # cross-sectional area = height x width (only need change one)
11 | # electrode widths, particle radii, volume fractions and
12 | # separator width.
13 |
14 | # Define parameter set and additional parameters needed for the cost function
15 | parameter_set = pybop.ParameterSet("Chen2020", formation_concentrations=True)
16 | parameter_set.update(
17 | {
18 | "Electrolyte density [kg.m-3]": Parameter("Separator density [kg.m-3]"),
19 | "Negative electrode active material density [kg.m-3]": Parameter(
20 | "Negative electrode density [kg.m-3]"
21 | ),
22 | "Negative electrode carbon-binder density [kg.m-3]": Parameter(
23 | "Negative electrode density [kg.m-3]"
24 | ),
25 | "Positive electrode active material density [kg.m-3]": Parameter(
26 | "Positive electrode density [kg.m-3]"
27 | ),
28 | "Positive electrode carbon-binder density [kg.m-3]": Parameter(
29 | "Positive electrode density [kg.m-3]"
30 | ),
31 | },
32 | check_already_exists=False,
33 | )
34 |
35 | # Define model
36 | model = pybop.lithium_ion.SPMe(parameter_set=parameter_set)
37 |
38 | # Fitting parameters
39 | parameters = pybop.Parameters(
40 | pybop.Parameter(
41 | "Positive electrode thickness [m]",
42 | prior=pybop.Gaussian(7.56e-05, 0.1e-05),
43 | bounds=[65e-06, 10e-05],
44 | ),
45 | pybop.Parameter(
46 | "Positive particle radius [m]",
47 | prior=pybop.Gaussian(5.22e-06, 0.1e-06),
48 | bounds=[2e-06, 9e-06],
49 | ),
50 | )
51 |
52 | # Define test protocol
53 | experiment = pybop.Experiment(
54 | [
55 | "Discharge at 1C until 2.5 V (10 seconds period)",
56 | "Hold at 2.5 V for 30 minutes or until 10 mA (10 seconds period)",
57 | ],
58 | )
59 | signal = ["Voltage [V]", "Current [A]"]
60 |
61 | # Generate problem
62 | problem = pybop.DesignProblem(
63 | model,
64 | parameters,
65 | experiment,
66 | signal=signal,
67 | initial_state={"Initial SoC": 1.0},
68 | update_capacity=True,
69 | )
70 |
71 | # Generate multiple cost functions and combine them
72 | cost1 = pybop.GravimetricEnergyDensity(problem)
73 | cost2 = pybop.VolumetricEnergyDensity(problem)
74 | cost = pybop.WeightedCost(cost1, cost2, weights=[1, 1e-3])
75 |
76 | # Run optimisation
77 | optim = pybop.PSO(
78 | cost, verbose=True, allow_infeasible_solutions=False, max_iterations=10
79 | )
80 | results = optim.run()
81 | print(f"Initial gravimetric energy density: {cost1(optim.x0):.2f} Wh.kg-1")
82 | print(f"Optimised gravimetric energy density: {cost1(results.x):.2f} Wh.kg-1")
83 | print(f"Initial volumetric energy density: {cost2(optim.x0):.2f} Wh.m-3")
84 | print(f"Optimised volumetric energy density: {cost2(results.x):.2f} Wh.m-3")
85 |
86 | # Plot the timeseries output
87 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison")
88 |
89 | # Plot the cost landscape with optimisation path
90 | pybop.plot.surface(optim)
91 |
--------------------------------------------------------------------------------
/examples/scripts/design_optimisation/maximising_power.py:
--------------------------------------------------------------------------------
1 | from pybamm import Parameter
2 |
3 | import pybop
4 |
5 | # Define parameter set and additional parameters needed for the cost function
6 | parameter_set = pybop.ParameterSet("Chen2020", formation_concentrations=True)
7 | parameter_set.update(
8 | {
9 | "Electrolyte density [kg.m-3]": Parameter("Separator density [kg.m-3]"),
10 | "Negative electrode active material density [kg.m-3]": Parameter(
11 | "Negative electrode density [kg.m-3]"
12 | ),
13 | "Negative electrode carbon-binder density [kg.m-3]": Parameter(
14 | "Negative electrode density [kg.m-3]"
15 | ),
16 | "Positive electrode active material density [kg.m-3]": Parameter(
17 | "Positive electrode density [kg.m-3]"
18 | ),
19 | "Positive electrode carbon-binder density [kg.m-3]": Parameter(
20 | "Positive electrode density [kg.m-3]"
21 | ),
22 | },
23 | check_already_exists=False,
24 | )
25 |
26 | # Define model
27 | model = pybop.lithium_ion.SPMe(parameter_set=parameter_set)
28 |
29 | # Define useful quantities
30 | nominal_capacity = parameter_set["Nominal cell capacity [A.h]"]
31 | target_c_rate = 2
32 | discharge_rate = target_c_rate * nominal_capacity
33 |
34 | # Fitting parameters
35 | parameters = pybop.Parameters(
36 | pybop.Parameter(
37 | "Positive electrode thickness [m]",
38 | prior=pybop.Gaussian(7.56e-05, 0.5e-05),
39 | bounds=[65e-06, 10e-05],
40 | ),
41 | pybop.Parameter(
42 | "Nominal cell capacity [A.h]", # controls the C-rate in the experiment
43 | prior=pybop.Gaussian(discharge_rate, 0.2),
44 | bounds=[0.8 * discharge_rate, 1.2 * discharge_rate],
45 | ),
46 | )
47 |
48 | # Define test protocol
49 | experiment = pybop.Experiment(
50 | ["Discharge at 1C for 30 minutes or until 2.5 V (5 seconds period)"],
51 | )
52 | signal = ["Voltage [V]", "Current [A]"]
53 |
54 | # Generate problem
55 | problem = pybop.DesignProblem(
56 | model,
57 | parameters,
58 | experiment,
59 | signal=signal,
60 | initial_state={"Initial SoC": 1.0},
61 | )
62 |
63 | # Generate multiple cost functions and combine them
64 | cost1 = pybop.GravimetricPowerDensity(problem, target_time=3600 / target_c_rate)
65 | cost2 = pybop.VolumetricPowerDensity(problem, target_time=3600 / target_c_rate)
66 | cost = pybop.WeightedCost(cost1, cost2, weights=[1, 1e-3])
67 |
68 | # Run optimisation
69 | optim = pybop.XNES(
70 | cost, verbose=True, allow_infeasible_solutions=False, max_iterations=10
71 | )
72 | results = optim.run()
73 | print(f"Initial gravimetric power density: {cost1(optim.x0):.2f} W.kg-1")
74 | print(f"Optimised gravimetric power density: {cost1(results.x):.2f} W.kg-1")
75 | print(f"Initial volumetric power density: {cost2(optim.x0):.2f} W.m-3")
76 | print(f"Optimised volumetric power density: {cost2(results.x):.2f} W.m-3")
77 | print(
78 | f"Optimised discharge rate: {results.x[-1]:.2f} A = {results.x[-1] / nominal_capacity:.2f} C"
79 | )
80 |
81 | # Plot the timeseries output
82 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison")
83 |
84 | # Plot the cost landscape with optimisation path
85 | pybop.plot.surface(optim)
86 |
--------------------------------------------------------------------------------
/examples/scripts/getting_started/ask-tell-interface.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import numpy as np
4 |
5 | import pybop
6 |
7 | # Get the current directory location and convert to absolute path
8 | current_dir = os.path.dirname(os.path.abspath(__file__))
9 | dataset_path = os.path.join(
10 | current_dir, "../../data/synthetic/spm_charge_discharge_75.csv"
11 | )
12 |
13 | # Define model
14 | parameter_set = pybop.ParameterSet.pybamm("Chen2020")
15 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set)
16 |
17 | # Fitting parameters
18 | parameters = pybop.Parameters(
19 | pybop.Parameter(
20 | "Negative electrode active material volume fraction",
21 | prior=pybop.Gaussian(0.55, 0.05),
22 | ),
23 | pybop.Parameter(
24 | "Positive electrode active material volume fraction",
25 | prior=pybop.Gaussian(0.55, 0.05),
26 | ),
27 | )
28 |
29 | # Import the synthetic dataset, set model initial state
30 | csv_data = np.loadtxt(dataset_path, delimiter=",", skiprows=1)
31 | initial_state = {"Initial open-circuit voltage [V]": csv_data[0, 2]}
32 | model.set_initial_state(initial_state=initial_state)
33 |
34 | # Form dataset
35 | dataset = pybop.Dataset(
36 | {
37 | "Time [s]": csv_data[:, 0],
38 | "Current function [A]": csv_data[:, 1],
39 | "Voltage [V]": csv_data[:, 2],
40 | "Bulk open-circuit voltage [V]": csv_data[:, 3],
41 | }
42 | )
43 |
44 |
45 | signal = ["Voltage [V]", "Bulk open-circuit voltage [V]"]
46 | # Construct the problem and cost classes
47 | problem = pybop.FittingProblem(
48 | model,
49 | parameters,
50 | dataset,
51 | signal=signal,
52 | )
53 | cost = pybop.Minkowski(problem, p=2)
54 |
55 | # We construct the optimiser class the same as normal
56 | # but will be using the `optimiser` attribute directly
57 | # for this example. This interface works for all
58 | # non SciPy-based optimisers.
59 | # Warning: not all arguments are supported via this
60 | # interface.
61 | optim = pybop.AdamW(cost)
62 |
63 | # Create storage vars
64 | x_best = []
65 | f_best = []
66 |
67 | # Run optimisation
68 | for i in range(50):
69 | x = optim.optimiser.ask()
70 | f = [cost(x[0], calculate_grad=True)]
71 | optim.optimiser.tell(f)
72 |
73 | # Store best solution so far
74 | x_best.append(optim.optimiser.x_best())
75 | f_best.append(optim.optimiser.x_best())
76 |
77 | if i % 10 == 0:
78 | print(
79 | f"Iteration: {i} | Cost: {optim.optimiser.f_best()} | Parameters: {optim.optimiser.x_best()}"
80 | )
81 |
82 | # Plot the timeseries output
83 | pybop.plot.problem(
84 | problem, problem_inputs=optim.optimiser.x_best(), title="Optimised Comparison"
85 | )
86 |
--------------------------------------------------------------------------------
/examples/scripts/getting_started/functional_parameters.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import pybamm
3 |
4 | import pybop
5 |
6 | # This example demonstrates how to use a pybamm.FunctionalParameter to
7 | # optimise functional parameters using PyBOP.
8 |
9 | # Method: Define a new scalar parameter for use in a functional parameter
10 | # that already exists in the model, for example an exchange current density.
11 |
12 |
13 | # Load parameter set
14 | parameter_set = pybop.ParameterSet("Chen2020")
15 |
16 |
17 | # Define a new function using pybamm parameters
18 | def positive_electrode_exchange_current_density(c_e, c_s_surf, c_s_max, T):
19 | # New parameters
20 | j0_ref = pybamm.Parameter(
21 | "Positive electrode reference exchange-current density [A.m-2]"
22 | )
23 | alpha = pybamm.Parameter("Positive electrode charge transfer coefficient")
24 |
25 | # Existing parameters
26 | c_e_init = pybamm.Parameter("Initial concentration in electrolyte [mol.m-3]")
27 |
28 | return (
29 | j0_ref
30 | * ((c_e / c_e_init) * (c_s_surf / c_s_max) * (1 - c_s_surf / c_s_max)) ** alpha
31 | )
32 |
33 |
34 | # Give default values to the new scalar parameters and pass the new function
35 | parameter_set.update(
36 | {
37 | "Positive electrode reference exchange-current density [A.m-2]": 1,
38 | "Positive electrode charge transfer coefficient": 0.5,
39 | },
40 | check_already_exists=False,
41 | )
42 | parameter_set["Positive electrode exchange-current density [A.m-2]"] = (
43 | positive_electrode_exchange_current_density
44 | )
45 |
46 | # Model definition
47 | model = pybop.lithium_ion.SPM(
48 | parameter_set=parameter_set, options={"contact resistance": "true"}
49 | )
50 |
51 | # Fitting parameters
52 | parameters = pybop.Parameters(
53 | pybop.Parameter(
54 | "Positive electrode reference exchange-current density [A.m-2]",
55 | prior=pybop.Gaussian(1, 0.1),
56 | ),
57 | pybop.Parameter(
58 | "Positive electrode charge transfer coefficient",
59 | prior=pybop.Gaussian(0.5, 0.1),
60 | ),
61 | )
62 |
63 | # Generate data
64 | sigma = 0.001
65 | t_eval = np.arange(0, 900, 3)
66 | values = model.predict(t_eval=t_eval)
67 | corrupt_values = values["Voltage [V]"].data + np.random.normal(0, sigma, len(t_eval))
68 |
69 | # Form dataset
70 | dataset = pybop.Dataset(
71 | {
72 | "Time [s]": t_eval,
73 | "Current function [A]": values["Current [A]"].data,
74 | "Voltage [V]": corrupt_values,
75 | }
76 | )
77 |
78 | # Generate problem, cost function, and optimisation class
79 | problem = pybop.FittingProblem(model, parameters, dataset)
80 | cost = pybop.RootMeanSquaredError(problem)
81 | optim = pybop.SciPyMinimize(cost, sigma0=0.1, max_iterations=125, verbose=True)
82 |
83 | # Run optimisation
84 | results = optim.run()
85 |
86 | # Plot the timeseries output
87 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison")
88 |
89 | # Plot convergence
90 | pybop.plot.convergence(optim)
91 |
92 | # Plot the parameter traces
93 | pybop.plot.parameters(optim)
94 |
95 | # Plot the cost landscape with optimisation path
96 | pybop.plot.surface(optim)
97 |
--------------------------------------------------------------------------------
/examples/scripts/getting_started/jax-solver-example.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 |
3 | import pybop
4 |
5 | # Parameter set and model definition
6 | parameter_set = pybop.ParameterSet("Chen2020")
7 |
8 | # The Jaxified IDAKLU performs very well on high iteration
9 | # identification tasks, due to the just-in-time compilation
10 | model = pybop.lithium_ion.SPMe(parameter_set=parameter_set)
11 |
12 | # Fitting parameters
13 | parameters = pybop.Parameters(
14 | pybop.Parameter(
15 | "Negative electrode active material volume fraction",
16 | initial_value=0.55,
17 | prior=pybop.Gaussian(0.6, 0.03),
18 | bounds=[0.5, 0.8],
19 | ),
20 | pybop.Parameter(
21 | "Positive electrode active material volume fraction",
22 | initial_value=0.55,
23 | prior=pybop.Gaussian(0.6, 0.03),
24 | ),
25 | )
26 |
27 | # Generate data
28 | sigma = 0.002
29 | experiment = pybop.Experiment(
30 | [
31 | (
32 | "Charge at 0.5C for 3 minutes (3 second period)",
33 | "Discharge at 0.5C for 3 minutes (3 second period)",
34 | ),
35 | ]
36 | )
37 | values = model.predict(initial_state={"Initial SoC": 0.5}, experiment=experiment)
38 |
39 |
40 | def noisy(data, sigma):
41 | return data + np.random.normal(0, sigma, len(data))
42 |
43 |
44 | # Form dataset
45 | dataset = pybop.Dataset(
46 | {
47 | "Time [s]": values["Time [s]"].data,
48 | "Current function [A]": values["Current [A]"].data,
49 | "Voltage [V]": noisy(values["Voltage [V]"].data, sigma),
50 | }
51 | )
52 |
53 | # Construct the Problem
54 | problem = pybop.FittingProblem(model, parameters, dataset)
55 |
56 | # By selecting a Jax based cost function, the IDAKLU solver will be
57 | # jaxified (wrapped in a Jax compiled expression) and used for optimisation
58 | cost = pybop.JaxLogNormalLikelihood(problem, sigma0=sigma)
59 |
60 | # Test gradient-based optimiser
61 | optim = pybop.IRPropMin(
62 | cost, sigma0=0.02, max_unchanged_iterations=35, max_iterations=100, verbose=True
63 | )
64 |
65 | results = optim.run()
66 |
67 | # Plot convergence
68 | pybop.plot.convergence(optim)
69 |
70 | # Plot parameter trace
71 | pybop.plot.parameters(optim)
72 |
73 | # Plot voronoi surface
74 | pybop.plot.surface(optim)
75 |
--------------------------------------------------------------------------------
/examples/scripts/getting_started/linked_parameters.py:
--------------------------------------------------------------------------------
1 | from pybamm import Parameter
2 |
3 | import pybop
4 |
5 | # The aim of this script is to show how to systematically update
6 | # design parameters which depend on the optimisation parameters.
7 |
8 | # Define parameter set and additional parameters needed for the cost function
9 | parameter_set = pybop.ParameterSet("Chen2020", formation_concentrations=True)
10 | parameter_set.update(
11 | {
12 | "Electrolyte density [kg.m-3]": Parameter("Separator density [kg.m-3]"),
13 | "Negative electrode active material density [kg.m-3]": Parameter(
14 | "Negative electrode density [kg.m-3]"
15 | ),
16 | "Negative electrode carbon-binder density [kg.m-3]": Parameter(
17 | "Negative electrode density [kg.m-3]"
18 | ),
19 | "Positive electrode active material density [kg.m-3]": Parameter(
20 | "Positive electrode density [kg.m-3]"
21 | ),
22 | "Positive electrode carbon-binder density [kg.m-3]": Parameter(
23 | "Positive electrode density [kg.m-3]"
24 | ),
25 | },
26 | check_already_exists=False,
27 | )
28 |
29 | # Link parameters
30 | parameter_set.update(
31 | {
32 | "Positive electrode porosity": 1
33 | - Parameter("Positive electrode active material volume fraction")
34 | }
35 | )
36 |
37 | # Define model
38 | model = pybop.lithium_ion.SPMe(parameter_set=parameter_set)
39 |
40 | # Fitting parameters
41 | parameters = pybop.Parameters(
42 | pybop.Parameter(
43 | "Positive electrode thickness [m]",
44 | prior=pybop.Gaussian(7.56e-05, 0.1e-05),
45 | bounds=[65e-06, 10e-05],
46 | ),
47 | pybop.Parameter(
48 | "Positive electrode active material volume fraction",
49 | prior=pybop.Gaussian(0.6, 0.15),
50 | bounds=[0.1, 0.9],
51 | ),
52 | )
53 |
54 | # Define test protocol
55 | experiment = pybop.Experiment(
56 | [
57 | "Discharge at 1C until 2.5 V (10 seconds period)",
58 | "Hold at 2.5 V for 30 minutes or until 10 mA (10 seconds period)",
59 | ],
60 | )
61 | signal = ["Voltage [V]", "Current [A]"]
62 |
63 | # Generate problem
64 | problem = pybop.DesignProblem(
65 | model,
66 | parameters,
67 | experiment,
68 | signal=signal,
69 | initial_state={"Initial SoC": 1.0},
70 | update_capacity=True,
71 | )
72 |
73 | # Define the cost
74 | cost = pybop.GravimetricEnergyDensity(problem)
75 |
76 | # Run optimisation
77 | optim = pybop.XNES(
78 | cost, verbose=True, allow_infeasible_solutions=False, max_iterations=10
79 | )
80 | results = optim.run()
81 | print(f"Initial gravimetric energy density: {cost(optim.x0):.2f} Wh.kg-1")
82 | print(f"Optimised gravimetric energy density: {cost(results.x):.2f} Wh.kg-1")
83 |
84 | # Plot the timeseries output
85 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison")
86 |
87 | # Plot the cost landscape with optimisation path
88 | pybop.plot.surface(optim)
89 |
--------------------------------------------------------------------------------
/examples/scripts/getting_started/mcmc_example.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | import numpy as np
4 |
5 | import pybop
6 |
7 | # Set parallelization if on macOS / Unix
8 | parallel = True if sys.platform != "win32" else False
9 |
10 | # Parameter set and model definition
11 | parameter_set = pybop.ParameterSet("Chen2020")
12 | parameter_set.update(
13 | {
14 | "Negative electrode active material volume fraction": 0.63,
15 | "Positive electrode active material volume fraction": 0.71,
16 | }
17 | )
18 | synth_model = pybop.lithium_ion.SPMe(parameter_set=parameter_set)
19 |
20 | # Fitting parameters
21 | parameters = pybop.Parameters(
22 | pybop.Parameter(
23 | "Negative electrode active material volume fraction",
24 | prior=pybop.Gaussian(0.68, 0.02),
25 | transformation=pybop.LogTransformation(),
26 | ),
27 | pybop.Parameter(
28 | "Positive electrode active material volume fraction",
29 | prior=pybop.Gaussian(0.65, 0.02),
30 | transformation=pybop.LogTransformation(),
31 | ),
32 | )
33 |
34 | # Generate data
35 | init_soc = 0.5
36 | sigma = 0.005
37 | experiment = pybop.Experiment(
38 | [
39 | ("Discharge at 0.5C for 3 minutes (5 second period)",),
40 | ]
41 | )
42 | values = synth_model.predict(
43 | initial_state={"Initial SoC": init_soc}, experiment=experiment
44 | )
45 |
46 |
47 | def noisy(data, sigma):
48 | return data + np.random.normal(0, sigma, len(data))
49 |
50 |
51 | # Form dataset
52 | dataset = pybop.Dataset(
53 | {
54 | "Time [s]": values["Time [s]"].data,
55 | "Current function [A]": values["Current [A]"].data,
56 | "Voltage [V]": noisy(values["Voltage [V]"].data, sigma),
57 | }
58 | )
59 |
60 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set)
61 | signal = ["Voltage [V]"]
62 |
63 | # Generate problem, likelihood, and sampler
64 | problem = pybop.FittingProblem(
65 | model, parameters, dataset, signal=signal, initial_state={"Initial SoC": init_soc}
66 | )
67 | likelihood = pybop.GaussianLogLikelihood(problem)
68 | posterior = pybop.LogPosterior(likelihood)
69 |
70 | sampler = pybop.DifferentialEvolutionMCMC(
71 | posterior,
72 | chains=3,
73 | max_iterations=250, # Reduced for CI, increase for improved posteriors
74 | warm_up=100,
75 | verbose=True,
76 | parallel=parallel, # (macOS/WSL/Linux only)
77 | )
78 | chains = sampler.run()
79 |
80 | # Summary statistics
81 | posterior_summary = pybop.PosteriorSummary(chains)
82 | print(posterior_summary.get_summary_statistics())
83 | posterior_summary.plot_trace()
84 | posterior_summary.summary_table()
85 | posterior_summary.plot_posterior()
86 | posterior_summary.plot_chains()
87 | posterior_summary.effective_sample_size()
88 | print(f"rhat: {posterior_summary.rhat()}")
89 |
--------------------------------------------------------------------------------
/examples/scripts/getting_started/multi_fitting.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 |
3 | import pybop
4 |
5 | # Parameter set and model definition
6 | parameter_set = pybop.ParameterSet("Chen2020")
7 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set)
8 |
9 | # Create initial SOC, experiment objects
10 | init_soc = [{"Initial SoC": 0.8}, {"Initial SoC": 0.6}]
11 | experiment = [
12 | pybop.Experiment([("Discharge at 0.5C for 2 minutes (4 second period)")]),
13 | pybop.Experiment([("Discharge at 1C for 1 minutes (4 second period)")]),
14 | ]
15 |
16 | # Fitting parameters
17 | parameters = pybop.Parameters(
18 | pybop.Parameter(
19 | "Negative electrode active material volume fraction",
20 | prior=pybop.Gaussian(0.68, 0.05),
21 | true_value=parameter_set["Negative electrode active material volume fraction"],
22 | ),
23 | pybop.Parameter(
24 | "Positive electrode active material volume fraction",
25 | prior=pybop.Gaussian(0.58, 0.05),
26 | true_value=parameter_set["Positive electrode active material volume fraction"],
27 | ),
28 | )
29 |
30 | # Generate a dataset and a fitting problem
31 | sigma = 0.002
32 | values = model.predict(initial_state=init_soc[0], experiment=experiment[0])
33 | dataset_1 = pybop.Dataset(
34 | {
35 | "Time [s]": values["Time [s]"].data,
36 | "Current function [A]": values["Current [A]"].data,
37 | "Voltage [V]": values["Voltage [V]"].data
38 | + np.random.normal(0, sigma, len(values["Voltage [V]"].data)),
39 | }
40 | )
41 | problem_1 = pybop.FittingProblem(model, parameters, dataset_1)
42 |
43 | # Generate a second dataset and problem
44 | model = model.new_copy()
45 | values = model.predict(initial_state=init_soc[1], experiment=experiment[1])
46 | dataset_2 = pybop.Dataset(
47 | {
48 | "Time [s]": values["Time [s]"].data,
49 | "Current function [A]": values["Current [A]"].data,
50 | "Voltage [V]": values["Voltage [V]"].data
51 | + np.random.normal(0, sigma, len(values["Voltage [V]"].data)),
52 | }
53 | )
54 | problem_2 = pybop.FittingProblem(model, parameters, dataset_2)
55 |
56 | # Combine the problems into one
57 | problem = pybop.MultiFittingProblem(problem_1, problem_2)
58 |
59 | # Generate the cost function and optimisation class
60 | cost = pybop.SumSquaredError(problem)
61 | optim = pybop.CuckooSearch(
62 | cost,
63 | verbose=True,
64 | sigma0=0.05,
65 | max_unchanged_iterations=20,
66 | max_iterations=100,
67 | )
68 |
69 | # Run optimisation
70 | results = optim.run()
71 | print("True parameters:", parameters.true_value())
72 |
73 | # Plot the timeseries output
74 | pybop.plot.problem(problem_1, problem_inputs=results.x, title="Optimised Comparison")
75 | pybop.plot.problem(problem_2, problem_inputs=results.x, title="Optimised Comparison")
76 |
77 | # Plot convergence
78 | pybop.plot.convergence(optim)
79 |
80 | # Plot the parameter traces
81 | pybop.plot.parameters(optim)
82 |
83 | # Plot the cost landscape with optimisation path
84 | pybop.plot.surface(optim)
85 |
--------------------------------------------------------------------------------
/examples/scripts/getting_started/multi_start_optimisation.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 |
3 | import pybop
4 |
5 | # Define model and use the high-performant IDAKLU solver for sensitivities
6 | parameter_set = pybop.ParameterSet("Chen2020")
7 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set)
8 |
9 | # Fitting parameters
10 | parameters = pybop.Parameters(
11 | pybop.Parameter(
12 | "Negative electrode active material volume fraction",
13 | prior=pybop.Gaussian(0.6, 0.1),
14 | ),
15 | pybop.Parameter(
16 | "Positive electrode active material volume fraction",
17 | prior=pybop.Gaussian(0.6, 0.1),
18 | ),
19 | )
20 |
21 | # Generate data
22 | sigma = 0.001
23 | t_eval = np.arange(0, 900, 3)
24 | values = model.predict(t_eval=t_eval)
25 | corrupt_values = values["Voltage [V]"].data + np.random.normal(
26 | 0, sigma, len(values["Voltage [V]"].data)
27 | )
28 |
29 | # Form dataset
30 | dataset = pybop.Dataset(
31 | {
32 | "Time [s]": values["Time [s]"].data,
33 | "Current function [A]": values["Current [A]"].data,
34 | "Voltage [V]": corrupt_values,
35 | }
36 | )
37 |
38 | # Generate problem, cost function classes
39 | problem = pybop.FittingProblem(model, parameters, dataset)
40 | cost = pybop.RootMeanSquaredError(problem)
41 |
42 | # Construct the optimiser with 10 multistart runs
43 | # Each of these runs has a random starting position sampled
44 | # from the parameter priors
45 | optim = pybop.GradientDescent(
46 | cost, sigma0=[0.6, 0.02], max_iterations=50, multistart=10, verbose=True
47 | )
48 |
49 | # Run optimisation
50 | results = optim.run()
51 |
52 | # We can plot the timeseries output, for the best run
53 | # using the results.x attribute
54 | pybop.plot.problem(problem, problem_inputs=results.x_best, title="Optimised Comparison")
55 |
56 | # Plot convergence
57 | pybop.plot.convergence(optim)
58 |
59 | # Plot the parameter traces
60 | pybop.plot.parameters(optim)
61 |
62 | # Plot the cost landscape with optimisation path
63 | bounds = np.asarray([[0.5, 0.8], [0.4, 0.7]])
64 | pybop.plot.surface(optim, bounds=bounds)
65 |
--------------------------------------------------------------------------------
/examples/scripts/getting_started/simple_BPX.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 |
3 | import pybop
4 |
5 | # Define model
6 | parameter_set = pybop.ParameterSet(json_path="examples/parameters/example_BPX.json")
7 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set)
8 |
9 | # Fitting parameters
10 | parameters = pybop.Parameters(
11 | pybop.Parameter(
12 | "Negative particle radius [m]",
13 | prior=pybop.Gaussian(6e-06, 0.1e-6),
14 | bounds=[1e-6, 9e-6],
15 | true_value=parameter_set["Negative particle radius [m]"],
16 | ),
17 | pybop.Parameter(
18 | "Positive particle radius [m]",
19 | prior=pybop.Gaussian(4.5e-07, 0.1e-6),
20 | bounds=[1e-7, 9e-7],
21 | true_value=parameter_set["Positive particle radius [m]"],
22 | ),
23 | )
24 |
25 | # Generate data
26 | sigma = 0.001
27 | t_eval = np.arange(0, 900, 5)
28 | values = model.predict(t_eval=t_eval)
29 | corrupt_values = values["Voltage [V]"].data + np.random.normal(0, sigma, len(t_eval))
30 |
31 | # Form dataset
32 | dataset = pybop.Dataset(
33 | {
34 | "Time [s]": t_eval,
35 | "Current function [A]": values["Current [A]"].data,
36 | "Voltage [V]": corrupt_values,
37 | }
38 | )
39 |
40 | # Generate problem, cost function, and optimisation class
41 | problem = pybop.FittingProblem(model, parameters, dataset)
42 | cost = pybop.SumSquaredError(problem)
43 | optim = pybop.CMAES(cost, max_iterations=40, verbose=True)
44 |
45 | # Run the optimisation
46 | results = optim.run()
47 | print("True parameters:", parameters.true_value())
48 |
49 | # Plot the timeseries output
50 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison")
51 |
52 | # Plot convergence
53 | pybop.plot.convergence(optim)
54 |
55 | # Plot the parameter traces
56 | pybop.plot.parameters(optim)
57 |
58 | # Plot the cost landscape with optimisation path
59 | pybop.plot.surface(optim)
60 |
--------------------------------------------------------------------------------
/examples/scripts/getting_started/simple_dfn.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import numpy as np
4 |
5 | import pybop
6 |
7 | # Get the current directory location and convert to absolute path
8 | current_dir = os.path.dirname(os.path.abspath(__file__))
9 | dataset_path = os.path.join(
10 | current_dir, "../../data/synthetic/dfn_charge_discharge_75.csv"
11 | )
12 |
13 | # Define model
14 | parameter_set = pybop.ParameterSet("Chen2020")
15 | model = pybop.lithium_ion.DFN(parameter_set=parameter_set)
16 |
17 | # Fitting parameters
18 | parameters = pybop.Parameters(
19 | pybop.Parameter(
20 | "Negative electrode active material volume fraction",
21 | prior=pybop.Gaussian(0.68, 0.05),
22 | initial_value=0.65,
23 | bounds=[0.4, 0.9],
24 | ),
25 | pybop.Parameter(
26 | "Positive electrode active material volume fraction",
27 | prior=pybop.Gaussian(0.58, 0.05),
28 | initial_value=0.65,
29 | bounds=[0.4, 0.9],
30 | ),
31 | )
32 |
33 | # Import the synthetic dataset, set model initial state
34 | csv_data = np.loadtxt(dataset_path, delimiter=",", skiprows=1)
35 | initial_state = {"Initial open-circuit voltage [V]": csv_data[0, 2]}
36 | model.set_initial_state(initial_state=initial_state)
37 |
38 | # Form dataset
39 | dataset = pybop.Dataset(
40 | {
41 | "Time [s]": csv_data[:, 0],
42 | "Current function [A]": csv_data[:, 1],
43 | "Voltage [V]": csv_data[:, 2],
44 | "Bulk open-circuit voltage [V]": csv_data[:, 3],
45 | }
46 | )
47 |
48 | signal = ["Voltage [V]", "Bulk open-circuit voltage [V]"]
49 | # Generate problem, cost function, and optimisation class
50 | problem = pybop.FittingProblem(
51 | model,
52 | parameters,
53 | dataset,
54 | signal=signal,
55 | )
56 | cost = pybop.RootMeanSquaredError(problem)
57 |
58 | optim = pybop.IRPropPlus(
59 | cost,
60 | verbose=True,
61 | max_iterations=60,
62 | max_unchanged_iterations=15,
63 | compute_sensitivities=True,
64 | n_sensitivity_samples=64, # Decrease samples for CI (increase for higher accuracy)
65 | )
66 |
67 | # Run optimisation
68 | results = optim.run()
69 |
70 | # Plot the timeseries output
71 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison")
72 |
73 | # Plot convergence
74 | pybop.plot.convergence(optim)
75 |
76 | # Plot the parameter traces
77 | pybop.plot.parameters(optim)
78 |
79 | # Plot the cost landscape with optimisation path
80 | pybop.plot.surface(optim)
81 |
--------------------------------------------------------------------------------
/examples/scripts/getting_started/weighted_cost.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 |
3 | import pybop
4 |
5 | # Parameter set and model definition
6 | parameter_set = pybop.ParameterSet("Chen2020")
7 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set)
8 |
9 | # Fitting parameters
10 | parameters = pybop.Parameters(
11 | pybop.Parameter(
12 | "Negative electrode active material volume fraction",
13 | prior=pybop.Gaussian(0.68, 0.05),
14 | bounds=[0.5, 0.8],
15 | true_value=parameter_set["Negative electrode active material volume fraction"],
16 | ),
17 | pybop.Parameter(
18 | "Positive electrode active material volume fraction",
19 | prior=pybop.Gaussian(0.58, 0.05),
20 | bounds=[0.4, 0.7],
21 | true_value=parameter_set["Positive electrode active material volume fraction"],
22 | ),
23 | )
24 |
25 | # Generate data
26 | sigma = 0.001
27 | experiment = pybop.Experiment(
28 | [
29 | (
30 | "Discharge at 0.5C for 3 minutes (3 second period)",
31 | "Charge at 0.5C for 3 minutes (3 second period)",
32 | ),
33 | ]
34 | * 2
35 | )
36 | values = model.predict(experiment=experiment, initial_state={"Initial SoC": 0.5})
37 |
38 |
39 | def noisy(data, sigma):
40 | return data + np.random.normal(0, sigma, len(data))
41 |
42 |
43 | # Form dataset
44 | dataset = pybop.Dataset(
45 | {
46 | "Time [s]": values["Time [s]"].data,
47 | "Current function [A]": values["Current [A]"].data,
48 | "Voltage [V]": noisy(values["Voltage [V]"].data, sigma),
49 | }
50 | )
51 |
52 | # Generate problem, cost function, and optimisation class
53 | problem = pybop.FittingProblem(model, parameters, dataset)
54 | cost1 = pybop.SumSquaredError(problem)
55 | cost2 = pybop.RootMeanSquaredError(problem)
56 | weighted_cost = pybop.WeightedCost(cost1, cost2, weights=[0.1, 1])
57 |
58 | for cost in [weighted_cost, cost1, cost2]:
59 | optim = pybop.IRPropMin(cost, max_iterations=60)
60 |
61 | # Run the optimisation
62 | results = optim.run()
63 | print("True parameters:", parameters.true_value())
64 |
65 | # Plot the timeseries output
66 | pybop.plot.problem(problem, problem_inputs=results.x, title="Optimised Comparison")
67 |
68 | # Plot convergence
69 | pybop.plot.convergence(optim)
70 |
71 | # Plot the cost landscape with optimisation path
72 | pybop.plot.surface(optim)
73 |
--------------------------------------------------------------------------------
/examples/standalone/cost.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 |
3 | import pybop
4 |
5 |
6 | class StandaloneCost(pybop.BaseCost):
7 | """
8 | A standalone cost function example that inherits from pybop.BaseCost.
9 |
10 | This class represents a simple cost function without a problem object, used for demonstration purposes.
11 | It is a quadratic function of one variable with a constant term, defined by
12 | the formula: cost(x) = x^2 + 42.
13 |
14 | Parameters
15 | ----------
16 | problem : object, optional
17 | A dummy problem instance used to initialize the superclass. This is not
18 | used in the current class but is accepted for compatibility with the
19 | BaseCost interface.
20 | parameters : pybop.Parameters
21 | A pybop.Parameters object storing a dictionary of parameters and their
22 | properties, for example their initial value and bounds.
23 |
24 | Methods
25 | -------
26 | __call__(x)
27 | Calculate the cost for a given parameter value.
28 | """
29 |
30 | def __init__(self, problem=None):
31 | """
32 | Initialise the StandaloneCost class with optional problem instance.
33 |
34 | The problem object is not utilised in this subclass. The parameters, including
35 | their initial value and bounds, are defined within this standalone cost object.
36 | """
37 | super().__init__(problem)
38 |
39 | self._parameters = pybop.Parameters(
40 | pybop.Parameter(
41 | "x",
42 | initial_value=4.2,
43 | bounds=[-1, 10],
44 | ),
45 | )
46 | self.x0 = self._parameters.initial_value()
47 |
48 | def compute(
49 | self, y: dict = None, dy: np.ndarray = None, calculate_grad: bool = False
50 | ):
51 | """
52 | Compute the cost for a given parameter value.
53 |
54 | The cost function is defined as cost(x) = x^2 + 42, where x is the
55 | parameter value.
56 |
57 | Returns
58 | -------
59 | float
60 | The calculated cost value for the given parameter.
61 | """
62 | return np.asarray(self._parameters["x"].value ** 2 + 42)
63 |
--------------------------------------------------------------------------------
/examples/standalone/model.py:
--------------------------------------------------------------------------------
1 | import pybamm
2 |
3 | from pybop.models.base_model import BaseModel
4 |
5 |
6 | class ExponentialDecay(BaseModel):
7 | """
8 | Exponential decay model with two parameters y0 and k
9 |
10 | dy/dt = -ky
11 | y(0) = y0
12 |
13 | """
14 |
15 | def __init__(
16 | self,
17 | name: str = "Constant Model",
18 | parameter_set: pybamm.ParameterValues = None,
19 | n_states: int = 1,
20 | ):
21 | super().__init__(name=name, parameter_set=parameter_set)
22 |
23 | self.n_states = n_states
24 | if n_states < 1:
25 | raise ValueError("The number of states (n_states) must be greater than 0")
26 | self.pybamm_model = pybamm.BaseModel()
27 | ys = [pybamm.Variable(f"y_{i}") for i in range(n_states)]
28 | k = pybamm.Parameter("k")
29 | y0 = pybamm.Parameter("y0")
30 | self.pybamm_model.rhs = {y: -k * y for y in ys}
31 | self.pybamm_model.initial_conditions = {y: y0 for y in ys}
32 | self.pybamm_model.variables = {"y_0": ys[0], "2y": 2 * ys[0]}
33 |
34 | default_parameter_values = pybamm.ParameterValues(
35 | {
36 | "k": 0.1,
37 | "y0": 1,
38 | }
39 | )
40 |
41 | self._unprocessed_model = self.pybamm_model
42 |
43 | self.default_parameter_values = (
44 | default_parameter_values
45 | if self._parameter_set is None
46 | else self._parameter_set
47 | )
48 | self._parameter_set = self.default_parameter_values
49 | self._unprocessed_parameter_set = self._parameter_set
50 |
51 | self._geometry = self.pybamm_model.default_geometry
52 | self._submesh_types = self.pybamm_model.default_submesh_types
53 | self._var_pts = self.pybamm_model.default_var_pts
54 | self._spatial_methods = self.pybamm_model.default_spatial_methods
55 | self._solver = pybamm.CasadiSolver(mode="fast")
56 | self._model_with_set_params = None
57 | self._built_model = None
58 | self._built_initial_soc = None
59 | self._mesh = None
60 | self._disc = None
61 | self.geometric_parameters = {}
62 |
--------------------------------------------------------------------------------
/examples/standalone/optimiser.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from scipy.optimize import minimize
3 |
4 | from pybop import BaseOptimiser, OptimisationResult
5 |
6 |
7 | class StandaloneOptimiser(BaseOptimiser):
8 | """
9 | Defines an example standalone optimiser without a Cost.
10 | """
11 |
12 | def __init__(self, cost=None, **optimiser_kwargs):
13 | # Define cost function
14 | def cost(x):
15 | x1, x2 = x
16 | return (x1 - 2) ** 2 + (x2 - 4) ** 4
17 |
18 | # Set initial values and other options
19 | optimiser_options = dict(
20 | x0=np.array([0, 0]),
21 | bounds=None,
22 | method="Nelder-Mead",
23 | jac=False,
24 | maxiter=100,
25 | )
26 | optimiser_options.update(optimiser_kwargs)
27 | super().__init__(cost, **optimiser_options)
28 |
29 | def _set_up_optimiser(self):
30 | """
31 | Parse optimiser options.
32 | """
33 | # Reformat bounds
34 | if isinstance(self.bounds, dict):
35 | self._scipy_bounds = [
36 | (lower, upper)
37 | for lower, upper in zip(self.bounds["lower"], self.bounds["upper"])
38 | ]
39 | else:
40 | self._scipy_bounds = self.bounds
41 |
42 | # Parse additional options and remove them from the options dictionary
43 | self._options = self.unset_options
44 | self.unset_options = dict()
45 | self._options["options"] = self._options.pop("options", dict())
46 | if "maxiter" in self._options.keys():
47 | # Nest this option within an options dictionary for SciPy minimize
48 | self._options["options"]["maxiter"] = self._options.pop("maxiter")
49 |
50 | def _run(self):
51 | """
52 | Executes the optimisation process using SciPy's minimize function.
53 |
54 | Returns
55 | -------
56 | x : numpy.ndarray
57 | The best parameter set found by the optimisation.
58 | final_cost : float
59 | The final cost associated with the best parameters.
60 | """
61 | self.log = [[self.x0]]
62 |
63 | # Add callback storing history of parameter values
64 | def callback(x):
65 | self.log.append([x])
66 |
67 | # Run optimiser
68 | result = minimize(
69 | self.cost,
70 | self.x0,
71 | bounds=self._scipy_bounds,
72 | callback=callback,
73 | **self._options,
74 | )
75 |
76 | return OptimisationResult(
77 | optim=self,
78 | x=result.x,
79 | n_iterations=result.nit,
80 | scipy_result=result,
81 | )
82 |
83 | def name(self):
84 | """
85 | Provides the name of the optimisation strategy.
86 |
87 | Returns
88 | -------
89 | str
90 | The name 'StandaloneOptimiser'.
91 | """
92 | return "StandaloneOptimiser"
93 |
--------------------------------------------------------------------------------
/examples/standalone/problem.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 |
3 | from pybop import BaseProblem, Inputs
4 |
5 |
6 | class StandaloneProblem(BaseProblem):
7 | """
8 | Defines an example standalone problem without a Model.
9 | """
10 |
11 | def __init__(
12 | self,
13 | parameters,
14 | dataset,
15 | model=None,
16 | check_model=True,
17 | signal=None,
18 | additional_variables=None,
19 | initial_state=None,
20 | ):
21 | super().__init__(parameters, model, check_model, signal, additional_variables)
22 | self._dataset = dataset.data
23 |
24 | # Check that the dataset contains time and current
25 | for name in ["Time [s]", *self.signal]:
26 | if name not in self._dataset:
27 | raise ValueError(f"expected {name} in list of dataset")
28 |
29 | self._domain_data = self._dataset[self.domain]
30 | self.n_data = len(self._domain_data)
31 | if np.any(self._domain_data < 0):
32 | raise ValueError("Times can not be negative.")
33 | if np.any(self._domain_data[:-1] >= self._domain_data[1:]):
34 | raise ValueError("Times must be increasing.")
35 |
36 | for signal in self.signal:
37 | if len(self._dataset[signal]) != self.n_data:
38 | raise ValueError(
39 | f"Time data and {signal} data must be the same length."
40 | )
41 | self._target = {signal: self._dataset[signal] for signal in self.signal}
42 |
43 | def evaluate(self, inputs: Inputs):
44 | """
45 | Evaluate the model with the given parameters and return the signal.
46 |
47 | Parameters
48 | ----------
49 | inputs : Inputs
50 | Parameters for evaluation of the model.
51 |
52 | Returns
53 | -------
54 | dict[str, np.ndarray[np.float64]]
55 | The model output y(t) simulated with the given inputs.
56 | """
57 | return {
58 | signal: inputs["Gradient"] * self._domain_data + inputs["Intercept"]
59 | for signal in self.signal
60 | }
61 |
62 | def evaluateS1(self, inputs):
63 | """
64 | Evaluate the model with the given parameters and return the signal and its derivatives.
65 |
66 | Parameters
67 | ----------
68 | inputs : Inputs
69 | Parameters for evaluation of the model.
70 |
71 | Returns
72 | -------
73 | tuple[dict[str, np.ndarray[np.float64]], dict[str, dict[str, np.ndarray]]]
74 | A tuple containing the simulation result y(t) and the sensitivities dy/dx(t) for each
75 | parameter x and signal y.
76 | """
77 |
78 | y = self.evaluate(inputs)
79 |
80 | dy = dict.fromkeys(inputs.keys())
81 | dy["Gradient"] = {signal: self._domain_data for signal in self.signal}
82 | dy["Intercept"] = {
83 | signal: np.zeros((self.n_outputs, self.n_data)) for signal in self.signal
84 | }
85 |
86 | return (y, dy)
87 |
--------------------------------------------------------------------------------
/papers/Hallemans et al/Data/LGM50LT/Notes on LG M50LT OCP.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/papers/Hallemans et al/Data/LGM50LT/Notes on LG M50LT OCP.pdf
--------------------------------------------------------------------------------
/papers/Hallemans et al/Data/LGM50LT/OCP_LGM50LT.mat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/papers/Hallemans et al/Data/LGM50LT/OCP_LGM50LT.mat
--------------------------------------------------------------------------------
/papers/Hallemans et al/Data/LGM50LT/impedanceLGM50LT_Hybrid_4h.mat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/papers/Hallemans et al/Data/LGM50LT/impedanceLGM50LT_Hybrid_4h.mat
--------------------------------------------------------------------------------
/papers/Hallemans et al/Data/Z_SPMegrouped_SOC_chen2020.mat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/papers/Hallemans et al/Data/Z_SPMegrouped_SOC_chen2020.mat
--------------------------------------------------------------------------------
/papers/Hallemans et al/Data/timeDomainSimulation_SPMegrouped.mat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/papers/Hallemans et al/Data/timeDomainSimulation_SPMegrouped.mat
--------------------------------------------------------------------------------
/papers/Hallemans et al/Fig11_tauSensitivity.py:
--------------------------------------------------------------------------------
1 | import matplotlib.pyplot as plt
2 | import numpy as np
3 | from scipy.io import savemat
4 |
5 | import pybop
6 | from pybop.models.lithium_ion.basic_SPMe import convert_physical_to_grouped_parameters
7 |
8 | SOC = 0.2
9 |
10 | factor = 2
11 | Nparams = 11
12 | Nfreq = 60
13 | fmin = 2e-4
14 | fmax = 1e3
15 |
16 | # Get grouped parameters
17 | R0 = 0.01
18 |
19 | parameter_set = pybop.ParameterSet.pybamm("Chen2020")
20 | parameter_set["Electrolyte diffusivity [m2.s-1]"] = 1.769e-10
21 | parameter_set["Electrolyte conductivity [S.m-1]"] = 1e16
22 | parameter_set["Negative electrode conductivity [S.m-1]"] = 1e16
23 | parameter_set["Positive electrode conductivity [S.m-1]"] = 1e16
24 |
25 | grouped_parameters = convert_physical_to_grouped_parameters(parameter_set)
26 | grouped_parameters["Series resistance [Ohm]"] = R0
27 | model_options = {"surface form": "differential", "contact resistance": "true"}
28 | var_pts = {"x_n": 100, "x_s": 20, "x_p": 100, "r_n": 100, "r_p": 100}
29 |
30 | ## Change parameters
31 | parameter_name = "Negative particle diffusion time scale [s]"
32 | param0 = grouped_parameters[parameter_name]
33 | params = np.logspace(np.log10(param0 / factor), np.log10(param0 * factor), Nparams)
34 |
35 | # Simulate impedance at these parameter values
36 | frequencies = np.logspace(np.log10(fmin), np.log10(fmax), Nfreq)
37 |
38 | impedances = 1j * np.zeros((Nfreq, Nparams))
39 | for ii, param in enumerate(params):
40 | grouped_parameters[parameter_name] = param
41 | model = pybop.lithium_ion.GroupedSPMe(
42 | parameter_set=grouped_parameters,
43 | eis=True,
44 | options=model_options,
45 | var_pts=var_pts,
46 | )
47 | model.build(
48 | initial_state={"Initial SoC": SOC},
49 | )
50 | simulation = model.simulateEIS(inputs=None, f_eval=frequencies)
51 | impedances[:, ii] = simulation["Impedance"]
52 |
53 | fig, ax = plt.subplots()
54 | for ii in range(Nparams):
55 | ax.plot(
56 | np.real(impedances[:, ii]),
57 | -np.imag(impedances[:, ii]),
58 | )
59 | ax.set(xlabel=r"$Z_r(\omega)$ [$\Omega$]", ylabel=r"$-Z_j(\omega)$ [$\Omega$]")
60 | ax.grid()
61 | ax.set_aspect("equal", "box")
62 | plt.show()
63 |
64 | mdic = {"Z": impedances, "f": frequencies, "name": parameter_name}
65 | savemat("Data/Z_SPMegrouped_taudn_20.mat", mdic)
66 |
--------------------------------------------------------------------------------
/papers/Hallemans et al/Fig2_timeDomainSimulation.py:
--------------------------------------------------------------------------------
1 | import matplotlib.pyplot as plt
2 | import numpy as np
3 | from scipy.io import savemat
4 |
5 | import pybop
6 | from pybop.models.lithium_ion.basic_SPMe import convert_physical_to_grouped_parameters
7 |
8 | ## Grouped parameter set
9 | R0 = 0.01
10 |
11 | parameter_set = pybop.ParameterSet.pybamm("Chen2020")
12 | parameter_set["Electrolyte diffusivity [m2.s-1]"] = 1.769e-10
13 | parameter_set["Electrolyte conductivity [S.m-1]"] = 1e16
14 | parameter_set["Negative electrode conductivity [S.m-1]"] = 1e16
15 | parameter_set["Positive electrode conductivity [S.m-1]"] = 1e16
16 |
17 | grouped_parameters = convert_physical_to_grouped_parameters(parameter_set)
18 | grouped_parameters["Series resistance [Ohm]"] = R0
19 | model_options = {"surface form": "differential", "contact resistance": "true"}
20 | var_pts = {"x_n": 100, "x_s": 20, "x_p": 100, "r_n": 100, "r_p": 100}
21 |
22 | ## Create model
23 | model = pybop.lithium_ion.GroupedSPMe(
24 | parameter_set=grouped_parameters, eis=True, var_pts=var_pts, options=model_options
25 | )
26 |
27 | ## Test model in the time domain
28 | SOC0 = 0.9
29 | model.build(initial_state={"Initial SoC": SOC0})
30 |
31 | Ts = 10 # Sampling period
32 | T = 2 * 60 * 60 # 2h
33 | N = int(T / Ts)
34 | time = np.linspace(0, T - Ts, N)
35 |
36 | i_relax7 = np.zeros([7 * int(60 / Ts)]) # 7 min
37 | i_relax20 = np.zeros([20 * int(60 / Ts)]) # 20 min
38 | i_discharge = 5 * np.ones([53 * int(60 / Ts)]) # 53 min
39 | i_charge = -5 * np.ones([20 * int(60 / Ts)]) # 20 min
40 | current = np.concatenate((i_relax7, i_discharge, i_relax20, i_charge, i_relax20))
41 |
42 | experiment = pybop.Dataset(
43 | {
44 | "Time [s]": time,
45 | "Current function [A]": current,
46 | }
47 | )
48 | model.set_current_function(dataset=experiment)
49 | simulation = model.predict(t_eval=time)
50 |
51 | # Plot traces
52 | fig, ax = plt.subplots()
53 | ax.plot(simulation["Time [s]"].data, simulation["Current [A]"].data)
54 | ax.set(xlabel="time [s]", ylabel="Current [A]")
55 | ax.grid()
56 | plt.show()
57 |
58 | fig, ax = plt.subplots()
59 | ax.plot(simulation["Time [s]"].data, simulation["Voltage [V]"].data)
60 | ax.set(xlabel="time [s]", ylabel="Voltage [V]")
61 | ax.grid()
62 | plt.show()
63 |
64 | ## Save data
65 | t = simulation["Time [s]"].data
66 | i = simulation["Current [A]"].data
67 | v = simulation["Voltage [V]"].data
68 |
69 | mdic = {"t": t, "i": i, "v": v, "SOC0": SOC0}
70 | savemat("Data/timeDomainSimulation_SPMegrouped.mat", mdic)
71 |
--------------------------------------------------------------------------------
/papers/Hallemans et al/Fig5_impedance_models.py:
--------------------------------------------------------------------------------
1 | import matplotlib.pyplot as plt
2 | import numpy as np
3 | from scipy.io import savemat
4 |
5 | import pybop
6 |
7 | Nfreq = 60
8 | SOC = 0.5
9 | fmin = 2e-4
10 | fmax = 1e3
11 |
12 | frequencies = np.logspace(np.log10(fmin), np.log10(fmax), Nfreq)
13 | impedances = 1j * np.zeros((Nfreq, 3))
14 |
15 | # Define model
16 | parameter_set = pybop.ParameterSet.pybamm("Chen2020")
17 | parameter_set["Contact resistance [Ohm]"] = 0.01
18 |
19 | model_options = {"surface form": "differential", "contact resistance": "true"}
20 | var_pts = {"x_n": 100, "x_s": 20, "x_p": 100, "r_n": 100, "r_p": 100}
21 |
22 |
23 | ## SPM
24 | model = pybop.lithium_ion.SPM(
25 | parameter_set=parameter_set, options=model_options, eis=True, var_pts=var_pts
26 | )
27 | model.build(initial_state={"Initial SoC": SOC})
28 |
29 | simulation = model.simulateEIS(inputs=None, f_eval=frequencies)
30 | impedances[:, 0] = simulation["Impedance"]
31 |
32 | ## SPMe
33 | model = pybop.lithium_ion.SPMe(
34 | parameter_set=parameter_set, options=model_options, eis=True, var_pts=var_pts
35 | )
36 | model.build(initial_state={"Initial SoC": SOC})
37 | simulation = model.simulateEIS(inputs=None, f_eval=frequencies)
38 | impedances[:, 1] = simulation["Impedance"]
39 |
40 | ## DFN
41 | model = pybop.lithium_ion.DFN(
42 | parameter_set=parameter_set, options=model_options, eis=True, var_pts=var_pts
43 | )
44 | model.build(initial_state={"Initial SoC": SOC})
45 | simulation = model.simulateEIS(inputs=None, f_eval=frequencies)
46 | impedances[:, 2] = simulation["Impedance"]
47 |
48 | ## Plot
49 | fig, ax = plt.subplots()
50 | for ii in range(3):
51 | ax.plot(
52 | np.real(impedances[:, ii]),
53 | -np.imag(impedances[:, ii]),
54 | )
55 | ax.set(xlabel=r"$Z_r(\omega)$ [$\Omega$]", ylabel=r"$-Z_j(\omega)$ [$\Omega$]")
56 | ax.grid()
57 | ax.set_aspect("equal", "box")
58 | plt.show()
59 |
60 | ## Save
61 | mdic = {"Z": impedances, "f": frequencies}
62 | savemat("Data/Z_SPM_SPMe_DFN_Pybop_chen2020.mat", mdic)
63 |
--------------------------------------------------------------------------------
/papers/Hallemans et al/Fig7_impedance_SOC_SPMegrouped.py:
--------------------------------------------------------------------------------
1 | import matplotlib.pyplot as plt
2 | import numpy as np
3 |
4 | import pybop
5 | from pybop.models.lithium_ion.basic_SPMe import convert_physical_to_grouped_parameters
6 |
7 | ## Group parameter set
8 | R0 = 0.01
9 |
10 | parameter_set = pybop.ParameterSet.pybamm("Chen2020")
11 | parameter_set["Electrolyte diffusivity [m2.s-1]"] = 1.769e-10
12 | parameter_set["Electrolyte conductivity [S.m-1]"] = 1e16
13 | parameter_set["Negative electrode conductivity [S.m-1]"] = 1e16
14 | parameter_set["Positive electrode conductivity [S.m-1]"] = 1e16
15 |
16 | grouped_parameters = convert_physical_to_grouped_parameters(parameter_set)
17 | grouped_parameters["Series resistance [Ohm]"] = R0
18 |
19 | ## Create model
20 | model_options = {"surface form": "differential", "contact resistance": "true"}
21 | var_pts = {"x_n": 100, "x_s": 20, "x_p": 100, "r_n": 100, "r_p": 100}
22 | model = pybop.lithium_ion.GroupedSPMe(
23 | parameter_set=grouped_parameters, eis=True, var_pts=var_pts, options=model_options
24 | )
25 |
26 | ## Simulate impedance
27 | Nfreq = 60
28 | fmin = 2e-4
29 | fmax = 1e3
30 | NSOC = 9
31 | frequencies = np.logspace(np.log10(fmin), np.log10(fmax), Nfreq)
32 | SOCs = np.linspace(0.1, 0.9, NSOC)
33 |
34 | impedances = 1j * np.zeros((Nfreq, NSOC))
35 | for ii, SOC in enumerate(SOCs):
36 | model.build(initial_state={"Initial SoC": SOC})
37 | simulation = model.simulateEIS(inputs=None, f_eval=frequencies)
38 | impedances[:, ii] = simulation["Impedance"]
39 |
40 | fig, ax = plt.subplots()
41 | for ii in range(len(SOCs)):
42 | ax.plot(
43 | np.real(impedances[:, ii]),
44 | -np.imag(impedances[:, ii]),
45 | )
46 | ax.set(xlabel=r"$Z_r(\omega)$ [$\Omega$]", ylabel=r"$-Z_j(\omega)$ [$\Omega$]")
47 | ax.grid()
48 | ax.set_aspect("equal", "box")
49 | plt.show()
50 |
51 | ## Save data
52 | # mdic = {"Z": impedances, "f": frequencies, "SOC": SOCs}
53 | # savemat("Data/Z_SPMegrouped_SOC_chen2020.mat", mdic)
54 |
--------------------------------------------------------------------------------
/papers/Hallemans et al/Fig8_groupedParamSensitivity.py:
--------------------------------------------------------------------------------
1 | import matplotlib.pyplot as plt
2 | import numpy as np
3 | from scipy.io import savemat
4 |
5 | import pybop
6 | from pybop.models.lithium_ion.basic_SPMe import convert_physical_to_grouped_parameters
7 |
8 | #
9 | factor = 2
10 | Nparams = 11
11 | SOC = 0.5
12 | Nfreq = 60
13 | fmin = 2e-4
14 | fmax = 1e3
15 |
16 | # Get grouped parameters
17 | R0 = 0.01
18 |
19 | parameter_set = pybop.ParameterSet.pybamm("Chen2020")
20 | parameter_set["Electrolyte diffusivity [m2.s-1]"] = 1.769e-10
21 | parameter_set["Electrolyte conductivity [S.m-1]"] = 1e16
22 | parameter_set["Negative electrode conductivity [S.m-1]"] = 1e16
23 | parameter_set["Positive electrode conductivity [S.m-1]"] = 1e16
24 |
25 | grouped_parameters = convert_physical_to_grouped_parameters(parameter_set)
26 | grouped_parameters["Series resistance [Ohm]"] = R0
27 | model_options = {"surface form": "differential", "contact resistance": "true"}
28 |
29 | var_pts = {"x_n": 100, "x_s": 20, "x_p": 100, "r_n": 100, "r_p": 100}
30 |
31 | ## Change parameters
32 | parameter_name = "Negative electrode relative porosity"
33 |
34 | # "Positive particle diffusion time scale [s]"
35 | # "Positive electrode electrolyte diffusion time scale [s]"
36 | # "Separator electrolyte diffusion time scale [s]"
37 | # "Positive electrode charge transfer time scale [s]"
38 | # "Series resistance [Ohm]"
39 | # "Positive electrode relative porosity"
40 | # "Cation transference number"
41 | # "Reference electrolyte capacity [A.s]"
42 | # "Positive electrode capacitance [F]"
43 | # "Positive theoretical electrode capacity [As]"
44 | # "Positive electrode relative thickness"
45 | # "Measured cell capacity [A.s]"
46 |
47 | param0 = grouped_parameters[parameter_name]
48 |
49 | params = np.logspace(np.log10(param0 / factor), np.log10(param0 * factor), Nparams)
50 |
51 | # Simulate impedance at these parameter values
52 | frequencies = np.logspace(np.log10(fmin), np.log10(fmax), Nfreq)
53 |
54 | impedances = 1j * np.zeros((Nfreq, Nparams))
55 | for ii, param in enumerate(params):
56 | grouped_parameters[parameter_name] = param
57 | model = pybop.lithium_ion.GroupedSPMe(
58 | parameter_set=grouped_parameters,
59 | eis=True,
60 | options=model_options,
61 | var_pts=var_pts,
62 | )
63 | model.build(
64 | initial_state={"Initial SoC": SOC},
65 | )
66 | simulation = model.simulateEIS(inputs=None, f_eval=frequencies)
67 | impedances[:, ii] = simulation["Impedance"]
68 |
69 | fig, ax = plt.subplots()
70 | for ii in range(Nparams):
71 | ax.plot(
72 | np.real(impedances[:, ii]),
73 | -np.imag(impedances[:, ii]),
74 | )
75 | ax.set(xlabel=r"$Z_r(\omega)$ [$\Omega$]", ylabel=r"$-Z_j(\omega)$ [$\Omega$]")
76 | ax.grid()
77 | ax.set_aspect("equal", "box")
78 | plt.show()
79 |
80 | mdic = {"Z": impedances, "f": frequencies, "name": parameter_name}
81 | savemat("Data/Sensitivity/Z_SPMegrouped_zetan.mat", mdic)
82 |
--------------------------------------------------------------------------------
/papers/Hallemans et al/README.md:
--------------------------------------------------------------------------------
1 | # Physics-based battery model parametrisation from impedance data
2 |
3 | This directory contains the code used for [Hallemans et al.](http://arxiv.org/abs/2412.10896) article.
4 |
5 | Note: These scripts were ran using `PyBOP` v24.12, and are not maintained for future PyBOP releases.
6 |
--------------------------------------------------------------------------------
/pybop/_evaluation.py:
--------------------------------------------------------------------------------
1 | import jax.numpy as jnp
2 | import numpy as np
3 | from pints import Evaluator as PintsEvaluator
4 |
5 |
6 | class SequentialJaxEvaluator(PintsEvaluator):
7 | """
8 | Sequential evaluates a function (or callable object)
9 | for either a single or multiple positions. This class is based
10 | off the PintsSequentialEvaluator class, with additions for
11 | PyBOP's JAX cost classes.
12 |
13 | Parameters
14 | ----------
15 | function : callable
16 | The function to evaluate. This function should accept an input and
17 | optionally additional arguments, returning either a single value or a tuple.
18 | args : sequence, optional
19 | A sequence containing extra arguments to be passed to the function.
20 | If specified, the function will be called as `function(x, *args)`.
21 | """
22 |
23 | def _evaluate(self, positions):
24 | scores = [self._function(x, *self._args) for x in positions]
25 |
26 | # If gradient provided, convert jnp to np and return
27 | if isinstance(scores[0], tuple):
28 | return [(score[0].item(), score[1]) for score in scores]
29 |
30 | return np.asarray(scores)
31 |
32 |
33 | class SciPyEvaluator(PintsEvaluator):
34 | """
35 | Evaluates a function (or callable object) for the SciPy optimisers
36 | for either a single or multiple positions.
37 |
38 | Parameters
39 | ----------
40 | function : callable
41 | The function to evaluate. This function should accept an input and
42 | optionally additional arguments, returning either a single value or a tuple.
43 | args : sequence, optional
44 | A sequence containing extra arguments to be passed to the function.
45 | If specified, the function will be called as `function(x, *args)`.
46 | """
47 |
48 | def _evaluate(self, positions):
49 | scores = [self._function(x, *self._args) for x in [positions]]
50 |
51 | if not isinstance(scores[0], tuple):
52 | return np.asarray(scores)[0]
53 |
54 | # If gradient provided, convert jnp to np and return
55 | if isinstance(scores[0][0], jnp.ndarray):
56 | return [(score[0].item(), score[1]) for score in scores][0]
57 | return [(score[0], score[1]) for score in scores][0]
58 |
--------------------------------------------------------------------------------
/pybop/_experiment.py:
--------------------------------------------------------------------------------
1 | from pybamm import Experiment
2 |
3 |
4 | class Experiment(Experiment):
5 | """
6 | Light wrapper of the PyBaMM Experiment class for generating experiment conditions for PyBaMM models.
7 | Credit: PyBaMM
8 |
9 | Base class for experimental conditions under which to run the model. In general, a
10 | list of operating conditions should be passed in. Each operating condition should
11 | be either a `pybamm.step._Step` class, created using one of the methods
12 | `pybamm.step.current`, `pybamm.step.c_rate`, `pybamm.step.voltage`
13 | , `pybamm.step.power`, `pybamm.step.resistance`, or
14 | `pybamm.step.string`, or a string, in which case the string is passed to
15 | `pybamm.step.string`.
16 |
17 | Parameters
18 | ----------
19 | operating_conditions : list
20 | List of operating conditions
21 | period : string, optional
22 | Period (1/frequency) at which to record outputs. Default is 1 minute. Can be
23 | overwritten by individual operating conditions.
24 | temperature: float, optional
25 | The ambient air temperature in degrees Celsius at which to run the experiment.
26 | Default is None whereby the ambient temperature is taken from the parameter set.
27 | This value is overwritten if the temperature is specified in a step.
28 | termination : list, optional
29 | List of conditions under which to terminate the experiment. Default is None.
30 | This is different from the termination for individual steps. Termination for
31 | individual steps is specified in the step itself, and the simulation moves to
32 | the next step when the termination condition is met
33 | (e.g. 2.5V discharge cut-off). Termination for the
34 | experiment as a whole is specified here, and the simulation stops when the
35 | termination condition is met (e.g. 80% capacity).
36 | """
37 |
38 | def __init__(
39 | self,
40 | operating_conditions,
41 | period=None,
42 | temperature=None,
43 | termination=None,
44 | ):
45 | super().__init__(
46 | operating_conditions,
47 | period,
48 | temperature,
49 | termination,
50 | )
51 |
--------------------------------------------------------------------------------
/pybop/_version.py:
--------------------------------------------------------------------------------
1 | import importlib.metadata
2 |
3 | __version__ = importlib.metadata.version("pybop")
4 |
--------------------------------------------------------------------------------
/pybop/applications/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/pybop/applications/__init__.py
--------------------------------------------------------------------------------
/pybop/costs/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/pybop/costs/__init__.py
--------------------------------------------------------------------------------
/pybop/models/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/pybop/models/__init__.py
--------------------------------------------------------------------------------
/pybop/models/_exponential_decay.py:
--------------------------------------------------------------------------------
1 | import pybamm
2 |
3 | from pybop.models.base_model import BaseModel
4 |
5 |
6 | class ExponentialDecayModel(BaseModel):
7 | """
8 | Exponential decay model defined by the equation:
9 |
10 | dy/dt = -k * y, y(0) = y0
11 |
12 | Note: The output variables are named "y_{i}" for each state.
13 | For example, the first state is "y_0", the second is "y_1", etc.
14 | Attributes:
15 | n_states (int): Number of states in the system (default is 1).
16 | pybamm_model (pybamm.BaseModel): PyBaMM model representation.
17 | default_parameter_values (pybamm.ParameterValues): Default parameter values
18 | for the model, with "k" (decay rate) and "y0" (initial condition).
19 |
20 | Parameters:
21 | name (str): Name of the model (default: "Experimental Decay Model").
22 | parameter_set (pybamm.ParameterValues): Parameter values for the model.
23 | n_states (int): Number of states in the system. Must be >= 1.
24 | """
25 |
26 | def __init__(
27 | self,
28 | name: str = "Experimental Decay Model",
29 | parameter_set: pybamm.ParameterValues = None,
30 | n_states: int = 1,
31 | solver=None,
32 | ):
33 | if n_states < 1:
34 | raise ValueError("The number of states (n_states) must be at least 1.")
35 |
36 | super().__init__(name=name, parameter_set=parameter_set)
37 |
38 | self.n_states = n_states
39 | if solver is None:
40 | self._solver = pybamm.CasadiSolver
41 | self._solver.mode = "fast with events"
42 | self._solver.max_step_decrease_count = 1
43 | else:
44 | self._solver = solver
45 |
46 | # Initialise the PyBaMM model, variables, parameters
47 | self.pybamm_model = pybamm.BaseModel()
48 | ys = [pybamm.Variable(f"y_{i}") for i in range(n_states)]
49 | k = pybamm.Parameter("k")
50 | y0 = pybamm.Parameter("y0")
51 |
52 | # Set up the right-hand side and initial conditions
53 | self.pybamm_model.rhs = {y: -k * y for y in ys}
54 | self.pybamm_model.initial_conditions = {y: y0 for y in ys}
55 |
56 | # Define model outputs and set default values
57 | self.pybamm_model.variables = {f"y_{en}": i for en, i in enumerate(ys)} | {
58 | "Time [s]": pybamm.t
59 | }
60 | self.default_parameter_values = parameter_set or pybamm.ParameterValues(
61 | {"k": 0.1, "y0": 1}
62 | )
63 |
64 | # Store model attributes to be used by the solver
65 | self._unprocessed_model = self.pybamm_model
66 | self._parameter_set = self.default_parameter_values
67 | self._unprocessed_parameter_set = self._parameter_set
68 |
69 | # Geometry and solver setup
70 | self._geometry = self.pybamm_model.default_geometry
71 | self._submesh_types = self.pybamm_model.default_submesh_types
72 | self._var_pts = self.pybamm_model.default_var_pts
73 | self._spatial_methods = self.pybamm_model.default_spatial_methods
74 | self._solver = pybamm.CasadiSolver(mode="fast")
75 |
76 | # Additional attributes for solver and discretisation
77 | self._model_with_set_params = None
78 | self._built_model = None
79 | self._built_initial_soc = None
80 | self._mesh = None
81 | self._disc = None
82 | self.geometric_parameters = {}
83 |
--------------------------------------------------------------------------------
/pybop/models/empirical/__init__.py:
--------------------------------------------------------------------------------
1 | #
2 | # Import lithium ion based models
3 | #
4 | from .base_ecm import ECircuitModel
5 | from .ecm import Thevenin
6 |
--------------------------------------------------------------------------------
/pybop/models/empirical/ecm.py:
--------------------------------------------------------------------------------
1 | from pybamm import equivalent_circuit as pybamm_equivalent_circuit
2 |
3 | from pybop.models.empirical.base_ecm import ECircuitModel
4 |
5 |
6 | class Thevenin(ECircuitModel):
7 | """
8 | The Thevenin class represents an equivalent circuit model based on the Thevenin model in PyBaMM.
9 |
10 | This class encapsulates the PyBaMM equivalent circuit Thevenin model, providing an interface
11 | to define the parameters, geometry, submesh types, variable points, spatial methods, and solver
12 | to be used for simulations.
13 |
14 | Parameters
15 | ----------
16 | name : str, optional
17 | A name for the model instance. Defaults to "Equivalent Circuit Thevenin Model".
18 | **model_kwargs : optional
19 | Valid PyBaMM model option keys and their values, for example:
20 | parameter_set : pybamm.ParameterValues or dict, optional
21 | The parameters for the model. If None, default parameters provided by PyBaMM are used.
22 | geometry : dict, optional
23 | The geometry definitions for the model. If None, default geometry from PyBaMM is used.
24 | submesh_types : dict, optional
25 | The types of submeshes to use. If None, default submesh types from PyBaMM are used.
26 | var_pts : dict, optional
27 | The discretization points for each variable in the model. If None, default points from PyBaMM are used.
28 | spatial_methods : dict, optional
29 | The spatial methods used for discretization. If None, default spatial methods from PyBaMM are used.
30 | solver : pybamm.Solver, optional
31 | The solver to use for simulating the model. If None, the default solver from PyBaMM is used.
32 | build : bool, optional
33 | If True, the model is built upon creation (default: False).
34 | options : dict, optional
35 | A dictionary of options to customise the behaviour of the PyBaMM model.
36 | """
37 |
38 | def __init__(
39 | self,
40 | name="Equivalent Circuit Thevenin Model",
41 | eis=False,
42 | **model_kwargs,
43 | ):
44 | super().__init__(
45 | pybamm_model=pybamm_equivalent_circuit.Thevenin,
46 | name=name,
47 | eis=eis,
48 | **model_kwargs,
49 | )
50 |
--------------------------------------------------------------------------------
/pybop/models/lithium_ion/__init__.py:
--------------------------------------------------------------------------------
1 | #
2 | # Import lithium ion based models
3 | #
4 | from .base_echem import EChemBaseModel
5 | from .echem import SPM, SPMe, DFN, MPM, MSMR, WeppnerHuggins, SPDiffusion, GroupedSPMe
6 |
--------------------------------------------------------------------------------
/pybop/observers/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/pybop/observers/__init__.py
--------------------------------------------------------------------------------
/pybop/optimisers/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/pybop/optimisers/__init__.py
--------------------------------------------------------------------------------
/pybop/optimisers/optimisation.py:
--------------------------------------------------------------------------------
1 | from pybop import XNES, BasePintsOptimiser, BaseSciPyOptimiser
2 |
3 |
4 | class Optimisation:
5 | """
6 | A high-level class for optimisation using PyBOP or PINTS optimisers.
7 |
8 | This class provides an alternative API to the `PyBOP.Optimiser()` API,
9 | specifically allowing for single user-friendly interface for the
10 | optimisation process.The class can be used with either PyBOP or PINTS
11 | optimisers.
12 |
13 | Parameters
14 | ----------
15 | cost : pybop.BaseCost or pints.ErrorMeasure
16 | An objective function to be optimized, which can be either a pybop.Cost
17 | optimiser : pybop.Optimiser or subclass of pybop.BaseOptimiser, optional
18 | An optimiser from either the PINTS or PyBOP framework to perform the optimisation (default: None).
19 | sigma0 : float or sequence, optional
20 | Initial step size or standard deviation for the optimiser (default: None).
21 | verbose : bool, optional
22 | If True, the optimisation progress is printed (default: False).
23 | physical_viability : bool, optional
24 | If True, the feasibility of the optimised parameters is checked (default: True).
25 | allow_infeasible_solutions : bool, optional
26 | If True, infeasible parameter values will be allowed in the optimisation (default: True).
27 |
28 | Attributes
29 | ----------
30 | All attributes from the pybop.optimiser() class
31 |
32 | """
33 |
34 | def __init__(self, cost, optimiser=None, **optimiser_kwargs):
35 | self.__dict__["optim"] = (
36 | None # Pre-define optimiser to avoid recursion during initialisation
37 | )
38 | if optimiser is None:
39 | self.optim = XNES(cost, **optimiser_kwargs)
40 | elif issubclass(optimiser, BasePintsOptimiser):
41 | self.optim = optimiser(cost, **optimiser_kwargs)
42 | elif issubclass(optimiser, BaseSciPyOptimiser):
43 | self.optim = optimiser(cost, **optimiser_kwargs)
44 | else:
45 | raise ValueError("Unknown optimiser type")
46 |
47 | def run(self):
48 | return self.optim.run()
49 |
50 | def __getattr__(self, attr):
51 | if "optim" in self.__dict__ and hasattr(self.optim, attr):
52 | return getattr(self.optim, attr)
53 | raise AttributeError(
54 | f"'{self.__class__.__name__}' object has no attribute '{attr}'"
55 | )
56 |
57 | def __setattr__(self, name: str, value) -> None:
58 | if (
59 | name in self.__dict__
60 | or "optim" not in self.__dict__
61 | or not hasattr(self.optim, name)
62 | ):
63 | object.__setattr__(self, name, value)
64 | else:
65 | setattr(self.optim, name, value)
66 |
--------------------------------------------------------------------------------
/pybop/parameters/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/pybop/parameters/__init__.py
--------------------------------------------------------------------------------
/pybop/plot/__init__.py:
--------------------------------------------------------------------------------
1 | #
2 | # Import plots
3 | #
4 | from .plotly_manager import PlotlyManager
5 | from .standard_plots import StandardPlot, StandardSubplot, trajectories
6 | from .contour import contour
7 | from .dataset import dataset
8 | from .convergence import convergence
9 | from .parameters import parameters
10 | from .problem import problem
11 | from .nyquist import nyquist
12 | from .voronoi import surface
13 |
--------------------------------------------------------------------------------
/pybop/plot/convergence.py:
--------------------------------------------------------------------------------
1 | from pybop.plot.standard_plots import StandardPlot
2 |
3 |
4 | def convergence(optim, show=True, **layout_kwargs):
5 | """
6 | Plot the convergence of the optimisation algorithm.
7 |
8 | Parameters
9 | -----------
10 | optim : object
11 | Optimisation object containing the cost function and optimiser.
12 | show : bool, optional
13 | If True, the figure is shown upon creation (default: True).
14 | **layout_kwargs : optional
15 | Valid Plotly layout keys and their values,
16 | e.g. `xaxis_title="Time [s]"` or
17 | `xaxis={"title": "Time [s]", font={"size":14}}`
18 |
19 | Returns
20 | ---------
21 | fig : plotly.graph_objs.Figure
22 | The Plotly figure object for the convergence plot.
23 | """
24 |
25 | # Extract log from the optimisation object
26 | cost_log = optim.log.cost_best
27 |
28 | # Generate a list of iteration numbers
29 | iteration_numbers = list(range(1, len(cost_log) + 1))
30 |
31 | # Create a plot dictionary
32 | plot_dict = StandardPlot(
33 | x=iteration_numbers,
34 | y=cost_log,
35 | layout_options=dict(
36 | xaxis_title="Iteration",
37 | yaxis_title="Cost",
38 | title="Convergence",
39 | ),
40 | trace_names=optim.name(),
41 | )
42 |
43 | # Generate and display the figure
44 | fig = plot_dict(show=False)
45 | fig.update_layout(**layout_kwargs)
46 | if show:
47 | fig.show()
48 |
49 | return fig
50 |
--------------------------------------------------------------------------------
/pybop/plot/dataset.py:
--------------------------------------------------------------------------------
1 | from pybop.plot.standard_plots import StandardPlot, trajectories
2 |
3 |
4 | def dataset(dataset, signal=None, trace_names=None, show=True, **layout_kwargs):
5 | """
6 | Quickly plot a PyBOP Dataset using Plotly.
7 |
8 | Parameters
9 | ----------
10 | dataset : object
11 | A PyBOP dataset.
12 | signal : list or str, optional
13 | The name of the time series to plot (default: "Voltage [V]").
14 | trace_names : list or str, optional
15 | Name(s) for the trace(s) (default: "Data").
16 | show : bool, optional
17 | If True, the figure is shown upon creation (default: True).
18 | **layout_kwargs : optional
19 | Valid Plotly layout keys and their values,
20 | e.g. `xaxis_title="Time / s"` or
21 | `xaxis={"title": "Time [s]", font={"size":14}}`
22 |
23 | Returns
24 | -------
25 | plotly.graph_objs.Figure
26 | The Plotly figure object for the scatter plot.
27 | """
28 |
29 | # Get data dictionary
30 | if signal is None:
31 | signal = ["Voltage [V]"]
32 | dataset.check(signal=signal)
33 |
34 | # Compile ydata and labels or legend
35 | y = [dataset[s] for s in signal]
36 | if len(signal) == 1:
37 | yaxis_title = signal[0]
38 | if trace_names is None:
39 | trace_names = ["Data"]
40 | else:
41 | yaxis_title = "Output"
42 | if trace_names is None:
43 | trace_names = StandardPlot.remove_brackets(signal)
44 |
45 | # Create the figure
46 | fig = trajectories(
47 | x=dataset[dataset.domain],
48 | y=y,
49 | trace_names=trace_names,
50 | show=False,
51 | xaxis_title=StandardPlot.remove_brackets(dataset.domain),
52 | yaxis_title=yaxis_title,
53 | )
54 | fig.update_layout(**layout_kwargs)
55 | if show:
56 | fig.show()
57 |
58 | return fig
59 |
--------------------------------------------------------------------------------
/pybop/plot/parameters.py:
--------------------------------------------------------------------------------
1 | from pybop import GaussianLogLikelihood
2 | from pybop.plot.standard_plots import StandardSubplot
3 |
4 |
5 | def parameters(optim, show=True, **layout_kwargs):
6 | """
7 | Plot the evolution of parameters during the optimization process using Plotly.
8 |
9 | Parameters
10 | ----------
11 | optim : object
12 | Optimisation object containing the history of parameter values and associated cost.
13 | show : bool, optional
14 | If True, the figure is shown upon creation (default: True).
15 | **layout_kwargs : optional
16 | Valid Plotly layout keys and their values,
17 | e.g. `xaxis_title="Time [s]"` or
18 | `xaxis={"title": "Time [s]", font={"size":14}}`
19 |
20 | Returns
21 | -------
22 | plotly.graph_objs.Figure
23 | A Plotly figure object showing the parameter evolution over iterations.
24 | """
25 |
26 | # Extract parameters and log from the optimisation object
27 | parameters = optim.cost.parameters
28 | x = list(range(len(optim.log.x)))
29 | y = [list(item) for item in zip(*optim.log.x)]
30 |
31 | # Create lists of axis titles and trace names
32 | axis_titles = []
33 | trace_names = parameters.keys()
34 | for name in trace_names:
35 | axis_titles.append(("Function Call", name))
36 |
37 | if isinstance(optim.cost, GaussianLogLikelihood):
38 | axis_titles.append(("Function Call", "Sigma"))
39 | trace_names.append("Sigma")
40 |
41 | # Set subplot layout options
42 | layout_options = dict(
43 | title="Parameter Convergence",
44 | width=1024,
45 | height=576,
46 | legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
47 | )
48 |
49 | # Create a plot dictionary
50 | plot_dict = StandardSubplot(
51 | x=x,
52 | y=y,
53 | axis_titles=axis_titles,
54 | layout_options=layout_options,
55 | trace_names=trace_names,
56 | trace_name_width=50,
57 | )
58 |
59 | # Generate the figure and update the layout
60 | fig = plot_dict(show=False)
61 | fig.update_layout(**layout_kwargs)
62 | if show:
63 | fig.show()
64 |
65 | return fig
66 |
--------------------------------------------------------------------------------
/pybop/problems/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/pybop/problems/__init__.py
--------------------------------------------------------------------------------
/pybop/samplers/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pybop-team/PyBOP/7e84d75358087dafee6bfa4a8ae1d8f1d905154b/pybop/samplers/__init__.py
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools>=64"]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [project]
6 | name = "pybop"
7 | version = "25.3"
8 | authors = [
9 | {name = "The PyBOP Team"},
10 | ]
11 | maintainers = [
12 | {name = "The PyBOP Team"},
13 | ]
14 | description = "Python Battery Optimisation and Parameterisation"
15 | readme = {file = "README.md", content-type = "text/markdown"}
16 | license = { file = "LICENSE" }
17 | classifiers = [
18 | "Development Status :: 3 - Alpha",
19 | "License :: OSI Approved :: BSD License",
20 | "Programming Language :: Python :: 3.9",
21 | "Programming Language :: Python :: 3.10",
22 | "Programming Language :: Python :: 3.11",
23 | "Programming Language :: Python :: 3.12",
24 | "Intended Audience :: Science/Research",
25 | "Topic :: Scientific/Engineering",
26 | ]
27 | requires-python = ">=3.9, <3.13"
28 | dependencies = [
29 | "pybamm[jax]>=24.5.1",
30 | "numpy>=1.16, <2.0",
31 | "scipy>=1.3",
32 | "pints>=0.5",
33 | "SALib>=1.5",
34 | ]
35 |
36 | [project.optional-dependencies]
37 | plot = ["plotly>=5.0"]
38 | docs = [
39 | "pydata-sphinx-theme",
40 | "sphinx>=6",
41 | "sphinx-autobuild",
42 | "sphinx-autoapi",
43 | "sphinx_copybutton",
44 | "sphinx_favicon",
45 | "sphinx_design",
46 | "myst-parser",
47 | ]
48 | dev = [
49 | "nox[uv]",
50 | "nbmake",
51 | "pre-commit",
52 | "pytest>=6",
53 | "pytest-cov",
54 | "pytest-mock",
55 | "pytest-xdist",
56 | "ruff",
57 | ]
58 | scifem = [
59 | "scikit-fem>=8.1.0" # scikit-fem is a dependency for the multi-dimensional pybamm models
60 | ]
61 | bpx = [
62 | "bpx>=0.5, <0.6",
63 | ]
64 | all = ["pybop[plot,scifem,bpx]"]
65 |
66 | [tool.setuptools.packages.find]
67 | include = ["pybop", "pybop.*"]
68 |
69 | [project.urls]
70 | Homepage = "https://github.com/pybop-team/PyBOP"
71 | Documentation = "https://pybop-docs.readthedocs.io"
72 | Repository = "https://github.com/pybop-team/PyBOP"
73 | Releases = "https://github.com/pybop-team/PyBOP/releases"
74 | Changelog = "https://github.com/pybop-team/PyBOP/blob/develop/CHANGELOG.md"
75 |
76 | [tool.pytest.ini_options]
77 | addopts = "--showlocals -v -n auto"
78 |
79 | [tool.ruff]
80 | extend-include = ["*.ipynb"]
81 | extend-exclude = ["__init__.py"]
82 | fix = true
83 |
84 | [tool.ruff.lint]
85 | select = [
86 | "A", # flake8-builtins: Check for Python builtins being used as variables or parameters
87 | "B", # flake8-bugbear: Find likely bugs and design problems
88 | "E", # pycodestyle errors
89 | "W", # pycodestyle warnings
90 | "F", # pyflakes: Detect various errors by parsing the source file
91 | "I", # isort: Check and enforce import ordering
92 | "ISC", # flake8-implicit-str-concat: Check for implicit string concatenation
93 | "TID", # flake8-tidy-imports: Validate import hygiene
94 | "UP", # pyupgrade: Automatically upgrade syntax for newer versions of Python
95 | "SLF001", # flake8-string-format: Check for private object name access
96 | ]
97 |
98 | ignore = ["E501","E741"]
99 |
100 | [tool.ruff.lint.per-file-ignores]
101 | "tests/*" = ["SLF001"]
102 | "**.ipynb" = ["E402", "E703"]
103 |
104 | [tool.ruff.lint.flake8-tidy-imports]
105 | ban-relative-imports = "all"
106 |
--------------------------------------------------------------------------------
/readthedocs.yaml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | build:
4 | os: ubuntu-20.04
5 | tools:
6 | python: "3.11"
7 |
8 | # Build documentation in the "docs/" directory with Sphinx
9 | sphinx:
10 | configuration: docs/conf.py
11 |
12 | formats:
13 | - htmlzip
14 | - pdf
15 | - epub
16 |
17 | python:
18 | install:
19 | - method: pip
20 | path: .[docs]
21 |
--------------------------------------------------------------------------------
/scripts/ci/build_matrix.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # This helper script generates a matrix for further use in the
4 | # scheduled/nightly builds for PyBOP, i.e., in scheduled_tests.yaml
5 | # It generates a matrix of all combinations of the following variables:
6 | # - python_version: 3.X
7 | # - os: ubuntu-latest, windows-latest, macos-13 (amd64), macos-14 (arm64)
8 | # - pybamm_version: the last X versions of PyBaMM from PyPI, excluding release candidates
9 |
10 | # To update the matrix, the variables below can be modified as needed.
11 |
12 | python_version=("3.9" "3.10" "3.11" "3.12")
13 | os=("ubuntu-latest" "windows-latest" "macos-13" "macos-14")
14 | # This command fetches the last PyBaMM version excluding release candidates from PyPI
15 | pybamm_version=($(curl -s https://pypi.org/pypi/pybamm/json | jq -r '.releases | keys[]' | grep -v rc | sort -V | tail -n 1 | paste -sd " " -))
16 |
17 | # This command fetches the last PyBaMM versions including release candidates from PyPI
18 | #pybamm_version=($(curl -s https://pypi.org/pypi/pybamm/json | jq -r '.releases | keys[]' | sort -V | tail -n 1 | paste -sd " " -))
19 |
20 | # open dict
21 | json='{
22 | "include": [
23 | '
24 |
25 | # loop through each combination of variables to generate matrix components
26 | for py_ver in "${python_version[@]}"; do
27 | for os_type in "${os[@]}"; do
28 | for pybamm_ver in "${pybamm_version[@]}"; do
29 | json+='{
30 | "os": "'$os_type'",
31 | "python_version": "'$py_ver'",
32 | "pybamm_version": "'$pybamm_ver'"
33 | },'
34 | done
35 | done
36 | done
37 |
38 | # fix structure, removing trailing comma
39 | json=${json%,}
40 |
41 | # close dict
42 | json+='
43 | ]
44 | }'
45 |
46 | # Example for filtering out incompatible combinations
47 | #json=$(echo "$json" | jq -c 'del(.include[] | select(.pybamm_version == "23.9" and .python_version == "3.12"))')
48 |
49 | echo "$json" | jq -c .
50 |
--------------------------------------------------------------------------------
/tests/docs/test_docs.py:
--------------------------------------------------------------------------------
1 | import shutil
2 | import subprocess
3 | import sys
4 | from pathlib import Path
5 |
6 | import pytest
7 |
8 |
9 | class TestDocs:
10 | """A class to test the PyBOP documentation."""
11 |
12 | pytestmark = pytest.mark.docs
13 |
14 | def test_docs(self):
15 | """
16 | Check if the documentation can be built and run any doctests (currently not used).
17 |
18 | Credit: PyBaMM Team
19 | """
20 | print("Checking if docs can be built.")
21 | docs_path = Path("docs")
22 | build_path = docs_path / "_build" / "html"
23 |
24 | try:
25 | subprocess.run(
26 | [
27 | "sphinx-build",
28 | "-j",
29 | "auto",
30 | "-b",
31 | "html",
32 | str(docs_path),
33 | str(build_path),
34 | "--keep-going",
35 | ],
36 | check=True,
37 | capture_output=True,
38 | )
39 | except subprocess.CalledProcessError as e:
40 | print(f"FAILED with exit code {e.returncode}")
41 | print(f"stdout: {e.stdout.decode()}")
42 | print(f"stderr: {e.stderr.decode()}")
43 | sys.exit(e.returncode)
44 | finally:
45 | # Regardless of whether the doctests pass or fail, attempt to remove the built files.
46 | print("Deleting built files.")
47 | try:
48 | shutil.rmtree(build_path)
49 | except Exception as e:
50 | print(f"Error deleting built files: {e}")
51 |
--------------------------------------------------------------------------------
/tests/examples/test_examples.py:
--------------------------------------------------------------------------------
1 | import os
2 | import runpy
3 |
4 | import pytest
5 |
6 | import pybop
7 |
8 |
9 | class TestExamples:
10 | """
11 | A class to test the example scripts.
12 | """
13 |
14 | def list_of_examples():
15 | examples_list = []
16 | path_to_example_scripts = os.path.join(
17 | pybop.script_path, "..", "examples", "scripts"
18 | )
19 | for dirpath, _, filenames in os.walk(path_to_example_scripts):
20 | for file in filenames:
21 | if file.endswith(".py"):
22 | examples_list.append(os.path.join(dirpath, file))
23 | return examples_list
24 |
25 | @pytest.mark.parametrize("example", list_of_examples())
26 | @pytest.mark.examples
27 | def test_example_scripts(self, example):
28 | runpy.run_path(example)
29 |
--------------------------------------------------------------------------------
/tests/unit/test_classifier.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import pytest
3 |
4 | import pybop
5 |
6 |
7 | class TestClassifier:
8 | """
9 | A class to test the classification of different optimisation results.
10 | """
11 |
12 | pytestmark = pytest.mark.unit
13 |
14 | @pytest.fixture
15 | def problem(self):
16 | model = pybop.empirical.Thevenin()
17 | experiment = pybop.Experiment(
18 | [
19 | "Discharge at 0.5C for 2 minutes (4 seconds period)",
20 | "Charge at 0.5C for 2 minutes (4 seconds period)",
21 | ]
22 | )
23 | solution = model.predict(experiment=experiment)
24 | dataset = pybop.Dataset(
25 | {
26 | "Time [s]": solution["Time [s]"].data,
27 | "Current function [A]": solution["Current [A]"].data,
28 | "Voltage [V]": solution["Voltage [V]"].data,
29 | }
30 | )
31 | parameters = pybop.Parameters(
32 | pybop.Parameter(
33 | "R0 [Ohm]",
34 | prior=pybop.Uniform(0.001, 0.1),
35 | bounds=[1e-4, 0.1],
36 | ),
37 | )
38 | return pybop.FittingProblem(model, parameters, dataset)
39 |
40 | def test_classify_using_hessian_invalid(self, problem):
41 | cost = pybop.SumSquaredError(problem)
42 | optim = pybop.Optimisation(cost=cost)
43 | x = np.asarray([0.001])
44 | results = pybop.OptimisationResult(x=x, optim=optim)
45 |
46 | with pytest.raises(
47 | ValueError,
48 | match="The function classify_using_hessian currently only works"
49 | " in the case of 2 parameters, and dx must have the same length as x.",
50 | ):
51 | pybop.classify_using_hessian(results)
52 |
--------------------------------------------------------------------------------
/tests/unit/test_dataset.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import pytest
3 |
4 | import pybop
5 |
6 |
7 | class TestDataset:
8 | """
9 | Class to test dataset construction.
10 | """
11 |
12 | pytestmark = pytest.mark.unit
13 |
14 | def test_dataset(self):
15 | # Construct and simulate model
16 | model = pybop.lithium_ion.SPM()
17 | solution = model.predict(t_eval=np.linspace(0, 10, 100))
18 |
19 | # Form dataset
20 | data_dictionary = {
21 | "Time [s]": solution["Time [s]"].data,
22 | "Current [A]": solution["Current [A]"].data,
23 | "Voltage [V]": solution["Voltage [V]"].data,
24 | }
25 | dataset = pybop.Dataset(data_dictionary)
26 |
27 | # Test repr
28 | print(dataset)
29 |
30 | # Test data structure
31 | assert dataset.data == data_dictionary
32 | assert np.all(dataset["Time [s]"] == solution["Time [s]"].data)
33 |
34 | # Test exception for non-dictionary inputs
35 | with pytest.raises(
36 | TypeError, match="The input to pybop.Dataset must be a dictionary."
37 | ):
38 | pybop.Dataset(["StringInputShouldNotWork"])
39 | with pytest.raises(
40 | TypeError, match="The input to pybop.Dataset must be a dictionary."
41 | ):
42 | pybop.Dataset(solution["Time [s]"].data)
43 |
44 | # Test conversion of pybamm solution into dictionary
45 | assert dataset.data == pybop.Dataset(solution).data
46 |
47 | # Test set and get item
48 | test_current = solution["Current [A]"].data + np.ones_like(
49 | solution["Current [A]"].data
50 | )
51 | dataset["Current [A]"] = test_current
52 | assert np.all(dataset["Current [A]"] == test_current)
53 | with pytest.raises(ValueError):
54 | dataset["Time"]
55 |
56 | # Test conversion of single signal to list
57 | assert dataset.check()
58 |
59 | # Test get subset
60 | dataset = dataset.get_subset(list(range(5)))
61 | assert len(dataset[dataset.domain]) == 5
62 |
63 | # Form frequency dataset
64 | data_dictionary = {
65 | "Frequency [Hz]": np.linspace(-10, 0, 10),
66 | "Current [A]": np.zeros(10),
67 | "Impedance": np.zeros(10),
68 | }
69 | frequency_dataset = pybop.Dataset(data_dictionary)
70 |
71 | with pytest.raises(ValueError, match="Frequencies cannot be negative."):
72 | frequency_dataset.check(domain="Frequency [Hz]", signal="Impedance")
73 |
--------------------------------------------------------------------------------
/tests/unit/test_experiment.py:
--------------------------------------------------------------------------------
1 | import pybamm
2 | import pytest
3 |
4 | import pybop
5 |
6 |
7 | class TestExperiment:
8 | """
9 | Class to test the experiment class.
10 | """
11 |
12 | pytestmark = pytest.mark.unit
13 |
14 | def test_experiment(self):
15 | # Define example protocol
16 | protocol = [("Discharge at 1 C for 20 seconds")]
17 |
18 | # Construct matching experiments
19 | pybop_experiment = pybop.Experiment(protocol)
20 | pybamm_experiment = pybamm.Experiment(protocol)
21 |
22 | assert [step.to_dict() for step in pybop_experiment.steps] == [
23 | step.to_dict() for step in pybamm_experiment.steps
24 | ]
25 |
26 | assert pybop_experiment.cycle_lengths == pybamm_experiment.cycle_lengths
27 |
28 | assert str(pybop_experiment) == str(pybamm_experiment)
29 |
30 | assert repr(pybop_experiment) == repr(pybamm_experiment)
31 |
32 | assert pybop_experiment.termination == pybamm_experiment.termination
33 |
--------------------------------------------------------------------------------
/tests/unit/test_import.py:
--------------------------------------------------------------------------------
1 | import importlib
2 | import sys
3 | from unittest.mock import patch
4 |
5 | import pytest
6 |
7 |
8 | class TestImport:
9 | pytestmark = pytest.mark.unit
10 |
11 | def test_multiprocessing_init_non_win32(self, monkeypatch):
12 | """Test multiprocessing init on non-Windows platforms"""
13 | monkeypatch.setattr(sys, "platform", "linux")
14 | # Unload pybop and its sub-modules
15 | self.unload_pybop()
16 | with patch("multiprocessing.set_start_method") as mock_set_start_method:
17 | importlib.import_module("pybop")
18 | mock_set_start_method.assert_called_once_with("fork")
19 |
20 | def test_multiprocessing_init_win32(self, monkeypatch):
21 | """Test multiprocessing init on Windows"""
22 | monkeypatch.setattr(sys, "platform", "win32")
23 | self.unload_pybop()
24 | with patch("multiprocessing.set_start_method") as mock_set_start_method:
25 | importlib.import_module("pybop")
26 | mock_set_start_method.assert_called_once_with("spawn")
27 |
28 | def unload_pybop(self):
29 | """
30 | Unload pybop and its sub-modules. Credit PyBaMM team:
31 | https://github.com/pybamm-team/PyBaMM/blob/90c1c357a97dfd5c8c6a9092a70dddf0dac978db/tests/unit/test_util.py
32 | """
33 | # Unload pybop and its sub-modules
34 | for module_name in list(sys.modules.keys()):
35 | base_module_name = module_name.split(".")[0]
36 | if base_module_name == "pybop":
37 | sys.modules.pop(module_name)
38 |
--------------------------------------------------------------------------------
/tests/unit/test_solvers.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import pybamm
3 | import pytest
4 |
5 | import pybop
6 |
7 |
8 | class TestSolvers:
9 | """
10 | A class to test the forward model solver interface
11 | """
12 |
13 | pytestmark = pytest.mark.unit
14 |
15 | @pytest.fixture(
16 | params=[
17 | pybamm.IDAKLUSolver(atol=1e-4, rtol=1e-4),
18 | pybamm.CasadiSolver(atol=1e-4, rtol=1e-4, mode="safe"),
19 | pybamm.CasadiSolver(atol=1e-4, rtol=1e-4, mode="fast with events"),
20 | ]
21 | )
22 | def solver(self, request):
23 | solver = request.param
24 | return solver.copy()
25 |
26 | @pytest.fixture
27 | def model(self, solver):
28 | parameter_set = pybop.ParameterSet.pybamm("Marquis2019")
29 | model = pybop.lithium_ion.SPM(parameter_set=parameter_set, solver=solver)
30 | return model
31 |
32 | def test_solvers_with_model_predict(self, model, solver):
33 | assert model.solver == solver
34 | assert model.solver.atol == 1e-4
35 | assert model.solver.rtol == 1e-4
36 |
37 | # Ensure solver is functional
38 | sol = model.predict(t_eval=np.linspace(0, 1, 100))
39 | assert np.isfinite(sol["Voltage [V]"].data).all()
40 |
41 | signals = ["Voltage [V]", "Bulk open-circuit voltage [V]"]
42 | additional_vars = [
43 | "Maximum negative particle concentration",
44 | "Positive electrode volume-averaged concentration [mol.m-3]",
45 | ]
46 |
47 | parameters = pybop.Parameters(
48 | pybop.Parameter(
49 | "Negative electrode conductivity [S.m-1]", prior=pybop.Uniform(0.1, 100)
50 | )
51 | )
52 | dataset = pybop.Dataset(
53 | {
54 | "Time [s]": sol["Time [s]"].data,
55 | "Current function [A]": sol["Current [A]"].data,
56 | "Voltage [V]": sol["Voltage [V]"].data,
57 | "Bulk open-circuit voltage [V]": sol[
58 | "Bulk open-circuit voltage [V]"
59 | ].data,
60 | }
61 | )
62 | problem = pybop.FittingProblem(
63 | model,
64 | parameters=parameters,
65 | dataset=dataset,
66 | signal=signals,
67 | additional_variables=additional_vars,
68 | )
69 |
70 | y = problem.evaluate(inputs={"Negative electrode conductivity [S.m-1]": 10})
71 |
72 | for signal in signals:
73 | assert np.isfinite(y[signal].data).all()
74 |
75 | if isinstance(model.solver, pybamm.IDAKLUSolver):
76 | assert model.solver.output_variables is not None
77 |
--------------------------------------------------------------------------------