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

139b Add unit tests for plotting and Thevenin model #212

Merged
merged 12 commits into from
Feb 23, 2024
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
NicolaCourtier marked this conversation as resolved.
Show resolved Hide resolved

@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):
NicolaCourtier marked this conversation as resolved.
Show resolved Hide resolved
# 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)
Loading