Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add testing framework and non-geometric parameterisation methods #20

Merged
merged 18 commits into from
Aug 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
5955c94
Progress: Updt. model build structure for parameterisation groups
BradyPlanden Aug 4, 2023
fb082fc
Progress: Add parameter_sets class with model class inclusion
BradyPlanden Aug 5, 2023
d332a3f
Updt. parameterisation for model_build
BradyPlanden Aug 9, 2023
52362fd
Updt parameterisation cls w/ build_model(), set_init_soc(), set_params()
BradyPlanden Aug 9, 2023
57d3025
Non-geometric parameterisation fitting complete, rm simulation class …
BradyPlanden Aug 10, 2023
5df3696
Add optim step method w/ lambda, Add initial test framework w/ nox + …
BradyPlanden Aug 16, 2023
66d5856
Fix test_on_push for nox
BradyPlanden Aug 16, 2023
c0001e5
Updt numpy version requirement, add fail-fast false condition
BradyPlanden Aug 16, 2023
38bddb0
Updt dependancy requirements for python 3.8 support
BradyPlanden Aug 16, 2023
1fb9fac
Updt README w/ build status
BradyPlanden Aug 16, 2023
cc10421
Test icon syntax fix
BradyPlanden Aug 16, 2023
6b7ab48
Updt. x0 to be sampled from priors, Progress: API to list formation
BradyPlanden Aug 17, 2023
d50702e
API change to observation & params definition to type::list
BradyPlanden Aug 17, 2023
ee7fe83
refactor API change for observ. & param class type::list, tests updat…
BradyPlanden Aug 18, 2023
d8b8077
Refactor parameterisation & spm class, Add BaseModel class
BradyPlanden Aug 18, 2023
e3f49c5
Refactor #2 of parameterisation & spm class, Aligned init. parameter …
BradyPlanden Aug 18, 2023
d21249b
Data generation in test methods, debug parameter initilisation proble…
BradyPlanden Aug 18, 2023
4735367
Updt. to default parameter set for a stable & working state
BradyPlanden Aug 18, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/test_on_push.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: PyBOP

on: [push]

jobs:
build:

runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11"]

steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install --upgrade pip nox
pip install -e .
- name: Test with nox
run: |
nox
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# PyBOP - A *Py*thon package for *B*attery *O*ptimisation and *P*arameterisation

<div align="center">

[![Build Status](https://github.com/pybop-team/PyBOP/actions/workflows/test_on_push.yaml/badge.svg?branch=develop)](https://github.com/pybop-team/PyBOP/actions/workflows/test_on_push.yaml)

</div>

PyBOP aims to be a modular library for the parameterisation and optimisation of battery models, with a particular focus on classes built around [PyBaMM](https://github.com/pybamm-team/PyBaMM) models. The figure below gives the current conceptual idea of PyBOP's structure. This will likely evolve as development progresses.

<p align="center">
Expand Down
48 changes: 30 additions & 18 deletions examples/Initial_API.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,42 @@
import numpy as np

# Form observations
Measurements = pd.read_csv("examples/Chen_example.csv", comment='#').to_numpy()
observations = dict(
Time = pybop.Observed(["Time [s]"], Measurements[:,0]),
Current = pybop.Observed(["Current function [A]"], Measurements[:,1]),
Voltage = pybop.Observed(["Voltage [V]"], Measurements[:,2])
)
Measurements = pd.read_csv("examples/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]),
]

# Define model
model = pybop.models.lithium_ion.SPM()
parameter_set = pybop.ParameterSet("pybamm", "Chen2020")
model = pybop.models.lithium_ion.SPM(parameter_set=parameter_set)

# Fitting parameters
params = (
pybop.Parameter("Electrode height [m]", prior = pybop.Gaussian(0,1), bounds = (0.03,0.1)),
pybop.Parameter("Negative particle radius [m]", prior = pybop.Uniform(0,1), bounds = (0.1e-6,0.8e-6)),
pybop.Parameter("Positive particle radius [m]", prior = pybop.Uniform(0,1), bounds = (0.1e-5,0.8e-5))
params = [
pybop.Parameter(
"Negative electrode active material volume fraction",
prior=pybop.Gaussian(0.75, 0.05),
bounds=[0.65, 0.85],
),
pybop.Parameter(
"Positive electrode active material volume fraction",
prior=pybop.Gaussian(0.65, 0.05),
bounds=[0.55, 0.75],
),
]

parameterisation = pybop.Parameterisation(
model, observations=observations, fit_parameters=params
)

parameterisation = pybop.Parameterisation(model, observations=observations, fit_parameters=params)

# get RMSE estimate
results, last_optim, num_evals = parameterisation.rmse(method="nlopt")
# get RMSE estimate using NLOpt
results, last_optim, num_evals = parameterisation.rmse(
signal="Voltage [V]", method="nlopt"
)

# get MAP estimate, starting at a random initial point in parameter space
# parameterisation.map(x0=[p.sample() for p in params])
# parameterisation.map(x0=[p.sample() for p in params])

# or sample from posterior
# parameterisation.sample(1000, n_chains=4, ....)
Expand All @@ -35,4 +47,4 @@
# parameterisation.sober()


#Optimisation = pybop.optimisation(model, cost=cost, parameters=parameters, observation=observation)
# Optimisation = pybop.optimisation(model, cost=cost, parameters=parameters, observation=observation)
16 changes: 16 additions & 0 deletions noxfile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import nox

# nox options
nox.options.reuse_existing_virtualenvs = True

# @nox.session
# def lint(session):
# session.install('flake8')
# session.run('flake8', 'example.py')


@nox.session
def tests(session):
session.run_always('pip', 'install', '-e', '.')
session.install('pytest')
session.run('pytest')
11 changes: 6 additions & 5 deletions pybop/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,14 @@
#
# Model Classes
#
from .models import BaseModel
from .models import lithium_ion

#
# Parameterisation class
#
from .parameter_sets import ParameterSet

#
# Parameterisation class
#
Expand All @@ -47,11 +53,6 @@
#
from .priors import Gaussian, Uniform, Exponential

#
# Simulation class
#
from .simulation import Simulation

#
# Optimisation class
#
Expand Down
24 changes: 24 additions & 0 deletions pybop/models/BaseModel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import pybop


class BaseModel:
"""
Base class for PyBOP models
"""

def __init__(self, name="Base Model"):
# self.pybamm_model = None
self.name = name
# self.parameter_set = None

def build(self):
"""
Build the model
"""
pass

def sim(self):
"""
Simulate the model
"""
pass
184 changes: 179 additions & 5 deletions pybop/models/lithium_ion/spm.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,188 @@
import pybop
import pybamm
from ..BaseModel import BaseModel


class SPM:
class SPM(BaseModel):
"""

Composition of the SPM class in PyBaMM.

"""

def __init__(self):
def __init__(
self,
name="Single Particle Model",
parameter_set=None,
geometry=None,
submesh_types=None,
var_pts=None,
spatial_methods=None,
solver=None,
):
super().__init__()
self.pybamm_model = pybamm.lithium_ion.SPM()
self.name = "Single Particle Model"
self._unprocessed_model = self.pybamm_model
self.name = name

self.parameter_set = parameter_set or self.pybamm_model.default_parameter_values
self._unprocessed_parameter_set = self.parameter_set

self.geometry = geometry or self.pybamm_model.default_geometry
self.submesh_types = submesh_types or self.pybamm_model.default_submesh_types
self.var_pts = var_pts or self.pybamm_model.default_var_pts
self.spatial_methods = (
spatial_methods or self.pybamm_model.default_spatial_methods
)
self.solver = solver or self.pybamm_model.default_solver

self._model_with_set_params = None
self._built_model = None
self._built_initial_soc = None
self._mesh = None
self._disc = None

def build_model(
self,
observations,
fit_parameters,
check_model=True,
init_soc=None,
):
"""
Build the model (if not built already).
"""
if init_soc is not None:
self.set_init_soc(init_soc)

if self._built_model:
return

elif self.pybamm_model.is_discretised:
self._model_with_set_params = self.pybamm_model
self._built_model = self.pybamm_model
else:
self.set_params(observations, fit_parameters)
self._mesh = pybamm.Mesh(self.geometry, self.submesh_types, self.var_pts)
self._disc = pybamm.Discretisation(self.mesh, self.spatial_methods)
self._built_model = self._disc.process_model(
self._model_with_set_params, inplace=False, check_model=check_model
)
# Set t_eval
self.time_data = self._parameter_set["Current function [A]"].x[0]

# Clear solver
self._solver._model_set_up = {}

def set_init_soc(self, init_soc):
"""
Set the initial state of charge.
"""
if self._built_initial_soc != init_soc:
# reset
self._model_with_set_params = None
self._built_model = None
self.op_conds_to_built_models = None
self.op_conds_to_built_solvers = None

param = self.pybamm_model.param
self.parameter_set = (
self._unprocessed_parameter_set.set_initial_stoichiometries(
init_soc, param=param, inplace=False
)
)
# Save solved initial SOC in case we need to rebuild the model
self._built_initial_soc = init_soc

def set_params(self, observations, fit_parameters):
"""
Set the parameters in the model.
"""
if self.model_with_set_params:
return

try:
self.parameter_set["Current function [A]"] = pybamm.Interpolant(
observations["Time [s]"].data,
observations["Current function [A]"].data,
pybamm.t,
)
except:
raise ValueError("Current function not supplied")

# set input parameters in parameter set from fitting parameters
for i in fit_parameters:
self.parameter_set[i] = "[input]"

self._model_with_set_params = self._parameter_set.process_model(
self._unprocessed_model, inplace=False
)
self._parameter_set.process_geometry(self.geometry)
self.pybamm_model = self._model_with_set_params

def sim(self):
"""
Simulate the model
"""

@property
def built_model(self):
return self._built_model

@property
def parameter_set(self):
return self._parameter_set

@parameter_set.setter
def parameter_set(self, parameter_set):
self._parameter_set = parameter_set.copy()

@property
def model_with_set_params(self):
return self._model_with_set_params

@property
def built_model(self):
return self._built_model

@property
def geometry(self):
return self._geometry

@geometry.setter
def geometry(self, geometry):
self._geometry = geometry.copy()

@property
def submesh_types(self):
return self._submesh_types

@submesh_types.setter
def submesh_types(self, submesh_types):
self._submesh_types = submesh_types.copy()

@property
def mesh(self):
return self._mesh

@property
def var_pts(self):
return self._var_pts

@var_pts.setter
def var_pts(self, var_pts):
self._var_pts = var_pts.copy()

@property
def spatial_methods(self):
return self._spatial_methods

@spatial_methods.setter
def spatial_methods(self, spatial_methods):
self._spatial_methods = spatial_methods.copy()

@property
def solver(self):
return self._solver

@solver.setter
def solver(self, solver):
self._solver = solver.copy()
17 changes: 17 additions & 0 deletions pybop/parameter_sets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import pybamm
import pybop


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")
Loading