Repository: pybamm-team/PyBaMM

Issue:
Tag experiments [former Reference Performance Test (RPT) experiment]
### Description

The capacity of the battery (e.g. when doing ageing tests) is typically done through RPTs, an experimental step consisting of a full charge and a CC discharge at a given C-rate until the cut-off voltage is hit. The capacity in that discharge is the "measured capacity".

These tests can be easily run with the current features (though the experiment needs to be split and solution reinitialised which can be cumbersome). However, this is a fairly standard test which is intercalated between many other steps, and often we only care about the measurements during the RPT. Possible ways around that would be to define a separate RPT class and then:
* Store RPT subsolutions (or maybe even just the capacity measure) as a separate variable in the solution.
* Alternatively, just store a list with the indices corresponding the RPT so they can be easily accessed.

Anyway, what are people thoughts on that?

### Motivation

Fairly standard experiment type used in ageing experiments.

### Possible Implementation

Define a separate RPT experimental step (ties with #2322)

### Additional context

This might make #2219 no longer needed, as the main motivation was doing RPT.

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index de93a95d0..a38d5b8c7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 # [Unreleased](https://github.com/pybamm-team/PyBaMM/)
 
+## Features
+
+- Steps in `Experiment` can now be tagged and cycle numbers be searched based on those tags ([#2593](https://github.com/pybamm-team/PyBaMM/pull/2593)).
+
 # [v22.12](https://github.com/pybamm-team/PyBaMM/tree/v22.12) - 2022-12-31
 
 ## Features
diff --git a/pybamm/experiments/experiment.py b/pybamm/experiments/experiment.py
index 2dc432b71..c907e767d 100644
--- a/pybamm/experiments/experiment.py
+++ b/pybamm/experiments/experiment.py
@@ -145,7 +145,7 @@ class Experiment:
 
     def read_string(self, cond, drive_cycles):
         """
-        Convert a string to a tuple of the right format
+        Convert a string to a dictionary of the right format
 
         Parameters
         ----------
@@ -163,6 +163,11 @@ class Experiment:
             cond_CC, cond_CV = cond.split(" then ")
             op_CC = self.read_string(cond_CC, drive_cycles)
             op_CV = self.read_string(cond_CV, drive_cycles)
+            tag_CC = op_CC["tags"] or []
+            tag_CV = op_CV["tags"] or []
+            tags = list(np.unique(tag_CC + tag_CV))
+            if len(tags) == 0:
+                tags = None
             outputs = {
                 "type": "CCCV",
                 "Voltage input [V]": op_CV["Voltage input [V]"],
@@ -171,6 +176,7 @@ class Experiment:
                 "dc_data": None,
                 "string": cond,
                 "events": op_CV["events"],
+                "tags": tags,
             }
             if "Current input [A]" in op_CC:
                 outputs["Current input [A]"] = op_CC["Current input [A]"]
@@ -178,6 +184,13 @@ class Experiment:
                 outputs["C-rate input [-]"] = op_CC["C-rate input [-]"]
             return outputs
 
+        # Read tags
+        if " [" in cond:
+            cond, tag_str = cond.split(" [")
+            tags = tag_str[0:-1].split(",")
+        else:
+            tags = None
+
         # Read period
         if " period)" in cond:
             cond, time_period = cond.split(" (")
@@ -254,6 +267,7 @@ class Experiment:
             "dc_data": dc_data,
             "string": cond,
             "events": events,
+            "tags": tags,
         }
 
     def unit_to_type(self, electric):
@@ -440,3 +454,16 @@ class Experiment:
             if op["events"] == {k: v for k, v in next_op.items() if k in op["events"]}:
                 return True
         return False
+
+    def search_tag(self, tag):
+        cycles = []
+        for i, cycle in enumerate(self.operating_conditions_cycles):
+            for cond in cycle:
+                if " [" in cond:
+                    cond, tag_str = cond.split(" [")
+                    tags = tag_str[0:-1].split(",")
+                    if tag in tags:
+                        cycles.append(i)
+                        break
+
+        return cycles


Test patch:
diff --git a/tests/unit/test_experiments/test_experiment.py b/tests/unit/test_experiments/test_experiment.py
index f4ed7356d..af0d5ea23 100644
--- a/tests/unit/test_experiments/test_experiment.py
+++ b/tests/unit/test_experiments/test_experiment.py
@@ -21,14 +21,14 @@ class TestExperiment(unittest.TestCase):
 
         experiment = pybamm.Experiment(
             [
-                "Discharge at 1C for 0.5 hours",
-                "Discharge at C/20 for 0.5 hours",
+                "Discharge at 1C for 0.5 hours [tag1]",
+                "Discharge at C/20 for 0.5 hours [tag2,tag3]",
                 "Charge at 0.5 C for 45 minutes",
                 "Discharge at 1 A for 0.5 hours",
                 "Charge at 200 mA for 45 minutes (1 minute period)",
                 "Discharge at 1W for 0.5 hours",
                 "Charge at 200mW for 45 minutes",
-                "Rest for 10 minutes (5 minute period)",
+                "Rest for 10 minutes (5 minute period) [tag1,tag3]",
                 "Hold at 1V for 20 seconds",
                 "Charge at 1 C until 4.1V",
                 "Hold at 4.1 V until 50mA",
@@ -53,6 +53,7 @@ class TestExperiment(unittest.TestCase):
                     "dc_data": None,
                     "string": "Discharge at 1C for 0.5 hours",
                     "events": None,
+                    "tags": ["tag1"],
                 },
                 {
                     "C-rate input [-]": 0.05,
@@ -62,6 +63,7 @@ class TestExperiment(unittest.TestCase):
                     "dc_data": None,
                     "string": "Discharge at C/20 for 0.5 hours",
                     "events": None,
+                    "tags": ["tag2", "tag3"],
                 },
                 {
                     "C-rate input [-]": -0.5,
@@ -71,6 +73,7 @@ class TestExperiment(unittest.TestCase):
                     "dc_data": None,
                     "string": "Charge at 0.5 C for 45 minutes",
                     "events": None,
+                    "tags": None,
                 },
                 {
                     "Current input [A]": 1,
@@ -80,6 +83,7 @@ class TestExperiment(unittest.TestCase):
                     "dc_data": None,
                     "string": "Discharge at 1 A for 0.5 hours",
                     "events": None,
+                    "tags": None,
                 },
                 {
                     "Current input [A]": -0.2,
@@ -89,6 +93,7 @@ class TestExperiment(unittest.TestCase):
                     "dc_data": None,
                     "string": "Charge at 200 mA for 45 minutes",
                     "events": None,
+                    "tags": None,
                 },
                 {
                     "Power input [W]": 1,
@@ -98,6 +103,7 @@ class TestExperiment(unittest.TestCase):
                     "dc_data": None,
                     "string": "Discharge at 1W for 0.5 hours",
                     "events": None,
+                    "tags": None,
                 },
                 {
                     "Power input [W]": -0.2,
@@ -107,6 +113,7 @@ class TestExperiment(unittest.TestCase):
                     "dc_data": None,
                     "string": "Charge at 200mW for 45 minutes",
                     "events": None,
+                    "tags": None,
                 },
                 {
                     "Current input [A]": 0,
@@ -116,6 +123,7 @@ class TestExperiment(unittest.TestCase):
                     "dc_data": None,
                     "string": "Rest for 10 minutes",
                     "events": None,
+                    "tags": ["tag1", "tag3"],
                 },
                 {
                     "Voltage input [V]": 1,
@@ -125,6 +133,7 @@ class TestExperiment(unittest.TestCase):
                     "dc_data": None,
                     "string": "Hold at 1V for 20 seconds",
                     "events": None,
+                    "tags": None,
                 },
                 {
                     "C-rate input [-]": -1,
@@ -134,6 +143,7 @@ class TestExperiment(unittest.TestCase):
                     "dc_data": None,
                     "string": "Charge at 1 C until 4.1V",
                     "events": {"Voltage input [V]": 4.1, "type": "voltage"},
+                    "tags": None,
                 },
                 {
                     "Voltage input [V]": 4.1,
@@ -143,6 +153,7 @@ class TestExperiment(unittest.TestCase):
                     "dc_data": None,
                     "string": "Hold at 4.1 V until 50mA",
                     "events": {"Current input [A]": 0.05, "type": "current"},
+                    "tags": None,
                 },
                 {
                     "Voltage input [V]": 3,
@@ -152,6 +163,7 @@ class TestExperiment(unittest.TestCase):
                     "dc_data": None,
                     "string": "Hold at 3V until C/50",
                     "events": {"C-rate input [-]": 0.02, "type": "C-rate"},
+                    "tags": None,
                 },
                 {
                     "C-rate input [-]": 1 / 3,
@@ -161,6 +173,7 @@ class TestExperiment(unittest.TestCase):
                     "dc_data": None,
                     "string": "Discharge at C/3 for 2 hours or until 2.5 V",
                     "events": {"Voltage input [V]": 2.5, "type": "voltage"},
+                    "tags": None,
                 },
             ],
         )
@@ -180,18 +193,21 @@ class TestExperiment(unittest.TestCase):
         self.assertEqual(experiment.operating_conditions[-3]["type"], "current")
         self.assertEqual(experiment.operating_conditions[-3]["time"], time_0)
         self.assertEqual(experiment.operating_conditions[-3]["period"], period_0)
+        self.assertEqual(experiment.operating_conditions[-3]["tags"], None)
         np.testing.assert_array_equal(
             experiment.operating_conditions[-2]["dc_data"], drive_cycle_1
         )
         self.assertEqual(experiment.operating_conditions[-2]["type"], "voltage")
         self.assertEqual(experiment.operating_conditions[-2]["time"], time_1)
         self.assertEqual(experiment.operating_conditions[-2]["period"], period_1)
+        self.assertEqual(experiment.operating_conditions[-2]["tags"], None)
         np.testing.assert_array_equal(
             experiment.operating_conditions[-1]["dc_data"], drive_cycle_2
         )
         self.assertEqual(experiment.operating_conditions[-1]["type"], "power")
         self.assertEqual(experiment.operating_conditions[-1]["time"], time_2)
         self.assertEqual(experiment.operating_conditions[-1]["period"], period_2)
+        self.assertEqual(experiment.operating_conditions[-1]["tags"], None)
         self.assertEqual(experiment.period, 20)
 
     def test_read_strings_cccv_combined(self):
@@ -217,6 +233,7 @@ class TestExperiment(unittest.TestCase):
                     "dc_data": None,
                     "string": "Discharge at C/20 for 0.5 hours",
                     "events": None,
+                    "tags": None,
                 },
                 {
                     "type": "CCCV",
@@ -227,6 +244,7 @@ class TestExperiment(unittest.TestCase):
                     "dc_data": None,
                     "string": "Charge at 0.5 C until 1V then hold at 1V until C/50",
                     "events": {"C-rate input [-]": 0.02, "type": "C-rate"},
+                    "tags": None,
                 },
                 {
                     "C-rate input [-]": 0.05,
@@ -236,6 +254,7 @@ class TestExperiment(unittest.TestCase):
                     "dc_data": None,
                     "string": "Discharge at C/20 for 0.5 hours",
                     "events": None,
+                    "tags": None,
                 },
             ],
         )
@@ -259,6 +278,7 @@ class TestExperiment(unittest.TestCase):
                     "dc_data": None,
                     "string": "Charge at 0.5 C until 2V",
                     "events": {"Voltage input [V]": 2, "type": "voltage"},
+                    "tags": None,
                 },
                 {
                     "Voltage input [V]": 1,
@@ -268,6 +288,7 @@ class TestExperiment(unittest.TestCase):
                     "dc_data": None,
                     "string": "Hold at 1V until C/50",
                     "events": {"C-rate input [-]": 0.02, "type": "C-rate"},
+                    "tags": None,
                 },
             ],
         )
@@ -289,6 +310,7 @@ class TestExperiment(unittest.TestCase):
                     "dc_data": None,
                     "string": "Charge at 0.5 C for 2 minutes",
                     "events": None,
+                    "tags": None,
                 },
                 {
                     "Voltage input [V]": 1,
@@ -298,6 +320,7 @@ class TestExperiment(unittest.TestCase):
                     "dc_data": None,
                     "string": "Hold at 1V until C/50",
                     "events": {"C-rate input [-]": 0.02, "type": "C-rate"},
+                    "tags": None,
                 },
             ],
         )
@@ -321,6 +344,7 @@ class TestExperiment(unittest.TestCase):
                     "dc_data": None,
                     "string": "Discharge at C/20 for 0.5 hours",
                     "events": None,
+                    "tags": None,
                 },
                 {
                     "C-rate input [-]": -0.2,
@@ -330,6 +354,7 @@ class TestExperiment(unittest.TestCase):
                     "dc_data": None,
                     "string": "Charge at C/5 for 45 minutes",
                     "events": None,
+                    "tags": None,
                 },
                 {
                     "C-rate input [-]": 0.05,
@@ -339,6 +364,7 @@ class TestExperiment(unittest.TestCase):
                     "dc_data": None,
                     "string": "Discharge at C/20 for 0.5 hours",
                     "events": None,
+                    "tags": None,
                 },
                 {
                     "C-rate input [-]": -0.2,
@@ -348,6 +374,7 @@ class TestExperiment(unittest.TestCase):
                     "dc_data": None,
                     "string": "Charge at C/5 for 45 minutes",
                     "events": None,
+                    "tags": None,
                 },
             ],
         )
@@ -447,6 +474,31 @@ class TestExperiment(unittest.TestCase):
                 ["Discharge at 1 C for 20 seconds"], termination="1 capacity"
             )
 
+    def test_search_tag(self):
+        experiment = pybamm.Experiment(
+            [
+                ("Discharge at 1C for 0.5 hours [tag1]",),
+                "Discharge at C/20 for 0.5 hours [tag2,tag3]",
+                (
+                    "Charge at 0.5 C for 45 minutes [tag2]",
+                    "Discharge at 1 A for 0.5 hours [tag3]",
+                ),
+                "Charge at 200 mA for 45 minutes (1 minute period) [tag5]",
+                (
+                    "Discharge at 1W for 0.5 hours [tag4]",
+                    "Charge at 200mW for 45 minutes [tag4]",
+                ),
+                "Rest for 10 minutes (5 minute period) [tag1,tag3,tag4]",
+            ]
+        )
+
+        self.assertEqual(experiment.search_tag("tag1"), [0, 5])
+        self.assertEqual(experiment.search_tag("tag2"), [1, 2])
+        self.assertEqual(experiment.search_tag("tag3"), [1, 2, 5])
+        self.assertEqual(experiment.search_tag("tag4"), [4, 5])
+        self.assertEqual(experiment.search_tag("tag5"), [3])
+        self.assertEqual(experiment.search_tag("no_tag"), [])
+
 
 if __name__ == "__main__":
     print("Add -v for more debug output")