diff --git a/CHANGELOG.md b/CHANGELOG.md index 388d3ceef8..d34578b7b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Features +- Added option to express experiments (and extract solutions) in terms of cycles of operating condition ([#1309](https://github.com/pybamm-team/PyBaMM/pull/1309)) - Reformatted the `BasicDFNHalfCell` to be consistent with the other models ([#1282](https://github.com/pybamm-team/PyBaMM/pull/1282)) - Added option to make the total interfacial current density a state ([#1280](https://github.com/pybamm-team/PyBaMM/pull/1280)) - Added functionality to initialize a model using the solution from another model ([#1278](https://github.com/pybamm-team/PyBaMM/pull/1278)) diff --git a/examples/notebooks/Getting Started/Tutorial 5 - Run experiments.ipynb b/examples/notebooks/Getting Started/Tutorial 5 - Run experiments.ipynb index d14cac014d..659e79d908 100644 --- a/examples/notebooks/Getting Started/Tutorial 5 - Run experiments.ipynb +++ b/examples/notebooks/Getting Started/Tutorial 5 - Run experiments.ipynb @@ -47,11 +47,11 @@ "source": [ "experiment = pybamm.Experiment(\n", " [\n", - " \"Discharge at C/10 for 10 hours or until 3.3 V\",\n", + " (\"Discharge at C/10 for 10 hours or until 3.3 V\",\n", " \"Rest for 1 hour\",\n", " \"Charge at 1 A until 4.1 V\",\n", " \"Hold at 4.1 V until 50 mA\",\n", - " \"Rest for 1 hour\",\n", + " \"Rest for 1 hour\"),\n", " ] * 3\n", ")" ] @@ -60,7 +60,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "In this case, the experiment consists of a cycle of constant current C/10 discahrge, a one hour rest, a constant current (1 A) constant voltage (4.1 V) and another one hour rest, all of it repeated three times (notice the `* 3`).\n", + "A cycle is defined by a tuple of operating instructions. In this case, the experiment consists of a cycle of constant current C/10 discharge, a one hour rest, a constant current (1 A) constant voltage (4.1 V) and another one hour rest, all of it repeated three times (notice the * 3).\n", "\n", "Then we can choose our model" ] @@ -146,33 +146,30 @@ "\n", "Optionally, each instruction can contain at the end the expression \"(x minute period)\" in which the period at which to record the simulation outputs during that instruction. To change the period for the whole experiment we can pass it as a keyword argument in the experiment.\n", "\n", - "Additionally, we can use the operators `+` and `*` to combine and repeat lists:" + "Additionally, we can use the operators `+` and `*` on lists in order to combine and repeat cycles:" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 4, "metadata": {}, "outputs": [ { + "output_type": "execute_result", "data": { "text/plain": [ - "['Discharge at 1C for 0.5 hours',\n", - " 'Discharge at C/20 for 0.5 hours',\n", - " 'Discharge at 1C for 0.5 hours',\n", - " 'Discharge at C/20 for 0.5 hours',\n", - " 'Discharge at 1C for 0.5 hours',\n", - " 'Discharge at C/20 for 0.5 hours',\n", - " 'Charge at 0.5 C for 45 minutes']" + "[('Discharge at 1C for 0.5 hours', 'Discharge at C/20 for 0.5 hours'),\n", + " ('Discharge at 1C for 0.5 hours', 'Discharge at C/20 for 0.5 hours'),\n", + " ('Discharge at 1C for 0.5 hours', 'Discharge at C/20 for 0.5 hours'),\n", + " ('Charge at 0.5 C for 45 minutes',)]" ] }, - "execution_count": 6, "metadata": {}, - "output_type": "execute_result" + "execution_count": 4 } ], "source": [ - "[\"Discharge at 1C for 0.5 hours\", \"Discharge at C/20 for 0.5 hours\"] * 3 + [\"Charge at 0.5 C for 45 minutes\"]" + "[(\"Discharge at 1C for 0.5 hours\", \"Discharge at C/20 for 0.5 hours\")] * 3 + [(\"Charge at 0.5 C for 45 minutes\",)]" ] }, { @@ -206,9 +203,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.8" + "version": "3.8.2-final" } }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file diff --git a/examples/scripts/experimental_protocols/cccv.py b/examples/scripts/experimental_protocols/cccv.py index f6b98dcb21..073f4e5d5c 100644 --- a/examples/scripts/experimental_protocols/cccv.py +++ b/examples/scripts/experimental_protocols/cccv.py @@ -7,11 +7,13 @@ pybamm.set_logging_level("INFO") experiment = pybamm.Experiment( [ - "Discharge at C/10 for 10 hours or until 3.3 V", - "Rest for 1 hour", - "Charge at 1 A until 4.1 V", - "Hold at 4.1 V until 50 mA", - "Rest for 1 hour", + ( + "Discharge at C/10 for 10 hours or until 3.3 V", + "Rest for 1 hour", + "Charge at 1 A until 4.1 V", + "Hold at 4.1 V until 50 mA", + "Rest for 1 hour", + ), ] * 3 ) @@ -23,7 +25,7 @@ fig, ax = plt.subplots() for i in range(3): # Extract sub solutions - sol = sim.solution.sub_solutions[i * 5] + sol = sim.solution.cycles[i][0] # Extract variables t = sol["Time [h]"].entries V = sol["Terminal voltage [V]"].entries diff --git a/examples/scripts/experimental_protocols/gitt.py b/examples/scripts/experimental_protocols/gitt.py index 0ad881bec5..5a13a2a1b2 100644 --- a/examples/scripts/experimental_protocols/gitt.py +++ b/examples/scripts/experimental_protocols/gitt.py @@ -4,7 +4,9 @@ import pybamm pybamm.set_logging_level("INFO") -experiment = pybamm.Experiment(["Discharge at C/20 for 1 hour", "Rest for 1 hour"] * 20) +experiment = pybamm.Experiment( + [("Discharge at C/20 for 1 hour", "Rest for 1 hour")] * 20 +) model = pybamm.lithium_ion.DFN() sim = pybamm.Simulation(model, experiment=experiment, solver=pybamm.CasadiSolver()) sim.solve() diff --git a/pybamm/experiments/experiment.py b/pybamm/experiments/experiment.py index fb7bbef648..402eabbcf7 100644 --- a/pybamm/experiments/experiment.py +++ b/pybamm/experiments/experiment.py @@ -45,6 +45,35 @@ class Experiment: def __init__(self, operating_conditions, parameters=None, period="1 minute"): self.period = self.convert_time_to_seconds(period.split()) + operating_conditions_cycles = [] + for cycle in operating_conditions: + # Check types and convert strings to 1-tuples + if (isinstance(cycle, tuple) or isinstance(cycle, str)) and all( + [isinstance(cond, str) for cond in cycle] + ): + operating_conditions_cycles.append( + cycle if isinstance(cycle, tuple) else (cycle,) + ) + else: + try: + # Condition is not a string + badly_typed_conditions = [ + cond for cond in cycle if not isinstance(cond, str) + ] + except TypeError: + # Cycle is not a tuple or string + badly_typed_conditions = [] + badly_typed_conditions = badly_typed_conditions or [cycle] + raise TypeError( + """Operating conditions should be strings or tuples of strings, not {}. For example: {} + """.format( + type(badly_typed_conditions[0]), examples + ) + ) + self.cycle_lengths = [len(cycle) for cycle in operating_conditions_cycles] + operating_conditions = [ + cond for cycle in operating_conditions_cycles for cond in cycle + ] self.operating_conditions_strings = operating_conditions self.operating_conditions, self.events = self.read_operating_conditions( operating_conditions @@ -78,17 +107,9 @@ def read_operating_conditions(self, operating_conditions): converted_operating_conditions = [] events = [] for cond in operating_conditions: - if isinstance(cond, str): - next_op, next_event = self.read_string(cond) - converted_operating_conditions.append(next_op) - events.append(next_event) - else: - raise TypeError( - """Operating conditions should be strings, not {}. For example: {} - """.format( - type(cond), examples - ) - ) + next_op, next_event = self.read_string(cond) + converted_operating_conditions.append(next_op) + events.append(next_event) return converted_operating_conditions, events diff --git a/pybamm/simulation.py b/pybamm/simulation.py index c821b44e80..c99d227754 100644 --- a/pybamm/simulation.py +++ b/pybamm/simulation.py @@ -473,6 +473,19 @@ def solve( "or reducing the period.\n\n" ) break + if hasattr(self.solution, "_sub_solutions"): + # Construct solution.cycles (a list of tuples) from sub_solutions + self.solution.cycles = [] + for cycle_num, cycle_length in enumerate(self.experiment.cycle_lengths): + cycle_start_idx = sum(self.experiment.cycle_lengths[0:cycle_num]) + self.solution.cycles.append( + tuple( + [ + self.solution.sub_solutions[cycle_start_idx + idx] + for idx in range(cycle_length) + ] + ) + ) pybamm.logger.info( "Finish experiment simulation, took {}".format(timer.time()) ) diff --git a/tests/unit/test_experiments/test_experiment.py b/tests/unit/test_experiments/test_experiment.py index 0579ebebb7..e2743902e0 100644 --- a/tests/unit/test_experiments/test_experiment.py +++ b/tests/unit/test_experiments/test_experiment.py @@ -82,6 +82,25 @@ def test_read_strings_repeat(self): ) self.assertEqual(experiment.period, 60) + def test_cycle_unpacking(self): + experiment = pybamm.Experiment( + [ + ("Discharge at C/20 for 0.5 hours", "Charge at C/5 for 45 minutes"), + ("Discharge at C/20 for 0.5 hours"), + "Charge at C/5 for 45 minutes", + ] + ) + self.assertEqual( + experiment.operating_conditions, + [ + (0.05, "C", 1800.0, 60.0), + (-0.2, "C", 2700.0, 60.0), + (0.05, "C", 1800.0, 60.0), + (-0.2, "C", 2700.0, 60.0), + ], + ) + self.assertEqual(experiment.cycle_lengths, [2, 1, 1]) + def test_str_repr(self): conds = ["Discharge at 1 C for 20 seconds", "Charge at 0.5 W for 10 minutes"] experiment = pybamm.Experiment(conds) @@ -94,9 +113,13 @@ def test_str_repr(self): def test_bad_strings(self): with self.assertRaisesRegex( - TypeError, "Operating conditions should be strings" + TypeError, "Operating conditions should be strings or tuples of strings" ): pybamm.Experiment([1, 2, 3]) + with self.assertRaisesRegex( + TypeError, "Operating conditions should be strings or tuples of strings" + ): + pybamm.Experiment([(1, 2, 3)]) with self.assertRaisesRegex(ValueError, "Operating conditions must contain"): pybamm.Experiment(["Discharge at 1 A at 2 hours"]) with self.assertRaisesRegex(ValueError, "instruction must be"): diff --git a/tests/unit/test_solvers/test_solution.py b/tests/unit/test_solvers/test_solution.py index 2bc57f14c0..9285d253c6 100644 --- a/tests/unit/test_solvers/test_solution.py +++ b/tests/unit/test_solvers/test_solution.py @@ -69,6 +69,23 @@ def test_append(self): sol1.sub_solutions[1].inputs["a"], 2 * np.ones_like(t2)[np.newaxis, :] ) + def test_cycles(self): + model = pybamm.lithium_ion.DFN() + experiment = pybamm.Experiment( + [ + ("Discharge at C/20 for 0.5 hours", "Charge at C/20 for 15 minutes"), + ("Discharge at C/20 for 0.5 hours", "Charge at C/20 for 15 minutes"), + ] + ) + sim = pybamm.Simulation(model, experiment=experiment) + sim.solve() + num_cycles = len(experiment.cycle_lengths) + for idx, sub_solution in enumerate(sim.solution.sub_solutions): + cycle_sub_solution = sim.solution.cycles[idx // num_cycles][ + idx % num_cycles + ] + self.assertEqual(cycle_sub_solution, sub_solution) + def test_total_time(self): sol = pybamm.Solution([], None) sol.set_up_time = 0.5