Repository: qutech/qupulse
Issue: parameter_names does not include constrained parameters in some non-atomic pulse template

The property `parameter_names` should include all parameters that are needed to instaniate a pulse template. This is currently not the case for
```python
SequencePulseTemplate
RepetitionPulseTemplate
ForLoopPulseTemplate
```

Changed files:
tests/pulses/point_pulse_template_tests.py, tests/pulses/mapping_pulse_template_tests.py, tests/pulses/loop_pulse_template_tests.py, tests/pulses/sequence_pulse_template_tests.py, tests/pulses/table_pulse_template_tests.py, tests/pulses/repetition_pulse_template_tests.py, tests/pulses/multi_channel_pulse_template_tests.py, tests/pulses/function_pulse_tests.py, qctoolkit/pulses/table_pulse_template.py, qctoolkit/pulses/sequence_pulse_template.py, qctoolkit/pulses/loop_pulse_template.py

diff --git a/qctoolkit/pulses/loop_pulse_template.py b/qctoolkit/pulses/loop_pulse_template.py
index d4d1993e..df738e22 100644
--- a/qctoolkit/pulses/loop_pulse_template.py
+++ b/qctoolkit/pulses/loop_pulse_template.py
@@ -191,7 +191,7 @@ class ForLoopPulseTemplate(LoopPulseTemplate, MeasurementDefiner, ParameterConst
     def parameter_names(self) -> Set[str]:
         parameter_names = self.body.parameter_names.copy()
         parameter_names.remove(self._loop_index)
-        return parameter_names | self._loop_range.parameter_names | self.constrained_parameters
+        return parameter_names | self._loop_range.parameter_names | self.constrained_parameters | self.measurement_parameters
 
     def _body_parameter_generator(self, parameters: Dict[str, Parameter], forward=True) -> Generator:
         loop_range_parameters = dict((parameter_name, parameters[parameter_name].get_value())
diff --git a/qctoolkit/pulses/sequence_pulse_template.py b/qctoolkit/pulses/sequence_pulse_template.py
index b61e14d4..7bb57ae8 100644
--- a/qctoolkit/pulses/sequence_pulse_template.py
+++ b/qctoolkit/pulses/sequence_pulse_template.py
@@ -87,7 +87,7 @@ class SequencePulseTemplate(PulseTemplate, ParameterConstrainer, MeasurementDefi
 
     @property
     def parameter_names(self) -> Set[str]:
-        return self.constrained_parameters.union(*(st.parameter_names for st in self.__subtemplates))
+        return self.constrained_parameters.union(self.measurement_parameters, *(st.parameter_names for st in self.__subtemplates))
 
     @property
     def subtemplates(self) -> List[MappingPulseTemplate]:
diff --git a/qctoolkit/pulses/table_pulse_template.py b/qctoolkit/pulses/table_pulse_template.py
index 3e80ac3f..edc9e9e3 100644
--- a/qctoolkit/pulses/table_pulse_template.py
+++ b/qctoolkit/pulses/table_pulse_template.py
@@ -198,7 +198,7 @@ class TablePulseTemplate(AtomicPulseTemplate, ParameterConstrainer):
 
     @property
     def parameter_names(self) -> Set[str]:
-        return self.table_parameters | self.measurement_parameters
+        return self.table_parameters | self.measurement_parameters | self.constrained_parameters
 
     @property
     def is_interruptable(self) -> bool:
diff --git a/tests/pulses/function_pulse_tests.py b/tests/pulses/function_pulse_tests.py
index e3a41300..ad68028d 100644
--- a/tests/pulses/function_pulse_tests.py
+++ b/tests/pulses/function_pulse_tests.py
@@ -64,7 +64,8 @@ class FunctionPulsePropertyTest(FunctionPulseTest):
         self.assertEqual({'A'}, self.fpt.defined_channels)
 
     def test_parameter_names(self):
-        self.assertEqual(self.fpt.parameter_names, {'a', 'b', 'c', 'd', 'x', 'z', 'j', 'u'})
+        fpt = FunctionPulseTemplate('k*r*sin(x)', '1.14*d', parameter_constraints={'x < hugo'}, measurements=[('meas', 'f', 1)])
+        self.assertEqual(fpt.parameter_names, {'k', 'r', 'x', 'd', 'hugo', 'f'})
 
     def test_duration(self):
         self.assertEqual(self.fpt.duration, self.s2)
diff --git a/tests/pulses/loop_pulse_template_tests.py b/tests/pulses/loop_pulse_template_tests.py
index bd13879f..b5d6fa00 100644
--- a/tests/pulses/loop_pulse_template_tests.py
+++ b/tests/pulses/loop_pulse_template_tests.py
@@ -145,9 +145,9 @@ class ForLoopPulseTemplateTest(unittest.TestCase):
 
     def test_parameter_names(self):
         dt = DummyPulseTemplate(parameter_names={'i', 'k'})
-        flt = ForLoopPulseTemplate(body=dt, loop_index='i', loop_range=('a', 'b', 'c'))
-
-        self.assertEqual(flt.parameter_names, {'k', 'a', 'b', 'c'})
+        flt = ForLoopPulseTemplate(body=dt, loop_index='i', loop_range=('a', 'b', 'c'),
+                                   parameter_constraints={'c > hugo'}, measurements=[('meas', 'd', 1)])
+        self.assertEqual({'k', 'a', 'b', 'c', 'd', 'hugo'}, flt.parameter_names)
 
     def test_parameter_names_param_only_in_constraint(self) -> None:
         flt = ForLoopPulseTemplate(body=DummyPulseTemplate(parameter_names={'k', 'i'}), loop_index='i',
diff --git a/tests/pulses/mapping_pulse_template_tests.py b/tests/pulses/mapping_pulse_template_tests.py
index 0b3a94c9..88f10a4b 100644
--- a/tests/pulses/mapping_pulse_template_tests.py
+++ b/tests/pulses/mapping_pulse_template_tests.py
@@ -149,6 +149,12 @@ class MappingTemplateTests(unittest.TestCase):
         self.assertIs(st_4.template, st_3)
         self.assertEqual(st_4.parameter_mapping, {'t': 't', 'k': 'k', 'bar': 't*l'})
 
+    def test_parameter_names(self) -> None:
+        template = DummyPulseTemplate(parameter_names={'foo'}, measurement_names={'meas1'})
+        mt = MappingPulseTemplate(template, parameter_mapping={'foo': 't*k'}, parameter_constraints={'t >= m'},
+                                  measurement_mapping={'meas1': 'meas2'})
+        self.assertEqual({'t', 'k', 'm'}, mt.parameter_names)
+
     def test_get_updated_channel_mapping(self):
         template = DummyPulseTemplate(defined_channels={'foo', 'bar'})
         st = MappingPulseTemplate(template, channel_mapping={'bar': 'kneipe'})
diff --git a/tests/pulses/multi_channel_pulse_template_tests.py b/tests/pulses/multi_channel_pulse_template_tests.py
index 45e7620b..63d49c8c 100644
--- a/tests/pulses/multi_channel_pulse_template_tests.py
+++ b/tests/pulses/multi_channel_pulse_template_tests.py
@@ -83,6 +83,11 @@ class AtomicMultiChannelPulseTemplateTest(unittest.TestCase):
 
         self.assertEqual(template.duration, 't1')
 
+    def test_parameter_names(self) -> None:
+        template = AtomicMultiChannelPulseTemplate(*zip(self.subtemplates, self.param_maps, self.chan_maps),
+                                                   parameter_constraints={'pp1 > hugo'}, measurements={('meas', 'd', 1)})
+        self.assertEqual({'pp1', 'pp2', 'pp3', 'hugo', 'd'}, template.parameter_names)
+
     def test_mapping_template_pure_conversion(self):
         template = AtomicMultiChannelPulseTemplate(*zip(self.subtemplates, self.param_maps, self.chan_maps))
 
diff --git a/tests/pulses/point_pulse_template_tests.py b/tests/pulses/point_pulse_template_tests.py
index 73da7723..7200889c 100644
--- a/tests/pulses/point_pulse_template_tests.py
+++ b/tests/pulses/point_pulse_template_tests.py
@@ -68,11 +68,11 @@ class PointPulseTemplateTests(unittest.TestCase):
                          {'A', 'B', 't', 'C'})
 
     def test_parameter_names(self):
-        self.assertEqual(PointPulseTemplate([(1, 'A'), ('t+6', 'B+C')],
+        self.assertEqual({'a', 'b', 'n', 'A', 'B', 't', 'C'},
+                         PointPulseTemplate([(1, 'A'), ('t+6', 'B+C')],
                                             [0, 'asd'],
                                             measurements=[('M', 'n', 1)],
-                                            parameter_constraints=['a < b']).parameter_names,
-                         {'a', 'b', 'n', 'A', 'B', 't', 'C'})
+                                            parameter_constraints=['a < b']).parameter_names)
 
     def test_integral(self) -> None:
         pulse = PointPulseTemplate(
diff --git a/tests/pulses/repetition_pulse_template_tests.py b/tests/pulses/repetition_pulse_template_tests.py
index e5819da9..5e73e37a 100644
--- a/tests/pulses/repetition_pulse_template_tests.py
+++ b/tests/pulses/repetition_pulse_template_tests.py
@@ -48,6 +48,12 @@ class RepetitionPulseTemplateTest(unittest.TestCase):
         body.parameter_names_ = {'foo', 't', 'bar'}
         self.assertEqual(body.parameter_names, t.parameter_names)
 
+    def test_parameter_names(self) -> None:
+        body = DummyPulseTemplate(parameter_names={'foo', 'bar'})
+        t = RepetitionPulseTemplate(body, 5, parameter_constraints={'foo > hugo'}, measurements=[('meas', 'd', 0)])
+
+        self.assertEqual({'foo', 'bar', 'hugo', 'd'}, t.parameter_names)
+
     @unittest.skip('is interruptable not implemented for loops')
     def test_is_interruptable(self) -> None:
         body = DummyPulseTemplate(is_interruptable=False)
diff --git a/tests/pulses/sequence_pulse_template_tests.py b/tests/pulses/sequence_pulse_template_tests.py
index 37c58b37..503dbe11 100644
--- a/tests/pulses/sequence_pulse_template_tests.py
+++ b/tests/pulses/sequence_pulse_template_tests.py
@@ -61,9 +61,10 @@ class SequencePulseTemplateTest(unittest.TestCase):
                                    DummyPulseTemplate(duration='b'))
         self.assertEqual(pt.duration, Expression('a+a+b'))
 
-    def test_parameter_names_param_only_in_constraint(self) -> None:
-        pt = SequencePulseTemplate(DummyPulseTemplate(parameter_names={'a'}), DummyPulseTemplate(parameter_names={'b'}), parameter_constraints=['a==b', 'a<c'])
-        self.assertEqual(pt.parameter_names, {'a','b','c'})
+    def test_parameter_names(self) -> None:
+        pt = SequencePulseTemplate(DummyPulseTemplate(parameter_names={'a'}), DummyPulseTemplate(parameter_names={'b'}),
+                                   parameter_constraints=['a==b', 'a<c'], measurements=[('meas', 'd', 1)])
+        self.assertEqual({'a', 'b', 'c', 'd'}, pt.parameter_names, )
 
     def test_build_waveform(self):
         wfs = [DummyWaveform(), DummyWaveform()]
diff --git a/tests/pulses/table_pulse_template_tests.py b/tests/pulses/table_pulse_template_tests.py
index e153697f..95bccfc7 100644
--- a/tests/pulses/table_pulse_template_tests.py
+++ b/tests/pulses/table_pulse_template_tests.py
@@ -111,6 +111,10 @@ class TablePulseTemplateTest(unittest.TestCase):
                                     ('b', 1),
                                     ('c*a', 'k')]}, parameter_constraints=['a*c < b'])
 
+    def test_parameter_names(self) -> None:
+        table = TablePulseTemplate({'a': [('foo', 'bar')]}, parameter_constraints=['foo < hugo'], measurements=[('meas', 'd', 2)])
+        self.assertEqual({'foo', 'bar', 'hugo', 'd'}, table.parameter_names)
+
     def test_time_is_0_on_construction(self) -> None:
         with self.assertWarns(ZeroDurationTablePulseTemplate):
             warnings.simplefilter('default', ZeroDurationTablePulseTemplate)