diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..dfe077042 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore index 7978daaa3..838f372b0 100644 --- a/.gitignore +++ b/.gitignore @@ -301,3 +301,6 @@ $RECYCLE.BIN/ *.lnk # End of https://www.toptal.com/developers/gitignore/api/python,macos,windows,linux,c + +# Visual Studio Code settings +.vscode/* diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 000000000..e1efab891 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,14 @@ +cff-version: 1.2.0 +title: 'Python Battery Optimisation and Parameterisation (PyBOP)' +message: >- + If you use this software, please cite it using the + metadata from this file. +type: software +authors: + - given-names: Brady + family-names: Planden + - given-names: Nicola + family-names: Courtier + - given-names: David + family-names: Howey +repository-code: 'https://www.github.com/pybop-team/pybop' diff --git a/examples/scripts/rmse_estimation.py b/examples/scripts/rmse_estimation.py index 1e8ea68b3..358b3a5c2 100644 --- a/examples/scripts/rmse_estimation.py +++ b/examples/scripts/rmse_estimation.py @@ -4,9 +4,9 @@ # Form observations Measurements = pd.read_csv("examples/scripts/Chen_example.csv", comment="#").to_numpy() observations = [ - pybop.Observed("Time [s]", Measurements[:, 0]), - pybop.Observed("Current function [A]", Measurements[:, 1]), - pybop.Observed("Voltage [V]", Measurements[:, 2]), + pybop.Dataset("Time [s]", Measurements[:, 0]), + pybop.Dataset("Current function [A]", Measurements[:, 1]), + pybop.Dataset("Voltage [V]", Measurements[:, 2]), ] # Define model diff --git a/pybop/__init__.py b/pybop/__init__.py index 072276283..f06a38fdf 100644 --- a/pybop/__init__.py +++ b/pybop/__init__.py @@ -24,30 +24,44 @@ script_path = os.path.dirname(__file__) # -# Model Classes +# Cost function class # +from .costs.error_costs import RMSE + +# +# Dataset class +# +from .datasets.base_dataset import Dataset + +# +# Model classes +# +from .models.base_model import BaseModel from .models import lithium_ion -from .models.BaseModel import BaseModel # -# Parameterisation class +# Main optimisation class # -from .parameters.parameter_set import ParameterSet -from .parameters.parameter import Parameter -from .datasets.observations import Observed +from .optimisation import Optimisation # -# Priors class +# Optimiser class +# +from .optimisers.base_optimiser import BaseOptimiser +from .optimisers.nlopt_optimize import NLoptOptimize +from .optimisers.scipy_minimize import SciPyMinimize + # +# Parameter classes +# +from .parameters.base_parameter import Parameter +from .parameters.base_parameter_set import ParameterSet from .parameters.priors import Gaussian, Uniform, Exponential # -# Optimisation class +# Plotting class # -from .optimisation import Optimisation -from .optimisers import BaseOptimiser -from .optimisers.NLoptOptimize import NLoptOptimize -from .optimisers.SciPyMinimize import SciPyMinimize +from .plotting.quick_plot import QuickPlot # # Remove any imported modules, so we don't expose them as part of pybop diff --git a/pybop/costs/error_costs.py b/pybop/costs/error_costs.py new file mode 100644 index 000000000..66bc28af8 --- /dev/null +++ b/pybop/costs/error_costs.py @@ -0,0 +1,92 @@ +import numpy as np + + +class RMSE: + """ + Defines the root mean square error cost function. + """ + + def __init__(self): + self.name = "RMSE" + + def compute(self, prediction, target): + # Check compatibility + if len(prediction) != len(target): + print( + "Length of vectors:", + len(prediction), + len(target), + ) + raise ValueError( + "Measurement and simulated data length mismatch, potentially due to reaching a voltage cut-off" + ) + + print("Last Values:", prediction[-1], target[-1]) + + # Compute the cost + try: + return np.sqrt(np.mean((prediction - target) ** 2)) + + except Exception as e: + print(f"Error in RMSE calculation: {e}") + return None + + +class MLE: + """ + Defines the cost function for maximum likelihood estimation. + """ + + def __init__(self): + self.name = "MLE" + + def compute(self, prediction, target): + # Compute the cost + try: + return 0 # update with MLE residual + + except Exception as e: + print(f"Error in RMSE calculation: {e}") + return None + + +class PEM: + """ + Defines the cost function for prediction error minimisation. + """ + + def __init__(self): + self.name = "PEM" + + def compute(self, prediction, target): + # Compute the cost + try: + return 0 # update with MLE residual + + except Exception as e: + print(f"Error in RMSE calculation: {e}") + return None + + +class MAP: + """ + Defines the cost function for maximum a posteriori estimation. + """ + + def __init__(self): + self.name = "MAP" + + def compute(self, prediction, target): + # Compute the cost + try: + return 0 # update with MLE residual + + except Exception as e: + print(f"Error in RMSE calculation: {e}") + return None + + def sample(self, n_chains): + """ + Sample from the posterior distribution. + """ + pass diff --git a/pybop/datasets/observations.py b/pybop/datasets/base_dataset.py similarity index 75% rename from pybop/datasets/observations.py rename to pybop/datasets/base_dataset.py index 417a9b876..ed194ae48 100644 --- a/pybop/datasets/observations.py +++ b/pybop/datasets/base_dataset.py @@ -1,9 +1,9 @@ import pybamm -class Observed: +class Dataset: """ - Class for experimental Observations. + Class for experimental observations. """ def __init__(self, name, data): @@ -11,7 +11,7 @@ def __init__(self, name, data): self.data = data def __repr__(self): - return f"Observation: {self.name} \n Data: {self.data}" + return f"Dataset: {self.name} \n Data: {self.data}" def Interpolant(self): if self.variable == "time": diff --git a/pybop/models/BaseModel.py b/pybop/models/base_model.py similarity index 99% rename from pybop/models/BaseModel.py rename to pybop/models/base_model.py index 184acc2f6..4ad8a2076 100644 --- a/pybop/models/BaseModel.py +++ b/pybop/models/base_model.py @@ -4,7 +4,7 @@ class BaseModel: """ - Base class for PyBOP models + Base class for pybop models. """ def __init__(self, name="Base Model"): diff --git a/pybop/models/lithium_ion/__init__.py b/pybop/models/lithium_ion/__init__.py index 50f61b1e9..69b51653b 100644 --- a/pybop/models/lithium_ion/__init__.py +++ b/pybop/models/lithium_ion/__init__.py @@ -1,5 +1,4 @@ # # Import lithium ion based models # - from .base_echem import SPM, SPMe diff --git a/pybop/models/lithium_ion/base_echem.py b/pybop/models/lithium_ion/base_echem.py index 542d6a399..ffa3b5775 100644 --- a/pybop/models/lithium_ion/base_echem.py +++ b/pybop/models/lithium_ion/base_echem.py @@ -1,5 +1,5 @@ import pybamm -from ..BaseModel import BaseModel +from ..base_model import BaseModel class SPM(BaseModel): diff --git a/pybop/optimisers/BaseOptimiser.py b/pybop/optimisers/base_optimiser.py similarity index 87% rename from pybop/optimisers/BaseOptimiser.py rename to pybop/optimisers/base_optimiser.py index 130ac5299..df6e253e5 100644 --- a/pybop/optimisers/BaseOptimiser.py +++ b/pybop/optimisers/base_optimiser.py @@ -10,10 +10,9 @@ def __init__(self): def optimise(self, cost_function, x0, bounds, method=None): """ - Optimise method to be overloaded by child classes. + Optimisiation method to be overloaded by child classes. """ - # Set up optimisation self.cost_function = cost_function self.x0 = x0 self.method = method diff --git a/pybop/optimisers/NLoptOptimize.py b/pybop/optimisers/nlopt_optimize.py similarity index 89% rename from pybop/optimisers/NLoptOptimize.py rename to pybop/optimisers/nlopt_optimize.py index e1c88f696..5af0dbab2 100644 --- a/pybop/optimisers/NLoptOptimize.py +++ b/pybop/optimisers/nlopt_optimize.py @@ -1,10 +1,10 @@ import nlopt -from .BaseOptimiser import BaseOptimiser +from .base_optimiser import BaseOptimiser class NLoptOptimize(BaseOptimiser): """ - Wrapper class for the NLOpt optimisation class. Extends the BaseOptimiser class. + Wrapper class for the NLOpt optimiser class. Extends the BaseOptimiser class. """ def __init__(self, method=None, x0=None, xtol=None): diff --git a/pybop/optimisers/SciPyMinimize.py b/pybop/optimisers/scipy_minimize.py similarity index 89% rename from pybop/optimisers/SciPyMinimize.py rename to pybop/optimisers/scipy_minimize.py index cd831e9b2..bb9500135 100644 --- a/pybop/optimisers/SciPyMinimize.py +++ b/pybop/optimisers/scipy_minimize.py @@ -1,10 +1,10 @@ from scipy.optimize import minimize -from .BaseOptimiser import BaseOptimiser +from .base_optimiser import BaseOptimiser class SciPyMinimize(BaseOptimiser): """ - Wrapper class for the Scipy optimisation class. Extends the BaseOptimiser class. + Wrapper class for the Scipy optimiser class. Extends the BaseOptimiser class. """ def __init__(self, cost_function, x0, bounds=None, options=None): diff --git a/pybop/parameters/parameter.py b/pybop/parameters/base_parameter.py similarity index 68% rename from pybop/parameters/parameter.py rename to pybop/parameters/base_parameter.py index f15bdfea5..c6b2e6cdf 100644 --- a/pybop/parameters/parameter.py +++ b/pybop/parameters/base_parameter.py @@ -3,8 +3,8 @@ class Parameter: Class for creating parameters in pybop. """ - def __init__(self, param, value=None, prior=None, bounds=None): - self.name = param + def __init__(self, name, value=None, prior=None, bounds=None): + self.name = name self.prior = prior self.value = value self.bounds = bounds @@ -15,5 +15,8 @@ def __init__(self, param, value=None, prior=None, bounds=None): # bounds checks and set defaults # implement methods to assign and retrieve parameters + def update(self, value): + self.value = value + def __repr__(self): - return f"Parameter: {self.name} \n Prior: {self.prior} \n Bounds: {self.bounds}" + return f"Parameter: {self.name} \n Prior: {self.prior} \n Bounds: {self.bounds} \n Value: {self.value}" diff --git a/pybop/parameters/base_parameter_set.py b/pybop/parameters/base_parameter_set.py new file mode 100644 index 000000000..e67b175ce --- /dev/null +++ b/pybop/parameters/base_parameter_set.py @@ -0,0 +1,16 @@ +import pybamm + + +class ParameterSet: + """ + Class for creating parameter sets in pybop. + """ + + def __new__(cls, method, name): + if method.casefold() == "pybamm": + try: + return pybamm.ParameterValues(name).copy() + except: + raise ValueError("Parameter set not found") + else: + raise ValueError("Only PyBaMM parameter sets are currently implemented") diff --git a/pybop/parameters/parameter_set.py b/pybop/parameters/parameter_set.py deleted file mode 100644 index 1e1dc23ec..000000000 --- a/pybop/parameters/parameter_set.py +++ /dev/null @@ -1,13 +0,0 @@ -import pybamm - - -class ParameterSet: - """ - Class for creating parameter sets in pybop. - """ - - def __new__(cls, method, name): - if method.casefold() == "pybamm": - return pybamm.ParameterValues(name).copy() - else: - raise ValueError("Only PybaMM parameter sets are currently implemented") diff --git a/pybop/parameters/priors.py b/pybop/parameters/priors.py index 7224b58a7..f98e9b767 100644 --- a/pybop/parameters/priors.py +++ b/pybop/parameters/priors.py @@ -57,7 +57,7 @@ def __repr__(self): class Exponential: """ - exponential prior class. + Exponential prior class. """ def __init__(self, scale): diff --git a/tests/unit/test_parameterisations.py b/tests/unit/test_parameterisations.py index 047e41847..8c72a6608 100644 --- a/tests/unit/test_parameterisations.py +++ b/tests/unit/test_parameterisations.py @@ -2,9 +2,10 @@ import pybamm import pytest import numpy as np +import unittest -class TestModelParameterisation: +class TestModelParameterisation(unittest.TestCase): """ A class to test the model parameterisation methods. """ @@ -20,9 +21,9 @@ def test_spm(self): solution = self.getdata(model, x0) observations = [ - pybop.Observed("Time [s]", solution["Time [s]"].data), - pybop.Observed("Current function [A]", solution["Current [A]"].data), - pybop.Observed("Voltage [V]", solution["Terminal voltage [V]"].data), + pybop.Dataset("Time [s]", solution["Time [s]"].data), + pybop.Dataset("Current function [A]", solution["Current [A]"].data), + pybop.Dataset("Voltage [V]", solution["Terminal voltage [V]"].data), ] # Fitting parameters @@ -63,9 +64,9 @@ def test_spme(self): solution = self.getdata(model, x0) observations = [ - pybop.Observed("Time [s]", solution["Time [s]"].data), - pybop.Observed("Current function [A]", solution["Current [A]"].data), - pybop.Observed("Voltage [V]", solution["Terminal voltage [V]"].data), + pybop.Dataset("Time [s]", solution["Time [s]"].data), + pybop.Dataset("Current function [A]", solution["Current [A]"].data), + pybop.Dataset("Voltage [V]", solution["Terminal voltage [V]"].data), ] # Fitting parameters @@ -149,3 +150,7 @@ def test_parameter_set(self): np.testing.assert_allclose( parameter_test["Negative electrode active material volume fraction"], 0.75 ) + + +if __name__ == "__main__": + unittest.main()