From 2b5fd73a5c452922ff2893115bddbadbba8ce4fe Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 17 Jul 2018 10:41:15 +0200 Subject: [PATCH 01/14] First draft of transformation and transforming waveform --- qctoolkit/_program/transformation.py | 55 ++++++++++++++++++++++++++ qctoolkit/_program/waveforms.py | 59 +++++++++++++++++++++++++++- 2 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 qctoolkit/_program/transformation.py diff --git a/qctoolkit/_program/transformation.py b/qctoolkit/_program/transformation.py new file mode 100644 index 000000000..a821429a4 --- /dev/null +++ b/qctoolkit/_program/transformation.py @@ -0,0 +1,55 @@ +from typing import Mapping, Set +from abc import abstractmethod + +import numpy as np +import pandas as pd + +from qctoolkit import ChannelID +from qctoolkit.comparable import Comparable + + +class Transformation(Comparable): + """Transforms numeric time-voltage values for multiple channels to other time-voltage values. The number and names + of input and output channels might differ.""" + + @abstractmethod + def __call__(self, time: np.ndarray, data: pd.DataFrame) -> pd.DataFrame: + """Apply transformation to data + Args: + time: + data: + + Returns: + + """ + + @abstractmethod + def get_output_channels(self, input_channels: Set[ChannelID]) -> Set[ChannelID]: + """Return the channel identifiers""" + + +class LinearTransformation(Transformation): + def __init__(self, transformation_matrix: pd.DataFrame): + """ + + Args: + transformation_matrix: columns are input and index are output channels + """ + self._matrix = transformation_matrix + + def __call__(self, time: np.ndarray, data: pd.DataFrame) -> Mapping[ChannelID, np.ndarray]: + data_in = pd.DataFrame(data) + if data_in.index != self._matrix.columns: + raise KeyError() + + return self._matrix @ data_in + + def get_output_channels(self, input_channels: Set[ChannelID]): + if input_channels != set(self._matrix.columns): + raise KeyError() + + return set(self._matrix.index) + + @property + def compare_key(self): + return self._matrix.to_dict() diff --git a/qctoolkit/_program/waveforms.py b/qctoolkit/_program/waveforms.py index 6c92768bd..5bea3e41d 100644 --- a/qctoolkit/_program/waveforms.py +++ b/qctoolkit/_program/waveforms.py @@ -6,10 +6,11 @@ import itertools from abc import ABCMeta, abstractmethod -from weakref import WeakValueDictionary +from weakref import WeakValueDictionary, ref from typing import Union, Set, Sequence, NamedTuple, Tuple, Any, List, Iterable import numpy as np +import pandas as pd from qctoolkit import ChannelID from qctoolkit.utils import checked_int_cast @@ -17,6 +18,7 @@ from qctoolkit.comparable import Comparable from qctoolkit.expressions import ExpressionScalar from qctoolkit.pulses.interpolation import InterpolationStrategy +from qctoolkit._program.transformation import Transformation __all__ = ["Waveform", "TableWaveform", "TableWaveformEntry", "FunctionWaveform", "SequenceWaveform", @@ -481,3 +483,58 @@ def duration(self) -> TimeType: def unsafe_get_subset_for_channels(self, channels: Set[ChannelID]) -> 'RepetitionWaveform': return RepetitionWaveform(body=self._body.unsafe_get_subset_for_channels(channels), repetition_count=self._repetition_count) + + + +class TransformingWaveform(Waveform): + def __init__(self, inner_waveform: Waveform, transformation: Transformation): + self._inner_waveform = inner_waveform + self._transformation = transformation + + self._cached_data = None + self._cached_times = lambda : None + + @property + def inner_waveform(self) -> Waveform: + return self._inner_waveform + + @property + def transformation(self) -> Transformation: + return self._transformation + + @property + def defined_channels(self) -> Set[ChannelID]: + return self.transformation.get_output_channels(self.inner_waveform.defined_channels) + + @property + def compare_key(self) -> Tuple[Any, int]: + return self.inner_waveform.compare_key, self.transformation.compare_key + + @property + def duration(self) -> TimeType: + return self.inner_waveform.duration + + def unsafe_get_subset_for_channels(self, channels: Set[ChannelID]) -> 'TransformingWaveform': + return TransformingWaveform(self.inner_waveform.get_subset_for_channels(channels), self.transformation) + + def unsafe_sample(self, + channel: ChannelID, + sample_times: np.ndarray, + output_array: Union[np.ndarray, None] = None) -> np.ndarray: + + if self._cached_times() is not sample_times: + inner_channels = tuple(self.inner_waveform.defined_channels) + inner_data = np.empty((len(self.inner_waveform.defined_channels), sample_times.size)) + + for idx, inner_channel in enumerate(self.inner_waveform.defined_channels): + self.inner_waveform.unsafe_sample(inner_channel, sample_times, + output_array=inner_data[idx, :]) + + inner_data = pd.DataFrame(inner_data, index=inner_channels) + + outer_data = self.transformation(sample_times, inner_data) + + self._cached_data = outer_data + self._cached_times = ref(sample_times) + + return self._cached_data.loc[channel].values From e1e3dfac08ad66640a6e430161ce5447a59e22a2 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 17 Jul 2018 14:33:21 +0200 Subject: [PATCH 02/14] Test for linear transformation --- qctoolkit/_program/transformation.py | 8 ++--- tests/_program/transformation_tests.py | 46 ++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 tests/_program/transformation_tests.py diff --git a/qctoolkit/_program/transformation.py b/qctoolkit/_program/transformation.py index a821429a4..c537983c7 100644 --- a/qctoolkit/_program/transformation.py +++ b/qctoolkit/_program/transformation.py @@ -20,7 +20,7 @@ def __call__(self, time: np.ndarray, data: pd.DataFrame) -> pd.DataFrame: data: Returns: - + transformed: A DataFrame that has been transformed with index == output_channels """ @abstractmethod @@ -39,14 +39,14 @@ def __init__(self, transformation_matrix: pd.DataFrame): def __call__(self, time: np.ndarray, data: pd.DataFrame) -> Mapping[ChannelID, np.ndarray]: data_in = pd.DataFrame(data) - if data_in.index != self._matrix.columns: - raise KeyError() + if set(data_in.index) != set(self._matrix.columns): + raise KeyError('Invalid input channels', set(data_in.index), set(self._matrix.columns)) return self._matrix @ data_in def get_output_channels(self, input_channels: Set[ChannelID]): if input_channels != set(self._matrix.columns): - raise KeyError() + raise KeyError('Invalid input channels', input_channels, set(self._matrix.columns)) return set(self._matrix.index) diff --git a/tests/_program/transformation_tests.py b/tests/_program/transformation_tests.py new file mode 100644 index 000000000..bfb19b1a9 --- /dev/null +++ b/tests/_program/transformation_tests.py @@ -0,0 +1,46 @@ +import unittest + +import pandas as pd +import numpy as np + +from qctoolkit._program.transformation import LinearTransformation + + + +class LinearTransformationTests(unittest.TestCase): + def test_compare_key(self): + trafo_dict = {'transformed_a': {'a': 1, 'b': -1, 'c': 0}, 'transformed_b': {'a': 1, 'b': 1, 'c': 1}} + trafo_matrix = pd.DataFrame(trafo_dict).T + trafo = LinearTransformation(trafo_matrix) + + self.assertEqual(trafo_matrix.to_dict(), trafo.compare_key) + + def test_get_output_channels(self): + trafo_dict = {'transformed_a': {'a': 1, 'b': -1, 'c': 0}, 'transformed_b': {'a': 1, 'b': 1, 'c': 1}} + trafo_matrix = pd.DataFrame(trafo_dict).T + trafo = LinearTransformation(trafo_matrix) + + self.assertEqual(trafo.get_output_channels({'a', 'b', 'c'}), {'transformed_a', 'transformed_b'}) + with self.assertRaisesRegex(KeyError, 'Invalid input channels'): + trafo.get_output_channels({'a', 'b'}) + + def test_call(self): + trafo_dict = {'transformed_a': {'a': 1., 'b': -1., 'c': 0.}, 'transformed_b': {'a': 1., 'b': 1., 'c': 1.}} + trafo_matrix = pd.DataFrame(trafo_dict).T + trafo = LinearTransformation(trafo_matrix) + + data = (np.arange(12.) + 1).reshape((3, 4)) + data = pd.DataFrame(data, index=list('abc')) + + transformed = trafo(np.full(4, np.NaN), data) + + expected = np.empty((2, 4)) + expected[0, :] = data.loc['a'] - data.loc['b'] + expected[1, :] = np.sum(data.values, axis=0) + + expected = pd.DataFrame(expected, index=['transformed_a', 'transformed_b']) + + pd.testing.assert_frame_equal(expected, transformed) + + with self.assertRaisesRegex(KeyError, 'Invalid input channels'): + trafo(np.full(4, np.NaN), data.loc[['a', 'b']]) From c4ec2a2ef3affea05bf3d05107554bb583f21ba1 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Wed, 18 Jul 2018 11:16:24 +0200 Subject: [PATCH 03/14] Correct implementation of channel subsets with a new class + tests for the waveforms --- qctoolkit/_program/waveforms.py | 60 ++++++++++--- tests/_program/waveforms_tests.py | 130 ++++++++++++++++++++++++++++- tests/pulses/sequencing_dummies.py | 9 +- 3 files changed, 182 insertions(+), 17 deletions(-) diff --git a/qctoolkit/_program/waveforms.py b/qctoolkit/_program/waveforms.py index 5bea3e41d..112c597e0 100644 --- a/qctoolkit/_program/waveforms.py +++ b/qctoolkit/_program/waveforms.py @@ -7,7 +7,7 @@ import itertools from abc import ABCMeta, abstractmethod from weakref import WeakValueDictionary, ref -from typing import Union, Set, Sequence, NamedTuple, Tuple, Any, List, Iterable +from typing import Union, Set, Sequence, NamedTuple, Tuple, Any, Iterable, FrozenSet, Optional import numpy as np import pandas as pd @@ -22,7 +22,7 @@ __all__ = ["Waveform", "TableWaveform", "TableWaveformEntry", "FunctionWaveform", "SequenceWaveform", - "MultiChannelWaveform", "RepetitionWaveform"] + "MultiChannelWaveform", "RepetitionWaveform", "TransformingWaveform"] class Waveform(Comparable, metaclass=ABCMeta): @@ -485,14 +485,15 @@ def unsafe_get_subset_for_channels(self, channels: Set[ChannelID]) -> 'Repetitio repetition_count=self._repetition_count) - class TransformingWaveform(Waveform): def __init__(self, inner_waveform: Waveform, transformation: Transformation): + """""" self._inner_waveform = inner_waveform self._transformation = transformation + # cache data of inner channels based identified and invalidated by the sample times self._cached_data = None - self._cached_times = lambda : None + self._cached_times = lambda: None @property def inner_waveform(self) -> Waveform: @@ -507,26 +508,25 @@ def defined_channels(self) -> Set[ChannelID]: return self.transformation.get_output_channels(self.inner_waveform.defined_channels) @property - def compare_key(self) -> Tuple[Any, int]: - return self.inner_waveform.compare_key, self.transformation.compare_key + def compare_key(self) -> Tuple[Waveform, Transformation]: + return self.inner_waveform, self.transformation @property def duration(self) -> TimeType: return self.inner_waveform.duration - def unsafe_get_subset_for_channels(self, channels: Set[ChannelID]) -> 'TransformingWaveform': - return TransformingWaveform(self.inner_waveform.get_subset_for_channels(channels), self.transformation) + def unsafe_get_subset_for_channels(self, channels: Set[ChannelID]) -> 'SubsetWaveform': + return SubsetWaveform(self, channel_subset=channels) def unsafe_sample(self, channel: ChannelID, sample_times: np.ndarray, output_array: Union[np.ndarray, None] = None) -> np.ndarray: - if self._cached_times() is not sample_times: inner_channels = tuple(self.inner_waveform.defined_channels) - inner_data = np.empty((len(self.inner_waveform.defined_channels), sample_times.size)) + inner_data = np.empty((len(inner_channels), sample_times.size)) - for idx, inner_channel in enumerate(self.inner_waveform.defined_channels): + for idx, inner_channel in enumerate(inner_channels): self.inner_waveform.unsafe_sample(inner_channel, sample_times, output_array=inner_data[idx, :]) @@ -537,4 +537,40 @@ def unsafe_sample(self, self._cached_data = outer_data self._cached_times = ref(sample_times) - return self._cached_data.loc[channel].values + if output_array is None: + output_array = self._cached_data.loc[channel].values + else: + output_array[:] = self._cached_data.loc[channel].values + + return output_array + + +class SubsetWaveform(Waveform): + def __init__(self, inner_waveform: Waveform, channel_subset: Set[ChannelID]): + self._inner_waveform = inner_waveform + self._channel_subset = frozenset(channel_subset) + + @property + def inner_waveform(self) -> Waveform: + return self._inner_waveform + + @property + def defined_channels(self) -> FrozenSet[ChannelID]: + return self._channel_subset + + @property + def duration(self) -> TimeType: + return self.inner_waveform.duration + + @property + def compare_key(self) -> Tuple[frozenset, Waveform]: + return self.defined_channels, self.inner_waveform + + def unsafe_get_subset_for_channels(self, channels: Set[ChannelID]) -> Waveform: + return self.inner_waveform.get_subset_for_channels(channels) + + def unsafe_sample(self, + channel: ChannelID, + sample_times: np.ndarray, + output_array: Union[np.ndarray, None]=None) -> np.ndarray: + return self.inner_waveform.unsafe_sample(channel, sample_times, output_array) diff --git a/tests/_program/waveforms_tests.py b/tests/_program/waveforms_tests.py index 8427a5721..4ead7898e 100644 --- a/tests/_program/waveforms_tests.py +++ b/tests/_program/waveforms_tests.py @@ -1,13 +1,16 @@ import unittest +from unittest import mock import numpy import numpy as np +import pandas as pd from qctoolkit.utils.types import time_from_float from qctoolkit.pulses.interpolation import HoldInterpolationStrategy, LinearInterpolationStrategy,\ JumpInterpolationStrategy from qctoolkit._program.waveforms import MultiChannelWaveform, RepetitionWaveform, SequenceWaveform,\ - TableWaveformEntry, TableWaveform + TableWaveformEntry, TableWaveform, TransformingWaveform, SubsetWaveform +from qctoolkit._program.transformation import Transformation from tests.pulses.sequencing_dummies import DummyWaveform, DummyInterpolationStrategy @@ -413,4 +416,127 @@ def test_simple_properties(self): class WaveformEntryTest(unittest.TestCase): def test_interpolation_exception(self): with self.assertRaises(TypeError): - TableWaveformEntry(1, 2, 3) \ No newline at end of file + TableWaveformEntry(1, 2, 3) + + +class TransformationDummy(Transformation): + def __init__(self, output_channels=None, transformed=None): + if output_channels: + self.get_output_channels = mock.MagicMock(return_value=output_channels) + + if transformed is not None: + type(self).__call__ = mock.MagicMock(return_value=transformed) + + def __call__(self, *args, **kwargs): + raise NotImplementedError() + + get_output_channels = () + + @property + def compare_key(self): + return id(self) + + +class TransformingWaveformTest(unittest.TestCase): + def test_simple_properties(self): + output_channels = {'c', 'd', 'e'} + + trafo = TransformationDummy(output_channels=output_channels) + + inner_wf = DummyWaveform(duration=1.5, defined_channels={'a', 'b'}) + trafo_wf = TransformingWaveform(inner_waveform=inner_wf, transformation=trafo) + + self.assertIs(trafo_wf.inner_waveform, inner_wf) + self.assertIs(trafo_wf.transformation, trafo) + self.assertEqual(trafo_wf.compare_key, (inner_wf, trafo)) + self.assertIs(trafo_wf.duration, inner_wf.duration) + self.assertIs(trafo_wf.defined_channels, output_channels) + trafo.get_output_channels.assert_called_once_with(inner_wf.defined_channels) + + def test_get_subset_for_channels(self): + output_channels = {'c', 'd', 'e'} + + trafo = TransformationDummy(output_channels=output_channels) + + inner_wf = DummyWaveform(duration=1.5, defined_channels={'a', 'b'}) + trafo_wf = TransformingWaveform(inner_waveform=inner_wf, transformation=trafo) + + subset_wf = trafo_wf.get_subset_for_channels({'c', 'd'}) + self.assertIsInstance(subset_wf, SubsetWaveform) + self.assertIs(subset_wf.inner_waveform, trafo_wf) + self.assertEqual(subset_wf.defined_channels, {'c', 'd'}) + + def test_unsafe_sample(self): + time = np.linspace(10, 20, num=25) + ch_a = np.exp(time) + ch_b = np.exp(-time) + ch_c = np.sinh(time) + ch_d = np.cosh(time) + ch_e = np.arctan(time) + + sample_output = {'a': ch_a, 'b': ch_b} + expected_call_data = pd.DataFrame(sample_output).T + + transformed = pd.DataFrame({'c': ch_c, 'd': ch_d, 'e': ch_e}).T + + trafo = TransformationDummy(transformed=transformed) + inner_wf = DummyWaveform(duration=1.5, defined_channels={'a', 'b'}, sample_output=sample_output) + trafo_wf = TransformingWaveform(inner_waveform=inner_wf, transformation=trafo) + + ch_d_out = trafo_wf.unsafe_sample('d', time) + np.testing.assert_equal(ch_d_out, ch_d) + + output = np.empty_like(time) + ch_d_out = trafo_wf.unsafe_sample('d', time, output_array=output) + self.assertIs(output, ch_d_out) + np.testing.assert_equal(ch_d_out, ch_d) + + call_list = TransformationDummy.__call__.call_args_list + self.assertEqual(len(call_list), 1) + + (pos_args, kw_args), = call_list + self.assertEqual(len(kw_args), 0) + + c_time, c_data = pos_args + np.testing.assert_equal(time, c_time) + pd.testing.assert_frame_equal(expected_call_data.sort_index(), c_data.sort_index()) + + +class SubsetWaveformTest(unittest.TestCase): + def test_simple_properties(self): + inner_wf = DummyWaveform(duration=1.5, defined_channels={'a', 'b', 'c'}) + + subset_wf = SubsetWaveform(inner_wf, {'a', 'c'}) + + self.assertIs(subset_wf.inner_waveform, inner_wf) + self.assertEqual(subset_wf.compare_key, (frozenset(['a', 'c']), inner_wf)) + self.assertIs(subset_wf.duration, inner_wf.duration) + self.assertEqual(subset_wf.defined_channels, {'a', 'c'}) + + def test_get_subset_for_channels(self): + subsetted = DummyWaveform(defined_channels={'a'}) + with mock.patch.object(DummyWaveform, + 'get_subset_for_channels', + mock.Mock(return_value=subsetted)) as get_subset_for_channels: + inner_wf = DummyWaveform(defined_channels={'a', 'b', 'c'}) + subset_wf = SubsetWaveform(inner_wf, {'a', 'c'}) + + actual_subsetted = subset_wf.get_subset_for_channels({'a'}) + get_subset_for_channels.assert_called_once_with({'a'}) + self.assertIs(subsetted, actual_subsetted) + + def test_unsafe_sample(self): + """Test perfect forwarding""" + time = {'time'} + output = {'output'} + expected_data = {'data'} + + with mock.patch.object(DummyWaveform, + 'unsafe_sample', + mock.Mock(return_value=expected_data)) as unsafe_sample: + inner_wf = DummyWaveform(defined_channels={'a', 'b', 'c'}) + subset_wf = SubsetWaveform(inner_wf, {'a', 'c'}) + + actual_data = subset_wf.unsafe_sample('g', time, output) + self.assertIs(expected_data, actual_data) + unsafe_sample.assert_called_once_with('g', time, output) diff --git a/tests/pulses/sequencing_dummies.py b/tests/pulses/sequencing_dummies.py index 9ee054ca4..7f5dd3555 100644 --- a/tests/pulses/sequencing_dummies.py +++ b/tests/pulses/sequencing_dummies.py @@ -1,5 +1,5 @@ """STANDARD LIBRARY IMPORTS""" -from typing import Tuple, List, Dict, Optional, Set, Any +from typing import Tuple, List, Dict, Optional, Set, Any, Union import copy import numpy @@ -136,7 +136,7 @@ def add_instruction(self, instruction: Instruction) -> None: class DummyWaveform(Waveform): - def __init__(self, duration: float=0, sample_output: numpy.ndarray=None, defined_channels={'A'}) -> None: + def __init__(self, duration: float=0, sample_output: Union[numpy.ndarray, dict]=None, defined_channels={'A'}) -> None: super().__init__() self.duration_ = time_from_float(duration) self.sample_output = sample_output @@ -166,7 +166,10 @@ def unsafe_sample(self, if output_array is None: output_array = numpy.empty_like(sample_times) if self.sample_output is not None: - output_array[:] = self.sample_output + if isinstance(self.sample_output, dict): + output_array[:] = self.sample_output[channel] + else: + output_array[:] = self.sample_output else: output_array[:] = sample_times return output_array From f166229188bb55d317a8d36aa760b41d270fd223 Mon Sep 17 00:00:00 2001 From: Lukas Prediger Date: Thu, 12 Jul 2018 15:46:54 +0200 Subject: [PATCH 04/14] Refactoring: Organized tests for PulseTemplates consistently. Signed-off-by: Lukas Prediger --- tests/pulses/function_pulse_tests.py | 8 ++-- tests/pulses/loop_pulse_template_tests.py | 25 +++++++------ .../multi_channel_pulse_template_tests.py | 25 +++++++------ tests/pulses/point_pulse_template_tests.py | 37 ++++++++++--------- .../pulse_template_parameter_mapping_tests.py | 23 ++++++------ tests/pulses/table_pulse_template_tests.py | 26 ++++++------- 6 files changed, 77 insertions(+), 67 deletions(-) diff --git a/tests/pulses/function_pulse_tests.py b/tests/pulses/function_pulse_tests.py index 7656259ba..e3a413001 100644 --- a/tests/pulses/function_pulse_tests.py +++ b/tests/pulses/function_pulse_tests.py @@ -82,6 +82,10 @@ def test_parameter_names_and_declarations_string_input(self) -> None: expected_parameter_names = {'foo', 'bar', 'hugo'} self.assertEqual(expected_parameter_names, template.parameter_names) + def test_integral(self) -> None: + pulse = FunctionPulseTemplate('sin(0.5*t+b)', '2*Tmax') + self.assertEqual({'default': Expression('2.0*cos(b) - 2.0*cos(1.0*Tmax+b)')}, pulse.integral) + class FunctionPulseSerializationTest(SerializableTests, unittest.TestCase): @@ -192,10 +196,6 @@ def test_requires_stop(self) -> None: def test_build_waveform_none(self): self.assertIsNone(self.fpt.build_waveform(self.valid_par_vals, channel_mapping={'A': None})) - def test_integral(self) -> None: - pulse = FunctionPulseTemplate('sin(0.5*t+b)', '2*Tmax') - self.assertEqual({'default': Expression('2.0*cos(b) - 2.0*cos(1.0*Tmax+b)')}, pulse.integral) - class TablePulseTemplateConstraintTest(ParameterConstrainerTest): def __init__(self, *args, **kwargs): diff --git a/tests/pulses/loop_pulse_template_tests.py b/tests/pulses/loop_pulse_template_tests.py index d349ab8d1..bd13879fb 100644 --- a/tests/pulses/loop_pulse_template_tests.py +++ b/tests/pulses/loop_pulse_template_tests.py @@ -154,6 +154,20 @@ def test_parameter_names_param_only_in_constraint(self) -> None: loop_range=('a', 'b', 'c',), parameter_constraints=['k<=f']) self.assertEqual(flt.parameter_names, {'k', 'a', 'b', 'c', 'f'}) + def test_integral(self) -> None: + dummy = DummyPulseTemplate(defined_channels={'A', 'B'}, + parameter_names={'t1', 'i'}, + integrals={'A': ExpressionScalar('t1-i*3.1'), 'B': ExpressionScalar('i')}) + + pulse = ForLoopPulseTemplate(dummy, 'i', (1, 8, 2)) + + expected = {'A': ExpressionScalar('Sum(t1-3.1*(1+2*i), (i, 0, 3))'), + 'B': ExpressionScalar('Sum((1+2*i), (i, 0, 3))') } + self.assertEqual(expected, pulse.integral) + + +class ForLoopTemplateSequencingTests(unittest.TestCase): + def test_build_sequence_constraint_on_loop_var_exception(self): """This test is to assure the status-quo behavior of ForLoopPT handling parameter constraints affecting the loop index variable. Please see https://github.com/qutech/qc-toolkit/issues/232 .""" @@ -212,17 +226,6 @@ def test_requires_stop(self): parameters['A'] = DummyParameter(requires_stop=True) self.assertTrue(flt.requires_stop(parameters, dict())) - def test_integral(self) -> None: - dummy = DummyPulseTemplate(defined_channels={'A', 'B'}, - parameter_names={'t1', 'i'}, - integrals={'A': ExpressionScalar('t1-i*3.1'), 'B': ExpressionScalar('i')}) - - pulse = ForLoopPulseTemplate(dummy, 'i', (1, 8, 2)) - - expected = {'A': ExpressionScalar('Sum(t1-3.1*(1+2*i), (i, 0, 3))'), - 'B': ExpressionScalar('Sum((1+2*i), (i, 0, 3))') } - self.assertEqual(expected, pulse.integral) - class ForLoopPulseTemplateSerializationTests(SerializableTests, unittest.TestCase): diff --git a/tests/pulses/multi_channel_pulse_template_tests.py b/tests/pulses/multi_channel_pulse_template_tests.py index c860e1caf..a17d5e812 100644 --- a/tests/pulses/multi_channel_pulse_template_tests.py +++ b/tests/pulses/multi_channel_pulse_template_tests.py @@ -120,6 +120,20 @@ def test_measurement_names(self): self.assertEqual(AtomicMultiChannelPulseTemplate(*sts).measurement_names, {'A', 'B', 'C'}) + def test_integral(self) -> None: + sts = [DummyPulseTemplate(duration='t1', defined_channels={'A'}, + integrals={'A': ExpressionScalar('2+k')}), + DummyPulseTemplate(duration='t1', defined_channels={'B', 'C'}, + integrals={'B': ExpressionScalar('t1-t0*3.1'), 'C': ExpressionScalar('l')})] + pulse = AtomicMultiChannelPulseTemplate(*sts) + self.assertEqual({'A': ExpressionScalar('2+k'), + 'B': ExpressionScalar('t1-t0*3.1'), + 'C': ExpressionScalar('l')}, + pulse.integral) + + +class MultiChannelPulseTemplateSequencingTests(unittest.TestCase): + def test_requires_stop(self): sts = [DummyPulseTemplate(duration='t1', defined_channels={'A'}, parameter_names={'a', 'b'}, requires_stop=False), DummyPulseTemplate(duration='t1', defined_channels={'B'}, parameter_names={'a', 'c'}, requires_stop=False)] @@ -184,17 +198,6 @@ def test_build_waveform_none(self): wf = pt.build_waveform(parameters, channel_mapping=channel_mapping) self.assertIsNone(wf) - def test_integral(self) -> None: - sts = [DummyPulseTemplate(duration='t1', defined_channels={'A'}, - integrals={'A': ExpressionScalar('2+k')}), - DummyPulseTemplate(duration='t1', defined_channels={'B', 'C'}, - integrals={'B': ExpressionScalar('t1-t0*3.1'), 'C': ExpressionScalar('l')})] - pulse = AtomicMultiChannelPulseTemplate(*sts) - self.assertEqual({'A': ExpressionScalar('2+k'), - 'B': ExpressionScalar('t1-t0*3.1'), - 'C': ExpressionScalar('l')}, - pulse.integral) - class AtomicMultiChannelPulseTemplateSerializationTests(SerializableTests, unittest.TestCase): diff --git a/tests/pulses/point_pulse_template_tests.py b/tests/pulses/point_pulse_template_tests.py index d84192d53..73da77231 100644 --- a/tests/pulses/point_pulse_template_tests.py +++ b/tests/pulses/point_pulse_template_tests.py @@ -74,6 +74,26 @@ def test_parameter_names(self): parameter_constraints=['a < b']).parameter_names, {'a', 'b', 'n', 'A', 'B', 't', 'C'}) + def test_integral(self) -> None: + pulse = PointPulseTemplate( + [(1, (2, 'b'), 'linear'), (3, (0, 0), 'jump'), (4, (2, 'c'), 'hold'), (5, (8, 'd'), 'hold')], + [0, 'other_channel'] + ) + self.assertEqual({0: ExpressionScalar(6), + 'other_channel': ExpressionScalar('1.0*b + 2.0*c')}, + pulse.integral) + + pulse = PointPulseTemplate( + [(1, ('2', 'b'), 'linear'), ('t0', (0, 0), 'jump'), (4, (2, 'c'), 'hold'), ('g', (8, 'd'), 'hold')], + ['symbolic', 1] + ) + self.assertEqual({'symbolic': ExpressionScalar('2.0*g - t0 - 1.0'), + 1: ExpressionScalar('b*(0.5*t0 - 0.5) + c*(g - 4.0) + c*(-t0 + 4.0)')}, + pulse.integral) + + +class PointPulseTemplateSequencingTests(unittest.TestCase): + def test_requires_stop_missing_param(self) -> None: table = PointPulseTemplate([('foo', 'v')], [0]) with self.assertRaises(ParameterNotProvidedException): @@ -182,23 +202,6 @@ def test_build_waveform_none_channel(self): self.assertIsInstance(wf, MultiChannelWaveform) self.assertEqual(wf.defined_channels, {1, 2}) - def test_integral(self) -> None: - pulse = PointPulseTemplate( - [(1, (2, 'b'), 'linear'), (3, (0, 0), 'jump'), (4, (2, 'c'), 'hold'), (5, (8, 'd'), 'hold')], - [0, 'other_channel'] - ) - self.assertEqual({0: ExpressionScalar(6), - 'other_channel': ExpressionScalar('1.0*b + 2.0*c')}, - pulse.integral) - - pulse = PointPulseTemplate( - [(1, ('2', 'b'), 'linear'), ('t0', (0, 0), 'jump'), (4, (2, 'c'), 'hold'), ('g', (8, 'd'), 'hold')], - ['symbolic', 1] - ) - self.assertEqual({'symbolic': ExpressionScalar('2.0*g - t0 - 1.0'), - 1: ExpressionScalar('b*(0.5*t0 - 0.5) + c*(g - 4.0) + c*(-t0 + 4.0)')}, - pulse.integral) - class TablePulseTemplateConstraintTest(ParameterConstrainerTest): def __init__(self, *args, **kwargs): diff --git a/tests/pulses/pulse_template_parameter_mapping_tests.py b/tests/pulses/pulse_template_parameter_mapping_tests.py index 289a26ddb..c548999b0 100644 --- a/tests/pulses/pulse_template_parameter_mapping_tests.py +++ b/tests/pulses/pulse_template_parameter_mapping_tests.py @@ -149,7 +149,6 @@ def test_nested_mapping_avoidance(self): self.assertIs(st_4.template, st_3) self.assertEqual(st_4.parameter_mapping, {'t': 't', 'k': 'k', 'bar': 't*l'}) - def test_get_updated_channel_mapping(self): template = DummyPulseTemplate(defined_channels={'foo', 'bar'}) st = MappingPulseTemplate(template, channel_mapping={'bar': 'kneipe'}) @@ -177,6 +176,18 @@ def test_get_updated_measurement_mapping(self): self.assertEqual(st.get_updated_measurement_mapping({'kneipe': 'meas1', 'foo': 'meas2', 'troet': 'meas3'}), {'foo': 'meas2', 'bar': 'meas1'}) + def test_integral(self) -> None: + dummy = DummyPulseTemplate(defined_channels={'A', 'B'}, + parameter_names={'k', 'f', 'b'}, + integrals={'A': Expression('2*k'), 'other': Expression('-3.2*f+b')}) + pulse = MappingPulseTemplate(dummy, parameter_mapping={'k': 'f', 'b': 2.3}, channel_mapping={'A': 'default'}, + allow_partial_parameter_mapping=True) + + self.assertEqual({'default': Expression('2*f'), 'other': Expression('-3.2*f+2.3')}, pulse.integral) + + +class MappingPulseTemplateSequencingTests(unittest.TestCase): + def test_build_sequence(self): measurement_mapping = {'meas1': 'meas2'} parameter_mapping = {'t': 'k'} @@ -207,16 +218,6 @@ def test_build_sequence(self): def test_requires_stop(self): pass - def test_integral(self) -> None: - dummy = DummyPulseTemplate(defined_channels={'A', 'B'}, - parameter_names={'k', 'f', 'b'}, - integrals={'A': Expression('2*k'), 'other': Expression('-3.2*f+b')}) - pulse = MappingPulseTemplate(dummy, parameter_mapping={'k': 'f', 'b': 2.3}, channel_mapping={'A': 'default'}, - allow_partial_parameter_mapping=True) - - self.assertEqual({'default': Expression('2*f'), 'other': Expression('-3.2*f+2.3')}, pulse.integral) - - class PulseTemplateParameterMappingExceptionsTests(unittest.TestCase): def test_missing_mapping_exception_str(self) -> None: diff --git a/tests/pulses/table_pulse_template_tests.py b/tests/pulses/table_pulse_template_tests.py index 237d486e9..e153697f9 100644 --- a/tests/pulses/table_pulse_template_tests.py +++ b/tests/pulses/table_pulse_template_tests.py @@ -411,6 +411,19 @@ def test_measurement_names(self): tpt = TablePulseTemplate({0: [(10, 1)]}, measurements=[('A', 2, 3), ('AB', 0, 1)]) self.assertEqual(tpt.measurement_names, {'A', 'AB'}) + def test_identifier(self) -> None: + identifier = 'some name' + pulse = TablePulseTemplate(entries={0: [(1, 0)]}, identifier=identifier) + self.assertEqual(pulse.identifier, identifier) + + def test_integral(self) -> None: + pulse = TablePulseTemplate(entries={0: [(1, 2, 'linear'), (3, 0, 'jump'), (4, 2, 'hold'), (5, 8, 'hold')], + 'other_channel': [(0, 7, 'linear'), (2, 0, 'hold'), (10, 0)], + 'symbolic': [(3, 'a', 'hold'), ('b', 4, 'linear'), ('c', Expression('d'), 'hold')]}) + self.assertEqual(pulse.integral, {0: Expression('6'), + 'other_channel': Expression(7), + 'symbolic': Expression('(b-3)*a + 0.5 * (c-b)*(d+4)')}) + class TablePulseTemplateConstraintTest(ParameterConstrainerTest): def __init__(self, *args, **kwargs): @@ -634,19 +647,6 @@ def test_requires_stop(self) -> None: for expected_result, parameter_set, condition_set in test_sets: self.assertEqual(expected_result, table.requires_stop(parameter_set, condition_set)) - def test_identifier(self) -> None: - identifier = 'some name' - pulse = TablePulseTemplate(entries={0: [(1, 0)]}, identifier=identifier) - self.assertEqual(pulse.identifier, identifier) - - def test_integral(self) -> None: - pulse = TablePulseTemplate(entries={0: [(1, 2, 'linear'), (3, 0, 'jump'), (4, 2, 'hold'), (5, 8, 'hold')], - 'other_channel': [(0, 7, 'linear'), (2, 0, 'hold'), (10, 0)], - 'symbolic': [(3, 'a', 'hold'), ('b', 4, 'linear'), ('c', Expression('d'), 'hold')]}) - self.assertEqual(pulse.integral, {0: Expression('6'), - 'other_channel': Expression(7), - 'symbolic': Expression('(b-3)*a + 0.5 * (c-b)*(d+4)')}) - class TablePulseConcatenationTests(unittest.TestCase): From 8ee3e79c272919896f721d3f1efd4460eff41f72 Mon Sep 17 00:00:00 2001 From: Lukas Prediger Date: Thu, 19 Jul 2018 09:28:29 +0200 Subject: [PATCH 05/14] Renamed file qctoolkit.pulses.pulse_template_parameter_mapping to qctoolkit.pulses.mapping_pulse_template --- qctoolkit/pulses/__init__.py | 2 +- ..._template_parameter_mapping.py => mapping_pulse_template.py} | 0 qctoolkit/pulses/multi_channel_pulse_template.py | 2 +- qctoolkit/pulses/sequence_pulse_template.py | 2 +- tests/pulses/__init__.py | 2 +- ...rameter_mapping_tests.py => mapping_pulse_template_tests.py} | 2 +- tests/pulses/sequence_pulse_template_tests.py | 2 +- 7 files changed, 6 insertions(+), 6 deletions(-) rename qctoolkit/pulses/{pulse_template_parameter_mapping.py => mapping_pulse_template.py} (100%) rename tests/pulses/{pulse_template_parameter_mapping_tests.py => mapping_pulse_template_tests.py} (99%) diff --git a/qctoolkit/pulses/__init__.py b/qctoolkit/pulses/__init__.py index 253e1c63f..b51690d71 100644 --- a/qctoolkit/pulses/__init__.py +++ b/qctoolkit/pulses/__init__.py @@ -4,7 +4,7 @@ from qctoolkit.pulses.function_pulse_template import FunctionPulseTemplate as FunctionPT from qctoolkit.pulses.loop_pulse_template import ForLoopPulseTemplate as ForLoopPT from qctoolkit.pulses.multi_channel_pulse_template import AtomicMultiChannelPulseTemplate as AtomicMultiChannelPT -from qctoolkit.pulses.pulse_template_parameter_mapping import MappingPulseTemplate as MappingPT +from qctoolkit.pulses.mapping_pulse_template import MappingPulseTemplate as MappingPT from qctoolkit.pulses.repetition_pulse_template import RepetitionPulseTemplate as RepetitionPT from qctoolkit.pulses.sequence_pulse_template import SequencePulseTemplate as SequencePT from qctoolkit.pulses.table_pulse_template import TablePulseTemplate as TablePT diff --git a/qctoolkit/pulses/pulse_template_parameter_mapping.py b/qctoolkit/pulses/mapping_pulse_template.py similarity index 100% rename from qctoolkit/pulses/pulse_template_parameter_mapping.py rename to qctoolkit/pulses/mapping_pulse_template.py diff --git a/qctoolkit/pulses/multi_channel_pulse_template.py b/qctoolkit/pulses/multi_channel_pulse_template.py index c40585257..d25014e44 100644 --- a/qctoolkit/pulses/multi_channel_pulse_template.py +++ b/qctoolkit/pulses/multi_channel_pulse_template.py @@ -19,7 +19,7 @@ from qctoolkit.utils.types import ChannelID, TimeType from qctoolkit._program.waveforms import MultiChannelWaveform from qctoolkit.pulses.pulse_template import PulseTemplate, AtomicPulseTemplate -from qctoolkit.pulses.pulse_template_parameter_mapping import MappingPulseTemplate, MappingTuple +from qctoolkit.pulses.mapping_pulse_template import MappingPulseTemplate, MappingTuple from qctoolkit.pulses.parameters import Parameter, ParameterConstrainer from qctoolkit.pulses.measurement import MeasurementDeclaration from qctoolkit.expressions import Expression, ExpressionScalar diff --git a/qctoolkit/pulses/sequence_pulse_template.py b/qctoolkit/pulses/sequence_pulse_template.py index 7b814cf59..b61e14d47 100644 --- a/qctoolkit/pulses/sequence_pulse_template.py +++ b/qctoolkit/pulses/sequence_pulse_template.py @@ -14,7 +14,7 @@ from qctoolkit.pulses.parameters import Parameter, ParameterConstrainer from qctoolkit.pulses.sequencing import InstructionBlock, Sequencer from qctoolkit.pulses.conditions import Condition -from qctoolkit.pulses.pulse_template_parameter_mapping import MappingPulseTemplate, MappingTuple +from qctoolkit.pulses.mapping_pulse_template import MappingPulseTemplate, MappingTuple from qctoolkit._program.waveforms import SequenceWaveform from qctoolkit.pulses.measurement import MeasurementDeclaration, MeasurementDefiner from qctoolkit.expressions import Expression, ExpressionScalar diff --git a/tests/pulses/__init__.py b/tests/pulses/__init__.py index 92cc63621..54923565f 100644 --- a/tests/pulses/__init__.py +++ b/tests/pulses/__init__.py @@ -8,7 +8,7 @@ 'multi_channel_pulse_template_tests', 'parameters_tests', 'plotting_tests', - 'pulse_template_parameter_mapping_tests', + 'mapping_pulse_template_tests.py', 'pulse_template_tests', 'repetition_pulse_template_tests', 'sample_pulse_generator', diff --git a/tests/pulses/pulse_template_parameter_mapping_tests.py b/tests/pulses/mapping_pulse_template_tests.py similarity index 99% rename from tests/pulses/pulse_template_parameter_mapping_tests.py rename to tests/pulses/mapping_pulse_template_tests.py index c548999b0..0b3a94c97 100644 --- a/tests/pulses/pulse_template_parameter_mapping_tests.py +++ b/tests/pulses/mapping_pulse_template_tests.py @@ -1,7 +1,7 @@ import unittest import itertools -from qctoolkit.pulses.pulse_template_parameter_mapping import MissingMappingException,\ +from qctoolkit.pulses.mapping_pulse_template import MissingMappingException,\ UnnecessaryMappingException, MappingPulseTemplate,\ AmbiguousMappingException, MappingCollisionException from qctoolkit.pulses.parameters import ParameterNotProvidedException diff --git a/tests/pulses/sequence_pulse_template_tests.py b/tests/pulses/sequence_pulse_template_tests.py index b43f172f8..37c58b378 100644 --- a/tests/pulses/sequence_pulse_template_tests.py +++ b/tests/pulses/sequence_pulse_template_tests.py @@ -6,7 +6,7 @@ from qctoolkit.expressions import Expression, ExpressionScalar from qctoolkit.pulses.table_pulse_template import TablePulseTemplate from qctoolkit.pulses.sequence_pulse_template import SequencePulseTemplate, SequenceWaveform -from qctoolkit.pulses.pulse_template_parameter_mapping import MappingPulseTemplate +from qctoolkit.pulses.mapping_pulse_template import MappingPulseTemplate from qctoolkit.pulses.parameters import ConstantParameter, ParameterConstraint, ParameterConstraintViolation from qctoolkit._program.instructions import MEASInstruction From cae443bca8c985a9bb06af0ed9f921abde81c88b Mon Sep 17 00:00:00 2001 From: Lukas Prediger Date: Thu, 12 Jul 2018 16:26:37 +0200 Subject: [PATCH 06/14] Re-added qctoolkit.pulses.pulse_template_parameter_mapping for backward compatibility tests. The file just forwards the import to mapping_pulse_template and issues a DeprecationWarning. Signed-off-by: Lukas Prediger --- .../pulse_template_parameter_mapping.py | 8 +++++++ .../pulse_template_parameter_mapping_tests.py | 21 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 qctoolkit/pulses/pulse_template_parameter_mapping.py create mode 100644 tests/pulses/pulse_template_parameter_mapping_tests.py diff --git a/qctoolkit/pulses/pulse_template_parameter_mapping.py b/qctoolkit/pulses/pulse_template_parameter_mapping.py new file mode 100644 index 000000000..b685b8480 --- /dev/null +++ b/qctoolkit/pulses/pulse_template_parameter_mapping.py @@ -0,0 +1,8 @@ +from qctoolkit.pulses.mapping_pulse_template import MappingPulseTemplate + +__all__ = [MappingPulseTemplate] + +import warnings +warnings.warn("MappingPulseTemplate was moved from qctoolkit.pulses.pulse_template_parameter_mapping to " + "qctoolkit.pulses.mapping_pulse_template. Please consider fixing your stored pulse templates by loading " + "and storing them anew.", DeprecationWarning) diff --git a/tests/pulses/pulse_template_parameter_mapping_tests.py b/tests/pulses/pulse_template_parameter_mapping_tests.py new file mode 100644 index 000000000..bff3f9e65 --- /dev/null +++ b/tests/pulses/pulse_template_parameter_mapping_tests.py @@ -0,0 +1,21 @@ +import unittest +import warnings + +from qctoolkit.serialization import Serializer +from tests.pulses.sequencing_dummies import DummyPulseTemplate +from tests.serialization_dummies import DummyStorageBackend + + +class TestPulseTemplateParameterMappingFileTests(unittest.TestCase): + + # ensure that a MappingPulseTemplate imported from pulse_template_parameter_mapping serializes as from mapping_pulse_template + def test_pulse_template_parameter_include(self) -> None: + with warnings.catch_warnings(record=True): + warnings.simplefilter('ignore', DeprecationWarning) + from qctoolkit.pulses.pulse_template_parameter_mapping import MappingPulseTemplate + dummy_t = DummyPulseTemplate() + map_t = MappingPulseTemplate(dummy_t) + serializer = Serializer(DummyStorageBackend()) + type_str = serializer.get_type_identifier(map_t) + self.assertEqual("qctoolkit.pulses.mapping_pulse_template.MappingPulseTemplate", type_str) + From e6d6632109807804a1401222c3963472f0c43133 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 19 Jul 2018 09:34:55 +0200 Subject: [PATCH 07/14] Add pandas to requirements --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8e479ea4b..81a4e49a3 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ package_dir={'qctoolkit': 'qctoolkit'}, packages=packages, tests_require=['pytest'], - install_requires=['sympy>=1.1.1', 'numpy'] + requires_typing, + install_requires=['sympy>=1.1.1', 'numpy', 'pandas'] + requires_typing, extras_require={ 'testing': ['pytest'], 'plotting': ['matplotlib'], From 6a8e02875668245fa3bb4f343a11eacd17547403 Mon Sep 17 00:00:00 2001 From: Lukas Prediger Date: Thu, 19 Jul 2018 10:03:33 +0200 Subject: [PATCH 08/14] Fixed imports in qctoolkit/_program/_loop.py --- qctoolkit/_program/_loop.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/qctoolkit/_program/_loop.py b/qctoolkit/_program/_loop.py index ddb4cce6a..6cbbd8be3 100644 --- a/qctoolkit/_program/_loop.py +++ b/qctoolkit/_program/_loop.py @@ -15,8 +15,7 @@ from qctoolkit.utils.types import MeasurementWindow from qctoolkit.utils import is_integer -from qctoolkit.pulses.sequence_pulse_template import SequenceWaveform -from qctoolkit.pulses.repetition_pulse_template import RepetitionWaveform +from qctoolkit._program.waveforms import SequenceWaveform, RepetitionWaveform __all__ = ['Loop', 'MultiChannelProgram', 'make_compatible'] From d68ce6c81cb3ceff1484f9165a0052204311abb3 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 19 Jul 2018 11:57:18 +0200 Subject: [PATCH 09/14] Add missing type annotations --- qctoolkit/_program/transformation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qctoolkit/_program/transformation.py b/qctoolkit/_program/transformation.py index c537983c7..a626e6ae7 100644 --- a/qctoolkit/_program/transformation.py +++ b/qctoolkit/_program/transformation.py @@ -1,4 +1,4 @@ -from typing import Mapping, Set +from typing import Mapping, Set, Dict from abc import abstractmethod import numpy as np @@ -44,12 +44,12 @@ def __call__(self, time: np.ndarray, data: pd.DataFrame) -> Mapping[ChannelID, n return self._matrix @ data_in - def get_output_channels(self, input_channels: Set[ChannelID]): + def get_output_channels(self, input_channels: Set[ChannelID]) -> Set[ChannelID]: if input_channels != set(self._matrix.columns): raise KeyError('Invalid input channels', input_channels, set(self._matrix.columns)) return set(self._matrix.index) @property - def compare_key(self): + def compare_key(self) -> Dict[ChannelID, Dict[ChannelID, float]]: return self._matrix.to_dict() From 20b7c805e192af21eb73bbea67ef58807caf9199 Mon Sep 17 00:00:00 2001 From: Lukas Prediger Date: Thu, 19 Jul 2018 13:07:51 +0200 Subject: [PATCH 10/14] __all__ is now a list of strings in pulse_template_parameter_mapping. --- qctoolkit/pulses/pulse_template_parameter_mapping.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qctoolkit/pulses/pulse_template_parameter_mapping.py b/qctoolkit/pulses/pulse_template_parameter_mapping.py index b685b8480..9ff42dae6 100644 --- a/qctoolkit/pulses/pulse_template_parameter_mapping.py +++ b/qctoolkit/pulses/pulse_template_parameter_mapping.py @@ -1,6 +1,6 @@ from qctoolkit.pulses.mapping_pulse_template import MappingPulseTemplate -__all__ = [MappingPulseTemplate] +__all__ = ["MappingPulseTemplate"] import warnings warnings.warn("MappingPulseTemplate was moved from qctoolkit.pulses.pulse_template_parameter_mapping to " From 39a4da43ed24742db60275f9d89ae4fcf9a4702f Mon Sep 17 00:00:00 2001 From: Lukas Prediger Date: Thu, 19 Jul 2018 14:06:13 +0200 Subject: [PATCH 11/14] Fix: Ensured that MappingPulseTemplate is also know under alias module qctoolkit.pulses.pulse_template_parameter_mapping to Serializable for backward compatible deserialization. --- qctoolkit/pulses/pulse_template_parameter_mapping.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qctoolkit/pulses/pulse_template_parameter_mapping.py b/qctoolkit/pulses/pulse_template_parameter_mapping.py index 9ff42dae6..06365b41e 100644 --- a/qctoolkit/pulses/pulse_template_parameter_mapping.py +++ b/qctoolkit/pulses/pulse_template_parameter_mapping.py @@ -6,3 +6,6 @@ warnings.warn("MappingPulseTemplate was moved from qctoolkit.pulses.pulse_template_parameter_mapping to " "qctoolkit.pulses.mapping_pulse_template. Please consider fixing your stored pulse templates by loading " "and storing them anew.", DeprecationWarning) + +from qctoolkit.serialization import SerializableMeta +SerializableMeta.deserialization_callbacks["qctoolkit.pulses.pulse_template_parameter_mapping.MappingPulseTemplate"] = SerializableMeta.deserialization_callbacks[MappingPulseTemplate.get_type_identifier()] From f2d73d74859c7402ab26a98fda292f07d25e45ac Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 2 Aug 2018 11:27:01 +0200 Subject: [PATCH 12/14] Include measurements defined on subtemplates of multi channel templates --- .../pulses/multi_channel_pulse_template.py | 12 +++++++- .../multi_channel_pulse_template_tests.py | 30 ++++++++++++++++++- tests/pulses/sequencing_dummies.py | 4 --- 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/qctoolkit/pulses/multi_channel_pulse_template.py b/qctoolkit/pulses/multi_channel_pulse_template.py index d25014e44..2665754be 100644 --- a/qctoolkit/pulses/multi_channel_pulse_template.py +++ b/qctoolkit/pulses/multi_channel_pulse_template.py @@ -21,7 +21,7 @@ from qctoolkit.pulses.pulse_template import PulseTemplate, AtomicPulseTemplate from qctoolkit.pulses.mapping_pulse_template import MappingPulseTemplate, MappingTuple from qctoolkit.pulses.parameters import Parameter, ParameterConstrainer -from qctoolkit.pulses.measurement import MeasurementDeclaration +from qctoolkit.pulses.measurement import MeasurementDeclaration, MeasurementWindow from qctoolkit.expressions import Expression, ExpressionScalar __all__ = ["AtomicMultiChannelPulseTemplate"] @@ -117,6 +117,16 @@ def build_waveform(self, parameters: Dict[str, numbers.Real], else: return MultiChannelWaveform(sub_waveforms) + def get_measurement_windows(self, + parameters: Dict[str, numbers.Real], + measurement_mapping: Dict[str, Optional[str]]) -> List[MeasurementWindow]: + measurements = super().get_measurement_windows(parameters=parameters, + measurement_mapping=measurement_mapping) + for st in self.subtemplates: + measurements.extend(st.get_measurement_windows(parameters=parameters, + measurement_mapping=measurement_mapping)) + return measurements + def requires_stop(self, parameters: Dict[str, Parameter], conditions: Dict[str, 'Condition']) -> bool: diff --git a/tests/pulses/multi_channel_pulse_template_tests.py b/tests/pulses/multi_channel_pulse_template_tests.py index a17d5e812..4f4601eb8 100644 --- a/tests/pulses/multi_channel_pulse_template_tests.py +++ b/tests/pulses/multi_channel_pulse_template_tests.py @@ -5,8 +5,9 @@ from qctoolkit.utils.types import time_from_float from qctoolkit.pulses.multi_channel_pulse_template import MultiChannelWaveform, MappingPulseTemplate, ChannelMappingException, AtomicMultiChannelPulseTemplate -from qctoolkit.pulses.parameters import ParameterConstraint, ParameterConstraintViolation +from qctoolkit.pulses.parameters import ParameterConstraint, ParameterConstraintViolation, ConstantParameter from qctoolkit.expressions import ExpressionScalar, Expression +from qctoolkit._program.instructions import InstructionBlock from tests.pulses.sequencing_dummies import DummyPulseTemplate, DummyWaveform from tests.serialization_dummies import DummySerializer @@ -198,6 +199,33 @@ def test_build_waveform_none(self): wf = pt.build_waveform(parameters, channel_mapping=channel_mapping) self.assertIsNone(wf) + def test_build_sequence(self): + wfs = [DummyWaveform(duration=1.1, defined_channels={'A'}), DummyWaveform(duration=1.1, defined_channels={'B'})] + sts = [DummyPulseTemplate(duration='t1', defined_channels={'A'}, waveform=wfs[0], measurements=[('m', 0, 1)]), + DummyPulseTemplate(duration='t1', defined_channels={'B'}, waveform=wfs[1]), + DummyPulseTemplate(duration='t1', defined_channels={'C'}, waveform=None)] + + pt = AtomicMultiChannelPulseTemplate(*sts, parameter_constraints=['a < b'], measurements=[('n', .1, .2)]) + + params = dict(a=ConstantParameter(1.0), b=ConstantParameter(1.1)) + measurement_mapping = dict(m='foo', n='bar') + channel_mapping = {'A': 'A', 'B': 'B', 'C': None} + + block = InstructionBlock() + pt.build_sequence(None, parameters=params, conditions={}, measurement_mapping=measurement_mapping, + channel_mapping=channel_mapping, instruction_block=block) + + expected_waveform = MultiChannelWaveform(wfs) + + expected_block = InstructionBlock() + measurements = [('bar', .1, .2), ('foo', 0, 1)] + expected_block.add_instruction_meas(measurements) + expected_block.add_instruction_exec(waveform=expected_waveform) + + self.assertEqual(len(block.instructions), len(expected_block.instructions)) + self.assertEqual(block.instructions[0].compare_key, expected_block.instructions[0].compare_key) + self.assertEqual(block.instructions[1].compare_key, expected_block.instructions[1].compare_key) + class AtomicMultiChannelPulseTemplateSerializationTests(SerializableTests, unittest.TestCase): diff --git a/tests/pulses/sequencing_dummies.py b/tests/pulses/sequencing_dummies.py index 7f5dd3555..f0ddeb40c 100644 --- a/tests/pulses/sequencing_dummies.py +++ b/tests/pulses/sequencing_dummies.py @@ -327,10 +327,6 @@ def duration(self): def parameter_names(self) -> Set[str]: return set(self.parameter_names_) - def get_measurement_windows(self, parameters: Dict[str, Parameter] = None) -> List[MeasurementWindow]: - """Return all measurement windows defined in this PulseTemplate.""" - raise NotImplementedError() - @property def build_sequence_calls(self): return len(self.build_sequence_arguments) From 28324f364a6df13884b236ad7f5c6e10b193861f Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 2 Aug 2018 11:42:32 +0200 Subject: [PATCH 13/14] Add test for add_measurement_windows --- tests/pulses/multi_channel_pulse_template_tests.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/pulses/multi_channel_pulse_template_tests.py b/tests/pulses/multi_channel_pulse_template_tests.py index 4f4601eb8..0ef379902 100644 --- a/tests/pulses/multi_channel_pulse_template_tests.py +++ b/tests/pulses/multi_channel_pulse_template_tests.py @@ -226,6 +226,19 @@ def test_build_sequence(self): self.assertEqual(block.instructions[0].compare_key, expected_block.instructions[0].compare_key) self.assertEqual(block.instructions[1].compare_key, expected_block.instructions[1].compare_key) + def test_get_measurement_windows(self): + wfs = [DummyWaveform(duration=1.1, defined_channels={'A'}), DummyWaveform(duration=1.1, defined_channels={'B'})] + sts = [DummyPulseTemplate(duration='t1', defined_channels={'A'}, waveform=wfs[0], measurements=[('m', 0, 1), + ('n', 0.3, 0.4)]), + DummyPulseTemplate(duration='t1', defined_channels={'B'}, waveform=wfs[1], measurements=[('m', 0.1, .2)])] + + pt = AtomicMultiChannelPulseTemplate(*sts, parameter_constraints=['a < b'], measurements=[('n', .1, .2)]) + + measurement_mapping = dict(m='foo', n='bar') + expected = [('bar', .1, .2), ('foo', 0, 1), ('bar', .3, .4), ('foo', .1, .2)] + meas_windows = pt.get_measurement_windows({}, measurement_mapping) + self.assertEqual(expected, meas_windows) + class AtomicMultiChannelPulseTemplateSerializationTests(SerializableTests, unittest.TestCase): From b4e42b9226e06d57142e100177afc0d96fa80a2b Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 2 Aug 2018 12:11:21 +0200 Subject: [PATCH 14/14] Fix parameter_names and measurement_names --- qctoolkit/pulses/multi_channel_pulse_template.py | 6 ++++-- .../pulses/multi_channel_pulse_template_tests.py | 16 +++++++++++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/qctoolkit/pulses/multi_channel_pulse_template.py b/qctoolkit/pulses/multi_channel_pulse_template.py index 2665754be..d95becab5 100644 --- a/qctoolkit/pulses/multi_channel_pulse_template.py +++ b/qctoolkit/pulses/multi_channel_pulse_template.py @@ -85,7 +85,9 @@ def duration(self) -> Expression: @property def parameter_names(self) -> Set[str]: - return set.union(*(st.parameter_names for st in self._subtemplates)) | self.constrained_parameters + return set.union(self.measurement_parameters, + self.constrained_parameters, + *(st.parameter_names for st in self._subtemplates)) @property def subtemplates(self) -> Sequence[AtomicPulseTemplate]: @@ -97,7 +99,7 @@ def defined_channels(self) -> Set[ChannelID]: @property def measurement_names(self) -> Set[str]: - return set.union(*(st.measurement_names for st in self._subtemplates)) + return super().measurement_names.union(*(st.measurement_names for st in self._subtemplates)) def build_waveform(self, parameters: Dict[str, numbers.Real], channel_mapping: Dict[ChannelID, Optional[ChannelID]]) -> Optional['MultiChannelWaveform']: diff --git a/tests/pulses/multi_channel_pulse_template_tests.py b/tests/pulses/multi_channel_pulse_template_tests.py index 0ef379902..45e7620bb 100644 --- a/tests/pulses/multi_channel_pulse_template_tests.py +++ b/tests/pulses/multi_channel_pulse_template_tests.py @@ -15,8 +15,6 @@ from tests.serialization_tests import SerializableTests - - class AtomicMultiChannelPulseTemplateTest(unittest.TestCase): def __init__(self,*args,**kwargs): super().__init__(*args,**kwargs) @@ -119,7 +117,19 @@ def test_measurement_names(self): sts = [DummyPulseTemplate(duration='t1', defined_channels={'A'}, parameter_names={'a', 'b'}, measurement_names={'A', 'C'}), DummyPulseTemplate(duration='t1', defined_channels={'B'}, parameter_names={'a', 'c'}, measurement_names={'A', 'B'})] - self.assertEqual(AtomicMultiChannelPulseTemplate(*sts).measurement_names, {'A', 'B', 'C'}) + self.assertEqual(AtomicMultiChannelPulseTemplate(*sts, measurements=[('D', 1, 2)]).measurement_names, + {'A', 'B', 'C', 'D'}) + + def test_parameter_names(self): + sts = [DummyPulseTemplate(duration='t1', defined_channels={'A'}, parameter_names={'a', 'b'}, + measurement_names={'A', 'C'}), + DummyPulseTemplate(duration='t1', defined_channels={'B'}, parameter_names={'a', 'c'}, + measurement_names={'A', 'B'})] + pt = AtomicMultiChannelPulseTemplate(*sts, measurements=[('D', 'd', 2)], parameter_constraints=['d < e']) + + self.assertEqual(pt.parameter_names, + {'a', 'b', 'c', 'd', 'e'}) + def test_integral(self) -> None: sts = [DummyPulseTemplate(duration='t1', defined_channels={'A'},