From adce331c3c26724e82cf1af943cac9d42e40183d Mon Sep 17 00:00:00 2001 From: Valentin Sulzer Date: Thu, 11 Jul 2024 09:50:29 -0400 Subject: [PATCH] Issue 4224 duration bug (#4239) * make longer default duration and calculate it for C-rate * add tests * typo * #4224 add warning for time termination and add abs * fix tests * #4224 keep non-C-rate default at 24h for performance reasons * trying to fix experiment * fix example * #4224 eric comments * fix bug --- .../tutorial-5-run-experiments.ipynb | 118 +++++++++++++----- pybamm/callbacks.py | 40 ++++-- pybamm/experiment/step/base_step.py | 23 +++- pybamm/experiment/step/steps.py | 5 + pybamm/simulation.py | 29 +++-- tests/unit/test_callbacks.py | 7 +- .../test_experiments/test_experiment_steps.py | 11 +- .../test_simulation_with_experiment.py | 19 +++ 8 files changed, 193 insertions(+), 59 deletions(-) diff --git a/docs/source/examples/notebooks/getting_started/tutorial-5-run-experiments.ipynb b/docs/source/examples/notebooks/getting_started/tutorial-5-run-experiments.ipynb index eb82f59719..85be34e421 100644 --- a/docs/source/examples/notebooks/getting_started/tutorial-5-run-experiments.ipynb +++ b/docs/source/examples/notebooks/getting_started/tutorial-5-run-experiments.ipynb @@ -25,18 +25,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "\n", - "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m23.3.1\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m24.0\u001b[0m\n", - "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpip install --upgrade pip\u001b[0m\n", "Note: you may need to restart the kernel to use updated packages.\n" ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "An NVIDIA GPU may be present on this machine, but a CUDA-enabled jaxlib is not installed. Falling back to cpu.\n" - ] } ], "source": [ @@ -163,18 +153,19 @@ "name": "stderr", "output_type": "stream", "text": [ - "At t = 522.66 and h = 1.1556e-13, the corrector convergence failed repeatedly or with |h| = hmin.\n" + "At t = 339.952 and h = 1.4337e-18, the corrector convergence failed repeatedly or with |h| = hmin.\n", + "At t = 522.687 and h = 4.04917e-14, the corrector convergence failed repeatedly or with |h| = hmin.\n" ] }, { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "5ab1f22de6af4878b6ca43d27ffc01c5", + "model_id": "93feca98298f4111909ae487e2a1e273", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "interactive(children=(FloatSlider(value=0.0, description='t', max=40.132949019384355, step=0.40132949019384356…" + "interactive(children=(FloatSlider(value=0.0, description='t', max=40.13268704803602, step=0.4013268704803602),…" ] }, "metadata": {}, @@ -183,7 +174,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 5, @@ -211,12 +202,12 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "7cdac234d74241a2814918053454f6a6", + "model_id": "4d6e43032f4e4aa6be5843c4916b4b50", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "interactive(children=(FloatSlider(value=0.0, description='t', max=13.076977041121545, step=0.13076977041121546…" + "interactive(children=(FloatSlider(value=0.0, description='t', max=13.076887099589111, step=0.1307688709958911)…" ] }, "metadata": {}, @@ -225,7 +216,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 6, @@ -255,7 +246,7 @@ { "data": { "text/plain": [ - "_Step(C-rate, 1.0, duration=1 hour, period=1 minute, temperature=25oC, tags=['tag1'], description=Discharge at 1C for 1 hour)" + "Step(1.0, duration=1 hour, period=1 minute, temperature=25oC, tags=['tag1'], description=Discharge at 1C for 1 hour, direction=Discharge)" ] }, "execution_count": 7, @@ -293,7 +284,7 @@ { "data": { "text/plain": [ - "_Step(current, 1, duration=1 hour, termination=2.5 V)" + "Step(1, duration=1 hour, termination=2.5 V, direction=Discharge)" ] }, "execution_count": 8, @@ -321,7 +312,7 @@ { "data": { "text/plain": [ - "_Step(current, 1.0, duration=1 hour, termination=2.5V, description=Discharge at 1A for 1 hour or until 2.5V)" + "Step(1.0, duration=1 hour, termination=2.5V, description=Discharge at 1A for 1 hour or until 2.5V, direction=Discharge)" ] }, "execution_count": 9, @@ -348,10 +339,78 @@ "execution_count": 10, "metadata": {}, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2024-07-10 14:41:02.625 - [WARNING] callbacks.on_experiment_infeasible_time(240): \n", + "\n", + "\tExperiment is infeasible: default duration (1.0 seconds) was reached during 'Step([[ 0.00000000e+00 0.00000000e+00]\n", + " [ 1.69491525e-02 5.31467428e-02]\n", + " [ 3.38983051e-02 1.05691312e-01]\n", + " [ 5.08474576e-02 1.57038356e-01]\n", + " [ 6.77966102e-02 2.06606093e-01]\n", + " [ 8.47457627e-02 2.53832900e-01]\n", + " [ 1.01694915e-01 2.98183679e-01]\n", + " [ 1.18644068e-01 3.39155918e-01]\n", + " [ 1.35593220e-01 3.76285385e-01]\n", + " [ 1.52542373e-01 4.09151388e-01]\n", + " [ 1.69491525e-01 4.37381542e-01]\n", + " [ 1.86440678e-01 4.60655989e-01]\n", + " [ 2.03389831e-01 4.78711019e-01]\n", + " [ 2.20338983e-01 4.91342062e-01]\n", + " [ 2.37288136e-01 4.98406004e-01]\n", + " [ 2.54237288e-01 4.99822806e-01]\n", + " [ 2.71186441e-01 4.95576416e-01]\n", + " [ 2.88135593e-01 4.85714947e-01]\n", + " [ 3.05084746e-01 4.70350133e-01]\n", + " [ 3.22033898e-01 4.49656065e-01]\n", + " [ 3.38983051e-01 4.23867214e-01]\n", + " [ 3.55932203e-01 3.93275778e-01]\n", + " [ 3.72881356e-01 3.58228370e-01]\n", + " [ 3.89830508e-01 3.19122092e-01]\n", + " [ 4.06779661e-01 2.76400033e-01]\n", + " [ 4.23728814e-01 2.30546251e-01]\n", + " [ 4.40677966e-01 1.82080288e-01]\n", + " [ 4.57627119e-01 1.31551282e-01]\n", + " [ 4.74576271e-01 7.95317480e-02]\n", + " [ 4.91525424e-01 2.66110874e-02]\n", + " [ 5.08474576e-01 -2.66110874e-02]\n", + " [ 5.25423729e-01 -7.95317480e-02]\n", + " [ 5.42372881e-01 -1.31551282e-01]\n", + " [ 5.59322034e-01 -1.82080288e-01]\n", + " [ 5.76271186e-01 -2.30546251e-01]\n", + " [ 5.93220339e-01 -2.76400033e-01]\n", + " [ 6.10169492e-01 -3.19122092e-01]\n", + " [ 6.27118644e-01 -3.58228370e-01]\n", + " [ 6.44067797e-01 -3.93275778e-01]\n", + " [ 6.61016949e-01 -4.23867214e-01]\n", + " [ 6.77966102e-01 -4.49656065e-01]\n", + " [ 6.94915254e-01 -4.70350133e-01]\n", + " [ 7.11864407e-01 -4.85714947e-01]\n", + " [ 7.28813559e-01 -4.95576416e-01]\n", + " [ 7.45762712e-01 -4.99822806e-01]\n", + " [ 7.62711864e-01 -4.98406004e-01]\n", + " [ 7.79661017e-01 -4.91342062e-01]\n", + " [ 7.96610169e-01 -4.78711019e-01]\n", + " [ 8.13559322e-01 -4.60655989e-01]\n", + " [ 8.30508475e-01 -4.37381542e-01]\n", + " [ 8.47457627e-01 -4.09151388e-01]\n", + " [ 8.64406780e-01 -3.76285385e-01]\n", + " [ 8.81355932e-01 -3.39155918e-01]\n", + " [ 8.98305085e-01 -2.98183679e-01]\n", + " [ 9.15254237e-01 -2.53832900e-01]\n", + " [ 9.32203390e-01 -2.06606093e-01]\n", + " [ 9.49152542e-01 -1.57038356e-01]\n", + " [ 9.66101695e-01 -1.05691312e-01]\n", + " [ 9.83050847e-01 -5.31467428e-02]\n", + " [ 1.00000000e+00 -1.22464680e-16]], duration=1.0, period=0.016949152542372836, direction=Rest)'. The returned solution only contains up to step 1 of cycle 1. Please specify a duration in the step instructions.\n" + ] + }, { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "730d5e19b17e447ebde5679de68c46ef", + "model_id": "6364b4579fc447e2a607f2f8414172ba", "version_major": 2, "version_minor": 0 }, @@ -365,7 +424,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 10, @@ -419,13 +478,14 @@ "output_type": "stream", "text": [ "[1] Joel A. E. Andersson, Joris Gillis, Greg Horn, James B. Rawlings, and Moritz Diehl. CasADi – A software framework for nonlinear optimization and optimal control. Mathematical Programming Computation, 11(1):1–36, 2019. doi:10.1007/s12532-018-0139-4.\n", - "[2] Marc Doyle, Thomas F. Fuller, and John Newman. Modeling of galvanostatic charge and discharge of the lithium/polymer/insertion cell. Journal of the Electrochemical society, 140(6):1526–1533, 1993. doi:10.1149/1.2221597.\n", - "[3] Charles R. Harris, K. Jarrod Millman, Stéfan J. van der Walt, Ralf Gommers, Pauli Virtanen, David Cournapeau, Eric Wieser, Julian Taylor, Sebastian Berg, Nathaniel J. Smith, and others. Array programming with NumPy. Nature, 585(7825):357–362, 2020. doi:10.1038/s41586-020-2649-2.\n", - "[4] Scott G. Marquis, Valentin Sulzer, Robert Timms, Colin P. Please, and S. Jon Chapman. An asymptotic derivation of a single particle model with electrolyte. Journal of The Electrochemical Society, 166(15):A3693–A3706, 2019. doi:10.1149/2.0341915jes.\n", - "[5] Peyman Mohtat, Suhak Lee, Jason B Siegel, and Anna G Stefanopoulou. Towards better estimability of electrode-specific state of health: decoding the cell expansion. Journal of Power Sources, 427:101–111, 2019.\n", - "[6] Valentin Sulzer, Scott G. Marquis, Robert Timms, Martin Robinson, and S. Jon Chapman. Python Battery Mathematical Modelling (PyBaMM). Journal of Open Research Software, 9(1):14, 2021. doi:10.5334/jors.309.\n", - "[7] Pauli Virtanen, Ralf Gommers, Travis E. Oliphant, Matt Haberland, Tyler Reddy, David Cournapeau, Evgeni Burovski, Pearu Peterson, Warren Weckesser, Jonathan Bright, and others. SciPy 1.0: fundamental algorithms for scientific computing in Python. Nature Methods, 17(3):261–272, 2020. doi:10.1038/s41592-019-0686-2.\n", - "[8] Andrew Weng, Jason B Siegel, and Anna Stefanopoulou. Differential voltage analysis for battery manufacturing process control. arXiv preprint arXiv:2303.07088, 2023.\n", + "[2] Von DAG Bruggeman. Berechnung verschiedener physikalischer konstanten von heterogenen substanzen. i. dielektrizitätskonstanten und leitfähigkeiten der mischkörper aus isotropen substanzen. Annalen der physik, 416(7):636–664, 1935.\n", + "[3] Marc Doyle, Thomas F. Fuller, and John Newman. Modeling of galvanostatic charge and discharge of the lithium/polymer/insertion cell. Journal of the Electrochemical society, 140(6):1526–1533, 1993. doi:10.1149/1.2221597.\n", + "[4] Charles R. Harris, K. Jarrod Millman, Stéfan J. van der Walt, Ralf Gommers, Pauli Virtanen, David Cournapeau, Eric Wieser, Julian Taylor, Sebastian Berg, Nathaniel J. Smith, and others. Array programming with NumPy. Nature, 585(7825):357–362, 2020. doi:10.1038/s41586-020-2649-2.\n", + "[5] Scott G. Marquis, Valentin Sulzer, Robert Timms, Colin P. Please, and S. Jon Chapman. An asymptotic derivation of a single particle model with electrolyte. Journal of The Electrochemical Society, 166(15):A3693–A3706, 2019. doi:10.1149/2.0341915jes.\n", + "[6] Peyman Mohtat, Suhak Lee, Jason B Siegel, and Anna G Stefanopoulou. Towards better estimability of electrode-specific state of health: decoding the cell expansion. Journal of Power Sources, 427:101–111, 2019.\n", + "[7] Valentin Sulzer, Scott G. Marquis, Robert Timms, Martin Robinson, and S. Jon Chapman. Python Battery Mathematical Modelling (PyBaMM). Journal of Open Research Software, 9(1):14, 2021. doi:10.5334/jors.309.\n", + "[8] Pauli Virtanen, Ralf Gommers, Travis E. Oliphant, Matt Haberland, Tyler Reddy, David Cournapeau, Evgeni Burovski, Pearu Peterson, Warren Weckesser, Jonathan Bright, and others. SciPy 1.0: fundamental algorithms for scientific computing in Python. Nature Methods, 17(3):261–272, 2020. doi:10.1038/s41592-019-0686-2.\n", + "[9] Andrew Weng, Jason B Siegel, and Anna Stefanopoulou. Differential voltage analysis for battery manufacturing process control. arXiv preprint arXiv:2303.07088, 2023.\n", "\n" ] } diff --git a/pybamm/callbacks.py b/pybamm/callbacks.py index c99c416cfb..486bc12efb 100644 --- a/pybamm/callbacks.py +++ b/pybamm/callbacks.py @@ -36,37 +36,37 @@ def on_experiment_start(self, logs): """ Called at the start of an experiment simulation. """ - pass + pass # pragma: no cover def on_cycle_start(self, logs): """ Called at the start of each cycle in an experiment simulation. """ - pass + pass # pragma: no cover def on_step_start(self, logs): """ Called at the start of each step in an experiment simulation. """ - pass + pass # pragma: no cover def on_step_end(self, logs): """ Called at the end of each step in an experiment simulation. """ - pass + pass # pragma: no cover def on_cycle_end(self, logs): """ Called at the end of each cycle in an experiment simulation. """ - pass + pass # pragma: no cover def on_experiment_end(self, logs): """ Called at the end of an experiment simulation. """ - pass + pass # pragma: no cover def on_experiment_error(self, logs): """ @@ -75,13 +75,19 @@ def on_experiment_error(self, logs): For example, this could be used to send an error alert with a bug report when running batch simulations in the cloud. """ - pass + pass # pragma: no cover - def on_experiment_infeasible(self, logs): + def on_experiment_infeasible_time(self, logs): """ - Called when an experiment simulation is infeasible. + Called when an experiment simulation is infeasible due to reaching maximum time. """ - pass + pass # pragma: no cover + + def on_experiment_infeasible_event(self, logs): + """ + Called when an experiment simulation is infeasible due to an event. + """ + pass # pragma: no cover ######################################################################################## @@ -212,7 +218,19 @@ def on_experiment_error(self, logs): error = logs["error"] pybamm.logger.error(f"Simulation error: {error}") - def on_experiment_infeasible(self, logs): + def on_experiment_infeasible_time(self, logs): + duration = logs["step duration"] + cycle_num = logs["cycle number"][0] + step_num = logs["step number"][0] + operating_conditions = logs["step operating conditions"] + self.logger.warning( + f"\n\n\tExperiment is infeasible: default duration ({duration} seconds) " + f"was reached during '{operating_conditions}'. The returned solution only " + f"contains up to step {step_num} of cycle {cycle_num}. " + "Please specify a duration in the step instructions." + ) + + def on_experiment_infeasible_event(self, logs): termination = logs["termination"] cycle_num = logs["cycle number"][0] step_num = logs["step number"][0] diff --git a/pybamm/experiment/step/base_step.py b/pybamm/experiment/step/base_step.py index 626094ba54..3c8ff3f1ae 100644 --- a/pybamm/experiment/step/base_step.py +++ b/pybamm/experiment/step/base_step.py @@ -71,6 +71,8 @@ def __init__( description=None, direction=None, ): + self.input_duration = duration + self.input_value = value # Check if drive cycle self.is_drive_cycle = isinstance(value, np.ndarray) if self.is_drive_cycle: @@ -84,8 +86,11 @@ def __init__( if t[0] != 0: raise ValueError("Drive cycle must start at t=0") + # Record whether the step uses the default duration + # This will be used by the experiment to check whether the step is feasible + self.uses_default_duration = duration is None # Set duration - if duration is None: + if self.uses_default_duration: duration = self.default_duration(value) self.duration = _convert_time_to_seconds(duration) @@ -179,8 +184,8 @@ def copy(self): A copy of the step. """ return self.__class__( - self.value, - duration=self.duration, + self.input_value, + duration=self.input_duration, termination=self.termination, period=self.period, temperature=self.temperature, @@ -243,7 +248,7 @@ def default_duration(self, value): t = value[:, 0] return t[-1] else: - return 24 * 3600 # 24 hours in seconds + return 24 * 3600 # one day in seconds def process_model(self, model, parameter_values): new_model = model.new_copy() @@ -343,10 +348,16 @@ def set_up(self, new_model, new_parameter_values): def _convert_time_to_seconds(time_and_units): """Convert a time in seconds, minutes or hours to a time in seconds""" - # If the time is a number, assume it is in seconds - if isinstance(time_and_units, numbers.Number) or time_and_units is None: + if time_and_units is None: return time_and_units + # If the time is a number, assume it is in seconds + if isinstance(time_and_units, numbers.Number): + if time_and_units <= 0: + raise ValueError("time must be positive") + else: + return time_and_units + # Split number and units units = time_and_units.lstrip("0123456789.- ") time = time_and_units[: -len(units)] diff --git a/pybamm/experiment/step/steps.py b/pybamm/experiment/step/steps.py index 1c156c444d..56be581546 100644 --- a/pybamm/experiment/step/steps.py +++ b/pybamm/experiment/step/steps.py @@ -157,6 +157,11 @@ def __init__(self, value, **kwargs): def current_value(self, variables): return self.value * pybamm.Parameter("Nominal cell capacity [A.h]") + def default_duration(self, value): + # "value" is C-rate, so duration is "1 / value" hours in seconds + # with a 2x safety factor + return 1 / abs(value) * 3600 * 2 + def c_rate(value, **kwargs): """ diff --git a/pybamm/simulation.py b/pybamm/simulation.py index 4ca8a4fdc3..9e2a899b08 100644 --- a/pybamm/simulation.py +++ b/pybamm/simulation.py @@ -680,6 +680,7 @@ def solve( logs["step number"] = (step_num, cycle_length) logs["step operating conditions"] = step_str + logs["step duration"] = step.duration callbacks.on_step_start(logs) inputs = { @@ -768,23 +769,33 @@ def solve( callbacks.on_step_end(logs) logs["termination"] = step_solution.termination - # Only allow events specified by experiment - if not ( + + # Check for some cases that would make the experiment end early + if step_termination == "final time" and step.uses_default_duration: + # reached the default duration of a step (typically we should + # reach an event before the default duration) + callbacks.on_experiment_infeasible_time(logs) + feasible = False + break + + elif not ( isinstance(step_solution, pybamm.EmptySolution) or step_termination == "final time" or "[experiment]" in step_termination ): - callbacks.on_experiment_infeasible(logs) + # Step has reached an event that is not specified in the + # experiment + callbacks.on_experiment_infeasible_event(logs) feasible = False break - if time_stop is not None: - max_time = cycle_solution.t[-1] - if max_time >= time_stop: - break + elif time_stop is not None and logs["experiment time"] >= time_stop: + # reached the time limit of the experiment + break - # Increment index for next iteration - idx += 1 + else: + # Increment index for next iteration, then continue + idx += 1 if save_this_cycle or feasible is False: self._solution = self._solution + cycle_solution diff --git a/tests/unit/test_callbacks.py b/tests/unit/test_callbacks.py index b36fef9ec6..649c7d9ec8 100644 --- a/tests/unit/test_callbacks.py +++ b/tests/unit/test_callbacks.py @@ -81,6 +81,7 @@ def test_logging_callback(self): "cycle number": (5, 12), "step number": (1, 4), "elapsed time": 0.45, + "step duration": 1, "step operating conditions": "Charge", "termination": "event", } @@ -96,10 +97,14 @@ def test_logging_callback(self): with open("test_callback.log") as f: self.assertIn("Cycle 5/12, step 1/4", f.read()) - callback.on_experiment_infeasible(logs) + callback.on_experiment_infeasible_event(logs) with open("test_callback.log") as f: self.assertIn("Experiment is infeasible: 'event'", f.read()) + callback.on_experiment_infeasible_time(logs) + with open("test_callback.log") as f: + self.assertIn("Experiment is infeasible: default duration", f.read()) + callback.on_experiment_end(logs) with open("test_callback.log") as f: self.assertIn("took 0.45", f.read()) diff --git a/tests/unit/test_experiments/test_experiment_steps.py b/tests/unit/test_experiments/test_experiment_steps.py index 9d1abcc133..4bb686986f 100644 --- a/tests/unit/test_experiments/test_experiment_steps.py +++ b/tests/unit/test_experiments/test_experiment_steps.py @@ -43,15 +43,20 @@ def test_step(self): with self.assertRaisesRegex(ValueError, "temperature units"): step = pybamm.step.current(1, temperature="298T") + with self.assertRaisesRegex(ValueError, "time must be positive"): + pybamm.step.current(1, duration=0) + def test_specific_steps(self): current = pybamm.step.current(1) self.assertIsInstance(current, pybamm.step.Current) self.assertEqual(current.value, 1) self.assertEqual(str(current), repr(current)) + self.assertEqual(current.duration, 24 * 3600) c_rate = pybamm.step.c_rate(1) self.assertIsInstance(c_rate, pybamm.step.CRate) self.assertEqual(c_rate.value, 1) + self.assertEqual(c_rate.duration, 3600 * 2) voltage = pybamm.step.voltage(1) self.assertIsInstance(voltage, pybamm.step.Voltage) @@ -145,19 +150,19 @@ def test_step_string(self): { "type": "CRate", "value": -1, - "duration": 86400, + "duration": 7200, "termination": [pybamm.step.VoltageTermination(4.1)], }, { "value": 4.1, "type": "Voltage", - "duration": 86400, + "duration": 3600 * 24, "termination": [pybamm.step.CurrentTermination(0.05)], }, { "value": 3, "type": "Voltage", - "duration": 86400, + "duration": 3600 * 24, "termination": [pybamm.step.CrateTermination(0.02)], }, { diff --git a/tests/unit/test_experiments/test_simulation_with_experiment.py b/tests/unit/test_experiments/test_simulation_with_experiment.py index e2879bf294..7784726fc2 100644 --- a/tests/unit/test_experiments/test_simulation_with_experiment.py +++ b/tests/unit/test_experiments/test_simulation_with_experiment.py @@ -10,6 +10,12 @@ from datetime import datetime +class ShortDurationCRate(pybamm.step.CRate): + def default_duration(self, value): + # Set a short default duration for testing early stopping due to infeasible time + return 1 + + class TestSimulationExperiment(TestCase): def test_set_up(self): experiment = pybamm.Experiment( @@ -272,6 +278,19 @@ def test_run_experiment_breaks_early_error(self): # Different callback - this is for coverage on the `Callback` class sol = sim.solve(callbacks=pybamm.callbacks.Callback()) + def test_run_experiment_infeasible_time(self): + experiment = pybamm.Experiment( + [ShortDurationCRate(1, termination="2.5V"), "Rest for 1 hour"] + ) + model = pybamm.lithium_ion.SPM() + parameter_values = pybamm.ParameterValues("Chen2020") + sim = pybamm.Simulation( + model, parameter_values=parameter_values, experiment=experiment + ) + sol = sim.solve() + self.assertEqual(len(sol.cycles), 1) + self.assertEqual(len(sol.cycles[0].steps), 1) + def test_run_experiment_termination_capacity(self): # with percent experiment = pybamm.Experiment(