Skip to content

Added support for time series temperature in an experiment step #4855

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

Open
wants to merge 8 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- Added support for particle size distributions combined with particle mechanics. ([#4807](https://github.com/pybamm-team/PyBaMM/pull/4807))
- Added InputParameter support in PyBamm experiments ([#4826](https://github.com/pybamm-team/PyBaMM/pull/4826))
- Added support for the `"pchip"` interpolator using the CasADI backend. ([#4871](https://github.com/pybamm-team/PyBaMM/pull/4871))
- Added support for time series temperature in an experiment step ([#4855](https://github.com/pybamm-team/PyBaMM/pull/4855))

## Breaking changes

Expand Down
6 changes: 6 additions & 0 deletions src/pybamm/experiment/experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ def __init__(

steps_unprocessed = [cond for cycle in cycles for cond in cycle]

self.temperature_interpolant = None

# Convert strings to pybamm.step.BaseStep objects
# We only do this once per unique step, to avoid unnecessary conversions
# Assign experiment period and temperature if not specified in step
Expand All @@ -75,6 +77,10 @@ def __init__(

self.steps = [processed_steps[repr(step)] for step in steps_unprocessed]
self.steps = self._set_next_start_time(self.steps)
for step in self.steps:
if getattr(step, "has_time_series_temperature", False):
self.temperature_interpolant = step.temperature
break

# Save the processed unique steps and the processed operating conditions
# for every step
Expand Down
41 changes: 36 additions & 5 deletions src/pybamm/experiment/step/base_step.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,9 @@ def __init__(
self.direction = direction

self.temperature = _convert_temperature_to_kelvin(temperature)

self.has_time_series_temperature = isinstance(
self.temperature, pybamm.Interpolant
)
if tags is None:
tags = []
elif isinstance(tags, str):
Expand Down Expand Up @@ -429,9 +431,13 @@ def record_tags(
hash_args += f", termination={termination}"
if period:
repr_args += f", period={period}"
if temperature:
repr_args += f", temperature={temperature}"
hash_args += f", temperature={temperature}"
if temperature is not None:
if isinstance(temperature, np.ndarray):
repr_args += ", temperature=<time-series>"
hash_args += ", temperature=<time-series>"
else:
repr_args += f", temperature={temperature}"
hash_args += f", temperature={temperature}"
if tags:
repr_args += f", tags={tags}"
if start_time:
Expand Down Expand Up @@ -539,8 +545,33 @@ def _convert_time_to_seconds(time_and_units):
return time_in_seconds


def process_temperature_input(temperature_and_units):
if isinstance(temperature_and_units, np.ndarray):
if temperature_and_units.ndim == 2 and temperature_and_units.shape[1] == 2:
# Assume first column is time (s) and second column is temperature (K)
times = temperature_and_units[:, 0]
temps = temperature_and_units[:, 1]
return pybamm.Interpolant(times, temps, pybamm.t, interpolator="linear")
else:
raise ValueError(
"Temperature time-series must be a 2D array with two columns (time, temperature)."
)


def _convert_temperature_to_kelvin(temperature_and_units):
"""Convert a temperature in Celsius or Kelvin to a temperature in Kelvin"""
"""
If the input is a 2D numpy array (time series), return an Interpolant.
Otherwise Convert a temperature in Celsius or Kelvin to a temperature in Kelvin
"""

# Check if the temperature input is a time-series array
if isinstance(temperature_and_units, np.ndarray):
return process_temperature_input(temperature_and_units)

# If it's already an Interpolant, do nothing further.
if isinstance(temperature_and_units, pybamm.Interpolant):
return temperature_and_units

# If the temperature is a number, assume it is in Kelvin
if isinstance(temperature_and_units, (int, float)) or temperature_and_units is None:
return temperature_and_units
Expand Down
5 changes: 4 additions & 1 deletion src/pybamm/experiment/step/steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,10 @@ class Voltage(BaseStepImplicit):
"""

def get_parameter_values(self, variables):
return {"Voltage function [V]": self.value}
params = {"Voltage function [V]": self.value}
if self.temperature is not None:
params["Ambient temperature [K]"] = self.temperature
return params

def get_submodel(self, model):
return pybamm.external_circuit.VoltageFunctionControl(
Expand Down
17 changes: 17 additions & 0 deletions src/pybamm/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,15 @@ def build(self, initial_soc=None, inputs=None):
if initial_soc is not None:
self.set_initial_soc(initial_soc, inputs=inputs)

if (
getattr(self, "experiment", None)
and hasattr(self.experiment, "temperature_interpolant")
and self.experiment.temperature_interpolant is not None
):
self.parameter_values.update(
{"Ambient temperature [K]": self.experiment.temperature_interpolant}
)

if self._built_model:
return
elif self._model.is_discretised:
Expand Down Expand Up @@ -444,6 +453,14 @@ def solve(
See :meth:`pybamm.BaseSolver.solve`.
"""
pybamm.telemetry.capture("simulation-solved")
if (
getattr(self, "experiment", None)
and hasattr(self.experiment, "temperature_interpolant")
and self.experiment.temperature_interpolant is not None
):
self.parameter_values.update(
{"Ambient temperature [K]": self.experiment.temperature_interpolant}
)

# Setup
if solver is None:
Expand Down
96 changes: 96 additions & 0 deletions tests/unit/test_experiments/test_experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import pybamm
import pytest
import numpy as np
from pybamm.experiment.step.base_step import process_temperature_input
import casadi
from scipy.interpolate import PchipInterpolator

Expand Down Expand Up @@ -218,6 +219,101 @@ def test_set_next_start_time(self):
# TODO: once #3176 is completed, the test should pass for
# operating_conditions_steps (or equivalent) as well

def test_temperature_time_series_simulation_build(self):
time_data = np.array([0, 600, 1200, 1800])
voltage_data = np.array([4.2, 4.0, 3.8, 3.6])
temperature_data = np.array([298.15, 310.15, 305.15, 300.00])

voltage_profile = np.column_stack((time_data, voltage_data))
temperature_profile = np.column_stack((time_data, temperature_data))

experiment = pybamm.Experiment(
[pybamm.step.voltage(voltage_profile, temperature=temperature_profile)]
)

model = pybamm.lithium_ion.DFN()

param_values = pybamm.ParameterValues("Marquis2019")
param_values.update({"Ambient temperature [K]": 298.15})

sim = pybamm.Simulation(
model, experiment=experiment, parameter_values=param_values
)
sim.build()

ambient_temp = sim.parameter_values["Ambient temperature [K]"]
assert hasattr(ambient_temp, "evaluate"), (
"Ambient temperature parameter is not time-dependent as expected."
)

t_eval = 600
interpolated_temp = ambient_temp.evaluate(t=t_eval)
np.testing.assert_allclose(interpolated_temp, 310.15, atol=1e-3)

t_eval2 = 1200
interpolated_temp2 = ambient_temp.evaluate(t=t_eval2)
np.testing.assert_allclose(interpolated_temp2, 305.15, atol=1e-3)

def test_process_temperature_valid(self):
time_data = np.array([0, 600, 1200, 1800])
temperature_data = np.array([298.15, 310.15, 305.15, 300.00])
temperature_profile = np.column_stack((time_data, temperature_data))

result = process_temperature_input(temperature_profile)

assert isinstance(result, pybamm.Interpolant), "Expected an Interpolant object"
np.testing.assert_allclose(result.evaluate(600), 310.15, atol=1e-3)
np.testing.assert_allclose(result.evaluate(1200), 305.15, atol=1e-3)

def test_process_temperature_invalid(self):
invalid_data = np.array([298.15, 310.15, 305.15, 300.00])
with pytest.raises(
ValueError, match="Temperature time-series must be a 2D array"
):
process_temperature_input(invalid_data)

invalid_data_2 = np.array([[0, 298.15, 310.15], [600, 305.15, 300.00]])
with pytest.raises(
ValueError, match="Temperature time-series must be a 2D array"
):
process_temperature_input(invalid_data_2)

def test_temperature_time_series_simulation_solve(self):
time_data = np.array([0, 600, 1200, 1800])
voltage_data = np.array([4.2, 4.0, 3.8, 3.6])
temperature_data = np.array([298.15, 310.15, 305.15, 300.00])

voltage_profile = np.column_stack((time_data, voltage_data))
temperature_profile = np.column_stack((time_data, temperature_data))

experiment = pybamm.Experiment(
[
pybamm.step.voltage(
voltage_profile, temperature=temperature_profile, duration=1800
)
]
)

model = pybamm.lithium_ion.DFN()

param_values = pybamm.ParameterValues("Marquis2019")

sim = pybamm.Simulation(
model, experiment=experiment, parameter_values=param_values
)

solution = sim.solve()

ambient_temp = sim.parameter_values["Ambient temperature [K]"]
assert hasattr(ambient_temp, "evaluate"), (
"Ambient temperature parameter is not time-dependent as expected."
)

assert solution is not None, "Solution object is None."
assert hasattr(solution, "t"), "Solution does not contain time vector."

np.testing.assert_allclose(solution.t[0], time_data[0], atol=1e-3)

def test_simulation_solve_updates_input_parameters(self):
model = pybamm.lithium_ion.SPM()

Expand Down
Loading