Skip to content

Commit

Permalink
139b Add unit tests for plotting and Thevenin model (#212)
Browse files Browse the repository at this point in the history
* Create test_plots.py

* Parametrize test_models

* Add check on n_states

* Add test for json parameter set

* Add test for json export

* Add test for invalid max values

* Add optimisation tests

* Add observer tests

* Add tests on observer evaluate

* Add tests on invalid parameter inputs

* Add invalid sample size tests
  • Loading branch information
NicolaCourtier authored Feb 23, 2024
1 parent 817a778 commit 54e97f5
Show file tree
Hide file tree
Showing 7 changed files with 267 additions and 36 deletions.
80 changes: 44 additions & 36 deletions tests/unit/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,19 @@ class TestModels:
A class to test the models.
"""

@pytest.mark.unit
def test_simulate_without_build_model(self):
# Define model
model = pybop.lithium_ion.SPM()
@pytest.fixture(
params=[
pybop.lithium_ion.SPM(),
pybop.lithium_ion.SPMe(),
pybop.empirical.Thevenin(),
]
)
def model(self, request):
model = request.param
return model.copy()

@pytest.mark.unit
def test_simulate_without_build_model(self, model):
with pytest.raises(
ValueError, match="Model must be built before calling simulate"
):
Expand All @@ -27,49 +35,47 @@ def test_simulate_without_build_model(self):
model.simulateS1(None, None)

@pytest.mark.unit
def test_predict_without_pybamm(self):
# Define model
model = pybop.lithium_ion.SPM()
def test_predict_without_pybamm(self, model):
model._unprocessed_model = None

with pytest.raises(ValueError):
model.predict(None, None)

@pytest.mark.unit
def test_predict_with_inputs(self):
# Define SPM
model = pybop.lithium_ion.SPM()
def test_predict_with_inputs(self, model):
# Define inputs
t_eval = np.linspace(0, 10, 100)
inputs = {
"Negative electrode active material volume fraction": 0.52,
"Positive electrode active material volume fraction": 0.63,
}

res = model.predict(t_eval=t_eval, inputs=inputs)
assert len(res["Terminal voltage [V]"].data) == 100
if isinstance(model, (pybop.lithium_ion.SPM, pybop.lithium_ion.SPMe)):
inputs = {
"Negative electrode active material volume fraction": 0.52,
"Positive electrode active material volume fraction": 0.63,
}
elif isinstance(model, (pybop.empirical.Thevenin)):
inputs = {
"R0 [Ohm]": 0.0002,
"R1 [Ohm]": 0.0001,
}
else:
raise ValueError("Inputs not defined for this type of model.")

# Define SPMe
model = pybop.lithium_ion.SPMe()
res = model.predict(t_eval=t_eval, inputs=inputs)
assert len(res["Terminal voltage [V]"].data) == 100
assert len(res["Voltage [V]"].data) == 100

@pytest.mark.unit
def test_predict_without_allow_infeasible_solutions(self):
# Define SPM
model = pybop.lithium_ion.SPM()
model.allow_infeasible_solutions = False
t_eval = np.linspace(0, 10, 100)
inputs = {
"Negative electrode active material volume fraction": 0.9,
"Positive electrode active material volume fraction": 0.9,
}

res = model.predict(t_eval=t_eval, inputs=inputs)
assert np.isinf(res).any()
def test_predict_without_allow_infeasible_solutions(self, model):
if isinstance(model, (pybop.lithium_ion.SPM, pybop.lithium_ion.SPMe)):
model.allow_infeasible_solutions = False
t_eval = np.linspace(0, 10, 100)
inputs = {
"Negative electrode active material volume fraction": 0.9,
"Positive electrode active material volume fraction": 0.9,
}

res = model.predict(t_eval=t_eval, inputs=inputs)
assert np.isinf(res).any()

@pytest.mark.unit
def test_build(self):
model = pybop.lithium_ion.SPM()
def test_build(self, model):
model.build()
assert model.built_model is not None

Expand All @@ -78,8 +84,7 @@ def test_build(self):
assert model.built_model is not None

@pytest.mark.unit
def test_rebuild(self):
model = pybop.lithium_ion.SPM()
def test_rebuild(self, model):
model.build()
initial_built_model = model._built_model
assert model._built_model is not None
Expand Down Expand Up @@ -201,6 +206,9 @@ def test_simulate(self):
solved = model.simulate(inputs, t_eval)
np.testing.assert_array_almost_equal(solved, expected, decimal=5)

with pytest.raises(ValueError):
ExponentialDecay(n_states=-1)

@pytest.mark.unit
def test_basemodel(self):
base = pybop.BaseModel()
Expand Down
33 changes: 33 additions & 0 deletions tests/unit/test_observers.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,36 @@ def test_observer(self, model, parameters, x0):
np.array([[2 * y]]),
decimal=4,
)

# Test with invalid inputs
with pytest.raises(ValueError):
observer.observe(-1)
with pytest.raises(ValueError):
observer.log_likelihood(
t_eval, np.array([1]), inputs=observer._state.inputs
)

# Test covariance
covariance = observer.get_current_covariance()
assert np.shape(covariance) == (n, n)

# Test evaluate with different inputs
observer._time_data = t_eval
observer.evaluate(x0)
observer.evaluate(parameters)

# Test evaluate with dataset
observer._dataset = pybop.Dataset(
{
"Time [s]": t_eval,
"Output": expected,
}
)
observer._target = expected
observer.evaluate(x0)

@pytest.mark.unit
def test_unbuilt_model(self, parameters):
model = ExponentialDecay()
with pytest.raises(ValueError):
pybop.Observer(parameters, model)
14 changes: 14 additions & 0 deletions tests/unit/test_optimisation.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,17 @@ def test_halting(self, cost):
optim.set_max_unchanged_iterations(1)
x, __ = optim.run()
assert optim._iterations == 2

# Test invalid maximum values
with pytest.raises(ValueError):
optim.set_max_evaluations(-1)
with pytest.raises(ValueError):
optim.set_max_unchanged_iterations(-1)
with pytest.raises(ValueError):
optim.set_max_unchanged_iterations(1, threshold=-1)

@pytest.mark.unit
def test_unphysical_result(self, cost):
# Trigger parameters not physically viable warning
optim = pybop.Optimisation(cost=cost)
optim.check_optimal_parameters(np.array([2]))
66 changes: 66 additions & 0 deletions tests/unit/test_parameter_sets.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,69 @@ def test_parameter_set(self):
np.testing.assert_allclose(
parameter_test["Negative electrode active material volume fraction"], 0.75
)

@pytest.mark.unit
def test_ecm_parameter_sets(self):
# Test importing a json file
json_params = pybop.ParameterSet(
json_path="examples/scripts/parameters/initial_ecm_parameters.json"
)
json_params.import_parameters()

params = pybop.ParameterSet(
params_dict={
"chemistry": "ecm",
"Initial SoC": 0.5,
"Initial temperature [K]": 25 + 273.15,
"Cell capacity [A.h]": 5,
"Nominal cell capacity [A.h]": 5,
"Ambient temperature [K]": 25 + 273.15,
"Current function [A]": 5,
"Upper voltage cut-off [V]": 4.2,
"Lower voltage cut-off [V]": 3.0,
"Cell thermal mass [J/K]": 1000,
"Cell-jig heat transfer coefficient [W/K]": 10,
"Jig thermal mass [J/K]": 500,
"Jig-air heat transfer coefficient [W/K]": 10,
"Open-circuit voltage [V]": pybop.empirical.Thevenin().default_parameter_values[
"Open-circuit voltage [V]"
],
"R0 [Ohm]": 0.001,
"Element-1 initial overpotential [V]": 0,
"Element-2 initial overpotential [V]": 0,
"R1 [Ohm]": 0.0002,
"R2 [Ohm]": 0.0003,
"C1 [F]": 10000,
"C2 [F]": 5000,
"Entropic change [V/K]": 0.0004,
}
)
params.import_parameters()

assert json_params.params == params.params

# Test exporting a json file
parameters = [
pybop.Parameter(
"R0 [Ohm]",
prior=pybop.Gaussian(0.0002, 0.0001),
bounds=[1e-4, 1e-2],
initial_value=0.001,
),
pybop.Parameter(
"R1 [Ohm]",
prior=pybop.Gaussian(0.0001, 0.0001),
bounds=[1e-5, 1e-2],
initial_value=0.0002,
),
]
params.export_parameters(
"examples/scripts/parameters/fit_ecm_parameters.json", fit_params=parameters
)

# Test error when there no parameters to export
empty_params = pybop.ParameterSet()
with pytest.raises(ValueError):
empty_params.export_parameters(
"examples/scripts/parameters/fit_ecm_parameters.json"
)
14 changes: 14 additions & 0 deletions tests/unit/test_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,17 @@ def test_parameter_margin(self, parameter):
assert parameter.margin == 1e-4
parameter.set_margin(margin=1e-3)
assert parameter.margin == 1e-3

@pytest.mark.unit
def test_invalid_inputs(self, parameter):
# Test error with invalid value
with pytest.raises(ValueError):
parameter.set_margin(margin=-1)

# Test error with no parameter value
with pytest.raises(ValueError):
parameter.update()

# Test error with opposite bounds
with pytest.raises(ValueError):
pybop.Parameter("Name", bounds=[0.7, 0.3])
87 changes: 87 additions & 0 deletions tests/unit/test_plots.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import pybop
import numpy as np
import pytest


class TestPlots:
"""
A class to test the plotting classes.
"""

@pytest.fixture
def model(self):
# Define an example model
return pybop.lithium_ion.SPM()

@pytest.mark.unit
def test_model_plots(self):
# Test plotting of Model objects
pass

@pytest.fixture
def problem(self, model):
# Define an example problem
parameters = [
pybop.Parameter(
"Negative particle radius [m]",
prior=pybop.Gaussian(6e-06, 0.1e-6),
bounds=[1e-6, 9e-6],
),
pybop.Parameter(
"Positive particle radius [m]",
prior=pybop.Gaussian(4.5e-06, 0.1e-6),
bounds=[1e-6, 9e-6],
),
]

# Generate data
t_eval = np.arange(0, 50, 2)
values = model.predict(t_eval=t_eval)

# Form dataset
dataset = pybop.Dataset(
{
"Time [s]": t_eval,
"Current function [A]": values["Current [A]"].data,
"Voltage [V]": values["Voltage [V]"].data,
}
)

# Generate problem
return pybop.FittingProblem(model, parameters, dataset)

@pytest.mark.unit
def test_problem_plots(self):
# Test plotting of Problem objects
pass

@pytest.fixture
def cost(self, problem):
# Define an example cost
return pybop.SumSquaredError(problem)

@pytest.mark.unit
def test_cost_plots(self, cost):
# Test plotting of Cost objects
pybop.quick_plot(cost.x0, cost, title="Optimised Comparison")

# Plot the cost landscape
pybop.plot_cost2d(cost, steps=5)

@pytest.fixture
def optim(self, cost):
# Define and run an example optimisation
optim = pybop.Optimisation(cost, optimiser=pybop.CMAES)
optim.run()
return optim

@pytest.mark.unit
def test_optim_plots(self, optim):
# Plot convergence
pybop.plot_convergence(optim)

# Plot the parameter traces
pybop.plot_parameters(optim)

# Plot the cost landscape with optimisation path
pybop.plot_cost2d(optim.cost, optim=optim, steps=5)
9 changes: 9 additions & 0 deletions tests/unit/test_priors.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,12 @@ def test_repr(self, Gaussian, Uniform, Exponential):
assert repr(Gaussian) == "Gaussian, mean: 0.5, sigma: 1"
assert repr(Uniform) == "Uniform, lower: 0, upper: 1"
assert repr(Exponential) == "Exponential, scale: 1"

@pytest.mark.unit
def test_invalid_size(self, Gaussian, Uniform, Exponential):
with pytest.raises(ValueError):
Gaussian.rvs(-1)
with pytest.raises(ValueError):
Uniform.rvs(-1)
with pytest.raises(ValueError):
Exponential.rvs(-1)

0 comments on commit 54e97f5

Please sign in to comment.