From 7df4446850bf65a62c18266a1b85dd83cbc619e5 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 30 May 2022 10:51:01 +0200 Subject: [PATCH 001/441] Use evaluate_in_scope where appropriate --- qupulse/pulses/mapping_pulse_template.py | 6 +++--- qupulse/pulses/multi_channel_pulse_template.py | 2 +- qupulse/pulses/point_pulse_template.py | 6 +++--- qupulse/pulses/table_pulse_template.py | 7 +++---- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/qupulse/pulses/mapping_pulse_template.py b/qupulse/pulses/mapping_pulse_template.py index af5b64e00..453b62ef8 100644 --- a/qupulse/pulses/mapping_pulse_template.py +++ b/qupulse/pulses/mapping_pulse_template.py @@ -1,4 +1,4 @@ -from typing import Optional, Set, Dict, Union, List, Any, Tuple +from typing import Optional, Set, Dict, Union, List, Any, Tuple, Mapping import itertools import numbers import collections @@ -113,7 +113,7 @@ def __init__(self, template: PulseTemplate, *, template = template.template self.__template = template - self.__parameter_mapping = FrozenDict(parameter_mapping) + self.__parameter_mapping: Mapping[str, Expression] = FrozenDict(parameter_mapping) self.__external_parameters = set(itertools.chain(*(expr.variables for expr in self.__parameter_mapping.values()))) self.__external_parameters |= self.constrained_parameters self.__measurement_mapping = measurement_mapping @@ -257,7 +257,7 @@ def map_parameter_values(self, parameters: Dict[str, numbers.Real], A new dictionary with mapped numeric values. """ self._validate_parameters(parameters=parameters, volatile=volatile) - return {parameter: mapping_function.evaluate_numeric(**parameters) + return {parameter: mapping_function.evaluate_in_scope(parameters) for parameter, mapping_function in self.__parameter_mapping.items()} def map_parameter_objects(self, parameters: Dict[str, Parameter], diff --git a/qupulse/pulses/multi_channel_pulse_template.py b/qupulse/pulses/multi_channel_pulse_template.py index b6c220b05..a9e6aa5d3 100644 --- a/qupulse/pulses/multi_channel_pulse_template.py +++ b/qupulse/pulses/multi_channel_pulse_template.py @@ -137,7 +137,7 @@ def build_waveform(self, parameters: Dict[str, numbers.Real], waveform = MultiChannelWaveform(sub_waveforms) if self._duration: - expected_duration = self._duration.evaluate_numeric(**parameters) + expected_duration = self._duration.evaluate_in_scope(parameters) if not isclose(expected_duration, waveform.duration): raise ValueError('The duration does not ' diff --git a/qupulse/pulses/point_pulse_template.py b/qupulse/pulses/point_pulse_template.py index 376e28be4..9f896a26f 100644 --- a/qupulse/pulses/point_pulse_template.py +++ b/qupulse/pulses/point_pulse_template.py @@ -26,8 +26,8 @@ class PointPulseEntry(TableEntry): def instantiate(self, parameters: Dict[str, numbers.Real], num_channels: int) -> Sequence[PointWaveformEntry]: - t = self.t.evaluate_numeric(**parameters) - vs = self.v.evaluate_numeric(**parameters) + t = self.t.evaluate_in_scope(parameters) + vs = self.v.evaluate_in_scope(parameters) if isinstance(vs, numbers.Number): vs = (vs,) * num_channels @@ -71,7 +71,7 @@ def build_waveform(self, for channel in self.defined_channels): return None - if self.duration.evaluate_numeric(**parameters) == 0: + if self.duration.evaluate_in_scope(parameters) == 0: return None mapped_channels = tuple(channel_mapping[c] for c in self._channels) diff --git a/qupulse/pulses/table_pulse_template.py b/qupulse/pulses/table_pulse_template.py index 4decd2576..289133da2 100644 --- a/qupulse/pulses/table_pulse_template.py +++ b/qupulse/pulses/table_pulse_template.py @@ -322,13 +322,12 @@ def build_waveform(self, if not instantiated: return None - if self.duration.evaluate_numeric(**parameters) == 0: - return None - waveforms = [TableWaveform.from_table(*ch_instantiated) for ch_instantiated in instantiated] - return MultiChannelWaveform.from_parallel(waveforms) + mc_waveform = MultiChannelWaveform.from_parallel(waveforms) + if mc_waveform.duration != 0: + return mc_waveform @staticmethod def from_array(times: np.ndarray, voltages: np.ndarray, channels: List[ChannelID]) -> 'TablePulseTemplate': From a3c1f8af9f17fb1ab0159b206dafcff4e2cdbcc7 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Wed, 16 Nov 2022 16:17:27 +0100 Subject: [PATCH 002/441] Rename examples and partition in groups --- doc/source/concepts/instantiating.rst | 2 +- doc/source/concepts/pulsetemplates.rst | 22 ++++---- doc/source/concepts/serialization.rst | 2 +- ...te.ipynb => 00AbstractPulseTemplate.ipynb} | 0 ...Pulse.ipynb => 00AdvancedTablePulse.ipynb} | 0 ...b => 00ArithmeticWithPulseTemplates.ipynb} | 0 ...sedPulses.ipynb => 00ComposedPulses.ipynb} | 16 +++--- ...te.ipynb => 00ConstantPulseTemplate.ipynb} | 0 ...ctionPulse.ipynb => 00FunctionPulse.ipynb} | 0 ...Template.ipynb => 00MappingTemplate.ipynb} | 6 +- ...es.ipynb => 00MultiChannelTemplates.ipynb} | 2 +- ...{03PointPulse.ipynb => 00PointPulse.ipynb} | 0 ...etrospectiveConstantChannelAddition.ipynb} | 2 +- doc/source/examples/00SimpleTablePulse.ipynb | 2 +- ...imeReversal.ipynb => 00TimeReversal.ipynb} | 0 ...easurements.ipynb => 01Measurements.ipynb} | 2 +- ...nts.ipynb => 01ParameterConstraints.ipynb} | 0 ...ulseStorage.ipynb => 01PulseStorage.ipynb} | 0 ...ePrograms.ipynb => 02CreatePrograms.ipynb} | 0 ...ynb => 03DynamicNuclearPolarisation.ipynb} | 0 ...pynb => 03FreeInductionDecayExample.ipynb} | 4 +- ...ipynb => 03GateConfigurationExample.ipynb} | 2 +- doc/source/examples/examples.rst | 55 +++++++++++++------ 23 files changed, 68 insertions(+), 49 deletions(-) rename doc/source/examples/{12AbstractPulseTemplate.ipynb => 00AbstractPulseTemplate.ipynb} (100%) rename doc/source/examples/{01AdvancedTablePulse.ipynb => 00AdvancedTablePulse.ipynb} (100%) rename doc/source/examples/{14ArithmeticWithPulseTemplates.ipynb => 00ArithmeticWithPulseTemplates.ipynb} (100%) rename doc/source/examples/{03xComposedPulses.ipynb => 00ComposedPulses.ipynb} (99%) rename doc/source/examples/{03ConstantPulseTemplate.ipynb => 00ConstantPulseTemplate.ipynb} (100%) rename doc/source/examples/{02FunctionPulse.ipynb => 00FunctionPulse.ipynb} (100%) rename doc/source/examples/{05MappingTemplate.ipynb => 00MappingTemplate.ipynb} (99%) rename doc/source/examples/{07MultiChannelTemplates.ipynb => 00MultiChannelTemplates.ipynb} (99%) rename doc/source/examples/{03PointPulse.ipynb => 00PointPulse.ipynb} (100%) rename doc/source/examples/{13RetrospectiveConstantChannelAddition.ipynb => 00RetrospectiveConstantChannelAddition.ipynb} (99%) rename doc/source/examples/{16TimeReversal.ipynb => 00TimeReversal.ipynb} (100%) rename doc/source/examples/{08Measurements.ipynb => 01Measurements.ipynb} (91%) rename doc/source/examples/{09ParameterConstraints.ipynb => 01ParameterConstraints.ipynb} (100%) rename doc/source/examples/{04PulseStorage.ipynb => 01PulseStorage.ipynb} (100%) rename doc/source/examples/{06CreatePrograms.ipynb => 02CreatePrograms.ipynb} (100%) rename doc/source/examples/{15DynamicNuclearPolarisation.ipynb => 03DynamicNuclearPolarisation.ipynb} (100%) rename doc/source/examples/{10FreeInductionDecayExample.ipynb => 03FreeInductionDecayExample.ipynb} (99%) rename doc/source/examples/{11GateConfigurationExample.ipynb => 03GateConfigurationExample.ipynb} (99%) diff --git a/doc/source/concepts/instantiating.rst b/doc/source/concepts/instantiating.rst index e999e3b51..fba88d883 100644 --- a/doc/source/concepts/instantiating.rst +++ b/doc/source/concepts/instantiating.rst @@ -19,7 +19,7 @@ with parameter constraints and returns an object of type :class:`.Loop` which represents a pulse as nested loops of atomic waveforms. This is another object tree structure but all parameters (including repetition counts) have been substituted by the corresponding numeric values passed into ``create_program``. The :class:`.Loop` object acts as your reference to the instantiated pulse. -See :ref:`/examples/06CreatePrograms.ipynb` for an example on usage of :meth:`.PulseTemplate.create_program`. +See :ref:`/examples/02CreatePrograms.ipynb` for an example on usage of :meth:`.PulseTemplate.create_program`. The second step of the instantiation is performed by the hardware backend and transparent to the user. Upon registering the pulse with the hardware backend via :meth:`qupulse.hardware.HardwareSetup.register_program`, the backend will determine which diff --git a/doc/source/concepts/pulsetemplates.rst b/doc/source/concepts/pulsetemplates.rst index 4b84d3fbc..1dbf5299c 100644 --- a/doc/source/concepts/pulsetemplates.rst +++ b/doc/source/concepts/pulsetemplates.rst @@ -62,20 +62,20 @@ Relevant Examples Examples demonstrating the construction of pulse templates and parameters from very simple to somewhat more complex pulses are * :ref:`/examples/00SimpleTablePulse.ipynb` -* :ref:`/examples/01AdvancedTablePulse.ipynb` -* :ref:`/examples/02FunctionPulse.ipynb` -* :ref:`/examples/03PointPulse.ipynb` -* :ref:`/examples/03xComposedPulses.ipynb` -* :ref:`/examples/03ConstantPulseTemplate.ipynb` -* :ref:`/examples/05MappingTemplate.ipynb` -* :ref:`/examples/07MultiChannelTemplates.ipynb` -* :ref:`/examples/14ArithmeticWithPulseTemplates.ipynb` +* :ref:`/examples/00AdvancedTablePulse.ipynb` +* :ref:`/examples/00FunctionPulse.ipynb` +* :ref:`/examples/00PointPulse.ipynb` +* :ref:`/examples/00ComposedPulses.ipynb` +* :ref:`/examples/00ConstantPulseTemplate.ipynb` +* :ref:`/examples/00MappingTemplate.ipynb` +* :ref:`/examples/00MultiChannelTemplates.ipynb` +* :ref:`/examples/00ArithmeticWithPulseTemplates.ipynb` -:ref:`/examples/09ParameterConstraints.ipynb` demonstrates the mentioned parameter constraints. +:ref:`/examples/01ParameterConstraints.ipynb` demonstrates the mentioned parameter constraints. -:ref:`/examples/08Measurements.ipynb` shows how to specify measurements. +:ref:`/examples/01Measurements.ipynb` shows how to specify measurements. -Finally, :ref:`/examples/06CreatePrograms.ipynb` illustrates usage of the :meth:`.PulseTemplate.create_program` method. +Finally, :ref:`/examples/02CreatePrograms.ipynb` illustrates usage of the :meth:`.PulseTemplate.create_program` method. .. rubric:: Footnotes .. [#tree] Regarded as objects in the programming language, each pulse template is a tree of PulseTemplate objects, where the atomic templates (:class:`.TablePulseTemplate` and :class:`.FunctionPulseTemplate` objects) are the leafs while the remaining ones form the inner nodes of the tree. diff --git a/doc/source/concepts/serialization.rst b/doc/source/concepts/serialization.rst index 080de40d1..e44566ffe 100644 --- a/doc/source/concepts/serialization.rst +++ b/doc/source/concepts/serialization.rst @@ -14,7 +14,7 @@ The :class:`.PulseStorage` offers a convenient dictionary-like interface for sto Finally, the :class:`.StorageBackend` interface abstracts the actual storage backend. While currently there only exists a few implementations of this interface, most importantly the :class:`.FilesystemStorageBackend`, this allows to support, e.g., database storage, in the future. :class:`.PulseStorage` requires an instance of :class:`.StorageBackend` which represents its persistent pulse storage during initialization. -For an example of how to use :class:`.PulseStorage` to store and load pulse templates, see :ref:`/examples/04PulseStorage.ipynb` in the examples section. +For an example of how to use :class:`.PulseStorage` to store and load pulse templates, see :ref:`/examples/01PulseStorage.ipynb` in the examples section. Global Pulse Registry ^^^^^^^^^^^^^^^^^^^^^^ diff --git a/doc/source/examples/12AbstractPulseTemplate.ipynb b/doc/source/examples/00AbstractPulseTemplate.ipynb similarity index 100% rename from doc/source/examples/12AbstractPulseTemplate.ipynb rename to doc/source/examples/00AbstractPulseTemplate.ipynb diff --git a/doc/source/examples/01AdvancedTablePulse.ipynb b/doc/source/examples/00AdvancedTablePulse.ipynb similarity index 100% rename from doc/source/examples/01AdvancedTablePulse.ipynb rename to doc/source/examples/00AdvancedTablePulse.ipynb diff --git a/doc/source/examples/14ArithmeticWithPulseTemplates.ipynb b/doc/source/examples/00ArithmeticWithPulseTemplates.ipynb similarity index 100% rename from doc/source/examples/14ArithmeticWithPulseTemplates.ipynb rename to doc/source/examples/00ArithmeticWithPulseTemplates.ipynb diff --git a/doc/source/examples/03xComposedPulses.ipynb b/doc/source/examples/00ComposedPulses.ipynb similarity index 99% rename from doc/source/examples/03xComposedPulses.ipynb rename to doc/source/examples/00ComposedPulses.ipynb index e7b026c1a..161799cf4 100644 --- a/doc/source/examples/03xComposedPulses.ipynb +++ b/doc/source/examples/00ComposedPulses.ipynb @@ -6,7 +6,7 @@ "source": [ "# Combining Pulse Templates\n", "\n", - "So far we have seen how to define simple pulses using the `TablePulseTemplate` ([Modelling a Simple TablePulseTemplate](00SimpleTablePulse.ipynb)), `FunctionPulseTemplate` ([Modelling Pulses Using Functions And Expressions](02FunctionPulse.ipynb)) and `PointPulseTemplate` ([The PointPulseTemplate](03PointPulse.ipynb)) classes. These are the elementary building blocks to create pulses and we call them *atomic* pulse templates.\n", + "So far we have seen how to define simple pulses using the `TablePulseTemplate` ([Modelling a Simple TablePulseTemplate](00SimpleTablePulse.ipynb)), `FunctionPulseTemplate` ([Modelling Pulses Using Functions And Expressions](00FunctionPulse.ipynb)) and `PointPulseTemplate` ([The PointPulseTemplate](00PointPulse.ipynb)) classes. These are the elementary building blocks to create pulses and we call them *atomic* pulse templates.\n", "\n", "We will now have a look at how to compose more complex pulse structures.\n", "\n", @@ -35,12 +35,12 @@ "first_point_pt = PointPT([(0, 'v_0'),\n", " (1, 'v_1', 'linear'),\n", " ('t', 'v_0+v_1', 'jump')],\n", - " channel_names={'A'},\n", - " measurements={('M', 1, 't-1')})\n", + " channel_names=('A',),\n", + " measurements=[('M', 1, 't-1')])\n", "second_point_pt = PointPT([(0, 'v_0+v_1'),\n", " ('t_2', 'v_0', 'linear')],\n", - " channel_names={'A'},\n", - " measurements={('M', 0, 1)})\n", + " channel_names=('A',),\n", + " measurements=[('M', 0, 1)])\n", "\n", "# define the SequencePT\n", "sequence_pt = SequencePT(first_point_pt, second_point_pt)\n", @@ -57,9 +57,9 @@ "\n", "The `SequencePT` will further have the union of all parameters defined in its subtemplates as its own parameter set. If two subtemplates defined parameters with the same name, they will be treated as the same parameters in the `SequencePT`.\n", "\n", - "Finally, `SequencePT` will also expose all measurements defined in subtemplates. It is also possible to define additional measurements in the constructor of `SequencePT`. See [Definition of Measurements](08Measurements.ipynb) for me info about measurements.\n", + "Finally, `SequencePT` will also expose all measurements defined in subtemplates. It is also possible to define additional measurements in the constructor of `SequencePT`. See [Definition of Measurements](01Measurements.ipynb) for me info about measurements.\n", "\n", - "There are several cases where the above constraints represent a problem: Subtemplates might not all be defined on the same channel, subtemplates might define parameters with the same name which should still be treated as different parameters in the sequence or names of measurements defined by different subtemplates might collide. To deal with these, we can wrap a subtemplate with the `MappingPulseTemplate` class which allows us to rename parameters, channels and measurements or even derive parameter values from other parameters using mathematical expressions. You can learn how to do all this in [Mapping with the MappingPulseTemplate](05MappingTemplate.ipynb).\n", + "There are several cases where the above constraints represent a problem: Subtemplates might not all be defined on the same channel, subtemplates might define parameters with the same name which should still be treated as different parameters in the sequence or names of measurements defined by different subtemplates might collide. To deal with these, we can wrap a subtemplate with the `MappingPulseTemplate` class which allows us to rename parameters, channels and measurements or even derive parameter values from other parameters using mathematical expressions. You can learn how to do all this in [Mapping with the MappingPulseTemplate](00MappingTemplate.ipynb).\n", "\n", "In our example above, however, we were taking care not to encounter these problems yet. Let's plot all of them with some parameters to see the results." ] @@ -4133,7 +4133,7 @@ "\n", "## AtomicMultiChannelPulseTemplate: Run Pulses in Parallel on Different Channels\n", "\n", - "So far we have only looked at pulses that affect the time-domain aspect of combining pulses. Another way to combine pulses is to parallelise them by executing them on different channels at the same time. This is of course already supported by simply creating atomic pulse templates (`TablePT`, `PointPT`, `FunctionPT`) on multiple channels. However, sometimes it is necessary to put already existing pulses in parallel. Instead of having to define a new atomic pulse template for this, we can make use of the `AtomicMuliChannelPulseTemplate` class. To learn more about how this works, see [Multi-Channel Pulses](07MultiChannelTemplates.ipynb).\n", + "So far we have only looked at pulses that affect the time-domain aspect of combining pulses. Another way to combine pulses is to parallelise them by executing them on different channels at the same time. This is of course already supported by simply creating atomic pulse templates (`TablePT`, `PointPT`, `FunctionPT`) on multiple channels. However, sometimes it is necessary to put already existing pulses in parallel. Instead of having to define a new atomic pulse template for this, we can make use of the `AtomicMuliChannelPulseTemplate` class. To learn more about how this works, see [Multi-Channel Pulses](00MultiChannelTemplates.ipynb).\n", "\n", "## Combining Combined Pulses\n", "\n", diff --git a/doc/source/examples/03ConstantPulseTemplate.ipynb b/doc/source/examples/00ConstantPulseTemplate.ipynb similarity index 100% rename from doc/source/examples/03ConstantPulseTemplate.ipynb rename to doc/source/examples/00ConstantPulseTemplate.ipynb diff --git a/doc/source/examples/02FunctionPulse.ipynb b/doc/source/examples/00FunctionPulse.ipynb similarity index 100% rename from doc/source/examples/02FunctionPulse.ipynb rename to doc/source/examples/00FunctionPulse.ipynb diff --git a/doc/source/examples/05MappingTemplate.ipynb b/doc/source/examples/00MappingTemplate.ipynb similarity index 99% rename from doc/source/examples/05MappingTemplate.ipynb rename to doc/source/examples/00MappingTemplate.ipynb index f0de8ed4c..d5dc11078 100644 --- a/doc/source/examples/05MappingTemplate.ipynb +++ b/doc/source/examples/00MappingTemplate.ipynb @@ -6,7 +6,7 @@ "source": [ "# Mapping with the MappingPulseTemplate\n", "\n", - "We will now have a look on how to remap parameters, channel ids and measurements. The definition of measurements is illustrated in [Definition of Measurements](08Measurements.ipynb). The `MappingPulseTemplate` class allows us to take any already existing `PulseTemplate` and specify a mapping for its parameters, channel ids and measurements. \n", + "We will now have a look on how to remap parameters, channel ids and measurements. The definition of measurements is illustrated in [Definition of Measurements](01Measurements.ipynb). The `MappingPulseTemplate` class allows us to take any already existing `PulseTemplate` and specify a mapping for its parameters, channel ids and measurements.\n", "\n", "This can be useful for simply renaming things, e.g., to avoid name collisions of parameters or change the name of a channel a pulse should be executed on, but can also be employed to derive the value of certain parameters from other parameters.\n", "\n", @@ -28,7 +28,7 @@ } ], "source": [ - "from qupulse.pulses import MappingPT, FunctionPT, SequencePT, AtomicMultiChannelPT\n", + "from qupulse.pulses import MappingPT, FunctionPT, AtomicMultiChannelPT\n", "\n", "sine = FunctionPT('a*sin(omega*t)', 't_duration')\n", "\n", @@ -88,7 +88,7 @@ "source": [ "## Mapping of Channel Ids and Measurement Names\n", "\n", - "Sometimes it is necessary to rename channels or measurements. Here we see a case where we want to play a sine and a cosine in parallel by using the `AtomicMultiChannelPulseTemplate` (for a more in depth explanation of multi-channel pulse template, see [Multi-Channel Pulses](07MultiChannelTemplates.ipynb)). Of course, this doesn't work as both pulses are by default defined on the 'default' channel." + "Sometimes it is necessary to rename channels or measurements. Here we see a case where we want to play a sine and a cosine in parallel by using the `AtomicMultiChannelPulseTemplate` (for a more in depth explanation of multi-channel pulse template, see [Multi-Channel Pulses](00MultiChannelTemplates.ipynb)). Of course, this doesn't work as both pulses are by default defined on the 'default' channel." ] }, { diff --git a/doc/source/examples/07MultiChannelTemplates.ipynb b/doc/source/examples/00MultiChannelTemplates.ipynb similarity index 99% rename from doc/source/examples/07MultiChannelTemplates.ipynb rename to doc/source/examples/00MultiChannelTemplates.ipynb index b0dd1e480..074601c2a 100644 --- a/doc/source/examples/07MultiChannelTemplates.ipynb +++ b/doc/source/examples/00MultiChannelTemplates.ipynb @@ -1680,7 +1680,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The constructor of `AtomicMultiChannelPulseTemplate` expects its subtemplates as positional arguments. Each of positional arguments is required to be either a `AtomicPulseTemplate`, a `MappingPulseTemplate` that wraps an `AtomicPulseTemplate` or a tuple that can be passed to `MappingPulseTemplate.from_tuple`(more examples in [Mapping with the MappingPulseTemplate](05MappingTemplate.ipynb)). The sets of channels on which the subtemplates are defined has to be distinct.\n", + "The constructor of `AtomicMultiChannelPulseTemplate` expects its subtemplates as positional arguments. Each of positional arguments is required to be either a `AtomicPulseTemplate`, a `MappingPulseTemplate` that wraps an `AtomicPulseTemplate` or a tuple that can be passed to `MappingPulseTemplate.from_tuple`(more examples in [Mapping with the MappingPulseTemplate](00MappingTemplate.ipynb)). The sets of channels on which the subtemplates are defined has to be distinct.\n", "Note that an exception will be raised during the sampling of the waveforms (i.e., during the sequencing process) if the subtemplates have different length." ] }, diff --git a/doc/source/examples/03PointPulse.ipynb b/doc/source/examples/00PointPulse.ipynb similarity index 100% rename from doc/source/examples/03PointPulse.ipynb rename to doc/source/examples/00PointPulse.ipynb diff --git a/doc/source/examples/13RetrospectiveConstantChannelAddition.ipynb b/doc/source/examples/00RetrospectiveConstantChannelAddition.ipynb similarity index 99% rename from doc/source/examples/13RetrospectiveConstantChannelAddition.ipynb rename to doc/source/examples/00RetrospectiveConstantChannelAddition.ipynb index ad941c8c0..89794b6f0 100644 --- a/doc/source/examples/13RetrospectiveConstantChannelAddition.ipynb +++ b/doc/source/examples/00RetrospectiveConstantChannelAddition.ipynb @@ -5,7 +5,7 @@ "metadata": {}, "source": [ "# ParallelConstantChannelPulseTemplate\n", - "One reoccuring problem is to add a constant channel to an already existing possibly complex pulse. The setting in this example requires us to put a trigger pulse before the example pulse written in [10FreeInductionDecayExample](10FreeInductionDecayExample.ipynb). Unfortunately, the trigger pulse has to be played on a seperate marker channel that is not included in the example pulse. Therefore we will add this channel to the pulse with the constant value 0.\n", + "One reoccuring problem is to add a constant channel to an already existing possibly complex pulse. The setting in this example requires us to put a trigger pulse before the example pulse written in [03FreeInductionDecayExample](03FreeInductionDecayExample.ipynb). Unfortunately, the trigger pulse has to be played on a seperate marker channel that is not included in the example pulse. Therefore, we will add this channel to the pulse with the constant value 0.\n", "\n", "Let us start with loading the experiment and defining the trigger pulse" ] diff --git a/doc/source/examples/00SimpleTablePulse.ipynb b/doc/source/examples/00SimpleTablePulse.ipynb index 8f603d42e..3da941c64 100644 --- a/doc/source/examples/00SimpleTablePulse.ipynb +++ b/doc/source/examples/00SimpleTablePulse.ipynb @@ -869,7 +869,7 @@ "source": [ "Alright, we got what we wanted. \n", "\n", - "Note that the time domain in pulse defintions does not correspond to any fixed real world time unit. The mapping from a single time unit in a pulse definition to real time in execution is made by setting a sample rate when instantiating pulses for execution from the pulse templates. For more on this, see [Instantiating Pulses](06CreatePrograms.ipynb).\n", + "Note that the time domain in pulse defintions does not correspond to any fixed real world time unit. The mapping from a single time unit in a pulse definition to real time in execution is made by setting a sample rate when instantiating pulses for execution from the pulse templates. For more on this, see [Instantiating Pulses](02CreatePrograms.ipynb).\n", "\n", "## Introducing Parameters\n", "Now we want to make the template parameterizable. This allows us to reuse the template for pulses with similar structure. Say we would like to have the same pulse, but the intermediate linear interpolation part should last 4 units of time instead of only 2. Instead of creating another template with hardcoded values, we instruct the `TablePulseTemplate` instance to rely on parameters." diff --git a/doc/source/examples/16TimeReversal.ipynb b/doc/source/examples/00TimeReversal.ipynb similarity index 100% rename from doc/source/examples/16TimeReversal.ipynb rename to doc/source/examples/00TimeReversal.ipynb diff --git a/doc/source/examples/08Measurements.ipynb b/doc/source/examples/01Measurements.ipynb similarity index 91% rename from doc/source/examples/08Measurements.ipynb rename to doc/source/examples/01Measurements.ipynb index 22bd3b1e3..c4bedbc44 100644 --- a/doc/source/examples/08Measurements.ipynb +++ b/doc/source/examples/01Measurements.ipynb @@ -47,7 +47,7 @@ "Note that measurement definitions may not exceed the duration of the pulse they are defined in. Doing so will result in an exception being raised during pulse instantiation.\n", "Note further that measurements for pulse templates that are empty, e.g. because their length as given by parameters turns out equal to zero, will be discarded during instantiation (without raising an exception).\n", "\n", - "When using non-atomic/composite pulse templates such as for example `SequencePulseTemplate`, they will \"inherit\" all the measurements from the subtemplates they are created with (see [Combining PulseTemplates](03xComposedPulses.ipynb) to learn more about composite pulse templates). To avoid name conflicts of measurements from different subtemplates, we can make use of mapping (via [MappingPulseTemplate](05MappingTemplate.ipynb)) to rename the measurements, as the example below demonstrates." + "When using non-atomic/composite pulse templates such as for example `SequencePulseTemplate`, they will \"inherit\" all the measurements from the subtemplates they are created with (see [Combining PulseTemplates](00ComposedPulses.ipynb) to learn more about composite pulse templates). To avoid name conflicts of measurements from different subtemplates, we can make use of mapping (via [MappingPulseTemplate](00MappingTemplate.ipynb)) to rename the measurements, as the example below demonstrates." ] }, { diff --git a/doc/source/examples/09ParameterConstraints.ipynb b/doc/source/examples/01ParameterConstraints.ipynb similarity index 100% rename from doc/source/examples/09ParameterConstraints.ipynb rename to doc/source/examples/01ParameterConstraints.ipynb diff --git a/doc/source/examples/04PulseStorage.ipynb b/doc/source/examples/01PulseStorage.ipynb similarity index 100% rename from doc/source/examples/04PulseStorage.ipynb rename to doc/source/examples/01PulseStorage.ipynb diff --git a/doc/source/examples/06CreatePrograms.ipynb b/doc/source/examples/02CreatePrograms.ipynb similarity index 100% rename from doc/source/examples/06CreatePrograms.ipynb rename to doc/source/examples/02CreatePrograms.ipynb diff --git a/doc/source/examples/15DynamicNuclearPolarisation.ipynb b/doc/source/examples/03DynamicNuclearPolarisation.ipynb similarity index 100% rename from doc/source/examples/15DynamicNuclearPolarisation.ipynb rename to doc/source/examples/03DynamicNuclearPolarisation.ipynb diff --git a/doc/source/examples/10FreeInductionDecayExample.ipynb b/doc/source/examples/03FreeInductionDecayExample.ipynb similarity index 99% rename from doc/source/examples/10FreeInductionDecayExample.ipynb rename to doc/source/examples/03FreeInductionDecayExample.ipynb index f201becfc..eb5ab7458 100644 --- a/doc/source/examples/10FreeInductionDecayExample.ipynb +++ b/doc/source/examples/03FreeInductionDecayExample.ipynb @@ -6,7 +6,7 @@ "source": [ "# Free Induction Decay - A Real Use Case\n", "\n", - "The following will give an example of a complex pulse using many of the features discussed in the previous tutorial examles: We will use two channels, parameters and parameter constraints, parameterized measurements and atomic and non-atomic pulse templates. This is based on real experiments. To see another, a bit more artificial example for a pulse setup use case that offers more verbose explanations, see [Gate Configuration - A Full Use Case](11GateConfigurationExample.ipynb).\n", + "The following will give an example of a complex pulse using many of the features discussed in the previous tutorial examles: We will use two channels, parameters and parameter constraints, parameterized measurements and atomic and non-atomic pulse templates. This is based on real experiments. To see another, a bit more artificial example for a pulse setup use case that offers more verbose explanations, see [Gate Configuration - A Full Use Case](03GateConfigurationExample.ipynb).\n", "\n", "We start by creating some atomic pulse templates using `PointPT` which will be the building blocks for the more complex pulse structure we have in mind." ] @@ -107,7 +107,7 @@ "source": [ "Let's use some reasonable (but low) values for our parameters and plot our `experiment` pulse (we set the number of repeititions of `looped_pulse` only to 2 so that the plot does not get too stuffed).\n", "\n", - "Note that we provide numpy arrays of length 2 for some parameters to assign different values for different channels (see also [The PointPulseTemplate](03PointPulse.ipynb))." + "Note that we provide numpy arrays of length 2 for some parameters to assign different values for different channels (see also [The PointPulseTemplate](00PointPulse.ipynb))." ] }, { diff --git a/doc/source/examples/11GateConfigurationExample.ipynb b/doc/source/examples/03GateConfigurationExample.ipynb similarity index 99% rename from doc/source/examples/11GateConfigurationExample.ipynb rename to doc/source/examples/03GateConfigurationExample.ipynb index d59ded922..95e2fe12a 100644 --- a/doc/source/examples/11GateConfigurationExample.ipynb +++ b/doc/source/examples/03GateConfigurationExample.ipynb @@ -13,7 +13,7 @@ "collapsed": true }, "source": [ - "An example for a real use case of qupulse is the search for and evaluation of parameters for pulses that represent quantum gate operations on a toy example. To see an example closer to reality but less verbose in explanations, please see [Free Induction Decay - A Real Use Case](10FreeInductionDecayExample.ipynb).\n", + "An example for a real use case of qupulse is the search for and evaluation of parameters for pulses that represent quantum gate operations on a toy example. To see an example closer to reality but less verbose in explanations, please see [Free Induction Decay - A Real Use Case](03FreeInductionDecayExample.ipynb).\n", "\n", "## Description of the Experiment\n", "The experiment will typically involve a set of gate pulses $G_j, 0 \\leq j \\lt N_{Gates}$.\n", diff --git a/doc/source/examples/examples.rst b/doc/source/examples/examples.rst index 95ebff450..4993b1df1 100644 --- a/doc/source/examples/examples.rst +++ b/doc/source/examples/examples.rst @@ -5,26 +5,45 @@ Examples All examples are provided as static text in this documentation and, additionally, as interactive jupyter notebooks accessible by running `jupyter notebook` in the `/doc/source/examples` directory of the source tree. + .. toctree:: + :caption: Pulse template types + :name: pt_types + 00SimpleTablePulse - 01AdvancedTablePulse - 02FunctionPulse - 03PointPulse - 03xComposedPulses - 03ConstantPulseTemplate - 04PulseStorage - 05MappingTemplate - 06CreatePrograms - 07MultiChannelTemplates - 08Measurements - 09ParameterConstraints - 10FreeInductionDecayExample - 11GateConfigurationExample - 12AbstractPulseTemplate - 13RetrospectiveConstantChannelAddition - 14ArithmeticWithPulseTemplates - 15DynamicNuclearPolarisation - 16TimeReversal + 00AdvancedTablePulse + 00FunctionPulse + 00PointPulse + 00ComposedPulses + 00ConstantPulseTemplate + 00MultiChannelTemplates + 00MappingTemplate + 00AbstractPulseTemplate + 00ArithmeticWithPulseTemplates + 00RetrospectiveConstantChannelAddition + 00TimeReversal + +.. toctree:: + :caption: Pulse template features + :name: pt_feat + + 01PulseStorage + 01Measurements + 01ParameterConstraints + +.. toctree:: + :caption: Physically motivated examples + :name: physical_examples + + 03FreeInductionDecayExample + 03GateConfigurationExample + 03DynamicNuclearPolarisation + +.. toctree:: + :caption: Pulse playback related examples + :name: hardware_examples + + 02CreatePrograms The `/doc/source/examples` directory also contains some outdated examples for features and functionality that has been changed. These examples start with the number nine and are currently left only for reference purposes. If you are just learning how to get around in qupulse please ignore them. \ No newline at end of file From f503831e4ef1df23120575df843f98ffd1bc7f5b Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 13 Dec 2022 15:10:49 +0100 Subject: [PATCH 003/441] Fix links to python and numpy documentation --- doc/source/conf.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 232f53c67..5c25f321c 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -98,7 +98,7 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = 'en' # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: @@ -320,8 +320,8 @@ # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { - 'python': ('https://docs.python.org/', None), - 'numpy': ('http://docs.scipy.org/doc/numpy/', None) + 'python': ('https://docs.python.org/3/', None), + 'numpy': ('https://numpy.org/doc/stable/', None) } nbsphinx_execute_arguments = [ From 555ebd3245ac1b64ac2cf3c04e5364fa198e672b Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 13 Dec 2022 15:26:32 +0100 Subject: [PATCH 004/441] Add some introduction text to documentation --- doc/source/index.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/source/index.rst b/doc/source/index.rst index 582bafe8c..6d653bce7 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -6,6 +6,10 @@ Welcome to qupulse's documentation! ====================================== +``qupulse`` is a python package to write, manage and playback arbitrarily nested quantum control pulses. This documentation contains concept explanations, jupyter notebook examples and the automatically generated API reference. The API reference does not cover parts of qupulse that are explicitly considered an implementation detail like ``qupulse._program``. + +You are encouraged to read the concept explanations and interactively explore the linked examples. To do this you can install qupulse via ``python -m pip install -e git+https://github.com/qutech/qupulse.git#egg=qupulse[default]`` which will clone the qupulse into ``./src/qupulse``. You can find the examples in ``doc/source/examples`` and open them with jupyter, Spyder or another IDE of your choice. + Contents: .. toctree:: From 5751f9686a1efa2d7229d35959a0de1cd96b7f5d Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 13 Dec 2022 15:27:23 +0100 Subject: [PATCH 005/441] Use correct literal syntax in examples.rst and update outdated example wildcard --- doc/source/examples/examples.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/examples/examples.rst b/doc/source/examples/examples.rst index 4993b1df1..210d2a5f7 100644 --- a/doc/source/examples/examples.rst +++ b/doc/source/examples/examples.rst @@ -3,7 +3,7 @@ Examples ======== -All examples are provided as static text in this documentation and, additionally, as interactive jupyter notebooks accessible by running `jupyter notebook` in the `/doc/source/examples` directory of the source tree. +All examples are provided as static text in this documentation and, additionally, as interactive jupyter notebooks accessible by running ``jupyter notebook`` in the ``/doc/source/examples`` directory of the source tree. .. toctree:: @@ -45,5 +45,5 @@ All examples are provided as static text in this documentation and, additionally 02CreatePrograms -The `/doc/source/examples` directory also contains some outdated examples for features and functionality that has been changed. These examples start with the number nine and are currently left only for reference purposes. +The ``/doc/source/examples`` directory also contains some outdated examples for features and functionality that has been changed. These examples start with an underscore i.e. ``_*.ipynb`` and are currently left only for reference purposes. If you are just learning how to get around in qupulse please ignore them. \ No newline at end of file From 178b4b75b6bb25042d9ca3868bb433aaab855164 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 13 Dec 2022 15:58:53 +0100 Subject: [PATCH 006/441] lazy load awgs modules and remove install_requirements --- qupulse/hardware/awgs/__init__.py | 33 +++++++++--------------------- qupulse/hardware/awgs/tektronix.py | 10 +-------- setup.cfg | 1 + 3 files changed, 12 insertions(+), 32 deletions(-) diff --git a/qupulse/hardware/awgs/__init__.py b/qupulse/hardware/awgs/__init__.py index d91e1833d..8fe5353ee 100644 --- a/qupulse/hardware/awgs/__init__.py +++ b/qupulse/hardware/awgs/__init__.py @@ -1,29 +1,16 @@ import sys import subprocess +import warnings -__all__ = ["install_requirements"] +import lazy_loader as lazy -try: - from qupulse.hardware.awgs.tabor import TaborAWGRepresentation, TaborChannelPair - __all__.extend(["TaborAWGRepresentation", "TaborChannelPair"]) -except ImportError: - pass -try: - from qupulse.hardware.awgs.tektronix import TektronixAWG - __all__.extend(["TektronixAWG"]) -except ImportError: - pass - - -def install_requirements(vendor: str): - package_repos = { - 'tektronix': 'tek_awg', - 'tabor': 'tabor_control' +__getattr__, __dir__, __all__ = lazy.attach( + __name__, + submodules=['base'], + submod_attrs={ + 'tabor': ['TaborAWGRepresentation', 'TaborChannelPair'], + 'tektronix': ['TektronixAWG'], + 'zihdawg': ['HDAWGRepresentation', 'HDAWGChannelGroup'], } - - if vendor not in package_repos: - raise ValueError('Vendor must be in {}'.format(set(package_repos.keys()))) - - repo = package_repos[vendor] - subprocess.check_call([sys.executable, "-m", "pip", "install", repo]) +) diff --git a/qupulse/hardware/awgs/tektronix.py b/qupulse/hardware/awgs/tektronix.py index 801cf2eed..5865388e6 100644 --- a/qupulse/hardware/awgs/tektronix.py +++ b/qupulse/hardware/awgs/tektronix.py @@ -7,12 +7,7 @@ import warnings import logging -try: - import tek_awg -except ImportError: # pragma: no cover - warnings.warn("Could not import Tektronix driver backend. " - "If you wish to use it execute qupulse.hardware.awgs.install_requirements('tektronix')") - raise +import tek_awg from qupulse.hardware.awgs.base import AWG, AWGAmplitudeOffsetHandling, ProgramOverwriteException from qupulse import ChannelID @@ -286,9 +281,6 @@ def __init__(self, device: tek_awg.TekAwg, super().__init__(identifier=identifier) self.logger = logger or logging.getLogger("qupulse.tektronix") - if device is None: - raise RuntimeError('Please install the tek_awg package or run "install_requirements" from this module') - self._device = device self._synchronized = False # this gets set to True by synchronize or clear and to False on error during manupulation diff --git a/setup.cfg b/setup.cfg index 155a01e52..64ea87505 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,6 +27,7 @@ install_requires = numpy cached_property;python_version<'3.8' frozendict + lazy_loader test_suite = tests [options.extras_require] From 0d26f13182d3ae6072c0cadefbd1cbf773eee075 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 13 Dec 2022 16:03:37 +0100 Subject: [PATCH 007/441] Fix type annotation error and remove unused import --- qupulse/hardware/awgs/__init__.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/qupulse/hardware/awgs/__init__.py b/qupulse/hardware/awgs/__init__.py index 8fe5353ee..e76d4d9b8 100644 --- a/qupulse/hardware/awgs/__init__.py +++ b/qupulse/hardware/awgs/__init__.py @@ -1,13 +1,9 @@ -import sys -import subprocess -import warnings - import lazy_loader as lazy __getattr__, __dir__, __all__ = lazy.attach( __name__, - submodules=['base'], + submodules={'base'}, submod_attrs={ 'tabor': ['TaborAWGRepresentation', 'TaborChannelPair'], 'tektronix': ['TektronixAWG'], From e4333ae71c3ba626c37a08c8466526d512082b3c Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 13 Dec 2022 16:03:53 +0100 Subject: [PATCH 008/441] Lazy dac loading --- qupulse/hardware/dacs/__init__.py | 14 +++++++++----- qupulse/hardware/dacs/dac_base.py | 17 ++++++++++------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/qupulse/hardware/dacs/__init__.py b/qupulse/hardware/dacs/__init__.py index 99a13581c..b2e3277cb 100644 --- a/qupulse/hardware/dacs/__init__.py +++ b/qupulse/hardware/dacs/__init__.py @@ -1,6 +1,10 @@ -from qupulse.hardware.dacs.dac_base import * +import lazy_loader as lazy -try: - from qupulse.hardware.dacs.alazar import * -except ImportError: - pass +__getattr__, __dir__, __all__ = lazy.attach( + __name__, + submodules={'alazar2'}, + submod_attrs={ + 'dac_base': ['DAC'], + 'alazar': ['AlazarCard'], + } +) diff --git a/qupulse/hardware/dacs/dac_base.py b/qupulse/hardware/dacs/dac_base.py index e68802576..cab2e7b7f 100644 --- a/qupulse/hardware/dacs/dac_base.py +++ b/qupulse/hardware/dacs/dac_base.py @@ -1,7 +1,10 @@ from abc import ABCMeta, abstractmethod -from typing import Dict, Tuple, Iterable +from typing import Dict, Tuple, Iterable, TYPE_CHECKING -import numpy +if TYPE_CHECKING: + import numpy +else: + numpy = None __all__ = ['DAC'] @@ -10,8 +13,8 @@ class DAC(metaclass=ABCMeta): """Representation of a data acquisition card""" @abstractmethod - def register_measurement_windows(self, program_name: str, windows: Dict[str, Tuple[numpy.ndarray, - numpy.ndarray]]) -> None: + def register_measurement_windows(self, program_name: str, windows: Dict[str, Tuple['numpy.ndarray', + 'numpy.ndarray']]) -> None: """Register measurement windows for a given program. Overwrites previously defined measurement windows for this program. @@ -24,8 +27,8 @@ def register_measurement_windows(self, program_name: str, windows: Dict[str, Tup @abstractmethod def set_measurement_mask(self, program_name: str, mask_name: str, - begins: numpy.ndarray, - lengths: numpy.ndarray) -> Tuple[numpy.ndarray, numpy.ndarray]: + begins: 'numpy.ndarray', + lengths: 'numpy.ndarray') -> Tuple['numpy.ndarray', 'numpy.ndarray']: """Set/overwrite a single the measurement mask for a program. Begins and lengths are in nanoseconds. Args: @@ -60,5 +63,5 @@ def clear(self) -> None: """Clears all registered programs.""" @abstractmethod - def measure_program(self, channels: Iterable[str]) -> Dict[str, numpy.ndarray]: + def measure_program(self, channels: Iterable[str]) -> Dict[str, 'numpy.ndarray']: """Get the last measurement's results of the specified operations/channels""" From 7bbc1ffe9a1cc46a64065f2cbb9dda1f1d9f0eaa Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 15 Dec 2022 12:13:07 +0100 Subject: [PATCH 009/441] Fix tektronix test --- tests/hardware/tektronix_tests.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/hardware/tektronix_tests.py b/tests/hardware/tektronix_tests.py index a1f326b65..773876e2e 100644 --- a/tests/hardware/tektronix_tests.py +++ b/tests/hardware/tektronix_tests.py @@ -324,10 +324,6 @@ def test_init(self): init_idle_patch = mock.patch('qupulse.hardware.awgs.tektronix.TektronixAWG.initialize_idle_program') synchronize_patch = mock.patch('qupulse.hardware.awgs.tektronix.TektronixAWG.synchronize') - with mock.patch('qupulse.hardware.awgs.tektronix.tek_awg', new=None): - with self.assertRaisesRegex(RuntimeError, 'tek_awg'): - TektronixAWG(self.make_dummy_tek_awg(), 'clear') - with self.patch_method('make_idle_waveform') as make_idle_waveform: with self.assertRaisesRegex(ValueError, 'synchronize'): TektronixAWG(self.make_dummy_tek_awg(), 'foo', idle_waveform_length=300) From 48edce205d7437cc2d42a684ad8baaaf0dac5596 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Wed, 21 Dec 2022 09:26:53 +0100 Subject: [PATCH 010/441] Simple plot_2d function without docsting and tests yet --- qupulse/pulses/plotting.py | 40 +++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/qupulse/pulses/plotting.py b/qupulse/pulses/plotting.py index 8514a6358..cc1173a26 100644 --- a/qupulse/pulses/plotting.py +++ b/qupulse/pulses/plotting.py @@ -6,13 +6,15 @@ - plot: Plot a pulse using matplotlib. """ -from typing import Dict, Tuple, Any, Optional, Set, List, Union +from typing import Dict, Tuple, Any, Optional, Set, List, Union, Mapping from numbers import Real +import matplotlib.pyplot as plt import numpy as np import warnings import operator import itertools +import functools from qupulse._program import waveforms from qupulse.utils.types import ChannelID, MeasurementWindow, has_type_interface @@ -253,6 +255,42 @@ def plot(pulse: PulseTemplate, return axes.get_figure() +@functools.singledispatch +def plot_2d(program: Loop, channels: Tuple[ChannelID, ChannelID], + sample_rate: float = None, + ax: plt.Axes = None, + plot_kwargs: Mapping = None): + _, rendered, _ = render(program, sample_rate, plot_channels=set(channels)) + x_y = np.array([rendered[channels[0]], rendered[channels[1]]]) + keep = np.full(x_y.shape[0], fill_value=True) + keep[1:] = np.any(x_y[1:, :] != x_y[:-1, :], axis=1) + x_y_plt = x_y[keep] + + plt.plot(x_y_plt[:, 0], x_y_plt[:, 1], ax=ax, **(plot_kwargs or {})) + plt.xlabel(channels[0], ax=ax) + plt.ylabel(channels[1], ax=ax) + + +@plot_2d.register +def _(pulse_template: PulseTemplate, + channels: Tuple[ChannelID, ChannelID], + sample_rate: float = None, + ax: plt.Axes = None, + plot_kwargs: Mapping = None, + parameters=None, + channel_mapping=None): + + if channel_mapping is None: + channel_mapping = {ch: ch if ch in channels else None + for ch in pulse_template.defined_channels} + create_program_kwargs = {'channel_mapping': channel_mapping} + if parameters is not None: + create_program_kwargs['parameters'] = parameters + + program = pulse_template.create_program(**create_program_kwargs) + return plot_2d(program, channels, sample_rate=sample_rate, ax=ax, plot_kwargs=plot_kwargs) + + class PlottingNotPossibleException(Exception): """Indicates that plotting is not possible because the sequencing process did not translate the entire given PulseTemplate structure.""" From e01e8ff6ca11a61a0492dff7b43b6efedb2d25f4 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Wed, 21 Dec 2022 09:34:08 +0100 Subject: [PATCH 011/441] Add docsting --- qupulse/pulses/plotting.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/qupulse/pulses/plotting.py b/qupulse/pulses/plotting.py index cc1173a26..3f59f3e10 100644 --- a/qupulse/pulses/plotting.py +++ b/qupulse/pulses/plotting.py @@ -260,6 +260,18 @@ def plot_2d(program: Loop, channels: Tuple[ChannelID, ChannelID], sample_rate: float = None, ax: plt.Axes = None, plot_kwargs: Mapping = None): + """Plot the pulse/program in the plane of the given channels to the specified axis. + + Args: + program: The program to plot + channels: (x_axis, y_axis) name tuple + sample_rate: Sample rate to use + ax: Axis to plot into. + plot_kwargs: Forwarded to the plot function. + + Returns: + + """ _, rendered, _ = render(program, sample_rate, plot_channels=set(channels)) x_y = np.array([rendered[channels[0]], rendered[channels[1]]]) keep = np.full(x_y.shape[0], fill_value=True) From 6efb8a67b478c015fdafb647a0cae7dad3641e9b Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 3 Jan 2023 14:55:41 +0100 Subject: [PATCH 012/441] Fix plotting function and specify default sample rate --- qupulse/pulses/plotting.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/qupulse/pulses/plotting.py b/qupulse/pulses/plotting.py index 7cc24ca70..cfe8d8c1e 100644 --- a/qupulse/pulses/plotting.py +++ b/qupulse/pulses/plotting.py @@ -258,28 +258,30 @@ def plot(pulse: PulseTemplate, def plot_2d(program: Loop, channels: Tuple[ChannelID, ChannelID], sample_rate: float = None, ax: plt.Axes = None, - plot_kwargs: Mapping = None): - """Plot the pulse/program in the plane of the given channels to the specified axis. + plot_kwargs: Mapping = None) -> plt.Figure: + """Plot the pulse/program in the plane of the given channels. Args: program: The program to plot channels: (x_axis, y_axis) name tuple - sample_rate: Sample rate to use + sample_rate: Sample rate to use. Defaults to max(1000 samples per program, 10 per nano second) ax: Axis to plot into. plot_kwargs: Forwarded to the plot function. - - Returns: - """ + if sample_rate is None: + sample_rate = max(1000 / program.duration, 10) + _, rendered, _ = render(program, sample_rate, plot_channels=set(channels)) x_y = np.array([rendered[channels[0]], rendered[channels[1]]]) - keep = np.full(x_y.shape[0], fill_value=True) - keep[1:] = np.any(x_y[1:, :] != x_y[:-1, :], axis=1) - x_y_plt = x_y[keep] + keep = np.full(x_y.shape[1], fill_value=True) + keep[1:] = np.any(x_y[:, 1:] != x_y[:, :-1], axis=0) + x_y_plt = x_y[:, keep] - plt.plot(x_y_plt[:, 0], x_y_plt[:, 1], ax=ax, **(plot_kwargs or {})) - plt.xlabel(channels[0], ax=ax) - plt.ylabel(channels[1], ax=ax) + ax = ax or plt.subplots()[1] + ax.plot(x_y_plt[0, :], x_y_plt[1, :], **(plot_kwargs or {})) + ax.set_xlabel(channels[0]) + ax.set_ylabel(channels[1]) + return ax.get_figure() @plot_2d.register @@ -289,7 +291,7 @@ def _(pulse_template: PulseTemplate, ax: plt.Axes = None, plot_kwargs: Mapping = None, parameters=None, - channel_mapping=None): + channel_mapping=None) -> plt.Figure: if channel_mapping is None: channel_mapping = {ch: ch if ch in channels else None From 9aa7aa41720b62d86ed29e0155c1aa2a880ebf32 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 3 Jan 2023 14:55:54 +0100 Subject: [PATCH 013/441] Add newspiece --- changes.d/703.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes.d/703.feature diff --git a/changes.d/703.feature b/changes.d/703.feature new file mode 100644 index 000000000..af8aeb0b4 --- /dev/null +++ b/changes.d/703.feature @@ -0,0 +1 @@ +New two dimensional plotting function ``qupulse.pulses.plotting.plot_2d``. From 0a6d0457a540903f492585855ea06744179b4958 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 3 Jan 2023 16:03:51 +0100 Subject: [PATCH 014/441] Add snake charge scan example --- doc/source/examples/03SnakeChargeScan.ipynb | 532 ++++++++++++++++++++ 1 file changed, 532 insertions(+) create mode 100644 doc/source/examples/03SnakeChargeScan.ipynb diff --git a/doc/source/examples/03SnakeChargeScan.ipynb b/doc/source/examples/03SnakeChargeScan.ipynb new file mode 100644 index 000000000..7fef6bbf9 --- /dev/null +++ b/doc/source/examples/03SnakeChargeScan.ipynb @@ -0,0 +1,532 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "fcefa3cf", + "metadata": {}, + "source": [ + "# Snake Charge Scan\n", + "\n", + "To manipulate an electron confined by gate-defined quantum dot, it is essential to control the number of electron i.e. the chemical potential of each quantum dot and the tunnel coupling among quantum dots. The charge stability diagram (CSD) represents electrostatic characteristics of such a quantum dot system for a give charge configuration which suggests the operation point and the work window for further experiements. However, the CSD depends on the sweep direction of gate voltages thus a charge state hysteresis in quantum dots has been observed and inverstigated. [1]\n", + "\n", + "[1] C. H. Yang, et al., Appl. Phys. Lett. 105, 183505 (2014)\n", + "\n", + "In this tutorial, a pulse for the bi-directional sweep of a CSD is constructed so that the hysteresis of charge occupancy in a double quantum dot system can be measured. For ease of analysis, 2 different measurement windows namingly `('x_neg', 'x_pos')` are defined providing the possibliy of inspecting two sweep direction individually. Options of `plot` function in `qupulse.pulses.plotting` will be explored as well." + ] + }, + { + "cell_type": "markdown", + "id": "394631b6", + "metadata": {}, + "source": [ + "## Task 1: Piece-wised voltage level\n", + "\n", + " ### Description:\n", + " Let 2 AWG channels `(X, Y)` hold at a given set of voltages `(x_start, y_start)` for specific time durations `t_hold`. \n", + "\n", + " ### Goal: \n", + " making a pulse without thinking the time consumption or the memory consumption.\n", + " \n", + " Firtly, we build a piece of voltage level with free parameters `t_hold` and `sample_rate` representing the time duration of such voltage level and the sample rate of AWG, respectively. With aforementioned parameters, a universal piece-wised pulse can be used as a building block of most experiments with flexibility of adjusting the pulse duration and hardward settings i.e. the sample rate of AWG." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "810a7af2", + "metadata": {}, + "outputs": [], + "source": [ + "import sympy\n", + "from qupulse.pulses import ConstantPT, RepetitionPT\n", + "\n", + "sample_rate, n_segments, t_hold = sympy.sympify('sample_rate, n_segments, t_hold')\n", + "t_segment = n_segments / sample_rate\n", + "\n", + "segment = ConstantPT(t_segment, {'X': 'x_start + x_i * x_step', \n", + " 'Y': 'y_start + y_i * y_step'}\n", + " )\n", + "body = RepetitionPT(segment, t_hold // t_segment, measurements=[('M', 0, 't_hold')])" + ] + }, + { + "cell_type": "markdown", + "id": "ad4cbdb1", + "metadata": {}, + "source": [ + "Now that a piece of voltage level is constructed with given voltages" + ] + }, + { + "cell_type": "markdown", + "id": "82adccc2", + "metadata": {}, + "source": [ + "A charge stability diagram can be abstracted by 2 nested for loop using `ForLoopPT` in qupulse. In order to define the name of measurement window for different sweep direction, the `ForLoopPT` is wrapped by a `MappingPT`.\n", + "\n", + "The voltage resolution of the scan is described by `{x_step, y_step}` which are converted by qupulse according to the user inputs `{x_start, x_stop, N_x, y_start, y_stop, N_y}` representing the start voltage and stop voltages on different scan axis and the number of voltage levels of each axes." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "fd72b962", + "metadata": {}, + "outputs": [], + "source": [ + "from qupulse.pulses import MappingPT, ForLoopPT\n", + "inner_loop_fwd = MappingPT(ForLoopPT(body, 'x_i', 'N_x'), measurement_mapping={'M': 'x_pos'})\n", + "inner_loop_bwd = MappingPT(ForLoopPT(body, 'x_i', ('N_x - 1', -1, -1)), measurement_mapping={'M': 'x_neg'})\n", + "\n", + "# concatinate two pulse templates by '@'\n", + "inner_loop = inner_loop_fwd @ inner_loop_bwd\n", + "outer_loop = ForLoopPT(inner_loop, 'y_i', 'N_y')\n", + "\n", + "# here we make a linear interpolation of sweep axes.\n", + "snake_sweep_seg = MappingPT(outer_loop, parameter_mapping={'x_step': '(x_stop - x_start) / (N_x - 1)',\n", + " 'y_step': '(y_stop - y_start) / (N_y - 1)'})" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "3468e95f-7070-4659-bdf6-ff19f6f1d2b1", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'N_y', 'x_stop', 'y_start', 'x_start', 'n_segments', 'y_stop', 'cds_res', 'sample_rate', 'N_x'}\n" + ] + } + ], + "source": [ + "\n", + "snake_cds = MappingPT(snake_sweep_seg,\n", + " parameter_mapping={'t_hold': 'cds_res'},\n", + " identifier='Snake_CDS')\n", + "print(snake_cds.parameter_names)" + ] + }, + { + "cell_type": "markdown", + "id": "5c65acf0", + "metadata": {}, + "source": [ + "Let's generate a pulse for a bi-directional charge scan with voltage resolution 0.3V/point on x-axis and 0.4V/point on y-axis. The time resolution `cds_res` here is arbitarily chosen for demonstration." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "8c7802fb-bff3-4c4e-80dc-d9be15f217ca", + "metadata": {}, + "outputs": [], + "source": [ + "default_params = {\n", + " 'n_segments': 2,\n", + " 'x_start': 0,\n", + " 'x_stop': 3,\n", + " 'y_start': 0,\n", + " 'y_stop': 2,\n", + " 'N_x': 10,\n", + " 'N_y': 5,\n", + " 'sample_rate': 1,\n", + " 'cds_res': 5\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "5aceae43", + "metadata": {}, + "source": [ + "Now, we use `plot` function to inspect the positive sweep of channel X which is highlighted in the following plot." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "71dd0045-292d-49f7-90ec-124b38d53566", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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xQcffICCggIMHjwYADBgwADUqlULaWlpAIDXX38d7dq1Q2xsLK5du4Z3330X586dw/PPP+/NYRAREVEl5fViqG/fvrh06RImTZqErKwstGzZEmvWrLFfVJ2RkQGV6q9Lm7KzszF06FBkZWUhJCQErVu3xk8//YQmTZp4awhERERUiXm9GAKAkSNHlntabPPmzQ7TM2bMwIwZMzwQFRERESlBpbubjIiIiMidWAwRERGRorEYIiIiIkVjMURERESKxmKIiIiIFI3FEBERESkaiyEiIiJSNBZDREREpGgshoiIiEjRfOIJ1L5ACIEiAUgCKLTKAGRvh1QlGC02h2k/rRrSLb4MlLmoGMyF72AufAdz4TtuJxfuwmIIxR/sKdvP4WR2teKG1T97N6AqJOnN9Y7TMSFYkpJc7gecuag4zIXvYC58B3PhO1zNhTvxNBkAs03GyWxTmfOiNDZWjC7y06qRFBNS5rw957JhKrKVOQ9gLtyNufAdzIXvYC58x53kwp2Ytxu8EGZGcpuWMBj0MJlMOJC+D5LE3eQKSZKwJCXZ4UNstNhKVf23wlzcOebCdzAXvoO58B3uysWdYtZuoJUAg0YFg0YNoVHBQ6crqxxJkuCvu7OPF3PhHsyF72AufAdz4TvckYs7xdNkREREpGgshoiIiEjRWAwRERGRorEYIiIiIkVjMURERESKxmKIiIiIFI3FEBERESkaiyEiIiJSNBZDREREpGgshoiIiEjRWAwRERGRorEYIiIiIkVjMURERESKxmKIiIiIFI3FEBERESkaiyEiIiJSNBZDREREpGgshoiIiEjRWAwRERGRorEYIiIiIkVjMURERESKxmKIiIiIFI3FEBERESkaiyEiIiJSNBZDREREpGgshoiIiEjRWAwRERGRovlEMfTRRx+hbt26MBgMuPvuu7F79+6b9l+yZAni4+NhMBjQvHlzrF692kOREhERUVXj9WJo0aJFGDduHCZPnox9+/YhISEB3bp1w8WLF8vs/9NPP6Ffv3547rnnsH//fvTu3Ru9e/fGkSNHPBw5ERERVQUabwfw/vvvY+jQoRg8eDAAYO7cufjuu+8wb948/OMf/yjVf+bMmejevTvGjx8PAHjjjTewbt06fPjhh5g7d67T27UVmWAxF0At2WCxyvBDIQBAKyxQWY1QWWWobCZohBmSsHmsTUBA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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from qupulse.pulses.plotting import plot\n", + "\n", + "_=plot(snake_cds, parameters=default_params, sample_rate=1, plot_measurements='x_pos')" + ] + }, + { + "cell_type": "markdown", + "id": "813d89e8", + "metadata": {}, + "source": [ + "Similaly the negative sweep is inspected by assigning the measurement window name `x_neg` to the input argument `plot_measurement` of `plot` function provided by qupulse." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "83fea4a4-07a8-4403-ac98-491de63017ab", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkMAAAHHCAYAAAC88FzIAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/NK7nSAAAACXBIWXMAAA9hAAAPYQGoP6dpAABcDklEQVR4nO3dd3xT5f4H8M/JbuigpRMsZRSo7FHAgldQqgx/KFcZclU2XqYgilouFxSu1AWIC+Qqw3VBHIiKKBuBAm3ZIiAIFLFllq6kSZM8vz9qA7EtJCWrPZ/365WXPc95zjnf53yT8PWsSEIIASIiIiKZUvg6ACIiIiJfYjFEREREssZiiIiIiGSNxRARERHJGoshIiIikjUWQ0RERCRrLIaIiIhI1lgMERERkayxGCIiIiJZYzFERH5n2bJlkCQJGRkZvg6FiGSAxRAR3dChQ4fQv39/xMXFQafToV69erj33nvx1ltv+To0t9u/fz8ee+wxxMbGQqvVIiwsDMnJyVi6dCmsVqu9nyRJ9pdKpUJYWBg6dOiASZMm4ciRIxWu++LFi5g0aRISEhIQEBCAyMhIdOrUCc899xwKCwu9NUQiqoDK1wEQkf/auXMn7r77btSvXx+jR49GdHQ0zp49i127dmHBggWYOHGir0N0m/fffx9jxoxBVFQUHn/8cTRp0gQFBQXYuHEjRo4ciezsbEybNs3e/95778WQIUMghEBeXh4OHDiA5cuX491338Urr7yCKVOm2PteuXIFiYmJyM/Px4gRI5CQkIDLly/j4MGDWLhwIcaOHYvAwEBfDJuIwGKIiG7gpZdeQkhICNLT01G7dm2HeRcuXPBNUB6wa9cujBkzBklJSVi7di2CgoLs8yZPnoyMjAwcPnzYYZmmTZvisccec2h7+eWX0bdvXzz99NNISEhAnz59AAAffPABsrKysGPHDnTp0sVhmfz8fGg0Gg+NjIicwdNkRFSpkydPokWLFuUKIQCIjIy0/y1JEiZMmIDVq1ejZcuW0Gq1aNGiBdatW+ewzJkzZzBu3Dg0a9YMAQEBqFOnDgYMGIDTp0/fNJbc3Fx06tQJt912G44dOwYAMJlMmDlzJuLj46HVahEbG4tnn30WJpPJpXG++OKLkCQJn3zyiUMhVCYxMRHDhg276Xrq1KmDFStWQKVS4aWXXrK3nzx5EkqlEnfccUe5ZYKDg6HT6VyKl4jci8UQEVUqLi4OmZmZ5Y6KVGT79u0YN24cHnnkEbz66qsoLi7Gww8/jMuXL9v7pKenY+fOnXjkkUfw5ptvYsyYMdi4cSO6d+8Og8FQ6bovXbqEe+65B+fPn8fWrVvRrFkz2Gw2PPDAA3j99dfRt29fvPXWW+jXrx/mz5+PQYMGOT1Gg8GAjRs34q677kL9+vWdXq4y9evXR7du3bBr1y7k5+cDKN2PVqsVH3300S2vn4g8QBARVeLHH38USqVSKJVKkZSUJJ599lnxww8/CLPZ7NAPgNBoNOLEiRP2tgMHDggA4q233rK3GQyGcttIS0sTAMSHH35ob1u6dKkAINLT00V2drZo0aKFaNSokTh9+rS9z0cffSQUCoX46aefHNa3aNEiAUDs2LHDqTGWxTlp0iSn+gtROt7x48dXOn/SpEkCgDhw4IAQQoicnBwREREhAIiEhAQxZswY8emnn4qrV686vU0i8hweGSKiSt17771IS0vDAw88gAMHDuDVV19Fz549Ua9ePaxZs8ahb3JyMho3bmyfbt26NYKDg/Hbb7/Z2wICAux/l5SU4PLly4iPj0ft2rWxd+/ectv//fff0a1bN5SUlGDbtm2Ii4uzz1u1ahVuv/12JCQk4NKlS/bXPffcAwDYvHmzU2MsO3pT0emxqiq7GLqgoAAAEBUVhQMHDmDMmDHIzc3FokWL8I9//AORkZGYPXs2hBBu2zYRuY7FEBHdUMeOHfHll18iNzcXe/bsQUpKCgoKCtC/f3+H28grOsUUGhqK3Nxc+7TRaMSMGTPst66Hh4cjIiICV69eRV5eXrnlH3/8cVy4cAFbt25FvXr1HOb9+uuv+PnnnxEREeHwatq0KQDnL/AODg4GcK1wcYeyW+WvL7BiYmKwcOFCZGdn49ixY3jzzTcRERGBGTNm4IMPPnDbtonIdbybjIicotFo0LFjR3Ts2BFNmzbF8OHDsWrVKsycORMAoFQqK1zu+qMeEydOxNKlSzF58mQkJSUhJCQEkiThkUcegc1mK7fsQw89hA8//BALFixAamqqwzybzYZWrVph3rx5FW43NjbWqXHFx8dDpVLh0KFDTvV3xuHDh6FUKtGwYcNy8yRJQtOmTdG0aVPcf//9aNKkCT755BOMGjXKbdsnItewGCIilyUmJgIAsrOzXVru888/x9ChQzF37lx7W3FxMa5evVph/4kTJyI+Ph4zZsxASEgInn/+efu8xo0b48CBA+jRowckSXJ9EH/S6/W45557sGnTJpw9e9bpIqoyWVlZ2Lp1K5KSkm566q1Ro0YIDQ11eT8SkXvxNBkRVWrz5s0VXs+ydu1aAECzZs1cWp9SqSy3vrfeesvh6c5/9e9//xvPPPMMUlJSsHDhQnv7wIEDce7cOfz3v/8tt4zRaERRUZHTcc2cORNCCDz++OMVPg06MzMTy5cvv+l6rly5gsGDB8NqteJf//qXvX337t0VxrNnzx5cvnzZ5f1IRO7FI0NEVKmJEyfCYDDg73//OxISEmA2m7Fz506sXLkSDRo0wPDhw11a3//93//ho48+QkhICJo3b460tDRs2LABderUueFyr732GvLy8jB+/HgEBQXhsccew+OPP47PPvsMY8aMwebNm9G1a1dYrVYcPXoUn332GX744Qf7Eayb6dKlC9555x2MGzcOCQkJDk+g3rJlC9asWYP//Oc/DsscP34cH3/8MYQQyM/Px4EDB7Bq1SoUFhZi3rx56NWrl73vRx99hE8++QR///vf0aFDB2g0Gvzyyy9YsmQJdDqdw5OticgHfHkrGxH5t++//16MGDFCJCQkiMDAQKHRaER8fLyYOHGiOH/+vL0fKrnVPC4uTgwdOtQ+nZubK4YPHy7Cw8NFYGCg6Nmzpzh69Gi5ftffWl/GarWKwYMHC5VKJVavXi2EEMJsNotXXnlFtGjRQmi1WhEaGio6dOggXnzxRZGXl+fyeDMzM8U//vEPUbduXaFWq0VoaKjo0aOHWL58ubBarQ7jLXspFApRu3Zt0a5dOzFp0iTx888/l1vvwYMHxdSpU0X79u1FWFiYUKlUIiYmRgwYMEDs3bvX5TiJyL0kIXhPJxEREckXrxkiIiIiWeM1Q0RUY+Xl5cFoNN6wT3R0tJeiISJ/xdNkRFRjDRs27KZ3gfErkIhYDBFRjXXkyBH88ccfN+yTnJzspWiIyF+xGCIiIiJZ4wXUREREJGuyu4DaZrPhjz/+QFBQ0C09wp+IiIi8RwiBgoIC1K1bFwqFe4/lyK4Y+uOPP275t4eIiIjIN86ePYvbbrvNreuUXTFU9sOJZ8+eRXBwsI+jqTqDwYCdO/ZBqw2HRqMBUPqDl4cP/QKFIhgCBrRs2QxC2Cps27fvICRJhXbtWkKr1aC4uNgnbQBgNpthMl1Cl67toNfrfblbq8SVXPjLfpdbLvxlHzMX/rOPmQv/2cfOthUVFeLvD9150x9ArgrZFUNlp8aCg4OrdTGkUqlQq1YtBAXVQUBA6YfTaDQgOCQHalUdWKz5qFMnAkLYKmwLCgyBpFChTp0I6HQ6GI0Gn7SVxV1QYERwcHC1/KJxJRf+st/llgt/2cfMhf/sY+bCf/axs206XWnsnrjEhRdQExERkayxGCIiIiJZYzFEREREsia7a4acZbVaUVJS4uswKmUymaBUSpAkK4QojVOSrNDpVFCpJKisSkCyQoKtwrYAvRqSVDotRAkkyeqTtrK4lUoJJpPJ7bdLAoBarYZSqXT7eomIqGZgMfQXQgjk5OTg6tWrvg7lhmw2G8LqBEKhKIYkmQAAAUqBVq2jUXrALwAajQEAKmxr1z4WgASt1gBJMiJAKXzSVha3VheIP/74wyPFEADUrl0b0dHRfLYUERGVw2LoL8oKocjISOj1er/9x9NqtcJgKIZCoYbizxhtQqC4uBgSlBCwQafTAkCFbUaDAZAkBAQEQCFJsAnhk7ayuG22Euj1OrcfwRFCwGAw4MKFCwCAmJgYt66fiIiqPxZD17FarfZCqE6dOr4O54asVissFhuUSg0UUunRFJuwwWq12QsfrVb7Z9/ybZYSCyBJ0Gq1UEgK2ITNJ23X4pag07m/GAKAgIAAAMCFCxcQGRnJU2ZEROSAF1Bfp+waoer4HAm6sbKc+vN1YERE5Bsshirgr6fGqOqYUyIiqgyLISIiIpI1FkMykJWVhcioYBw+fNDXoTile/fumDx5sq/DICIimWAxRNXOc889hwYNGqCgoMChvW/fvrjrrrtgs9l8FBkREVVHLIao2pk1axYCAwMxZcoUe9uSJUuwefNmLF261GPPKiIiopqJ/2rUEDabDfPnz0Xnzu1xW2w42rdviddee9mhz5kzp9Gnz32IjAzFPT3uQkbGHvu8y5cvY8zY0WjbtgUiI0PRsVM7rFq10mH5Xr2SMXXqFMya/QISEhqhceM4vPTSLIc+QUE6LFu2BIMHD0TDhrchKSkR3333rUOfX345goceegCRUaFo0PA2jB49HJcvX3Z6rFqtFsuXL8fy5cuxbt06ZGVl4amnnsKrr76Kxo0bO70eIiIigMXQTQkhYDBbfPISQjgd54yZ/8L8+a9jypSp+OmnPVi48L+IiIh06JOa+h88+eRT2LFjDxo1aowxY0bDYrEAAEymYrRu3QYff7wCu3dnYsTwURg9egT27st0WMenn34MvV6PtWvXY/bsl5D68kvYtGmDQ585qf/BQw89jE2btqFHj2SMGjUMV65cAQDk5eWhf/9+aN26DX7alobVq7/BhQsXMGzYUJfy0qFDB6SkpGDUqFF4/PHH0alTJ4wdO9aldRAREQF86OJNGUusaD7jB59s+8isntBrbp6igoICvPvu23j99fkYOHAwJCjRoEED3H333Q79xo6dgF69egMApj7zHLp174qTJ0/i9oTbUbduPYwbOwGQJNSqpcfYseOxfsOPWLPma7Rvn2hfR4sWLfHM088CkoRWrVph8eJF2LJlCzp3SrL3eezRxzFgwCAUFRYhJeXfeP/9xcjITEdy8r1YsuS/aNWqFV54Ybb9oYvvvvseEhLicfz4cdx+++1O75/p06dj6dKl2L17N44fP87b54mIqEp4ZKgGOHbsKEwmE7p3v/uG/Zo3b2n/OyoqGgBw8WLpz1RYrVbMm/c6unfvivr1YxAZFYqNG9fj3LnfHdbRsmUrh+no6Gj7OirqU6tWLQQHB9v7/HzkZ+zYsR3R0XUQGRWKyKhQdOjQBgBw8uRJV4aN9evXIycnBzabDenp6S4tS0REVIZHhm4iQK3EkVk9fbZtZ+h0Oqf6qdXX0l12FEWI0juv3nhjHt5//z3Mmj0H7du3R2CtQEx99mmUmM1/WYfaYVqSpHJ3b1Xcp/SUX1FREe67rydeeumVv/w2mRmNGzdyahwAkJubi9GjR2P69OkQQmDcuHHo1q0bwsPDnV4HERERwGLopiRJcupUlS/FxzdBQEAAtmzZjIEDB1dpHbt2paFnr97o338gatXSAwI4ceJXNIlv4tZYW7dqje/Wfou4uDho1BoAZb9NZi7drpMmTpyI6OhoTJs2DQDw9ddfY/z48Vi5cuVNliQiInLE02Q1gE6nw5SnnsGMGf/CZ5+twKnTvyEjIx3Lly91eh2NG8dj29YtSE/fjaNHj2Lik+PKnf5yh+HDRyI3NxfDhw9BZmYGfvvtJDZsWI9x48bCarU6tY6vvvoKq1atwvLly6FSqaBSqbB8+XKsXr0aX3zxhdtjJiKims2/D3mQ055//l9QqpR49dU5yMnJQVRUFEaOfMLp5Z999nmcPPErHnlkAPR6PUYMH4n77++L3D/vAnOX6OgYfPPN90hN/Q8eeLAPTCYTYmPro0ePHk49H+jSpUsYM2YMZs6ciZYtr10D1apVK8ycOZOny4iIyGUshmoIhUKBqVOfx/jxkyBBCQEb9PrSa4nq16+PC+fzIXDt2p6QkBDk5Fyxn5oKCwvDsmUf2+8mU0gK2IQNRYVF9mXWrdtQrm3lii8c2goKiu3Llvn99/MObY0aNcann660301Wdpqs7DqmLVu2VDrO8PBwnD9/vsJ506ZNs582IyIichZPkxEREZGssRgiIiIiWfNpMbRw4UK0bt0awcHBCA4ORlJSEr7//vsbLrNq1SokJCRAp9OhVatWWLt2rZeiJSIioprIp8XQbbfdhpdffhmZmZnIyMjAPffcgwcffBA///xzhf137tyJwYMHY+TIkdi3bx/69euHfv364fDhw16OnIiIiGoKn15A3bdvX4fpl156CQsXLsSuXbvQokWLcv0XLFiAXr16YerUqQCA2bNnY/369Xj77bexaNEir8TsC0II2P7yM2U2AQhR+sBCSOLPNgEIAPxVCo8RQsBY4vgIAIPZCpNVQG2xQbKUziu22ODCT8tRFbiaC34sPIe58B/MRdX4zd1kVqsVq1atQlFREZKSkirsk5aWhilTpji09ezZE6tXr650vSaTCSaTyT6dn5/vlni9RQiBkxeLYDBbKunh+KbXSEAddSVd6ZYIIdB/URoyz+RW0uOYw1Q9jRJDYlgReYKruYhR6TAwrLLPEN0K5sJ/MBdV5/MLqA8dOoTAwEBotVqMGTMGX331FZo3b15h37Ln51wvKioKOTk5la4/NTUVISEh9ldsbKxb4/c0m8ANCqHyzH8eHCL3M5ZYb/AlU945s4QSJsMjXM1FtkUJfuV7BnPhP5iLqvP5kaFmzZph//79yMvLw+eff46hQ4di69atlRZErkpJSXE4mpSfn1/tCqIyt8cEX/s9L5sNRYUGKJRqKBQK2ARw+qrBxxHKR8b0ZOg1pb8dZzAYsXPHXgQG1kNAQACKLTYMX3PQxxHKB3PhP5gL/8FcuMbnxZBGo0F8fDwAoEOHDkhPT8eCBQvw3nvvlesbHR1d7oF758+fR3R0dKXr12q10Gq17g3aRxSSBKXizzO8ApCk0jYFJPt1Q+Qdeo3y2m/WWZTQKiXoVAroVM79uC65D3PhP5gL/8FcuMbnp8n+ymazOVzjc72kpCRs3LjRoW39+vWVXmNEpbKyshAZFYzDh6vH/wl0794dkydP9nUYREQkEz4thlJSUrBt2zacPn0ahw4dQkpKCrZs2YJHH30UADBkyBCkpKTY+0+aNAnr1q3D3LlzcfToUbzwwgvIyMjAhAkTfDUE8rLZs2cjJiYGV/7ym2kHDhyAVqvFt99+66PIiIiouvJpMXThwgUMGTIEzZo1Q48ePZCeno4ffvgB9957L4DSIxrZ2dn2/l26dMGnn36KxYsXo02bNvj888+xevVqhx/spJotJSUFsbGxGD9+vL2tpKQEQ4cOxWOPPYb/+7//82F0RERUHfm0GPrggw9w+vRpmEwmXLhwARs2bLAXQkDpD3YuW7bMYZkBAwbg2LFjMJlMOHz4MPr06ePlqP2TzWbD0oUL8H93tkdsbDjat2+J11572aHPmTOn0afPfYiMDMU9Pe5CRsYe+7zLly9jzNjRaNu2BSIjQ9GxUzusWrXSYflevZIxdeoUzJr9AhISGqFx4zi89NIshz5BQTosW7YEgwcPRMOGtyEpKRHffed4tOaXX47goYceQGRUKBo0vA2jRw/H5cuXnRqnSqXChx9+iNWrV+Pzzz8HUPp8qqtXr2L+/PlO7y8iIqIyfnfNkN8RAjAX+eblwlP7Zs6cjiXvvIEnJk3FTz/twcKF/0VERKRDn9TU/+DJJ5/Cjh170KhRY4wZMxoWS+mNlSZTMVq3boOPP16B3bszMWL4KIwePQJ792U6rOPTTz+GXq/H2rXrMXv2S0h9+SVs2rTBoc+c1P/goYcexqZN29CjRzJGjRpmP62Vl5eH/v37oXXrNvhpWxpWr/4GFy5cwLBhQ50ea0JCAlJTUzF27Fj88MMPSE1NxdKlSxEcHOz0OoiIiMr4/G4yv1diAObU9c22p/0BqPQ37VZQUICF776N52e/igcGDEa0RokGDRrg7rvvdug3duwE9OrVGwAw9Znn0K17V5w8eRK3J9yOunXrYdzYCYAkoVYtPcaOHY/1G37EmjVfo337RPs6WrRoiWeefhaQJLRq1QqLFy/Cli1b0LnTtYvYH3v0cQwYMAhFhUVISfk33n9/MTIy05GcfC+WLPkvWrVqhRdemA2FVFqLv/vue0hIiMfx48dx++23O7VrJk2ahK+//hp9+vTBxIkTy42ViIjIWTwyVAMcO3YUJpMJnbp2u2G/5s2vXVsVFVX6OIKLFy8AKH0C+Lx5r6N7966oXz8GkVGh2LhxPc6d+91hHS1btnKYjo6Otq+joj61atVCcHCwvc/PR37Gjh3bER1dB5FRoYiMCkWHDm0AACdPnnR6zJIk4V//+hdsNhumT5/u9HJERER/xSNDN6PWlx6h8dW2nThTptPpnFud+lq6pT8f3iiEDQDwxhvz8P7772HW7Dlo3749AmsFYuqzT6PEbP7LOhx/60OSJNhsNif6lA6kqKgI993XEy+99Mq1B0gKAZvNjMaNGzk1jjIqlcrhv0RERFXBf0VuRpIATS3fbd+J64bi45sgICAAe3ZsxW31h1RpM7t2paFnr97o338gatUqLcJOnPgVTeKbVGl9lWndqjW+W/st4uLioFFrAAA2YYPVai7dLhERkZfxNFkNoNPp8NRTz2D+SzPxzecrcPr0b8jISMfy5UudXkfjxvHYtnUL0tN34+jRo5j45Lhyp7/cYfjwkcjNzcXw4UOQmZmB3347iQ0b1mPcuLGwWq03XwEREZGb8chQDfHc89OQX2LDu3Pn4MVnS3/QduTIJ5xe/tlnn8fJE7/ikUcGQK/XY8Twkbj//r7I/cvDDW9VdHQMvvnme6Sm/gcPPNgHJpMJsbH10aNHDygUrM2JiMj7WAzVEAqFAqOffAajn3wG0RolABv0+tJrierXr48L5/MhcO3anpCQEOTkXLGfmgoLC8OyZR/b7yZTSArYhA1FhUX2Zdat21CubeWKLxzaCgqK7cuW+f338w5tjRo1xqefrrTfTVZ2mqzsOqYtW7Y4Nebu3btDuPD4ASIioorwf8WJiIhI1lgMERERkayxGCIiIiJZYzFEREREssZiiIiIiGSNxRARERHJGoshIiIikjUWQ0RERCRrLIaIiIhI1vgEaieZzWZYLBavbU+lUkGj0Xhte0RERHLFYsgJZrMZe/bsQ1GhyWvbrBWoRadO7aBUqb22TSIiIjliMeQEi8WCokITtNoIaDRaj2/PbDahqPAiLBYLiyEiIiIP4zVDLtBotAgI0Hv85WrBdfHiRTRuVB/vvzXX3paevhthYUHYsmXTDZedM2c27khKxP/+9wkSO7ZFkyZxGDbscRQUFNj72Gw2vP76q2jeoikiImrjnh534ZtvvnZYzw8/fI+kpESEh4egd+978cknHyE6pg7y8vJcGgsREZG3sRiqASIiIvDuwvewcP4r+PnAPhQWFmD8+H/iiSfGonv3e266/KlTv+Hbb7/BRx/9Dx99tALbt/+EuXNftc9/8835+N//PsGCBW9jz569eOKJMZgwYQy2b98GADh9+hRGjR6OXr36YOfOdIwYMQqzZr3gmcESERG5GU+T1RA9e/bGw4OHIOXJJ9ChbTvo9Xq8+OJsp5a12WxYtOi/UEgKQJLwyCP/wJatmzEDL8JkMmHBgvn45pu1SLqjC2zChkcGRWHPnt1YsuR93PW37liy5AM0bhyPmTNnoVYtPRKaJeDnI4fx2muveHjUREREt47FUA0yZfpsPJzcBd98sxo//rgFWq1zp9vi6schKCgIRYVFAIDo6GhcvHgRAHDq9CkYjQY8+OD99v5CACUlZrRp0xYA8Ouvx9G2bTuHdXbo0NENIyIiIvI8FkM1yNkzp3DxfA5sNhvOns1CYmIHp5ZTqR0v0pYkCTabDQBgKCoEAHz++VeoV/c22ISA0WAAJAm1a4e4dwBEREQ+wGLIzwghICBgFQJWm4BNCKeWM5vN+Nekf6Jn37+jZdOmmDLlSXTp2hURERGAACBVLZ6mTZtBq9Xi7NmzuOtv3WETttIjSJKEWrX0AIAmTZpi3bq1Dsvt3ZtRtQ36ESEEjCVW+7TBbL1B74qVCAHJJlBssUEIG4SocipkzT25AIotNsBi/TMfzEVVMBf+g7lwHxZDfkQIgd+vGpFXUISiQgOOZOchIKDEqWVffHEGCgry8dysl6GvFYjv1/+A4U+MwtvLVkIjAXWqeId+YGAQxo6dgOeffxZCCNxxRxLO55zHnvQ9CA+vg8cfG4oRI0bi7bcXYPbsFzBy5GgcPnQQn3zyEYDSo0zVkRAC/RelIfNM7i2tZ8HZP+/KO/cLACBGpcPAMO89vLMmcFcuFl/UYvHaX+zTzIXrmAv/wVy4F4shF5jNnn3oooBAXkERSkrKb6eWRgVFJXXFtm1b8e47b2H5qm8RGBQMAHhpwXsY2PNOfPbhBxg4ZCScO75Useeem4aYmBjMff1VnDp9CsHBIWjdujWefTYFANCgQUO8/9+leOHFGXj//ffQudMdmDr1eUyePLHaPkXbWGKt9EsmMS4UAWplpctqlQo0DQ3A8VxjuXnZFiUskN8Xza1gLvwHc+E/mAv3YjHkBJVKhVqBWhQVXoTJg/WQVQgUFRoAAAn1a6N1bB17MaGQKj/Kctdd3ZB31QCbzYoiQzEkSYmoRvXx++8XcCa/+IbbnDbt35j+r5mwCZu9bfz4iZg4YZK9TZIkjBs3ARPGP1nhaTKg9G62nr36oFYtPRSSAq+8Ogd169aFTqe7pX3iDzKmJ0OvufbFEqBW3vCIlyRJmNk1Dnv2HoZaFQaLNR8JLZthzA/HvBFujVbVXOzYlQlJoUJiYltApcbwNQe9EG3Nxlz4D+bi1rEYcoJGo0GnTu08/ttkVpvAkezShxS2jq2DAJ2LT7uWJEjSn+d7Jcmrp6iWLvsAbdu2R716dbF79y4sWDAfw4eN9Nr2PUmvUUKvce2jIkkSNApArZCgEBK0Sj7Syx2qmgu1BEgSoFMpABVz4Q7Mhf9gLm4diyEnaTQaj5/ysdqE/Rohd23r7z2SkP37WfsFcWX1kRDAa6/Nw5AhQ92ynVO//YY33piHq1dzERsbi4kTJ2HMP8e7Zd1ERESexGKohntn+UpYSiyI0CgACOj+PNpkNBgQERnptu3MmvUSZs2eYz9NZj+dRkRE5OdYDNVwdW+rDwCI1igB2KDXl17DU3bdDxERkdzJ+yRhJYSTz/ah6oM5JSKiyrAYuo76zycxGwwGH0dC7laWU7W6ig9cIiKiGounya6jVCpRu3ZtXLhwAQCg1+u9ekeW1SYgLGYAQHFxMZSVPVgIgNVqhdlshkIhoPgzRpsQKCkxQ4ISAjYolJJ9fSVS6Wkyk6m0b0mJGZAkmExKKCTJvqy328rittlKUFysgFJZ+bMxqkIIAYPBgAsXLqB27dpuXz8REVV/LIb+Ijo6GgDsBZE32YTAhaulzwVSGXT2YqHCvjYbTCYzFAqVvWATQsBsNqP0gJ+AWq3GJWPp3WkmlQRAQKMpPTJiMpkASNBqNZAkCUIIn7SVxW2zWaDVaqBQeOZgZe3ate25JSIiuh6Lob+QJAkxMTGIjIxESYlzP4XhLkazBU98tR0A8O3EOxFwg+dGGI1GZGYcRq1a0dBqSy+KNpmKceRIFlSq2rBaC9G4aSO8sO0cAGB4TCAUogi3N48HhA0HDpyAJCnRuk0LaDUamEzFPmkri7uoKAcdElsiICDA7ftVrVbziBAREVWKxVAllEql1/8BtSksOFdQ+kN7Wp0OuhsUQzabDVargBBKSFLp0R4hSlBcbIFaJWCxWmETSvv6ikMFFMIKCCWEkGA0lEBSCODP5YUo8UlbWdxWq4BWq60RT6wmIqLqhRdQExERkaz5tBhKTU1Fx44dERQUhMjISPTr1w/Hjt3495uWLVsG6c+fmih78WgCERERVZVPi6GtW7di/Pjx2LVrF9avX4+SkhLcd999KCq68ZOLg4ODkZ2dbX+dOXPGSxETERFRTePTa4bWrVvnML1s2TJERkYiMzMTd911V6XLSZLEO4OIiIjILfzqmqG8vNJfbA8LC7thv8LCQsTFxSE2NhYPPvggfv7550r7mkwm5OfnO7yIiIiIyvhNMWSz2TB58mR07doVLVu2rLRfs2bNsGTJEnz99df4+OOPYbPZ0KVLF/z+++8V9k9NTUVISIj9FRsb66khEBERUTXkN8XQ+PHjcfjwYaxYseKG/ZKSkjBkyBC0bdsW3bp1w5dffomIiAi89957FfZPSUlBXl6e/XX27FlPhE9ERETVlF88Z2jChAn49ttvsW3bNtx2220uLatWq9GuXTucOHGiwvlarRZardYdYRIREVEN5NMjQ0IITJgwAV999RU2bdqEhg0burwOq9WKQ4cOISYmxgMREhERUU3n0yND48ePx6effoqvv/4aQUFByMnJAQCEhITYf5ZhyJAhqFevHlJTUwEAs2bNwh133IH4+HhcvXoVr732Gs6cOYNRo0b5bBxERERUffm0GFq4cCEAoHv37g7tS5cuxbBhwwAAWVlZDj/emZubi9GjRyMnJwehoaHo0KEDdu7ciebNm3srbCIiIqpBfFoMCSFu2mfLli0O0/Pnz8f8+fM9FBERERHJjd/cTUZERETkCyyGiIiISNZYDBEREZGssRgiIiIiWWMxRERERLLGYoiIiIhkjcUQERERyRqLISIiIpI1v/ihVjkTQsBYYgUAGMxWj22nRAhINoFiiw1C2CAEIHlsa9XP9XkAPJ0LoNhiAyzWP/PBXFyPufAfzIX/YC48i8WQDwkh0H9RGjLP5Hp8WwvOFpT+ce4XAECMSoeBYRaPb7c68GYeAGDxRS0Wr/3FPs1cXMNc+A/mwn8wF57H02Q+ZCyxVvjmTowLRYBaecvr1yolNA0NqHBetkWJmv3Wdl5leQCYC2/zTi4UzIUTmAv/wVx4Ho8M+YmM6cnQa0rf0AFqJSTp1g9KSpKEmV3jsGfvYahVYbBY85HQshnG/HDsltddU12fB8D9udixKxOSQoXExLaASo3haw7e8rprKubCfzAX/oO58AwWQ35Cr1FCr3F/OiRJgkYBqBUSFEKCVsmDgTfiqTwApblQS4AkATqVAlAxFzfCXPgP5sJ/MBeeIZ+REhEREVWAxRARERHJGoshIiIikjUWQ0RERCRrLIaIiIhI1lgMERERkayxGCIiIiJZYzFEREREssZiiIiIiGSNxRARERHJGoshIiIikjUWQ0RERCRrLIaIiIhI1lgMERERkayxGCIiIiJZYzFEREREssZiiIiIiGSNxRARERHJGoshIiIikjUWQ0RERCRrLIaIiIhI1lgMERERkayxGCIiIiJZYzFEREREssZiiIiIiGSNxRARERHJGoshIiIikjWfFkOpqano2LEjgoKCEBkZiX79+uHYsWM3XW7VqlVISEiATqdDq1atsHbtWi9ES0RERDWRT4uhrVu3Yvz48di1axfWr1+PkpIS3HfffSgqKqp0mZ07d2Lw4MEYOXIk9u3bh379+qFfv344fPiwFyMnIiKimkLly42vW7fOYXrZsmWIjIxEZmYm7rrrrgqXWbBgAXr16oWpU6cCAGbPno3169fj7bffxqJFizweMxEREdUsPi2G/iovLw8AEBYWVmmftLQ0TJkyxaGtZ8+eWL16tSdDcwshBIwlVvu0wWy9QW/PKxFAscUGWKwottggBCD5NCLvuT4Xvs4DIN9c+NtnAmAuyjAXfxICCqsRSmsxYDYAKuGFTZbPRQCKSyfMRXD5n25zafwKqxEKS2mTwmqESpggCSsUFgMUFlu5NkB9bbs2M8ymIihQArOpGEqbCQqp8mU90mYx3PK+rYzfFEM2mw2TJ09G165d0bJly0r75eTkICoqyqEtKioKOTk5FfY3mUwwmUz26fz8fPcE7CIhBPovSkPmmVyfbL8iiy9qsXjtL/bpGJUOA8MsPozIO5gL/+CPeQCYC3/i81wIgSbbByMwd1/pdLp3NisB0F83rQfwi+7PidddX58eQHIF7XeU/bGp8jb7dgsBbL3Wr7sTy7q7Ld/kuULUb+4mGz9+PA4fPowVK1a4db2pqakICQmxv2JjY926fmcZS6yVftEkxoUiQK30ShxapYSmoQEVzsu2KFGzv/JLVZYL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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "_=plot(snake_cds, parameters=default_params, sample_rate=1, plot_measurements='x_neg')" + ] + }, + { + "cell_type": "markdown", + "id": "aa0df99d", + "metadata": {}, + "source": [ + "When we try to instantiate such a pulse it will take some time but not severe yet. However it is not a set of parameters with physical meanings. In general, a bandwidth of charge sensing falls in relatively low frequency range say slower than MHz. In addition, a voltage resolution of mV is required to resolve the charge occupancy of a quantum dot. Therefore the following parameters are used in a real experiment:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "f7213346", + "metadata": {}, + "outputs": [], + "source": [ + "exp_params = {\n", + " 'n_segments': 192, # empirically determined.\n", + " 'x_start': -50e-3,\n", + " 'x_stop': 50e-3,\n", + " 'y_start': 0,\n", + " 'y_stop': 0.1, \n", + " 'N_x': 100, # voltage resolution: 1 mV\n", + " 'N_y': 100, # voltage resolution: 1 mV\n", + " 'sample_rate': 3.125e-3, # AWG sample rate: 3.125 MHz\n", + " 'cds_res': 1e6 # time resolution: 1 ms\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "2dd4c86d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Elapsed time: 0.05933679999998276 seconds\n", + "Elapsed time: 7.700837200000024 seconds\n" + ] + } + ], + "source": [ + "# Try with different time/voltage resolution or AWG sample rate by yourself!\n", + "\n", + "import timeit\n", + "# using arbitary parameters for simplicity. \n", + "simple_inst = timeit.timeit(lambda: snake_cds.create_program(parameters=default_params), number=1)\n", + "print(f'Elapsed time: {simple_inst} seconds')\n", + "\n", + "# in a real experiment:\n", + "exp_inst = timeit.timeit(lambda: snake_cds.create_program(parameters=exp_params), number=1)\n", + "print(f'Elapsed time: {exp_inst} seconds')\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "7301ba97", + "metadata": {}, + "source": [ + "## Task 2: Simplify the pulse\n", + "\n", + "With the current implementation the time consumption to instantiate pulse seems unecessarily long. But which step slows down the whole process significantly? Is there any space for improvement? \n", + "\n", + "### Description:\n", + "The main concern by now is building everything by qupluse including the measurement window. Therefore the resolution of data acquisation and the pulse interpolation are coupled resulting in a sophisticated construction of pulse for a trivial task. \n", + "\n", + "### Goal:\n", + "In the following section, we will try to explore a way to optimize the pulse by simplifying the definition of measurement windows.\n", + "\n", + "Supposing that the instrument for data acquisation can make on-fly process according to the measurement windows those defined in pulse templates. One needs only forward the starting time of a pulse and its time duration such that the data acquisation card will down-sample the raw data according to customized settings such as time resolution. In that case explicitly interpolating pulses is not necessary thus the nest level of pulse templates is simplified.\n", + "\n", + "Let's firsly simplify the nest level of a linear sweep. " + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "d2962063", + "metadata": {}, + "outputs": [], + "source": [ + "from qupulse.pulses import PointPT, ConstantPT, AtomicMultiChannelPT\n", + "import sympy\n", + "\n", + "tx_sweep = sympy.sympify('tx_sweep')\n", + "\n", + "# equivalently: tx_sweep = N_x * cds_res but now qupulse only takes care of the duration per scanline.\n", + "# Because N_x: number of data points per scanline and cds_res: time resolution per data point\n", + "# are only required by data aquisation device instead of qupulse.\n", + "\n", + "# make a linear sweep of channel X with 1 measurement window for the whole sweep time\n", + "# meanwhile define channel y that holding on at a voltage level during x_ramp.\n", + "scan_line = AtomicMultiChannelPT(\n", + " PointPT([(0, 'x_start'),\n", + " (tx_sweep, 'x_stop', 'linear')], channel_names='X'),\n", + " ConstantPT(tx_sweep, {'Y': 'y_start + y_step * y_i'}),\n", + " measurements=[('M', 0, tx_sweep)])" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "c323ae3d", + "metadata": {}, + "source": [ + "A complete bi-directional charge scan can be assembled as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "6c207d01", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'N_y', 'x_stop', 'y_start', 'x_start', 'y_stop', 'tx_sweep'}\n" + ] + } + ], + "source": [ + "from qupulse.pulses import MappingPT, TimeReversalPT, ForLoopPT\n", + "\n", + "pos_sweep = MappingPT(scan_line, measurement_mapping={'M': 'x_pos'})\n", + "neg_sweep = MappingPT(TimeReversalPT(scan_line), measurement_mapping={'M': 'x_neg'})\n", + "\n", + "loop = ForLoopPT(pos_sweep @ neg_sweep, 'y_i', 'N_y')\n", + "\n", + "snake_cds = MappingPT(loop, parameter_mapping={'y_step': '(y_stop - y_start) / (N_y - 1)'})\n", + "print(snake_cds.parameter_names)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "560d499a", + "metadata": {}, + "source": [ + "Now the `inner_loop` in task 1 is simplified by using the keyword `linear` provided by `PointPT` and has less complexity of defining measurement windows. We can now plot the pulse again by `qupulse.pulse.plotting.plot`:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "0221f05d", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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u+fXqlUL586dk/moEJEcWOwQkSK0atXK9mc/Pz/ccsstaNmypW1ZZGQkAODixYsAShqNt2zZYusBCgkJQZMmTQAAp06dKvM18vPzYTQay2yivvH1rR+9WV9r8ODBOHToEBo3boynnnoKGzduLPX8oKAg21UnIlIWg+gBEBEBgL+/v93vOp3Obpm1QLFYLACAnJwc9O3bF6+++mqpbUVHR5f5GhEREcjLy0NhYSECAgIqfX3ra7Vp0wZnzpzBt99+i++//x4PPfQQunXrhlWrVtnWv3r1KmrUqOHo7hKRF7HYISJVatOmDVavXo369evDYHDsr7LWrVsDAH799Vfbnx0VFhaGAQMGYMCAAfj3v/+Nnj174urVq6hevTqAkmbphIQEp7ZJRN7Bj7GISJXGjBmDq1ev4pFHHsHevXtx6tQpfPfddxgyZAjMZnOZz6lRowbatGmDHTt2OPVac+fOxWeffYZjx47hxIkTWLlyJaKiouy+J+iHH35A9+7d3dklIvIQFjtEpEq1atXCjz/+CLPZjO7du6Nly5YYP348qlatCr2+/L/ahg8fjk8//dSp1woNDcWsWbPQrl07tG/fHmfPnsU333xje51du3YhMzMT//73v93aJyLyDH6pIBH5lPz8fDRu3BgrVqxAYmKiLNscMGAA4uPjMXnyZFm2R0Ty4pUdIvIpQUFB+Oijjyr88kFnFBYWomXLlpgwYYIs2yMi+fHKDhEREWkar+wQERGRprHYISIiIk1jsUNERESaxmKHiIiINI3FDhEREWkaix0iIiLSNBY7REREpGksdoiIiEjTWOwQERGRpv0/IbrKW3xuVXQAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from qupulse.pulses.plotting import plot\n", + "\n", + "default_params = {\n", + " 'tx_sweep': 5,\n", + " 'N_y': 5,\n", + " 'x_start': 0,\n", + " 'x_stop': 3,\n", + " 'y_start': 0,\n", + " 'y_stop': 2,\n", + "}\n", + "_ = plot(snake_cds, parameters=default_params, plot_measurements=('x_pos','x_neg'))" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "2c15dbe5", + "metadata": {}, + "source": [ + "It is an obviously simple pulse definition, but how much time efficient do we gain from it remains unclear. Let's inspect the elapsed time of `creat_program`. " + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "67c5ecbc", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Elapsed time: 0.00655929999993532 seconds\n", + "Elapsed time: 0.09900640000023486 seconds\n" + ] + } + ], + "source": [ + "import timeit\n", + "\n", + "exp_params = {\n", + " 'tx_sweep': 100e6, # 100 ms each scanline\n", + " 'x_start': -50e-3,\n", + " 'x_stop': 50e-3,\n", + " 'y_start': 0,\n", + " 'y_stop': 0.1, \n", + " 'N_x': 100, # voltage resolution: 1 mV\n", + " 'N_y': 100, \n", + "}\n", + "\n", + "# using arbitary parameters for simplicity. \n", + "simple_inst = timeit.timeit(lambda: snake_cds.create_program(parameters=default_params), number=1)\n", + "print(f'Elapsed time: {simple_inst} seconds')\n", + "\n", + "# in a real experiment:\n", + "exp_inst = timeit.timeit(lambda: snake_cds.create_program(parameters=exp_params), number=1)\n", + "print(f'Elapsed time: {exp_inst} seconds')" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "0d7283f2", + "metadata": {}, + "source": [ + "## Task 3: Use convenient functions (with_*)\n", + "\n", + "### Description:\n", + "Equivalently the `scan_line` can be constructed by the helper functions `with_` since version 7.0.x thus only an atomic pulse template is required to build an elementary pulse. All the other modifications used above can be then replaced by `with_` functions for conveniency.\n", + "\n", + "### Goal:\n", + "Trying to use all available `with_*` functions to replace pulse templates used in Task 1, 2." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "fee666ed", + "metadata": {}, + "outputs": [], + "source": [ + "from qupulse.pulses import PointPT\n", + "import sympy\n", + "\n", + "tx_sweep = sympy.sympify('tx_sweep')\n", + "\n", + "# use helper functions\n", + "forward = PointPT([(0, 'x_start'),\n", + " (tx_sweep, 'x_stop', 'linear')],\n", + " channel_names=('X', 'Y'),\n", + " measurements=[('M', 0, tx_sweep)])\n", + "\n", + "backward = forward.with_time_reversal().with_mapping(measurement_mapping={'M': 'x_neg'})\n", + "forward = forward.with_mapping(measurement_mapping={'M': 'x_pos'})\n", + "\n", + "scan_x = forward.with_appended(backward)\n", + "scan_line = scan_x.with_parallel_channels({'Y': 'y_start + y_step * y_i'})\n", + "scan_line = scan_line.with_mapping(parameter_mapping={'y_step': '(y_stop - y_start) / (N_y - 1)'})\n", + "\n", + "snake_cds = scan_line.with_iteration('y_i', 'N_y')" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "7e996a57", + "metadata": {}, + "source": [ + "Here the `TimeReversalPT` is replaced by `with_time_reversal()`. Similarly, a `with_mapping()` function call can map `parameters`, `channel names` and `measurement names` as simple as using a `MappingPT` with corresponding keywords. `with_appended`, `with_parallel_channels` and `with_iteration` are the replacements of `@` or `SequencePT`, `ParallelChannelPT` and `ForLoopPT` respectively. Now we can plot the pulse with default parameters again and complete this example by in total 3 different approaches." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "f0daf1dd", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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3HPPITExEampqTCZTIiOjsaoUaOQmJiIadOm4dKlSzh48CCAsj6Ztm3bYtasWWjWrBnatm2LpUuXYvPmzTh27FiFz3H69GkEBASgdu3a5e679957MWbMGCQkJGDy5MmIjo7G5s2bAQAZGRlITEzEbbfdhvj4eNx222146KGHHB5fp04dnD59WuZXhYjkwGKHiBShdevW9t8DAgJw0003oVWrVvZlMTExAIDz588DKGs03rx5s70HKCwsDM2aNQMAnDhxosLnKCwshNForLCJ+sbnt330ZnuuoUOH4sCBA2jatCmefPJJfPvtt+UeHxwcbD/rRETKYhA9ACIiAAgMDHS4rdPpHJbZChSr1QoAyMvLQ9++ffHyyy+X21ZcXFyFzxEdHY2CggIUFxcjKCio2ue3PVe7du1w8uRJfP311/juu+/wwAMPoEePHli1apV9/cuXL6NWrVrO7i4R+RCLHSJSpXbt2mH16tVo0KABDAbn/pS1adMGAPDrr7/af3dWREQEBg4ciIEDB+Jf//oXevXqhcuXL6NmzZoAypql27Zt69I2icg3+DEWEanS2LFjcfnyZTz00EPYvXs3Tpw4gW+++QbDhg2DxWKp8DG1atVCu3btsH37dpee69VXX8Unn3yCI0eO4NixY1i5ciViY2Mdvifo+++/xz333OPJLhGRl7DYISJVqlOnDn744QdYLBbcc889aNWqFSZMmICoqCjo9ZX/aRs5ciT++9//uvRc4eHhmDNnDjp06ICOHTvi1KlT+Oqrr+zPs2PHDly9ehX/+te/PNonIvIOfqkgEfmVwsJCNG3aFMuXL0dycrIs2xw4cCCSkpIwdepUWbZHRPLimR0i8ivBwcH48MMPq/zyQVcUFxejVatWmDhxoizbIyL58cwOERERaRrP7BAREZGmsdghIiIiTWOxQ0RERJrGYoeIiIg0jcUOERERaRqLHSIiItI0FjtERESkaSx2iIiISNNY7BAREZGm/T/l/PQ0YEMlagAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from qupulse.pulses.plotting import plot\n", + "\n", + "default_params = {\n", + " 'tx_sweep': 5,\n", + " 'N_y': 5,\n", + " 'x_start': 0,\n", + " 'x_stop': 3,\n", + " 'y_start': 0,\n", + " 'y_stop': 2,\n", + "}\n", + "_ = plot(snake_cds, parameters=default_params, plot_measurements=('x_pos','x_neg'))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.7.9 ('my_expDev': venv)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3" + }, + "vscode": { + "interpreter": { + "hash": "0187fe0cb56dae19ef91679276d2cfcb20275eec37c3a24cf7a15a9f8efe135c" + } + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 144bc5b7cc07a307ed3ef2e22b2b087e998d5635 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 3 Jan 2023 16:04:22 +0100 Subject: [PATCH 015/441] Add vscode to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7124aaa3e..8ebe24db7 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ MATLAB/+qc/personalPaths.mat .idea/ .mypy_cache/* tests/hardware/WX2184C.exe +.vscode/* From 6bb7c084f6202f9e2feee2d8769b2e0944066af9 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 3 Jan 2023 17:24:51 +0100 Subject: [PATCH 016/441] First draft of learners guide --- doc/source/learners_guide.rst | 62 +++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 doc/source/learners_guide.rst diff --git a/doc/source/learners_guide.rst b/doc/source/learners_guide.rst new file mode 100644 index 000000000..050d3745a --- /dev/null +++ b/doc/source/learners_guide.rst @@ -0,0 +1,62 @@ +Learners Guide - writing pulses with qupulse +--------------------- + +This is a little guide through the documentation of qupulse with the idea that *you* as an interested person can find the materials corresponding to the desired skills. + +The following steps assume that you have qupulse installed and are able to run the example notebooks. + + +Basic pulse writing +^^^^^^^^^^^^^^^^^^^ + +.. topic:: Info + + **Estimated time:** + 30 minutes for reading + 60 minutes for the examples + 60 minutes for experimenting + + **Target group:** + + **Learning Goals:** The learner is able to define and save a parameterized nested pulse template. The learner can use pulse identifiers measurement windows and parameter constraints as needed. The learner is able to verify pulse and measurement windows are as intended for a given parameter set by plotting and inspecting. The learner can load pulses from a file and other valid datasources and use them as a building block in their own pulses. + +**Learning Task 1:** Read the concept section about :ref:`concept/pulsetemplates`. + +**Exercise Task 1:** Go through the following examples that introduce the shipped atomic pulse templates: + +* :ref:`/examples/00SimpleTablePulse.ipynb` +* :ref:`/examples/00AdvancedTablePulse.ipynb` +* :ref:`/examples/00FunctionPulse.ipynb` +* :ref:`/examples/00PointPulse.ipynb` +* :ref:`/examples/00ConstantPulseTemplate.ipynb` + +**Exercise Task 2:** Go through the following examples that introduce the most important composed pulse templates: + +* :ref:`/examples/00ComposedPulses.ipynb` +* :ref:`/examples/00MappingTemplate.ipynb` +* :ref:`/examples/00MultiChannelTemplates.ipynb` + +**Exercise Task 3:** Go through the following examples that introduce other useful pulse templates: + +* :ref:`/examples/00ArithmeticWithPulseTemplates.ipynb` +* :ref:`/examples/00RetrospectiveConstantChannelAddition.ipynb` +* :ref:`/examples/00TimeReversal.ipynb` + +**Learning Task 2:** Read the concept section about :ref:`serialization`. + +**Exercise Task 4:** Go through the :ref:`/examples/01PulseStorage.ipynb` example. It shows how to load and store pulse templates to disk. + +**Exercise Task 5:** Go through the :ref:`/examples/01Measurements.ipynb` example. It shows how to define and inspect measurement windows. + +**Exercise Task 6:** Go through the :ref:`/examples/01ParameterConstraints.ipynb` example. It shows how to use parameter constraints to enforce invariants. + + +Hardware limitations +^^^^^^^^^^^^^^^^^^^^ + +This section is under construction. + +Setup an experiment +^^^^^^^^^^^^^^^^^^^ + +This section is under construction. There is currently an outdated example :ref:`/examples/_HardwareSetup.ipynb` From d308c0f2a3c5bc9f1178ea772d9cc73279e5f096 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 3 Jan 2023 17:27:08 +0100 Subject: [PATCH 017/441] Link learners guide in index --- doc/source/index.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/source/index.rst b/doc/source/index.rst index 6d653bce7..e3c4a0961 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -10,6 +10,8 @@ Welcome to qupulse's documentation! You are encouraged to read the concept explanations and interactively explore the linked examples. To do this you can install qupulse via ``python -m pip install -e git+https://github.com/qutech/qupulse.git#egg=qupulse[default]`` which will clone the qupulse into ``./src/qupulse``. You can find the examples in ``doc/source/examples`` and open them with jupyter, Spyder or another IDE of your choice. +There is a :ref:`learners_guide` available to help with an efficient exploration of qupulse's features. + Contents: .. toctree:: From b6df54b6dc0e123f9528cc93254bb3c4fde6980b Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 3 Jan 2023 17:35:21 +0100 Subject: [PATCH 018/441] Fix some labels and links --- doc/source/index.rst | 3 ++- doc/source/learners_guide.rst | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/doc/source/index.rst b/doc/source/index.rst index e3c4a0961..cd332a029 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -10,7 +10,7 @@ Welcome to qupulse's documentation! You are encouraged to read the concept explanations and interactively explore the linked examples. To do this you can install qupulse via ``python -m pip install -e git+https://github.com/qutech/qupulse.git#egg=qupulse[default]`` which will clone the qupulse into ``./src/qupulse``. You can find the examples in ``doc/source/examples`` and open them with jupyter, Spyder or another IDE of your choice. -There is a :ref:`learners_guide` available to help with an efficient exploration of qupulse's features. +There is a :ref:`learners guide ` available to help with an efficient exploration of qupulse's features. Contents: @@ -21,6 +21,7 @@ Contents: concepts/concepts examples/examples _autosummary/qupulse + learners_guide qupulse API Documentation ========================= diff --git a/doc/source/learners_guide.rst b/doc/source/learners_guide.rst index 050d3745a..ddbe30a95 100644 --- a/doc/source/learners_guide.rst +++ b/doc/source/learners_guide.rst @@ -1,5 +1,7 @@ +.. _learners_guide: + Learners Guide - writing pulses with qupulse ---------------------- +-------------------------------------------- This is a little guide through the documentation of qupulse with the idea that *you* as an interested person can find the materials corresponding to the desired skills. @@ -20,7 +22,7 @@ Basic pulse writing **Learning Goals:** The learner is able to define and save a parameterized nested pulse template. The learner can use pulse identifiers measurement windows and parameter constraints as needed. The learner is able to verify pulse and measurement windows are as intended for a given parameter set by plotting and inspecting. The learner can load pulses from a file and other valid datasources and use them as a building block in their own pulses. -**Learning Task 1:** Read the concept section about :ref:`concept/pulsetemplates`. +**Learning Task 1:** Read the concept section about :ref:`pulsetemplates`. **Exercise Task 1:** Go through the following examples that introduce the shipped atomic pulse templates: From cf267411bed988afe793b3813031a6e420e1523b Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 3 Jan 2023 18:12:51 +0100 Subject: [PATCH 019/441] Use lazy loader for all root submodules --- qupulse/__init__.py | 9 ++++++--- qupulse/__init__.pyi | 24 ++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 qupulse/__init__.pyi diff --git a/qupulse/__init__.py b/qupulse/__init__.py index 33df6de57..6cae787b1 100644 --- a/qupulse/__init__.py +++ b/qupulse/__init__.py @@ -1,7 +1,10 @@ """A Quantum compUting PULse parametrization and SEquencing framework.""" -from qupulse.utils.types import MeasurementWindow, ChannelID -from . import pulses +import lazy_loader as lazy __version__ = '0.7' -__all__ = ["MeasurementWindow", "ChannelID", "pulses"] + +__getattr__, __dir__, __all__ = lazy.attach_stub(__name__, __file__) + +# we explicitly import qupulse to register all deserialization handles +from qupulse import pulses diff --git a/qupulse/__init__.pyi b/qupulse/__init__.pyi new file mode 100644 index 000000000..889724cb0 --- /dev/null +++ b/qupulse/__init__.pyi @@ -0,0 +1,24 @@ + +from . import pulses +from . import hardware +from . import utils +from . import _program + +from . import comparable +from . import expressions +from . import parameter_scope +from . import serialization + +from .utils.types import MeasurementWindow, ChannelID + +__all__ = ['pulses', + 'hardware', + 'utils', + '_program', + 'comparable', + 'expressions', + 'parameter_scope', + 'serialization', + 'MeasurementWindow', + 'ChannelID', + ] From 3ad32fddeb5435f2678c9124ac379c6e4670633f Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 3 Jan 2023 18:19:34 +0100 Subject: [PATCH 020/441] Move plotting module to root package --- qupulse/__init__.pyi | 2 + qupulse/plotting.py | 321 +++++++++++++++++ qupulse/pulses/plotting.py | 322 +----------------- .../pulses/arithmetic_pulse_template_tests.py | 6 +- tests/pulses/bug_tests.py | 2 +- tests/pulses/constant_pulse_template_tests.py | 8 +- .../multi_channel_pulse_template_tests.py | 7 +- tests/pulses/plotting_tests.py | 2 +- 8 files changed, 336 insertions(+), 334 deletions(-) create mode 100644 qupulse/plotting.py diff --git a/qupulse/__init__.pyi b/qupulse/__init__.pyi index 889724cb0..d61a81d6b 100644 --- a/qupulse/__init__.pyi +++ b/qupulse/__init__.pyi @@ -8,6 +8,7 @@ from . import comparable from . import expressions from . import parameter_scope from . import serialization +from . import plotting from .utils.types import MeasurementWindow, ChannelID @@ -21,4 +22,5 @@ __all__ = ['pulses', 'serialization', 'MeasurementWindow', 'ChannelID', + 'plotting', ] diff --git a/qupulse/plotting.py b/qupulse/plotting.py new file mode 100644 index 000000000..cfe8d8c1e --- /dev/null +++ b/qupulse/plotting.py @@ -0,0 +1,321 @@ +"""This module defines plotting functionality for instantiated PulseTemplates using matplotlib. + +Classes: + - PlottingNotPossibleException. +Functions: + - plot: Plot a pulse using matplotlib. +""" + +from typing import Dict, Tuple, Any, Optional, Set, List, Union, Mapping +from numbers import Real + +import matplotlib.pyplot as plt +import numpy as np +import warnings +import operator +import itertools +import functools + +from qupulse._program import waveforms +from qupulse.utils.types import ChannelID, MeasurementWindow, has_type_interface +from qupulse.pulses.pulse_template import PulseTemplate +from qupulse._program.waveforms import Waveform +from qupulse._program._loop import Loop, to_waveform + + +__all__ = ["render", "plot", "PlottingNotPossibleException"] + + +def render(program: Union[Loop], + sample_rate: Real = 10.0, + render_measurements: bool = False, + time_slice: Tuple[Real, Real] = None, + plot_channels: Optional[Set[ChannelID]] = None) -> Tuple[np.ndarray, Dict[ChannelID, np.ndarray], + List[MeasurementWindow]]: + """'Renders' a pulse program. + + Samples all contained waveforms into an array according to the control flow of the program. + + Args: + program: The pulse (sub)program to render. Can be represented either by a Loop object or the more + old-fashioned InstructionBlock. + sample_rate: The sample rate in GHz. + render_measurements: If True, the third return value is a list of measurement windows. + time_slice: The time slice to be rendered. If None, the entire pulse will be shown. + plot_channels: Only channels in this set are rendered. If None, all will. + + Returns: + A tuple (times, values, measurements). times is a numpy.ndarray of dimensions sample_count where + containing the time values. voltages is a dictionary of one numpy.ndarray of dimensions sample_count per + defined channel containing corresponding sampled voltage values for that channel. + measurements is a sequence of all measurements where each measurement is represented by a tuple + (name, start_time, duration). + """ + if has_type_interface(program, Loop): + waveform, measurements = _render_loop(program, render_measurements=render_measurements) + else: + raise ValueError('Cannot render an object of type %r' % type(program), program) + + if waveform is None: + return np.array([]), dict(), measurements + + if plot_channels is None: + channels = waveform.defined_channels + else: + channels = waveform.defined_channels & plot_channels + + if time_slice is None: + start_time, end_time = 0, waveform.duration + + elif time_slice[1] < time_slice[0] or time_slice[0] < 0 or time_slice[1] < 0: + raise ValueError("time_slice is not valid.") + + else: + start_time, end_time, *_ = time_slice + + # filter measurement windows + measurements = [(name, begin, length) + for name, begin, length in measurements + if begin < end_time and begin + length > start_time] + + sample_count = (end_time - start_time) * sample_rate + 1 + if sample_count < 2: + raise PlottingNotPossibleException(pulse=None, + description='cannot render sequence with less than 2 data points') + if not round(float(sample_count), 10).is_integer(): + warnings.warn(f"Sample count {sample_count} is not an integer. Will be rounded (this changes the sample rate).", + stacklevel=2) + + times = np.linspace(float(start_time), float(end_time), num=int(sample_count)) + times[-1] = np.nextafter(times[-1], times[-2]) + + voltages = {ch: waveforms._ALLOCATION_FUNCTION(times, **waveforms._ALLOCATION_FUNCTION_KWARGS) + for ch in channels} + for ch, ch_voltage in voltages.items(): + waveform.get_sampled(channel=ch, sample_times=times, output_array=ch_voltage) + + return times, voltages, measurements + + +def _render_loop(loop: Loop, + render_measurements: bool,) -> Tuple[Waveform, List[MeasurementWindow]]: + """Transform program into single waveform and measurement windows. + The specific implementation of render for Loop arguments.""" + waveform = to_waveform(loop) + + if render_measurements: + measurement_dict = loop.get_measurement_windows() + measurement_list = [] + for name, (begins, lengths) in measurement_dict.items(): + measurement_list.extend(zip(itertools.repeat(name), begins, lengths)) + measurements = sorted(measurement_list, key=operator.itemgetter(1)) + else: + measurements = [] + + return waveform, measurements + + +def plot(pulse: PulseTemplate, + parameters: Dict[str, Real]=None, + sample_rate: Optional[Real]=10, + axes: Any=None, + show: bool=True, + plot_channels: Optional[Set[ChannelID]]=None, + plot_measurements: Optional[Set[str]]=None, + stepped: bool=True, + maximum_points: int=10**6, + time_slice: Tuple[Real, Real]=None, + **kwargs) -> Any: # pragma: no cover + """Plots a pulse using matplotlib. + + The given pulse template will first be turned into a pulse program (represented by a Loop object) with the provided + parameters. The render() function is then invoked to obtain voltage samples over the entire duration of the pulse which + are then plotted in a matplotlib figure. + + Args: + pulse: The pulse to be plotted. + parameters: An optional mapping of parameter names to Parameter + objects. + sample_rate: The rate with which the waveforms are sampled for the plot in + samples per time unit. If None, then automatically determine the sample rate (default = 10) + axes: matplotlib Axes object the pulse will be drawn into if provided + show: If true, the figure will be shown + plot_channels: If specified only channels from this set will be plotted. If omitted all channels will be. + stepped: If true pyplot.step is used for plotting + plot_measurements: If specified measurements in this set will be plotted. If omitted no measurements will be. + maximum_points: If the sampled waveform is bigger, it is not plotted + time_slice: The time slice to be plotted. If None, the entire pulse will be shown. + kwargs: Forwarded to pyplot. Overwrites other settings. + Returns: + matplotlib.pyplot.Figure instance in which the pulse is rendered + Raises: + PlottingNotPossibleException if the sequencing is interrupted before it finishes, e.g., + because a parameter value could not be evaluated + all Exceptions possibly raised during sequencing + """ + from matplotlib import pyplot as plt + + channels = pulse.defined_channels + + if parameters is None: + parameters = dict() + + if sample_rate is None: + if time_slice is None: + duration = pulse.duration + else: + duration = time_slice[1]-time_slice[0] + if duration == 0: + sample_rate = 1 + else: + duration_per_sample = float(duration) / 1000 + sample_rate = 1 / duration_per_sample + + program = pulse.create_program(parameters=parameters, + channel_mapping={ch: ch for ch in channels}, + measurement_mapping={w: w for w in pulse.measurement_names}) + + if program is not None: + times, voltages, measurements = render(program, + sample_rate, + render_measurements=bool(plot_measurements), + time_slice=time_slice) + else: + times, voltages, measurements = np.array([]), dict(), [] + + duration = 0 + if times.size == 0: + warnings.warn("Pulse to be plotted is empty!") + elif times.size > maximum_points: + # todo [2018-05-30]: since it results in an empty return value this should arguably be an exception, not just a warning + warnings.warn(f"Sampled pulse of size {times.size} is lager than {maximum_points}", + stacklevel=2) + return None + else: + duration = times[-1] + + if time_slice is None: + time_slice = (0, duration) + + legend_handles = [] + if axes is None: + # plot to figure + figure = plt.figure() + axes = figure.add_subplot(111) + + if plot_channels is not None: + voltages = {ch: voltage + for ch, voltage in voltages.items() + if ch in plot_channels} + + for ch_name, voltage in voltages.items(): + label = 'channel {}'.format(ch_name) + if stepped: + line, = axes.step(times, voltage, **{**dict(where='post', label=label), **kwargs}) + else: + line, = axes.plot(times, voltage, **{**dict(label=label), **kwargs}) + legend_handles.append(line) + + if plot_measurements: + measurement_dict = dict() + for name, begin, length in measurements: + if name in plot_measurements: + measurement_dict.setdefault(name, []).append((begin, begin+length)) + + color_map = plt.cm.get_cmap('plasma') + meas_colors = {name: color_map(i/len(measurement_dict)) + for i, name in enumerate(measurement_dict.keys())} + for name, begin_end_list in measurement_dict.items(): + for begin, end in begin_end_list: + poly = axes.axvspan(begin, end, alpha=0.2, label=name, edgecolor='black', facecolor=meas_colors[name]) + legend_handles.append(poly) + + axes.legend(handles=legend_handles) + + max_voltage = max((max(channel, default=0) for channel in voltages.values()), default=0) + min_voltage = min((min(channel, default=0) for channel in voltages.values()), default=0) + + # add some margins in the presentation + axes.set_xlim(-0.5+time_slice[0], time_slice[1] + 0.5) + voltage_difference = max_voltage-min_voltage + if voltage_difference>0: + axes.set_ylim(min_voltage - 0.1*voltage_difference, max_voltage + 0.1*voltage_difference) + axes.set_xlabel('Time (ns)') + axes.set_ylabel('Voltage (a.u.)') + + if pulse.identifier: + axes.set_title(pulse.identifier) + + if show: + with warnings.catch_warnings(): + # do not show warnings in jupyter notebook with matplotlib inline backend + warnings.filterwarnings(action="ignore",message=".*which is a non-GUI backend, so cannot show the figure.*") + axes.get_figure().show() + return axes.get_figure() + + +@functools.singledispatch +def plot_2d(program: Loop, channels: Tuple[ChannelID, ChannelID], + sample_rate: float = None, + ax: plt.Axes = None, + plot_kwargs: Mapping = None) -> plt.Figure: + """Plot the pulse/program in the plane of the given channels. + + Args: + program: The program to plot + channels: (x_axis, y_axis) name tuple + sample_rate: Sample rate to use. Defaults to max(1000 samples per program, 10 per nano second) + ax: Axis to plot into. + plot_kwargs: Forwarded to the plot function. + """ + if sample_rate is None: + sample_rate = max(1000 / program.duration, 10) + + _, rendered, _ = render(program, sample_rate, plot_channels=set(channels)) + x_y = np.array([rendered[channels[0]], rendered[channels[1]]]) + keep = np.full(x_y.shape[1], fill_value=True) + keep[1:] = np.any(x_y[:, 1:] != x_y[:, :-1], axis=0) + x_y_plt = x_y[:, keep] + + ax = ax or plt.subplots()[1] + ax.plot(x_y_plt[0, :], x_y_plt[1, :], **(plot_kwargs or {})) + ax.set_xlabel(channels[0]) + ax.set_ylabel(channels[1]) + return ax.get_figure() + + +@plot_2d.register +def _(pulse_template: PulseTemplate, + channels: Tuple[ChannelID, ChannelID], + sample_rate: float = None, + ax: plt.Axes = None, + plot_kwargs: Mapping = None, + parameters=None, + channel_mapping=None) -> plt.Figure: + + if channel_mapping is None: + channel_mapping = {ch: ch if ch in channels else None + for ch in pulse_template.defined_channels} + create_program_kwargs = {'channel_mapping': channel_mapping} + if parameters is not None: + create_program_kwargs['parameters'] = parameters + + program = pulse_template.create_program(**create_program_kwargs) + return plot_2d(program, channels, sample_rate=sample_rate, ax=ax, plot_kwargs=plot_kwargs) + + +class PlottingNotPossibleException(Exception): + """Indicates that plotting is not possible because the sequencing process did not translate + the entire given PulseTemplate structure.""" + + def __init__(self, pulse, description = None) -> None: + super().__init__() + self.pulse = pulse + self.description = description + def __str__(self) -> str: + if self.description is None: + return "Plotting is not possible. There are parameters which cannot be computed." + else: + return "Plotting is not possible: %s." % self.description + + diff --git a/qupulse/pulses/plotting.py b/qupulse/pulses/plotting.py index cfe8d8c1e..9dfae4328 100644 --- a/qupulse/pulses/plotting.py +++ b/qupulse/pulses/plotting.py @@ -1,321 +1 @@ -"""This module defines plotting functionality for instantiated PulseTemplates using matplotlib. - -Classes: - - PlottingNotPossibleException. -Functions: - - plot: Plot a pulse using matplotlib. -""" - -from typing import Dict, Tuple, Any, Optional, Set, List, Union, Mapping -from numbers import Real - -import matplotlib.pyplot as plt -import numpy as np -import warnings -import operator -import itertools -import functools - -from qupulse._program import waveforms -from qupulse.utils.types import ChannelID, MeasurementWindow, has_type_interface -from qupulse.pulses.pulse_template import PulseTemplate -from qupulse._program.waveforms import Waveform -from qupulse._program._loop import Loop, to_waveform - - -__all__ = ["render", "plot", "PlottingNotPossibleException"] - - -def render(program: Union[Loop], - sample_rate: Real = 10.0, - render_measurements: bool = False, - time_slice: Tuple[Real, Real] = None, - plot_channels: Optional[Set[ChannelID]] = None) -> Tuple[np.ndarray, Dict[ChannelID, np.ndarray], - List[MeasurementWindow]]: - """'Renders' a pulse program. - - Samples all contained waveforms into an array according to the control flow of the program. - - Args: - program: The pulse (sub)program to render. Can be represented either by a Loop object or the more - old-fashioned InstructionBlock. - sample_rate: The sample rate in GHz. - render_measurements: If True, the third return value is a list of measurement windows. - time_slice: The time slice to be rendered. If None, the entire pulse will be shown. - plot_channels: Only channels in this set are rendered. If None, all will. - - Returns: - A tuple (times, values, measurements). times is a numpy.ndarray of dimensions sample_count where - containing the time values. voltages is a dictionary of one numpy.ndarray of dimensions sample_count per - defined channel containing corresponding sampled voltage values for that channel. - measurements is a sequence of all measurements where each measurement is represented by a tuple - (name, start_time, duration). - """ - if has_type_interface(program, Loop): - waveform, measurements = _render_loop(program, render_measurements=render_measurements) - else: - raise ValueError('Cannot render an object of type %r' % type(program), program) - - if waveform is None: - return np.array([]), dict(), measurements - - if plot_channels is None: - channels = waveform.defined_channels - else: - channels = waveform.defined_channels & plot_channels - - if time_slice is None: - start_time, end_time = 0, waveform.duration - - elif time_slice[1] < time_slice[0] or time_slice[0] < 0 or time_slice[1] < 0: - raise ValueError("time_slice is not valid.") - - else: - start_time, end_time, *_ = time_slice - - # filter measurement windows - measurements = [(name, begin, length) - for name, begin, length in measurements - if begin < end_time and begin + length > start_time] - - sample_count = (end_time - start_time) * sample_rate + 1 - if sample_count < 2: - raise PlottingNotPossibleException(pulse=None, - description='cannot render sequence with less than 2 data points') - if not round(float(sample_count), 10).is_integer(): - warnings.warn(f"Sample count {sample_count} is not an integer. Will be rounded (this changes the sample rate).", - stacklevel=2) - - times = np.linspace(float(start_time), float(end_time), num=int(sample_count)) - times[-1] = np.nextafter(times[-1], times[-2]) - - voltages = {ch: waveforms._ALLOCATION_FUNCTION(times, **waveforms._ALLOCATION_FUNCTION_KWARGS) - for ch in channels} - for ch, ch_voltage in voltages.items(): - waveform.get_sampled(channel=ch, sample_times=times, output_array=ch_voltage) - - return times, voltages, measurements - - -def _render_loop(loop: Loop, - render_measurements: bool,) -> Tuple[Waveform, List[MeasurementWindow]]: - """Transform program into single waveform and measurement windows. - The specific implementation of render for Loop arguments.""" - waveform = to_waveform(loop) - - if render_measurements: - measurement_dict = loop.get_measurement_windows() - measurement_list = [] - for name, (begins, lengths) in measurement_dict.items(): - measurement_list.extend(zip(itertools.repeat(name), begins, lengths)) - measurements = sorted(measurement_list, key=operator.itemgetter(1)) - else: - measurements = [] - - return waveform, measurements - - -def plot(pulse: PulseTemplate, - parameters: Dict[str, Real]=None, - sample_rate: Optional[Real]=10, - axes: Any=None, - show: bool=True, - plot_channels: Optional[Set[ChannelID]]=None, - plot_measurements: Optional[Set[str]]=None, - stepped: bool=True, - maximum_points: int=10**6, - time_slice: Tuple[Real, Real]=None, - **kwargs) -> Any: # pragma: no cover - """Plots a pulse using matplotlib. - - The given pulse template will first be turned into a pulse program (represented by a Loop object) with the provided - parameters. The render() function is then invoked to obtain voltage samples over the entire duration of the pulse which - are then plotted in a matplotlib figure. - - Args: - pulse: The pulse to be plotted. - parameters: An optional mapping of parameter names to Parameter - objects. - sample_rate: The rate with which the waveforms are sampled for the plot in - samples per time unit. If None, then automatically determine the sample rate (default = 10) - axes: matplotlib Axes object the pulse will be drawn into if provided - show: If true, the figure will be shown - plot_channels: If specified only channels from this set will be plotted. If omitted all channels will be. - stepped: If true pyplot.step is used for plotting - plot_measurements: If specified measurements in this set will be plotted. If omitted no measurements will be. - maximum_points: If the sampled waveform is bigger, it is not plotted - time_slice: The time slice to be plotted. If None, the entire pulse will be shown. - kwargs: Forwarded to pyplot. Overwrites other settings. - Returns: - matplotlib.pyplot.Figure instance in which the pulse is rendered - Raises: - PlottingNotPossibleException if the sequencing is interrupted before it finishes, e.g., - because a parameter value could not be evaluated - all Exceptions possibly raised during sequencing - """ - from matplotlib import pyplot as plt - - channels = pulse.defined_channels - - if parameters is None: - parameters = dict() - - if sample_rate is None: - if time_slice is None: - duration = pulse.duration - else: - duration = time_slice[1]-time_slice[0] - if duration == 0: - sample_rate = 1 - else: - duration_per_sample = float(duration) / 1000 - sample_rate = 1 / duration_per_sample - - program = pulse.create_program(parameters=parameters, - channel_mapping={ch: ch for ch in channels}, - measurement_mapping={w: w for w in pulse.measurement_names}) - - if program is not None: - times, voltages, measurements = render(program, - sample_rate, - render_measurements=bool(plot_measurements), - time_slice=time_slice) - else: - times, voltages, measurements = np.array([]), dict(), [] - - duration = 0 - if times.size == 0: - warnings.warn("Pulse to be plotted is empty!") - elif times.size > maximum_points: - # todo [2018-05-30]: since it results in an empty return value this should arguably be an exception, not just a warning - warnings.warn(f"Sampled pulse of size {times.size} is lager than {maximum_points}", - stacklevel=2) - return None - else: - duration = times[-1] - - if time_slice is None: - time_slice = (0, duration) - - legend_handles = [] - if axes is None: - # plot to figure - figure = plt.figure() - axes = figure.add_subplot(111) - - if plot_channels is not None: - voltages = {ch: voltage - for ch, voltage in voltages.items() - if ch in plot_channels} - - for ch_name, voltage in voltages.items(): - label = 'channel {}'.format(ch_name) - if stepped: - line, = axes.step(times, voltage, **{**dict(where='post', label=label), **kwargs}) - else: - line, = axes.plot(times, voltage, **{**dict(label=label), **kwargs}) - legend_handles.append(line) - - if plot_measurements: - measurement_dict = dict() - for name, begin, length in measurements: - if name in plot_measurements: - measurement_dict.setdefault(name, []).append((begin, begin+length)) - - color_map = plt.cm.get_cmap('plasma') - meas_colors = {name: color_map(i/len(measurement_dict)) - for i, name in enumerate(measurement_dict.keys())} - for name, begin_end_list in measurement_dict.items(): - for begin, end in begin_end_list: - poly = axes.axvspan(begin, end, alpha=0.2, label=name, edgecolor='black', facecolor=meas_colors[name]) - legend_handles.append(poly) - - axes.legend(handles=legend_handles) - - max_voltage = max((max(channel, default=0) for channel in voltages.values()), default=0) - min_voltage = min((min(channel, default=0) for channel in voltages.values()), default=0) - - # add some margins in the presentation - axes.set_xlim(-0.5+time_slice[0], time_slice[1] + 0.5) - voltage_difference = max_voltage-min_voltage - if voltage_difference>0: - axes.set_ylim(min_voltage - 0.1*voltage_difference, max_voltage + 0.1*voltage_difference) - axes.set_xlabel('Time (ns)') - axes.set_ylabel('Voltage (a.u.)') - - if pulse.identifier: - axes.set_title(pulse.identifier) - - if show: - with warnings.catch_warnings(): - # do not show warnings in jupyter notebook with matplotlib inline backend - warnings.filterwarnings(action="ignore",message=".*which is a non-GUI backend, so cannot show the figure.*") - axes.get_figure().show() - return axes.get_figure() - - -@functools.singledispatch -def plot_2d(program: Loop, channels: Tuple[ChannelID, ChannelID], - sample_rate: float = None, - ax: plt.Axes = None, - plot_kwargs: Mapping = None) -> plt.Figure: - """Plot the pulse/program in the plane of the given channels. - - Args: - program: The program to plot - channels: (x_axis, y_axis) name tuple - sample_rate: Sample rate to use. Defaults to max(1000 samples per program, 10 per nano second) - ax: Axis to plot into. - plot_kwargs: Forwarded to the plot function. - """ - if sample_rate is None: - sample_rate = max(1000 / program.duration, 10) - - _, rendered, _ = render(program, sample_rate, plot_channels=set(channels)) - x_y = np.array([rendered[channels[0]], rendered[channels[1]]]) - keep = np.full(x_y.shape[1], fill_value=True) - keep[1:] = np.any(x_y[:, 1:] != x_y[:, :-1], axis=0) - x_y_plt = x_y[:, keep] - - ax = ax or plt.subplots()[1] - ax.plot(x_y_plt[0, :], x_y_plt[1, :], **(plot_kwargs or {})) - ax.set_xlabel(channels[0]) - ax.set_ylabel(channels[1]) - return ax.get_figure() - - -@plot_2d.register -def _(pulse_template: PulseTemplate, - channels: Tuple[ChannelID, ChannelID], - sample_rate: float = None, - ax: plt.Axes = None, - plot_kwargs: Mapping = None, - parameters=None, - channel_mapping=None) -> plt.Figure: - - if channel_mapping is None: - channel_mapping = {ch: ch if ch in channels else None - for ch in pulse_template.defined_channels} - create_program_kwargs = {'channel_mapping': channel_mapping} - if parameters is not None: - create_program_kwargs['parameters'] = parameters - - program = pulse_template.create_program(**create_program_kwargs) - return plot_2d(program, channels, sample_rate=sample_rate, ax=ax, plot_kwargs=plot_kwargs) - - -class PlottingNotPossibleException(Exception): - """Indicates that plotting is not possible because the sequencing process did not translate - the entire given PulseTemplate structure.""" - - def __init__(self, pulse, description = None) -> None: - super().__init__() - self.pulse = pulse - self.description = description - def __str__(self) -> str: - if self.description is None: - return "Plotting is not possible. There are parameters which cannot be computed." - else: - return "Plotting is not possible: %s." % self.description - - +from qupulse.plotting import * diff --git a/tests/pulses/arithmetic_pulse_template_tests.py b/tests/pulses/arithmetic_pulse_template_tests.py index 7e1d0bd35..b4d821684 100644 --- a/tests/pulses/arithmetic_pulse_template_tests.py +++ b/tests/pulses/arithmetic_pulse_template_tests.py @@ -7,8 +7,8 @@ from qupulse.parameter_scope import DictScope from qupulse.expressions import ExpressionScalar -from qupulse.pulses import MappingPT, ConstantPT, RepetitionPT -from qupulse.pulses.plotting import render +from qupulse.pulses import ConstantPT +from qupulse.plotting import render from qupulse.pulses.arithmetic_pulse_template import ArithmeticAtomicPulseTemplate, ArithmeticPulseTemplate,\ ImplicitAtomicityInArithmeticPT, UnequalDurationWarningInArithmeticPT, try_operation from qupulse._program.waveforms import TransformingWaveform @@ -650,7 +650,7 @@ def setUp(self) -> None: self.parameters = dict(t_duration=10, omega=3.14*2/10, t_y=3.4) def test_scaling(self): - from qupulse.pulses import plotting + from qupulse import plotting parameters = {**self.parameters, 'foo': 5.3} t_ref, reference, _ = plotting.render(self.complex_pt.create_program(parameters=parameters)) diff --git a/tests/pulses/bug_tests.py b/tests/pulses/bug_tests.py index ef4035e9f..6be11fcf2 100644 --- a/tests/pulses/bug_tests.py +++ b/tests/pulses/bug_tests.py @@ -11,7 +11,7 @@ from qupulse.pulses.mapping_pulse_template import MappingPulseTemplate from qupulse.pulses.loop_pulse_template import ForLoopPulseTemplate -from qupulse.pulses.plotting import plot +from qupulse.plotting import plot from qupulse._program._loop import to_waveform from qupulse.utils import isclose diff --git a/tests/pulses/constant_pulse_template_tests.py b/tests/pulses/constant_pulse_template_tests.py index ead1ce597..f0e374869 100644 --- a/tests/pulses/constant_pulse_template_tests.py +++ b/tests/pulses/constant_pulse_template_tests.py @@ -1,11 +1,11 @@ import unittest -import qupulse.pulses.plotting +import qupulse.plotting import qupulse._program.waveforms import qupulse.utils.sympy from qupulse.pulses import TablePT, FunctionPT, AtomicMultiChannelPT, MappingPT from qupulse.pulses.multi_channel_pulse_template import AtomicMultiChannelPulseTemplate -from qupulse.pulses.plotting import plot +from qupulse.plotting import plot from qupulse.pulses.sequence_pulse_template import SequencePulseTemplate from qupulse._program._loop import make_compatible from qupulse._program.waveforms import ConstantWaveform @@ -35,11 +35,11 @@ def test_zero_duration(self): p2 = ConstantPulseTemplate(0, {'P1': 1.}) p3 = ConstantPulseTemplate(2, {'P1': 1.}) - _ = qupulse.pulses.plotting.render(p1.create_program()) + _ = qupulse.plotting.render(p1.create_program()) pulse = SequencePulseTemplate(p1, p2, p3) prog = pulse.create_program() - _ = qupulse.pulses.plotting.render(prog) + _ = qupulse.plotting.render(prog) self.assertEqual(pulse.duration, 12) diff --git a/tests/pulses/multi_channel_pulse_template_tests.py b/tests/pulses/multi_channel_pulse_template_tests.py index 32cac0f72..87ae72575 100644 --- a/tests/pulses/multi_channel_pulse_template_tests.py +++ b/tests/pulses/multi_channel_pulse_template_tests.py @@ -6,14 +6,13 @@ from qupulse.parameter_scope import DictScope from qupulse.pulses import RepetitionPT, ConstantPT -from qupulse.pulses.plotting import render -from qupulse.pulses.multi_channel_pulse_template import MultiChannelWaveform, MappingPulseTemplate,\ +from qupulse.plotting import render +from qupulse.pulses.multi_channel_pulse_template import MappingPulseTemplate,\ ChannelMappingException, AtomicMultiChannelPulseTemplate, ParallelChannelPulseTemplate,\ TransformingWaveform, ParallelChannelTransformation from qupulse.pulses.parameters import ParameterConstraint, ParameterConstraintViolation -from qupulse.expressions import ExpressionScalar, Expression +from qupulse.expressions import ExpressionScalar from qupulse._program.transformation import LinearTransformation, chain_transformations -from qupulse.utils.sympy import sympify from tests.pulses.sequencing_dummies import DummyPulseTemplate, DummyWaveform from tests.serialization_dummies import DummySerializer diff --git a/tests/pulses/plotting_tests.py b/tests/pulses/plotting_tests.py index 0abc56c85..0e96a2c90 100644 --- a/tests/pulses/plotting_tests.py +++ b/tests/pulses/plotting_tests.py @@ -6,7 +6,7 @@ import numpy from qupulse.pulses import ConstantPT -from qupulse.pulses.plotting import PlottingNotPossibleException, render, plot +from qupulse.plotting import PlottingNotPossibleException, render, plot from qupulse.pulses.table_pulse_template import TablePulseTemplate from qupulse.pulses.sequence_pulse_template import SequencePulseTemplate from qupulse._program._loop import Loop From 52b26937c80e7f580ea2e82a3ccfbff602e46af5 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 3 Jan 2023 18:22:06 +0100 Subject: [PATCH 021/441] Add docstring to explain why pulses/plotting.py is still there --- qupulse/pulses/plotting.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qupulse/pulses/plotting.py b/qupulse/pulses/plotting.py index 9dfae4328..8d98f3112 100644 --- a/qupulse/pulses/plotting.py +++ b/qupulse/pulses/plotting.py @@ -1 +1,4 @@ +"""Deprecated plotting location. Was moved to :py:`qupulse.plotting`. +No deprecation warning because we will keep it around forever.""" + from qupulse.plotting import * From 83c4db1d4cf5cd3b3a856ac3b108bc5072b23b17 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 3 Jan 2023 18:37:20 +0100 Subject: [PATCH 022/441] Reformulate --- doc/source/examples/03SnakeChargeScan.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/examples/03SnakeChargeScan.ipynb b/doc/source/examples/03SnakeChargeScan.ipynb index 7fef6bbf9..c71fbb91f 100644 --- a/doc/source/examples/03SnakeChargeScan.ipynb +++ b/doc/source/examples/03SnakeChargeScan.ipynb @@ -7,7 +7,7 @@ "source": [ "# Snake Charge Scan\n", "\n", - "To manipulate an electron confined by gate-defined quantum dot, it is essential to control the number of electron i.e. the chemical potential of each quantum dot and the tunnel coupling among quantum dots. The charge stability diagram (CSD) represents electrostatic characteristics of such a quantum dot system for a give charge configuration which suggests the operation point and the work window for further experiements. However, the CSD depends on the sweep direction of gate voltages thus a charge state hysteresis in quantum dots has been observed and inverstigated. [1]\n", + "To manipulate an electron confined in a gate-defined quantum dot, it is essential to control the number of electrons i.e. the chemical potential of each quantum dot and the tunnel coupling among quantum dots. The charge stability diagram (CSD) shows changes in the electron occupation of the system in response to changes in applied gate voltages. This information can be used to extract the operation point for further experiments. For an infinitely slow gate sweep and in absence of charging effects the CSD shows the charge occupation of the ground state. In the real world however, the CSD can depend on the sweep direction of gate voltages and a charge state hysteresis in quantum dots has been observed and investigated. [1]\n", "\n", "[1] C. H. Yang, et al., Appl. Phys. Lett. 105, 183505 (2014)\n", "\n", From 0da91bfbc116c1c19fd6df84c8e53149cd3b4704 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Wed, 4 Jan 2023 09:27:02 +0100 Subject: [PATCH 023/441] Add newspiece --- changes.d/735.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes.d/735.feature diff --git a/changes.d/735.feature b/changes.d/735.feature new file mode 100644 index 000000000..827aca302 --- /dev/null +++ b/changes.d/735.feature @@ -0,0 +1 @@ +The plotting module is now located at `qupulse.plotting`. There is a legacy alias at `qupulse.pulses.plotting`. From 0c5db9d9d2ae0d1641c980c25a0bb2f4052a6046 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 10 Jan 2023 15:15:20 +0100 Subject: [PATCH 024/441] Include pyi files in package_data --- setup.cfg | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/setup.cfg b/setup.cfg index 64ea87505..68647e6f9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -69,6 +69,12 @@ include = qupulse.* qctoolkit +[options.package_data] +qupulse = + *.pyi +qctoolkit = + *.pyi + [tool:pytest] testpaths = tests tests/pulses tests/hardware tests/backward_compatibility python_files=*_tests.py *_bug.py From 9303cd1bbb6156cd48e68087270940f0a2677da8 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 10 Jan 2023 17:12:03 +0100 Subject: [PATCH 025/441] Fix bug in ForLoopPT --- qupulse/pulses/loop_pulse_template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qupulse/pulses/loop_pulse_template.py b/qupulse/pulses/loop_pulse_template.py index f47520a73..101705ebc 100644 --- a/qupulse/pulses/loop_pulse_template.py +++ b/qupulse/pulses/loop_pulse_template.py @@ -130,7 +130,7 @@ def duration(self) -> ExpressionScalar: @property def parameter_names(self) -> Set[str]: - parameter_names = self.body.parameter_names.copy() + parameter_names = set(self.body.parameter_names) parameter_names.remove(self._loop_index) return parameter_names | self._loop_range.parameter_names | self.constrained_parameters | self.measurement_parameters From 92daf490954c548abc71630ea3683c9669bb4a51 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Wed, 11 Jan 2023 10:15:48 +0100 Subject: [PATCH 026/441] Update snake charge scan --- doc/source/examples/03SnakeChargeScan.ipynb | 506 +++++++------------- 1 file changed, 185 insertions(+), 321 deletions(-) diff --git a/doc/source/examples/03SnakeChargeScan.ipynb b/doc/source/examples/03SnakeChargeScan.ipynb index c71fbb91f..a5aebaaab 100644 --- a/doc/source/examples/03SnakeChargeScan.ipynb +++ b/doc/source/examples/03SnakeChargeScan.ipynb @@ -19,488 +19,352 @@ "id": "394631b6", "metadata": {}, "source": [ - "## Task 1: Piece-wised voltage level\n", + "## Create a linear voltage sweep\n", "\n", - " ### Description:\n", - " Let 2 AWG channels `(X, Y)` hold at a given set of voltages `(x_start, y_start)` for specific time durations `t_hold`. \n", + "The basic building block of any such a diagram is a linear sweep of a channel. The first thing we need to do is write such a sweep. There are two options to implement this on a conceptional level.\n", "\n", - " ### Goal: \n", - " making a pulse without thinking the time consumption or the memory consumption.\n", - " \n", - " Firtly, we build a piece of voltage level with free parameters `t_hold` and `sample_rate` representing the time duration of such voltage level and the sample rate of AWG, respectively. With aforementioned parameters, a universal piece-wised pulse can be used as a building block of most experiments with flexibility of adjusting the pulse duration and hardward settings i.e. the sample rate of AWG." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "810a7af2", - "metadata": {}, - "outputs": [], - "source": [ - "import sympy\n", - "from qupulse.pulses import ConstantPT, RepetitionPT\n", + "The first is to interpret the sweep as a continuous change of the voltage in time. The number of points here is just determined by the time resolution of our readout. Each readout point represents an average over a short time window of this continuous sweep.\n", "\n", - "sample_rate, n_segments, t_hold = sympy.sympify('sample_rate, n_segments, t_hold')\n", - "t_segment = n_segments / sample_rate\n", + "The second approach is to change the applied voltage in a fixed number of steps. Here the number of points is already a property of the pulse and each readout point is the average over the output at that point.\n", "\n", - "segment = ConstantPT(t_segment, {'X': 'x_start + x_i * x_step', \n", - " 'Y': 'y_start + y_i * y_step'}\n", - " )\n", - "body = RepetitionPT(segment, t_hold // t_segment, measurements=[('M', 0, 't_hold')])" - ] - }, - { - "cell_type": "markdown", - "id": "ad4cbdb1", - "metadata": {}, - "source": [ - "Now that a piece of voltage level is constructed with given voltages" - ] - }, - { - "cell_type": "markdown", - "id": "82adccc2", - "metadata": {}, - "source": [ - "A charge stability diagram can be abstracted by 2 nested for loop using `ForLoopPT` in qupulse. In order to define the name of measurement window for different sweep direction, the `ForLoopPT` is wrapped by a `MappingPT`.\n", - "\n", - "The voltage resolution of the scan is described by `{x_step, y_step}` which are converted by qupulse according to the user inputs `{x_start, x_stop, N_x, y_start, y_stop, N_y}` representing the start voltage and stop voltages on different scan axis and the number of voltage levels of each axes." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "fd72b962", - "metadata": {}, - "outputs": [], - "source": [ - "from qupulse.pulses import MappingPT, ForLoopPT\n", - "inner_loop_fwd = MappingPT(ForLoopPT(body, 'x_i', 'N_x'), measurement_mapping={'M': 'x_pos'})\n", - "inner_loop_bwd = MappingPT(ForLoopPT(body, 'x_i', ('N_x - 1', -1, -1)), measurement_mapping={'M': 'x_neg'})\n", + "In the following we will explore both variants and their hardware feasibility.\n", "\n", - "# concatinate two pulse templates by '@'\n", - "inner_loop = inner_loop_fwd @ inner_loop_bwd\n", - "outer_loop = ForLoopPT(inner_loop, 'y_i', 'N_y')\n", + "### Continuous voltage sweep\n", "\n", - "# here we make a linear interpolation of sweep axes.\n", - "snake_sweep_seg = MappingPT(outer_loop, parameter_mapping={'x_step': '(x_stop - x_start) / (N_x - 1)',\n", - " 'y_step': '(y_stop - y_start) / (N_y - 1)'})" + "Let us first explore the first option. We can use a `TablePT` or a `PointPT` to implement it." ] }, { "cell_type": "code", - "execution_count": 3, - "id": "3468e95f-7070-4659-bdf6-ff19f6f1d2b1", + "execution_count": 1, + "id": "7b6be5b4", "metadata": {}, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'N_y', 'x_stop', 'y_start', 'x_start', 'n_segments', 'y_stop', 'cds_res', 'sample_rate', 'N_x'}\n" - ] + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" } ], "source": [ + "import matplotlib.pyplot as plt\n", "\n", - "snake_cds = MappingPT(snake_sweep_seg,\n", - " parameter_mapping={'t_hold': 'cds_res'},\n", - " identifier='Snake_CDS')\n", - "print(snake_cds.parameter_names)" - ] - }, - { - "cell_type": "markdown", - "id": "5c65acf0", - "metadata": {}, - "source": [ - "Let's generate a pulse for a bi-directional charge scan with voltage resolution 0.3V/point on x-axis and 0.4V/point on y-axis. The time resolution `cds_res` here is arbitarily chosen for demonstration." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "8c7802fb-bff3-4c4e-80dc-d9be15f217ca", - "metadata": {}, - "outputs": [], - "source": [ - "default_params = {\n", - " 'n_segments': 2,\n", - " 'x_start': 0,\n", - " 'x_stop': 3,\n", - " 'y_start': 0,\n", - " 'y_stop': 2,\n", - " 'N_x': 10,\n", - " 'N_y': 5,\n", - " 'sample_rate': 1,\n", - " 'cds_res': 5\n", - "}" + "from qupulse.pulses import *\n", + "from qupulse.plotting import *\n", + "\n", + "linear_cont = TablePT({'X': [(0, 'x_start'),\n", + " ('tx_sweep', 'x_stop', 'linear')]},\n", + " measurements=[('M', 0, 'tx_sweep')])\n", + "\n", + "x_sweep_params = {'x_start': -3.3, 'x_stop': -1.5, 'tx_sweep': 10.}\n", + "\n", + "_ = plot(linear_cont, parameters=x_sweep_params, stepped=False, plot_measurements={'M'})" ] }, { "cell_type": "markdown", - "id": "5aceae43", + "id": "dd2a62bd", "metadata": {}, "source": [ - "Now, we use `plot` function to inspect the positive sweep of channel X which is highlighted in the following plot." + "You'll notice that this pulse does only have one measurement window over the whole range attached. For a sweep with `n` points we would maybe expect `n` measurement windows. However, measurement windows require a pulse template to be attached to and there is no way in qupulse (yet in 2023) to change or parameterize the number of measurement windows directly. You have to change the number of pulse template instantiations f.i. with a `RepetitionPT` or a `ForLoopPT` to change the number of measurement windows.\n", + "\n", + "We can however interpret the measurement window as the time window where the sweep happens and use it as such in our measurement configuration and data analysis. qupulse does not enforce or promote a particular meaning of measurement windows. qupulse simply tracks them through complex pulse trees and gives you their final position in an instantiated pulse.\n", + "\n", + "### Stepped sweep\n", + "\n", + "Next we will explore how to write a \"stepped\" sweep. We want the number of steps to be parameterized by `n_x` and the range by `x_start` and `x_stop`. The total duration of the sweep shall be `tx_sweep`." ] }, { "cell_type": "code", - "execution_count": 5, - "id": "71dd0045-292d-49f7-90ec-124b38d53566", + "execution_count": 2, + "id": "fcdfe86c", "metadata": {}, "outputs": [ { "data": { - "image/png": "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xQcffICCggIMHjwYADBgwADUqlULaWlpAIDXX38d7dq1Q2xsLK5du4Z3330X586dw/PPP+/NYRAREVEl5fViqG/fvrh06RImTZqErKwstGzZEmvWrLFfVJ2RkQGV6q9Lm7KzszF06FBkZWUhJCQErVu3xk8//YQmTZp4awhERERUiXm9GAKAkSNHlntabPPmzQ7TM2bMwIwZMzwQFRERESlBpbubjIiIiMidWAwRERGRorEYIiIiIkVjMURERESKxmKIiIiIFI3FEBERESkaiyEiIiJSNBZDREREpGgshoiIiEjRfOIJ1L5ACIEiAUgCKLTKAGRvh1QlGC02h2k/rRrSLb4MlLmoGMyF72AufAdz4TtuJxfuwmIIxR/sKdvP4WR2teKG1T97N6AqJOnN9Y7TMSFYkpJc7gecuag4zIXvYC58B3PhO1zNhTvxNBkAs03GyWxTmfOiNDZWjC7y06qRFBNS5rw957JhKrKVOQ9gLtyNufAdzIXvYC58x53kwp2Ytxu8EGZGcpuWMBj0MJlMOJC+D5LE3eQKSZKwJCXZ4UNstNhKVf23wlzcOebCdzAXvoO58B3uysWdYtZuoJUAg0YFg0YNoVHBQ6crqxxJkuCvu7OPF3PhHsyF72AufAdz4TvckYs7xdNkREREpGgshoiIiEjRWAwRERGRorEYIiIiIkVjMURERESKxmKIiIiIFI3FEBERESkaiyEiIiJSNBZDREREpGgshoiIiEjRWAwRERGRorEYIiIiIkVjMURERESKxmKIiIiIFI3FEBERESkaiyEiIiJSNBZDREREpGgshoiIiEjRWAwRERGRorEYIiIiIkVjMURERESKxmKIiIiIFI3FEBERESkaiyEiIiJSNBZDREREpGgshoiIiEjRWAwRERGRovlEMfTRRx+hbt26MBgMuPvuu7F79+6b9l+yZAni4+NhMBjQvHlzrF692kOREhERUVXj9WJo0aJFGDduHCZPnox9+/YhISEB3bp1w8WLF8vs/9NPP6Ffv3547rnnsH//fvTu3Ru9e/fGkSNHPBw5ERERVQUabwfw/vvvY+jQoRg8eDAAYO7cufjuu+8wb948/OMf/yjVf+bMmejevTvGjx8PAHjjjTewbt06fPjhh5g7d67T27UVmWAxF0At2WCxyvBDIQBAKyxQWY1QWWWobCZohBmSsHmsTUBA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", + "image/png": "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\n", "text/plain": [ - "
" + "
" ] }, - "metadata": {}, + "metadata": { + "needs_background": "light" + }, "output_type": "display_data" } ], "source": [ - "from qupulse.pulses.plotting import plot\n", + "step = ConstantPT(duration='tx_sweep / n_x', amplitude_dict={'X': 'x_start + x_i * (x_stop - x_start) / (n_x - 1)'},\n", + " measurements=[('M', 0, 'tx_sweep / n_x')])\n", "\n", - "_=plot(snake_cds, parameters=default_params, sample_rate=1, plot_measurements='x_pos')" + "# equivalent to ForLoopPT(step, 'x_i', 'n_x')\n", + "linear_step = step.with_iteration('x_i', 'n_x')\n", + "\n", + "x_sweep_params.update(n_x=10)\n", + "\n", + "_ = plot(linear_step, parameters=x_sweep_params, plot_measurements={'M'})" ] }, { "cell_type": "markdown", - "id": "813d89e8", + "id": "205f94ed", "metadata": {}, "source": [ - "Similaly the negative sweep is inspected by assigning the measurement window name `x_neg` to the input argument `plot_measurement` of `plot` function provided by qupulse." + "Here we see that each step has its own measurement window.\n", + "\n", + "## The snake: going forward and backward\n", + "\n", + "We can now utilize the `TimeReversalPT` to go backward and combine both direction with a `SequencePT`. Furthermore, we want to rename the measurement windows to discriminate between forward and backward measurements. We can utilize the `with_*` methods and the overloaded matrix multiplication operator `@` to make this very concise." ] }, { "cell_type": "code", - "execution_count": 6, - "id": "83fea4a4-07a8-4403-ac98-491de63017ab", + "execution_count": 3, + "id": "8ac1f53d", "metadata": {}, "outputs": [ { "data": { - "image/png": "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6cnQa0rf0AFqJSTp1g9KSpKEmV3jsGfvYahVYbBY85HQshnG/HDsltddU12fB8D9udixKxOSQoXExLaASo3haw7e8rprKubCfzAX/oO58AwWQ35Cr1FCr3F/OiRJgkYBqBUSFEKCVsmDgTfiqTwApblQS4AkATqVAlAxFzfCXPgP5sJ/MBeeIZ+REhEREVWAxRARERHJGoshIiIikjUWQ0RERCRrLIaIiIhI1lgMERERkayxGCIiIiJZYzFEREREssZiiIiIiGSNxRARERHJGoshIiIikjUWQ0RERCRrLIaIiIhI1lgMERERkayxGCIiIiJZYzFEREREssZiiIiIiGSNxRARERHJGoshIiIikjUWQ0RERCRrLIaIiIhI1lgMERERkayxGCIiIiJZYzFEREREssZiiIiIiGSNxRARERHJGoshIiIikjWfFkOpqano2LEjgoKCEBkZiX79+uHYsWM3XW7VqlVISEiATqdDq1atsHbtWi9ES0RERDWRT4uhrVu3Yvz48di1axfWr1+PkpIS3HfffSgqKqp0mZ07d2Lw4MEYOXIk9u3bh379+qFfv344fPiwFyMnIiKimkLly42vW7fOYXrZsmWIjIxEZmYm7rrrrgqXWbBgAXr16oWpU6cCAGbPno3169fj7bffxqJFizweMxEREdUsPi2G/iovLw8AEBYWVmmftLQ0TJkyxaGtZ8+eWL16tSdDcwshBIwlVvu0wWy9QW/PKxFAscUGWKwottggBCD5NCLvuT4Xvs4DIN9c+NtnAmAuyjAXfxICCqsRSmsxYDYAKuGFTZbPRQCKSyfMRXD5n25zafwKqxEKS2mTwmqESpggCSsUFgMUFlu5NkB9bbs2M8ymIihQArOpGEqbCQqp8mU90mYx3PK+rYzfFEM2mw2TJ09G165d0bJly0r75eTkICoqyqEtKioKOTk5FfY3mUwwmUz26fz8fPcE7CIhBPovSkPmmVyfbL8iiy9qsXjtL/bpGJUOA8MsPozIO5gL/+CPeQCYC3/i81wIgSbbByMwd1/pdLp3NisB0F83rQfwi+7PidddX58eQHIF7XeU/bGp8jb7dgsBbL3Wr7sTy7q7Ld/kuULUb+4mGz9+PA4fPowVK1a4db2pqakICQmxv2JjY926fmcZS6yVftEkxoUiQK30ShxapYSmoQEVzsu2KFGzv/JLVZYL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", + "image/png": "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\n", "text/plain": [ - "
" + "
" ] }, - "metadata": {}, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, "output_type": "display_data" } ], "source": [ - "_=plot(snake_cds, parameters=default_params, sample_rate=1, plot_measurements='x_neg')" + "snake_1d_cont = linear_cont.with_mapping({'M': 'x_fwd'}) @ linear_cont.with_time_reversal().with_mapping({'M': 'x_bwd'})\n", + "snake_1d_step = linear_step.with_mapping({'M': 'x_fwd'}) @ linear_step.with_time_reversal().with_mapping({'M': 'x_bwd'})\n", + "\n", + "_ = plot(snake_1d_cont, parameters=x_sweep_params, plot_measurements={'x_fwd', 'x_bwd'}, stepped=False)\n", + "_ = plot(snake_1d_step, parameters=x_sweep_params, plot_measurements={'x_fwd', 'x_bwd'})" ] }, { "cell_type": "markdown", - "id": "aa0df99d", - "metadata": {}, - "source": [ - "When we try to instantiate such a pulse it will take some time but not severe yet. However it is not a set of parameters with physical meanings. In general, a bandwidth of charge sensing falls in relatively low frequency range say slower than MHz. In addition, a voltage resolution of mV is required to resolve the charge occupancy of a quantum dot. Therefore the following parameters are used in a real experiment:" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "f7213346", + "id": "872a0324", "metadata": {}, - "outputs": [], "source": [ - "exp_params = {\n", - " 'n_segments': 192, # empirically determined.\n", - " 'x_start': -50e-3,\n", - " 'x_stop': 50e-3,\n", - " 'y_start': 0,\n", - " 'y_stop': 0.1, \n", - " 'N_x': 100, # voltage resolution: 1 mV\n", - " 'N_y': 100, # voltage resolution: 1 mV\n", - " 'sample_rate': 3.125e-3, # AWG sample rate: 3.125 MHz\n", - " 'cds_res': 1e6 # time resolution: 1 ms\n", - "}" + "## Two-dimensional sweep\n", + "\n", + "The next step is to make two-dimensional scans. We need to add the `Y` channel and loop over the desired voltages. We can use the `ParallelChannelPT` and `ForLoopPT` for that. The number of points in y-direction shall be `n_y`." ] }, { "cell_type": "code", - "execution_count": 8, - "id": "2dd4c86d", + "execution_count": 4, + "id": "c66d64e0", "metadata": {}, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "Elapsed time: 0.05933679999998276 seconds\n", - "Elapsed time: 7.700837200000024 seconds\n" - ] + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" } ], "source": [ - "# Try with different time/voltage resolution or AWG sample rate by yourself!\n", - "\n", - "import timeit\n", - "# using arbitary parameters for simplicity. \n", - "simple_inst = timeit.timeit(lambda: snake_cds.create_program(parameters=default_params), number=1)\n", - "print(f'Elapsed time: {simple_inst} seconds')\n", + "y_value = {'Y': 'y_start + y_i * (y_stop - y_start) / (n_y - 1)'}\n", "\n", - "# in a real experiment:\n", - "exp_inst = timeit.timeit(lambda: snake_cds.create_program(parameters=exp_params), number=1)\n", - "print(f'Elapsed time: {exp_inst} seconds')\n" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "7301ba97", - "metadata": {}, - "source": [ - "## Task 2: Simplify the pulse\n", + "# with_* methods\n", + "snake_step = snake_1d_step\\\n", + " .with_parallel_channels(y_value)\\\n", + " .with_iteration('y_i', 'n_y')\n", "\n", - "With the current implementation the time consumption to instantiate pulse seems unecessarily long. But which step slows down the whole process significantly? Is there any space for improvement? \n", + "# Direct class instantiation\n", + "snake_linear = ForLoopPT(ParallelChannelPT(snake_1d_cont, y_value), 'y_i', 'n_y')\n", "\n", - "### Description:\n", - "The main concern by now is building everything by qupluse including the measurement window. Therefore the resolution of data acquisation and the pulse interpolation are coupled resulting in a sophisticated construction of pulse for a trivial task. \n", + "sweep_params_2d = {**x_sweep_params, 'y_start': -1.1, 'y_stop': 0.6, 'n_y': 4}\n", "\n", - "### Goal:\n", - "In the following section, we will try to explore a way to optimize the pulse by simplifying the definition of measurement windows.\n", - "\n", - "Supposing that the instrument for data acquisation can make on-fly process according to the measurement windows those defined in pulse templates. One needs only forward the starting time of a pulse and its time duration such that the data acquisation card will down-sample the raw data according to customized settings such as time resolution. In that case explicitly interpolating pulses is not necessary thus the nest level of pulse templates is simplified.\n", - "\n", - "Let's firsly simplify the nest level of a linear sweep. " - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "d2962063", - "metadata": {}, - "outputs": [], - "source": [ - "from qupulse.pulses import PointPT, ConstantPT, AtomicMultiChannelPT\n", - "import sympy\n", - "\n", - "tx_sweep = sympy.sympify('tx_sweep')\n", - "\n", - "# equivalently: tx_sweep = N_x * cds_res but now qupulse only takes care of the duration per scanline.\n", - "# Because N_x: number of data points per scanline and cds_res: time resolution per data point\n", - "# are only required by data aquisation device instead of qupulse.\n", - "\n", - "# make a linear sweep of channel X with 1 measurement window for the whole sweep time\n", - "# meanwhile define channel y that holding on at a voltage level during x_ramp.\n", - "scan_line = AtomicMultiChannelPT(\n", - " PointPT([(0, 'x_start'),\n", - " (tx_sweep, 'x_stop', 'linear')], channel_names='X'),\n", - " ConstantPT(tx_sweep, {'Y': 'y_start + y_step * y_i'}),\n", - " measurements=[('M', 0, tx_sweep)])" + "_, (ax1, ax2) = plt.subplots(1, 2, sharey=True)\n", + "_ = plot(snake_linear, parameters=sweep_params_2d, plot_measurements={'x_fwd', 'x_bwd'}, stepped=False, axes=ax1)\n", + "_ = plot(snake_step, parameters=sweep_params_2d, plot_measurements={'x_fwd', 'x_bwd'}, axes=ax2)" ] }, { - "attachments": {}, "cell_type": "markdown", - "id": "c323ae3d", + "id": "157f6ece", "metadata": {}, "source": [ - "A complete bi-directional charge scan can be assembled as follows:" + "Now that we created a two-dimensional scan we can use the `plot_2d` function to inspect what it does in voltage space i.e. without a time axis and compare it to a regular \"forward only\" charge scan. First we create that one from the `linear_step` sweep defined above." ] }, { "cell_type": "code", - "execution_count": 10, - "id": "6c207d01", + "execution_count": 5, + "id": "8c7802fb-bff3-4c4e-80dc-d9be15f217ca", "metadata": {}, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'N_y', 'x_stop', 'y_start', 'x_start', 'y_stop', 'tx_sweep'}\n" - ] + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" } ], "source": [ - "from qupulse.pulses import MappingPT, TimeReversalPT, ForLoopPT\n", - "\n", - "pos_sweep = MappingPT(scan_line, measurement_mapping={'M': 'x_pos'})\n", - "neg_sweep = MappingPT(TimeReversalPT(scan_line), measurement_mapping={'M': 'x_neg'})\n", + "chrg_scan = ForLoopPT(ParallelChannelPT(linear_step, y_value), 'y_i', 'n_y')\n", "\n", - "loop = ForLoopPT(pos_sweep @ neg_sweep, 'y_i', 'N_y')\n", - "\n", - "snake_cds = MappingPT(loop, parameter_mapping={'y_step': '(y_stop - y_start) / (N_y - 1)'})\n", - "print(snake_cds.parameter_names)" + "_ = plot(chrg_scan, parameters=sweep_params_2d, plot_measurements={'M'})\n", + "default_params = {\n", + " 'n_segments': 2,\n", + " 'x_start': 0,\n", + " 'x_stop': 3,\n", + " 'y_start': 0,\n", + " 'y_stop': 2,\n", + " 'N_x': 10,\n", + " 'N_y': 5,\n", + " 'sample_rate': 1,\n", + " 'cds_res': 5\n", + "}" ] }, { - "attachments": {}, "cell_type": "markdown", - "id": "560d499a", + "id": "5aceae43", "metadata": {}, "source": [ - "Now the `inner_loop` in task 1 is simplified by using the keyword `linear` provided by `PointPT` and has less complexity of defining measurement windows. We can now plot the pulse again by `qupulse.pulse.plotting.plot`:" + "Now, we use `plot` function to inspect the positive sweep of channel X which is highlighted in the following plot." ] }, { "cell_type": "code", - "execution_count": 11, - "id": "0221f05d", + "execution_count": 6, + "id": "71dd0045-292d-49f7-90ec-124b38d53566", "metadata": {}, "outputs": [ { "data": { - "image/png": "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u+fXqlUL586dk/moEJEcWOwQkSK0atXK9mc/Pz/ccsstaNmypW1ZZGQkAODixYsAShqNt2zZYusBCgkJQZMmTQAAp06dKvM18vPzYTQay2yivvH1rR+9WV9r8ODBOHToEBo3boynnnoKGzduLPX8oKAg21UnIlIWg+gBEBEBgL+/v93vOp3Obpm1QLFYLACAnJwc9O3bF6+++mqpbUVHR5f5GhEREcjLy0NhYSECAgIqfX3ra7Vp0wZnzpzBt99+i++//x4PPfQQunXrhlWrVtnWv3r1KmrUqOHo7hKRF7HYISJVatOmDVavXo369evDYHDsr7LWrVsDAH799Vfbnx0VFhaGAQMGYMCAAfj3v/+Nnj174urVq6hevTqAkmbphIQEp7ZJRN7Bj7GISJXGjBmDq1ev4pFHHsHevXtx6tQpfPfddxgyZAjMZnOZz6lRowbatGmDHTt2OPVac+fOxWeffYZjx47hxIkTWLlyJaKiouy+J+iHH35A9+7d3dklIvIQFjtEpEq1atXCjz/+CLPZjO7du6Nly5YYP348qlatCr2+/L/ahg8fjk8//dSp1woNDcWsWbPQrl07tG/fHmfPnsU333xje51du3YhMzMT//73v93aJyLyDH6pIBH5lPz8fDRu3BgrVqxAYmKiLNscMGAA4uPjMXnyZFm2R0Ty4pUdIvIpQUFB+Oijjyr88kFnFBYWomXLlpgwYYIs2yMi+fHKDhEREWkar+wQERGRprHYISIiIk1jsUNERESaxmKHiIiINI3FDhEREWkaix0iIiLSNBY7REREpGksdoiIiEjTWOwQERGRpv0/IbrKW3xuVXQAAAAASUVORK5CYII=", + "image/png": "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\n", "text/plain": [ - "
" + "
" ] }, - "metadata": {}, + "metadata": { + "needs_background": "light" + }, "output_type": "display_data" } ], "source": [ - "from qupulse.pulses.plotting import plot\n", + "from qupulse.plotting import plot_2d\n", "\n", - "default_params = {\n", - " 'tx_sweep': 5,\n", - " 'N_y': 5,\n", - " 'x_start': 0,\n", - " 'x_stop': 3,\n", - " 'y_start': 0,\n", - " 'y_stop': 2,\n", - "}\n", - "_ = plot(snake_cds, parameters=default_params, plot_measurements=('x_pos','x_neg'))" + "f, (ax1, ax2) = plt.subplots(1, 2, sharey=True)\n", + "ax1.set_title(\"Regular\")\n", + "ax2.set_title(\"Snake\")\n", + "_ = plot_2d(chrg_scan, ('X', 'Y'), parameters=sweep_params_2d, ax=ax1)\n", + "_ = plot_2d(snake_step, ('X', 'Y'), parameters=sweep_params_2d, ax=ax2)" ] }, { - "attachments": {}, "cell_type": "markdown", - "id": "2c15dbe5", + "id": "813d89e8", + "metadata": {}, + "source": [] + }, + { + "cell_type": "markdown", + "id": "aa0df99d", "metadata": {}, "source": [ - "It is an obviously simple pulse definition, but how much time efficient do we gain from it remains unclear. Let's inspect the elapsed time of `creat_program`. " + "## Benchmark\n", + "\n", + "When we try to instantiate such a pulse it will take some time but not severe yet. However, a typical charge scan has a higher resolution. The qupulse parameter evaluation machinery can become a bottleneck here since it is run for every of the 100 points. There is work being done on optimizing it so see for yourself what the current status is." ] }, { "cell_type": "code", - "execution_count": 12, - "id": "67c5ecbc", + "execution_count": 7, + "id": "f7213346", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Elapsed time: 0.00655929999993532 seconds\n", - "Elapsed time: 0.09900640000023486 seconds\n" + "Elapsed time: 0.008839400000000275 seconds\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Elapsed time: 1.7463892000000003 seconds\n" ] } ], "source": [ "import timeit\n", "\n", + "# Try with different resolution by yourself!\n", "exp_params = {\n", - " 'tx_sweep': 100e6, # 100 ms each scanline\n", + " 'tx_sweep': 10240,\n", " 'x_start': -50e-3,\n", " 'x_stop': 50e-3,\n", " 'y_start': 0,\n", " 'y_stop': 0.1, \n", - " 'N_x': 100, # voltage resolution: 1 mV\n", - " 'N_y': 100, \n", + " 'n_x': 100, # voltage resolution: 1 mV\n", + " 'n_y': 100, # voltage resolution: 1 mV\n", "}\n", "\n", - "# using arbitary parameters for simplicity. \n", - "simple_inst = timeit.timeit(lambda: snake_cds.create_program(parameters=default_params), number=1)\n", + "\n", + "# using arbitary parameters for simplicity.\n", + "simple_inst = timeit.timeit(lambda: snake_step.create_program(parameters=sweep_params_2d), number=1)\n", "print(f'Elapsed time: {simple_inst} seconds')\n", "\n", "# in a real experiment:\n", - "exp_inst = timeit.timeit(lambda: snake_cds.create_program(parameters=exp_params), number=1)\n", + "exp_inst = timeit.timeit(lambda: snake_step.create_program(parameters=exp_params), number=1)\n", "print(f'Elapsed time: {exp_inst} seconds')" ] }, { - "attachments": {}, "cell_type": "markdown", - "id": "0d7283f2", - "metadata": {}, - "source": [ - "## Task 3: Use convenient functions (with_*)\n", - "\n", - "### Description:\n", - "Equivalently the `scan_line` can be constructed by the helper functions `with_` since version 7.0.x thus only an atomic pulse template is required to build an elementary pulse. All the other modifications used above can be then replaced by `with_` functions for conveniency.\n", - "\n", - "### Goal:\n", - "Trying to use all available `with_*` functions to replace pulse templates used in Task 1, 2." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "fee666ed", + "id": "69c86045", "metadata": {}, - "outputs": [], "source": [ - "from qupulse.pulses import PointPT\n", - "import sympy\n", + "## Hardware limitations and legacy pulses\n", "\n", - "tx_sweep = sympy.sympify('tx_sweep')\n", + "This section is under construction and will give an example and reasoning why old pulses often contain\n", "\n", - "# use helper functions\n", - "forward = PointPT([(0, 'x_start'),\n", - " (tx_sweep, 'x_stop', 'linear')],\n", - " channel_names=('X', 'Y'),\n", - " measurements=[('M', 0, tx_sweep)])\n", + "`RepetitionPT(ConstantPT(...))`\n", "\n", - "backward = forward.with_time_reversal().with_mapping(measurement_mapping={'M': 'x_neg'})\n", - "forward = forward.with_mapping(measurement_mapping={'M': 'x_pos'})\n", - "\n", - "scan_x = forward.with_appended(backward)\n", - "scan_line = scan_x.with_parallel_channels({'Y': 'y_start + y_step * y_i'})\n", - "scan_line = scan_line.with_mapping(parameter_mapping={'y_step': '(y_stop - y_start) / (N_y - 1)'})\n", - "\n", - "snake_cds = scan_line.with_iteration('y_i', 'N_y')" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "7e996a57", - "metadata": {}, - "source": [ - "Here the `TimeReversalPT` is replaced by `with_time_reversal()`. Similarly, a `with_mapping()` function call can map `parameters`, `channel names` and `measurement names` as simple as using a `MappingPT` with corresponding keywords. `with_appended`, `with_parallel_channels` and `with_iteration` are the replacements of `@` or `SequencePT`, `ParallelChannelPT` and `ForLoopPT` respectively. Now we can plot the pulse with default parameters again and complete this example by in total 3 different approaches." - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "f0daf1dd", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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3HPPITExEampqTCZTIiOjsaoUaOQmJiIadOm4dKlSzh48CCAsj6Ztm3bYtasWWjWrBnatm2LpUuXYvPmzTh27FiFz3H69GkEBASgdu3a5e679957MWbMGCQkJGDy5MmIjo7G5s2bAQAZGRlITEzEbbfdhvj4eNx222146KGHHB5fp04dnD59WuZXhYjkwGKHiBShdevW9t8DAgJw0003oVWrVvZlMTExAIDz588DKGs03rx5s70HKCwsDM2aNQMAnDhxosLnKCwshNForLCJ+sbnt330ZnuuoUOH4sCBA2jatCmefPJJfPvtt+UeHxwcbD/rRETKYhA9ACIiAAgMDHS4rdPpHJbZChSr1QoAyMvLQ9++ffHyyy+X21ZcXFyFzxEdHY2CggIUFxcjKCio2ue3PVe7du1w8uRJfP311/juu+/wwAMPoEePHli1apV9/cuXL6NWrVrO7i4R+RCLHSJSpXbt2mH16tVo0KABDAbn/pS1adMGAPDrr7/af3dWREQEBg4ciIEDB+Jf//oXevXqhcuXL6NmzZoAypql27Zt69I2icg3+DEWEanS2LFjcfnyZTz00EPYvXs3Tpw4gW+++QbDhg2DxWKp8DG1atVCu3btsH37dpee69VXX8Unn3yCI0eO4NixY1i5ciViY2Mdvifo+++/xz333OPJLhGRl7DYISJVqlOnDn744QdYLBbcc889aNWqFSZMmICoqCjo9ZX/aRs5ciT++9//uvRc4eHhmDNnDjp06ICOHTvi1KlT+Oqrr+zPs2PHDly9ehX/+te/PNonIvIOfqkgEfmVwsJCNG3aFMuXL0dycrIs2xw4cCCSkpIwdepUWbZHRPLimR0i8ivBwcH48MMPq/zyQVcUFxejVatWmDhxoizbIyL58cwOERERaRrP7BAREZGmsdghIiIiTWOxQ0RERJrGYoeIiIg0jcUOERERaRqLHSIiItI0FjtERESkaSx2iIiISNNY7BAREZGm/T/l/PQ0YEMlagAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "from qupulse.pulses.plotting import plot\n", - "\n", - "default_params = {\n", - " 'tx_sweep': 5,\n", - " 'N_y': 5,\n", - " 'x_start': 0,\n", - " 'x_stop': 3,\n", - " 'y_start': 0,\n", - " 'y_stop': 2,\n", - "}\n", - "_ = plot(snake_cds, parameters=default_params, plot_measurements=('x_pos','x_neg'))" + "and how this was superseeded by the `qupulse._program._loop.roll_constant_waveforms` function." ] } ], From 5614c5ec09b9918e7ff07800efa0e952d858914a Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Wed, 11 Jan 2023 10:19:21 +0100 Subject: [PATCH 027/441] Add to listed examples --- doc/source/concepts/pulsetemplates.rst | 4 +++- doc/source/examples/examples.rst | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/source/concepts/pulsetemplates.rst b/doc/source/concepts/pulsetemplates.rst index 1dbf5299c..9553f0e3f 100644 --- a/doc/source/concepts/pulsetemplates.rst +++ b/doc/source/concepts/pulsetemplates.rst @@ -75,7 +75,9 @@ Examples demonstrating the construction of pulse templates and parameters from v :ref:`/examples/01Measurements.ipynb` shows how to specify measurements. -Finally, :ref:`/examples/02CreatePrograms.ipynb` illustrates usage of the :meth:`.PulseTemplate.create_program` method. +:ref:`/examples/02CreatePrograms.ipynb` illustrates usage of the :meth:`.PulseTemplate.create_program` method. + +:ref:`physical_examples` show realistic use cases of pulse templates. .. rubric:: Footnotes .. [#tree] Regarded as objects in the programming language, each pulse template is a tree of PulseTemplate objects, where the atomic templates (:class:`.TablePulseTemplate` and :class:`.FunctionPulseTemplate` objects) are the leafs while the remaining ones form the inner nodes of the tree. diff --git a/doc/source/examples/examples.rst b/doc/source/examples/examples.rst index 210d2a5f7..c543712d4 100644 --- a/doc/source/examples/examples.rst +++ b/doc/source/examples/examples.rst @@ -35,6 +35,7 @@ All examples are provided as static text in this documentation and, additionally :caption: Physically motivated examples :name: physical_examples + 03SnakeChargeScan 03FreeInductionDecayExample 03GateConfigurationExample 03DynamicNuclearPolarisation From 65da6a9a068cba40d995ea7dee401234fe0325c3 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Wed, 11 Jan 2023 10:29:03 +0100 Subject: [PATCH 028/441] link snake scan --- doc/source/learners_guide.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/learners_guide.rst b/doc/source/learners_guide.rst index ddbe30a95..09b2fe44f 100644 --- a/doc/source/learners_guide.rst +++ b/doc/source/learners_guide.rst @@ -52,6 +52,7 @@ Basic pulse writing **Exercise Task 6:** Go through the :ref:`/examples/01ParameterConstraints.ipynb` example. It shows how to use parameter constraints to enforce invariants. +**Exercise Task 7:** Go through the :ref:`examples/03SnakeChargeScan.ipynb` example which shows a realistic pulse. Hardware limitations ^^^^^^^^^^^^^^^^^^^^ From f4d09d1feb3bb934238dd0400db697a1d5a00694 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Wed, 11 Jan 2023 15:47:13 +0100 Subject: [PATCH 029/441] Add a section about the program IR --- doc/source/concepts/concepts.rst | 3 ++- doc/source/concepts/instantiating.rst | 2 ++ doc/source/concepts/program.rst | 17 +++++++++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 doc/source/concepts/program.rst diff --git a/doc/source/concepts/concepts.rst b/doc/source/concepts/concepts.rst index ac17a2c44..f800614e8 100644 --- a/doc/source/concepts/concepts.rst +++ b/doc/source/concepts/concepts.rst @@ -6,4 +6,5 @@ This section explains the fundamental design concepts of qupulse. .. toctree:: pulsetemplates serialization - instantiating \ No newline at end of file + instantiating + program diff --git a/doc/source/concepts/instantiating.rst b/doc/source/concepts/instantiating.rst index fba88d883..1ca3edc7c 100644 --- a/doc/source/concepts/instantiating.rst +++ b/doc/source/concepts/instantiating.rst @@ -40,3 +40,5 @@ In contrast, the Zurich Instruments HDAWG allows arbitrary nesting levels and is However, as already mentioned, the user does not have to be concerned about this in regular use of qupulse, since this is dealt with transparently in the hardware backend. + +The section :ref:`program` touches the ideas behind the current program implementation i.e. :class:`.Loop`. diff --git a/doc/source/concepts/program.rst b/doc/source/concepts/program.rst new file mode 100644 index 000000000..0b34df0ae --- /dev/null +++ b/doc/source/concepts/program.rst @@ -0,0 +1,17 @@ +.. _program: + +Instantiated Pulse: Program +--------------------------- + +In qupulse an instantiated pulse template is called a program as it is something that an arbitrary waveform generator +(AWG) can execute/playback. It is created by the `create_program` method of the pulse template which returns a hardware +independent representation which is currently of type ``Loop``. Opposed to the `PulseTemplate` interfaces the interface of the program is currently not covered by the qupulse backward compatibility and stability guarantee. +This is reflected by the fact that it lives in the private module ``qupulse._program._loop``. + +There is no description of the details of the program object here to avoid outdated documentation. +The documentation is in the docstrings of the source code. +The program can be thought of as compact representation of a mapping :math:`\{t | 0 \le t \le t_{\texttt{duration}}} \rightarrow \mathbb{R}^n` from the time while the program lasts :math:´t´ to an n-dimensional voltage space :math:´\mathbb{R}^n´. +The dimensions are named by the channel names. + +The subpackage ``_qupulse._program`` also contains hardware specific translations of the programs for example a +transpiler to Zurich Instruments sequencing C in ``_qupulse._program.seqc``. From e94b4254dd5a5ec95b586492590c424a9ca789a0 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Wed, 11 Jan 2023 17:00:41 +0100 Subject: [PATCH 030/441] Add awg concept section --- doc/source/concepts/awgs.rst | 32 ++++++++++++++++++++++++++++++++ doc/source/concepts/concepts.rst | 2 ++ 2 files changed, 34 insertions(+) create mode 100644 doc/source/concepts/awgs.rst diff --git a/doc/source/concepts/awgs.rst b/doc/source/concepts/awgs.rst new file mode 100644 index 000000000..0b3497652 --- /dev/null +++ b/doc/source/concepts/awgs.rst @@ -0,0 +1,32 @@ +.. _hardware: + +How qupulse models AWGs +----------------------- + +This section is supposed to help you understand how qupulse sees AWGs and by extension help you understand the driver implementations in :py:mod:`~qupulse.hardware.awgs` and :py:mod:`~qupulse.hardware.feature_awg`. + +When a program is uploaded to an arbitrary waveform generator (AWG) it needs to brought in a form that the hardware +understands. +Most AWGs consist of three significant parts: + * The actual digital to analog converter (DAC) that outputs samples at a (semi-) fixed rate [1]_ + * A sequencer which tells the DAC what to do + * Waveform memory which contains sampled waveforms in a format that the DAC understands + +The sequencer feeds the data from the waveform memory to the DAC in the correct order. +Uploading a qupulse pulse to an AWG requires to sample the program, upload waveforms to the memory +and program the sequencer. + +The interface exposed by the vendor to program the sequencer reaches from a simple table like for +Tektronix' AWG5000 series to some kind of complex domain specific language (DSL) like Zurich Instrument' sequencing C. + +Basically all AWGs have some kind of limitations regarding the length of the waveform samples which is often of the +form :math:`n_{\texttt{samples}} = n_{\texttt{min}} + m \cdot n_{\texttt{div}}` with the minimal number of samples +:math:`n_{\texttt{min}}` and some divisor :math:`n_{\texttt{div}}`. + +.. topic:: Implementation detail (might be outdated) + + Holding a voltage for a long time was often best accomplished by repeating a waveform of :math:`n_{\texttt{min}}` to save waveform memory. + Earlier versions of qupulse required you to write your pulse in this way i.e. with a ``RepetitionPT``. + Now qupulse contains the function ``qupulse._program._loop.roll_constant_waveform`` which detects long constant waveforms and rolls them into corresponding repetitions. This should be done by the hardware backend automatically. + +.. [1] Some AWGs like the HDAWG can be programmed change the sample rate to a divisor of the "main" rate dynamically. diff --git a/doc/source/concepts/concepts.rst b/doc/source/concepts/concepts.rst index f800614e8..a191bb7e1 100644 --- a/doc/source/concepts/concepts.rst +++ b/doc/source/concepts/concepts.rst @@ -8,3 +8,5 @@ This section explains the fundamental design concepts of qupulse. serialization instantiating program + awgs + From 9264ef9121664f4ead659b588f56b59dbe5bf799 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Wed, 11 Jan 2023 17:01:08 +0100 Subject: [PATCH 031/441] Link AWG concepts in learners guide --- doc/source/learners_guide.rst | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/doc/source/learners_guide.rst b/doc/source/learners_guide.rst index 09b2fe44f..b3fbc62fe 100644 --- a/doc/source/learners_guide.rst +++ b/doc/source/learners_guide.rst @@ -57,7 +57,24 @@ Basic pulse writing Hardware limitations ^^^^^^^^^^^^^^^^^^^^ -This section is under construction. +This section is incomplete. + +.. topic:: Info + + **Estimated time:** + 20 minutes for reading + + **Target group:** People who want to use qupulse in an experiment. + + **Learning Goals:** + The learner can identify if a hardware limitation related exception that is raised is due to an error on their end and mitigate it. + The learner understands capabilities of at least one type of AWGs. + + +**Learning Task 1:** + +Read :ref:`program` and :ref:`awgs`. + Setup an experiment ^^^^^^^^^^^^^^^^^^^ From 4d82830b1577c14e2d016139fdf27f7d0cf12fc8 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Wed, 11 Jan 2023 17:08:20 +0100 Subject: [PATCH 032/441] Fix link to example --- doc/source/learners_guide.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/learners_guide.rst b/doc/source/learners_guide.rst index b3fbc62fe..87773eb83 100644 --- a/doc/source/learners_guide.rst +++ b/doc/source/learners_guide.rst @@ -52,7 +52,7 @@ Basic pulse writing **Exercise Task 6:** Go through the :ref:`/examples/01ParameterConstraints.ipynb` example. It shows how to use parameter constraints to enforce invariants. -**Exercise Task 7:** Go through the :ref:`examples/03SnakeChargeScan.ipynb` example which shows a realistic pulse. +**Exercise Task 7:** Go through the :ref:`/examples/03SnakeChargeScan.ipynb` example which shows a realistic pulse. Hardware limitations ^^^^^^^^^^^^^^^^^^^^ From b7793c14333fa6a3b92862f91195aef8ac9b4972 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 16 Jan 2023 10:11:53 +0100 Subject: [PATCH 033/441] Add more structure to learners guide and give more realistic time estimates. --- doc/source/learners_guide.rst | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/doc/source/learners_guide.rst b/doc/source/learners_guide.rst index 87773eb83..7ddce71a4 100644 --- a/doc/source/learners_guide.rst +++ b/doc/source/learners_guide.rst @@ -20,7 +20,7 @@ Basic pulse writing **Target group:** - **Learning Goals:** The learner is able to define and save a parameterized nested pulse template. The learner can use pulse identifiers measurement windows and parameter constraints as needed. The learner is able to verify pulse and measurement windows are as intended for a given parameter set by plotting and inspecting. The learner can load pulses from a file and other valid datasources and use them as a building block in their own pulses. + **Learning Goals:** The learner is able to define a parameterized nested pulse template. **Learning Task 1:** Read the concept section about :ref:`pulsetemplates`. @@ -44,6 +44,22 @@ Basic pulse writing * :ref:`/examples/00RetrospectiveConstantChannelAddition.ipynb` * :ref:`/examples/00TimeReversal.ipynb` +Pulse template features +^^^^^^^^^^^^^^^^^^^^^^^ + +.. topic:: Info + + **Estimated time:** + 20 minutes for reading + 60 minutes for the examples + 60 minutes for experimenting + + **Target group:** + + **Learning Goals:** The learner to save pulse templates to the file system. + The learner can use pulse identifiers measurement windows and parameter constraints as needed. The learner is able to verify pulse and measurement windows are as intended for a given parameter set by plotting and inspecting. The learner can load pulses from a file and other valid datasources and use them as a building block in their own pulses. + + **Learning Task 2:** Read the concept section about :ref:`serialization`. **Exercise Task 4:** Go through the :ref:`/examples/01PulseStorage.ipynb` example. It shows how to load and store pulse templates to disk. @@ -54,8 +70,10 @@ Basic pulse writing **Exercise Task 7:** Go through the :ref:`/examples/03SnakeChargeScan.ipynb` example which shows a realistic pulse. -Hardware limitations -^^^^^^^^^^^^^^^^^^^^ +Hardware capabilities and limitations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This section introduces aspects of the hardware that are relevant for every experimenter. This section is incomplete. From 0d3499ccd5b26af465378a3a4a80c5387219d3e8 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 16 Jan 2023 10:12:53 +0100 Subject: [PATCH 034/441] Include parameter in ConstantPT --- .../examples/00ConstantPulseTemplate.ipynb | 40 +++++-------------- 1 file changed, 10 insertions(+), 30 deletions(-) diff --git a/doc/source/examples/00ConstantPulseTemplate.ipynb b/doc/source/examples/00ConstantPulseTemplate.ipynb index a2fffaacc..41b71020a 100644 --- a/doc/source/examples/00ConstantPulseTemplate.ipynb +++ b/doc/source/examples/00ConstantPulseTemplate.ipynb @@ -4,30 +4,29 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# The ConstantPulseTemplate\n", + "# The ConstantPulseTemplate: Efficient constant voltage description.\n", "\n", "The `ConstantPulseTemplate`(or short `ConstantPT`) can be used to define pulse templates with all channels a constant value. The template is easy to define and allows backends to optimize the waveforms on an AWG." ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "{'A', 'B'}\n", - "{'A': Expression('10.0000000000000'), 'B': Expression('2.00000000000000')}\n" + "{'B', 'A'}\n", + "{'A': ExpressionScalar('10.0000000000000'), 'B': ExpressionScalar('1.0*b')}\n" ] } ], "source": [ - "%matplotlib inline\n", "from qupulse.pulses import ConstantPT\n", "\n", - "constant_template = ConstantPT(10, {'A': 1., 'B': .2})\n", + "constant_template = ConstantPT(10, {'A': 1., 'B': 'b * 0.1'})\n", "\n", "print(constant_template.defined_channels)\n", "print(constant_template.integral)" @@ -42,42 +41,23 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 2, "metadata": {}, "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "c:\\projects\\qupulse\\qupulse\\pulses\\plotting.py:236: UserWarning: Matplotlib is currently using module://ipykernel.pylab.backend_inline, which is a non-GUI backend, so cannot show the figure.\n", - " axes.get_figure().show()\n" - ] - }, { "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" + "text/plain": "
", + "image/png": "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SzFSUVGRFR8fb6WlpVm33HKL9fjjj3u7JKOMHTvWuvHGG71dhvF69eplDR061Kntr3/9qzVw4EAvVWQmSdbHH3/seG+3262oqCjrpZdecrT98ccfVkREhPXWW29VeT3M3FygkpISpaenKykpyak9KSlJW7du9VJVl4aCggJJUp06dbxciZlGjBihXr16qWvXrt4uxUirVq1S+/btdc8996hBgwa65pprtGDBAm+XZZwbb7xR69at0+7duyVJ//u//6vNmzerZ8+eXq7MbFlZWTpy5IjTsTEgIEC33HJLtRwbL7kHZ3ra0aNHVVZWpsjISKf2yMhIHTlyxEtVmc+yLI0ePVo33nij2rRp4+1yjPPBBx/o22+/1fbt271dirH279+vefPmafTo0Ro/fry2bdumkSNHKiAgQIMHD/Z2ecYYO3asCgoK1KpVK/n6+qqsrEwvvviiBgwY4O3SjHbm+FfRsfHgwYNVvn/CjYfYbDan95ZllWuD5zz66KP67rvvtHnzZm+XYpycnBw9/vjjWrt2rQIDA71djrHsdrvat2+vqVOnSpKuueYa/fjjj5o3bx7hxoNSU1P1H//xH1q+fLmuvPJKZWZmKiUlRTExMRoyZIi3yzOet46NhJsLVK9ePfn6+pabpcnPzy+XWOEZjz32mFatWqWNGzeqUaNG3i7HOOnp6crPz1diYqKjraysTBs3btScOXNUXFwsX19fL1ZohujoaF1xxRVOba1bt9aKFSu8VJGZnnzyST399NO69957JUlt27bVwYMHNW3aNMJNFYqKipJ0egYnOjra0V5dx0bW3Fwgf39/JSYmKi0tzak9LS1NHTp08FJVZrIsS48++qhWrlypzz//XHFxcd4uyUi33nqrvv/+e2VmZjpe7du31/3336/MzEyCjYd07Nix3K0Mdu/erSZNmnipIjOdPHlSPj7OhzpfX18uBa9icXFxioqKcjo2lpSUaMOGDdVybGTmxgNGjx6tQYMGqX379rrhhhs0f/58ZWdnKzk52dulGWXEiBFavny5/uu//kthYWGO2bKIiAgFBQV5uTpzhIWFlVvHFBISorp167K+yYNGjRqlDh06aOrUqerXr5+2bdum+fPna/78+d4uzSi9e/fWiy++qMaNG+vKK69URkaGZs6cqaFDh3q7tIve77//rr179zreZ2VlKTMzU3Xq1FHjxo2VkpKiqVOnKj4+XvHx8Zo6daqCg4N13333VX1xVX491iXizTfftJo0aWL5+/tbCQkJXJ5cBSRV+HrnnXe8XZrxuBS8anz66adWmzZtrICAAKtVq1bW/PnzvV2ScQoLC63HH3/caty4sRUYGGg1a9bMmjBhglVcXOzt0i5669evr/D/k4cMGWJZ1unLwZ977jkrKirKCggIsG6++Wbr+++/r5babJZlWVUfoQAAAKoHa24AAIBRCDcAAMAohBsAAGAUwg0AADAK4QYAABiFcAMAAIxCuAEAAEYh3AAAAKMQbgBUu0mTJunqq6/22v6fffZZPfTQQx4b7y9/+YtWrlzpsfEAXBjuUAzAo2w22zk/HzJkiOPp4nXr1q2mqv7Pzz//rPj4eH333Xdq2rSpR8ZctWqVxowZo59++qncQxoBVD/CDQCPOvNAU0lKTU3VxIkTnZ5+HRQUpIiICG+UJkmaOnWqNmzYoDVr1nhszLKyMsXExGjJkiXq0aOHx8YFUDn8JwYAj4qKinK8IiIiZLPZyrX9+2mpBx54QH379tXUqVMVGRmp2rVra/LkySotLdWTTz6pOnXqqFGjRlq8eLHTvnJzc9W/f39ddtllqlu3rvr06aMDBw6cs74PPvhAd9xxh1Nbp06dNHLkSD311FOqU6eOoqKiNGnSJKc+kyZNUuPGjRUQEKCYmBiNHDnS8Zmvr6969uyp999/v1K/GQDPItwAqBE+//xzHT58WBs3btTMmTM1adIk3X777brsssv09ddfKzk5WcnJycrJyZEknTx5Up07d1ZoaKg2btyozZs3KzQ0VLfddptKSkoq3Mevv/6qH374Qe3bty/32bvvvquQkBB9/fXXmjFjhqZMmaK0tDRJ0kcffaRZs2bp7bff1p49e/TJJ5+obdu2Tttfe+212rRpk4d/FQCVQbgBUCPUqVNHr7/+ulq2bKmhQ4eqZcuWOnnypMaPH6/4+HiNGzdO/v7+2rJli6TTMzA+Pj5auHCh2rZtq9atW+udd95Rdna2vvjiiwr3cfDgQVmWpZiYmHKftWvXTs8995zi4+M1ePBgtW/fXuvWrZMkZWdnKyoqSl27dlXjxo117bXX6sEHH3TavmHDhsrOzpbdbvfsDwPAbYQbADXClVde6bQYNzIy0ml2xNfXV3Xr1lV+fr4kKT09XXv37lVYWJhCQ0MVGhqqOnXq6I8//tC+ffsq3MepU6ckSYGBgeU+a9eundP76Ohox77uuecenTp1Ss2aNdODDz6ojz/+WKWlpU79g4KCZLfbVVxcXIlvD8CT/LxdAABIUq1atZze22y2CtvOzIzY7XYlJiZq2bJl5caqX79+hfuoV6+epNOnp/69z7n2FRsbq127diktLU3//Oc/9cgjj+jll1/Whg0bHNsdP35cwcHBCgoKcvUrA6gihBsAF6WEhASlpqaqQYMGCg8Pd2mbyy+/XOHh4dqxY4datGjh1v6CgoJ0xx136I477tCIESPUqlUrff/990pISJAk/fDDD45/A/AuTksBuCjdf//9qlevnvr06aNNmzYpKytLGzZs0OOPP65Dhw5VuI2Pj4+6du2qzZs3u7WvJUuWaNGiRfrhhx+0f/9+vffeewoKClKTJk0cfTZt2qSkpKQL+k4APINwA+CiFBwcrI0bN6px48b661//qtatW2vo0KE6derUOWdyHnroIX3wwQduLfytXbu2FixYoI4dO6pdu3Zat26dPv30U8dNCHNzc7V161b97W9/u+DvBeDCcRM/AJcUy7J0/fXXKyUlRQMGDPDImE8++aQKCgo0f/58j4wH4MIwcwPgkmKz2TR//vxyVztdiAYNGuj555/32HgALgwzNwAAwCjM3AAAAKMQbgAAgFEINwAAwCiEGwAAYBTCDQAAMArhBgAAGIVwAwAAjEK4AQAARiHcAAAAo/w/CSwDtMUQAowAAAAASUVORK5CYII=\n" }, + "metadata": {}, "output_type": "display_data" } ], "source": [ "from qupulse.pulses.plotting import plot\n", "\n", - "_ = plot(constant_template, sample_rate=100)" + "_ = plot(constant_template, parameters={'b': 2.2}, sample_rate=100)" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { From 6329e4aa4f854f72fcfcf75780964b1a5a07c670 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 17 Jan 2023 18:35:04 +0100 Subject: [PATCH 035/441] Restructure expression code and start specifying a clean interface --- qupulse/expressions/__init__.py | 59 ++++++++++ .../{expressions.py => expressions/legacy.py} | 64 ++--------- qupulse/expressions/protocol.py | 105 ++++++++++++++++++ 3 files changed, 174 insertions(+), 54 deletions(-) create mode 100644 qupulse/expressions/__init__.py rename qupulse/{expressions.py => expressions/legacy.py} (88%) create mode 100644 qupulse/expressions/protocol.py diff --git a/qupulse/expressions/__init__.py b/qupulse/expressions/__init__.py new file mode 100644 index 000000000..d2edd8a77 --- /dev/null +++ b/qupulse/expressions/__init__.py @@ -0,0 +1,59 @@ +from typing import Type, TypeVar +from numbers import Real + +import numpy as np +import sympy + +from . import legacy, protocol + + +__all__ = ["Expression", "ExpressionVector", "ExpressionScalar", + "NonNumericEvaluation", "ExpressionVariableMissingException"] + + +Expression: Type[protocol.Expression] = legacy.Expression +ExpressionScalar: Type[protocol.ExpressionScalar] = legacy.ExpressionScalar +ExpressionVector: Type[protocol.ExpressionVector] = legacy.ExpressionVector + + +ExpressionLike = TypeVar('ExpressionLike', str, Real, sympy.Expr, ExpressionScalar) + + +class ExpressionVariableMissingException(Exception): + """An exception indicating that a variable value was not provided during expression evaluation. + + See also: + qupulse.expressions.Expression + """ + + def __init__(self, variable: str, expression: Expression) -> None: + super().__init__() + self.variable = variable + self.expression = expression + + def __str__(self) -> str: + return f"Could not evaluate <{self.expression}>: A value for variable <{self.variable}> is missing!" + + +class NonNumericEvaluation(Exception): + """An exception that is raised if the result of evaluate_numeric is not a number. + + See also: + qupulse.expressions.Expression.evaluate_numeric + """ + + def __init__(self, expression: Expression, non_numeric_result, call_arguments): + self.expression = expression + self.non_numeric_result = non_numeric_result + self.call_arguments = call_arguments + + def __str__(self) -> str: + if isinstance(self.non_numeric_result, np.ndarray): + dtype = self.non_numeric_result.dtype + + if dtype == np.dtype('O'): + dtypes = set(map(type, self.non_numeric_result.flat)) + return f"The result of evaluate_numeric is an array with the types {dtypes} which is not purely numeric" + else: + dtype = type(self.non_numeric_result) + return f"The result of evaluate_numeric is of type {dtype} which is not a number" diff --git a/qupulse/expressions.py b/qupulse/expressions/legacy.py similarity index 88% rename from qupulse/expressions.py rename to qupulse/expressions/legacy.py index bbfad3eed..eeb64ee97 100644 --- a/qupulse/expressions.py +++ b/qupulse/expressions/legacy.py @@ -18,7 +18,9 @@ get_most_simple_representation, get_variables, evaluate_lamdified_exact_rational from qupulse.utils.types import TimeType -__all__ = ["Expression", "ExpressionVariableMissingException", "ExpressionScalar", "ExpressionVector", "ExpressionLike"] +import qupulse.expressions + +__all__ = ["Expression", "ExpressionScalar", "ExpressionVector"] _ExpressionType = TypeVar('_ExpressionType', bound='Expression') @@ -60,9 +62,9 @@ def _parse_evaluate_numeric_vector(vector_result: numpy.ndarray) -> numpy.ndarra if not issubclass(vector_result.dtype.type, allowed_scalar): obj_types = set(map(type, vector_result.flat)) if all(issubclass(obj_type, sympy.Integer) for obj_type in obj_types): - result = vector_result.astype(numpy.int64) + vector_result = vector_result.astype(numpy.int64) elif all(issubclass(obj_type, (sympy.Integer, sympy.Float)) for obj_type in obj_types): - result = vector_result.astype(float) + vector_result = vector_result.astype(float) else: raise ValueError("Could not parse vector result", vector_result) return vector_result @@ -98,7 +100,7 @@ def _parse_evaluate_numeric_arguments(self, eval_args: Mapping[str, Number]) -> # we forward qupulse errors, I down like this raise else: - raise ExpressionVariableMissingException(key_error.args[0], self) from key_error + raise qupulse.expressions.ExpressionVariableMissingException(key_error.args[0], self) from key_error def evaluate_in_scope(self, scope: Mapping) -> Union[Number, numpy.ndarray]: """Evaluate the expression by taking the variables from the given scope (typically of type Scope but it can be @@ -129,7 +131,7 @@ def evaluate_symbolic(self, substitutions: Mapping[Any, Any]) -> 'Expression': def _evaluate_to_time_dependent(self, scope: Mapping) -> Union['Expression', Number, numpy.ndarray]: try: return self.evaluate_numeric(**scope, t=sympy.symbols('t')) - except NonNumericEvaluation as non_num: + except qupulse.expressions.NonNumericEvaluation as non_num: return ExpressionScalar(non_num.non_numeric_result) except TypeError: return self.evaluate_symbolic(scope) @@ -212,7 +214,7 @@ def evaluate_in_scope(self, scope: Mapping) -> numpy.ndarray: try: return _parse_evaluate_numeric_vector(result) except ValueError as err: - raise NonNumericEvaluation(self, result, scope) from err + raise qupulse.expressions.NonNumericEvaluation(self, result, scope) from err def get_serialization_data(self) -> Sequence[str]: serialized_items = list(map(get_most_simple_representation, self._expression_items)) @@ -444,7 +446,7 @@ def evaluate_with_exact_rationals(self, scope: Mapping) -> Union[Number, numpy.n try: return _parse_evaluate_numeric(result) except ValueError as err: - raise NonNumericEvaluation(self, result, scope) from err + raise qupulse.expressions.NonNumericEvaluation(self, result, scope) from err def evaluate_in_scope(self, scope: Mapping) -> Union[Number, numpy.ndarray]: parsed_kwargs = self._parse_evaluate_numeric_arguments(scope) @@ -453,50 +455,4 @@ def evaluate_in_scope(self, scope: Mapping) -> Union[Number, numpy.ndarray]: try: return _parse_evaluate_numeric(result) except ValueError as err: - raise NonNumericEvaluation(self, result, scope) from err - - -class ExpressionVariableMissingException(Exception): - """An exception indicating that a variable value was not provided during expression evaluation. - - See also: - qupulse.expressions.Expression - """ - - def __init__(self, variable: str, expression: Expression) -> None: - super().__init__() - self.variable = variable - self.expression = expression - - def __str__(self) -> str: - return "Could not evaluate <{}>: A value for variable <{}> is missing!".format( - str(self.expression), self.variable) - - -class NonNumericEvaluation(Exception): - """An exception that is raised if the result of evaluate_numeric is not a number. - - See also: - qupulse.expressions.Expression.evaluate_numeric - """ - - def __init__(self, expression: Expression, non_numeric_result: Any, call_arguments: Mapping): - self.expression = expression - self.non_numeric_result = non_numeric_result - self.call_arguments = call_arguments - - def __str__(self) -> str: - if isinstance(self.non_numeric_result, numpy.ndarray): - dtype = self.non_numeric_result.dtype - - if dtype == numpy.dtype('O'): - dtypes = set(map(type, self.non_numeric_result.flat)) - "The result of evaluate_numeric is an array with the types {} " \ - "which is not purely numeric".format(dtypes) - else: - dtype = type(self.non_numeric_result) - return "The result of evaluate_numeric is of type {} " \ - "which is not a number".format(dtype) - - -ExpressionLike = TypeVar('ExpressionLike', str, Number, sympy.Expr, ExpressionScalar) + raise qupulse.expressions.NonNumericEvaluation(self, result, scope) from err diff --git a/qupulse/expressions/protocol.py b/qupulse/expressions/protocol.py new file mode 100644 index 000000000..58fbd0803 --- /dev/null +++ b/qupulse/expressions/protocol.py @@ -0,0 +1,105 @@ +"""This module contains the interface / protocol descriptions.""" + +from typing import Protocol +from typing import Mapping, Union, Sequence, Hashable, Any + +from numbers import Real + +import numpy as np + + +class Ordered(Protocol): + def __lt__(self, other): + pass + + def __le__(self, other): + pass + + def __gt__(self, other): + pass + + def __ge__(self, other): + pass + + +class Scalar(Protocol): + def __add__(self, other): + pass + + def __sub__(self, other): + pass + + def __mul__(self, other): + pass + + def __truediv__(self, other): + pass + + def __floordiv__(self, other): + pass + + def __ceil__(self): + pass + + def __floor__(self): + pass + + def __float__(self): + pass + + def __int__(self): + pass + + def __abs__(self): + pass + + + + +class Expression(Hashable, Protocol): + def evaluate_in_scope(self, scope: Mapping) -> Union[Real, np.ndarray]: + """Evaluate the expression by taking the variables from the given scope (typically of type Scope but it can be + any mapping.) + Args: + scope: + + Returns: + + """ + + def evaluate_symbolic(self, substitutions: Mapping[str, Any]) -> 'Expression': + """Substitute a part of the expression for another""" + + def evaluate_time_dependent(self, scope: Mapping) -> Union['Expression', Real, np.ndarray]: + """Evaluate to a time dependent expression or a constant.""" + @property + def variables(self) -> Sequence[str]: + """ Get all free variables in the expression. + + Returns: + A collection of all free variables occurring in the expression. + """ + raise NotImplementedError() + + @classmethod + def make(cls, + expression_or_dict, + numpy_evaluation=None) -> 'Expression': + """Backward compatible expression generation to allow creation from dict.""" + raise NotImplementedError() + + @property + def underlying_expression(self) -> Any: + """Return some internal unspecified representation""" + raise NotImplementedError() + + def get_serialization_data(self): + pass + + +class ExpressionScalar(Expression, Scalar, Ordered, Protocol): + pass + + +class ExpressionVector(Expression, Protocol): + pass From 4c9c52608f2d76e3a899cb3eae03624dd04b8a9b Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 17 Jan 2023 18:44:15 +0100 Subject: [PATCH 036/441] Catch zero duration waveforms already in Waveform instantiate method. --- qupulse/pulses/table_pulse_template.py | 7 ++++--- tests/pulses/table_pulse_template_tests.py | 15 +++++++++++---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/qupulse/pulses/table_pulse_template.py b/qupulse/pulses/table_pulse_template.py index 86902da29..656127b90 100644 --- a/qupulse/pulses/table_pulse_template.py +++ b/qupulse/pulses/table_pulse_template.py @@ -259,6 +259,9 @@ def get_entries_instantiated(self, parameters: Dict[str, numbers.Real]) \ duration = max(instantiated[-1].t for instantiated in instantiated_entries.values()) + if duration == 0: + return {} + # ensure that all channels have equal duration for channel, instantiated in instantiated_entries.items(): final_entry = instantiated[-1] @@ -324,9 +327,7 @@ def build_waveform(self, waveforms = [TableWaveform.from_table(*ch_instantiated) for ch_instantiated in instantiated] - mc_waveform = MultiChannelWaveform.from_parallel(waveforms) - if mc_waveform.duration != 0: - return mc_waveform + return MultiChannelWaveform.from_parallel(waveforms) @staticmethod def from_array(times: np.ndarray, voltages: np.ndarray, channels: List[ChannelID]) -> 'TablePulseTemplate': diff --git a/tests/pulses/table_pulse_template_tests.py b/tests/pulses/table_pulse_template_tests.py index 40414ccd5..46cf99ae8 100644 --- a/tests/pulses/table_pulse_template_tests.py +++ b/tests/pulses/table_pulse_template_tests.py @@ -219,15 +219,22 @@ def test_external_constraints(self): table.build_waveform(parameters=dict(v=1., w=2, t=0.1, x=1.2, y=1, h=2), channel_mapping={0: 0, 1: 1}) - def test_get_entries_instantiated_one_entry_float_float(self) -> None: + def test_get_entries_instantiated_empty(self): table = TablePulseTemplate({0: [(0, 2)]}) + self.assertEqual({}, table.get_entries_instantiated(dict())) + + def test_get_entries_instantiated_one_entry_float_float(self) -> None: + table = TablePulseTemplate({0: [(1, 2)]}) instantiated_entries = table.get_entries_instantiated(dict())[0] - self.assertEqual([(0, 2, HoldInterpolationStrategy())], instantiated_entries) + self.assertEqual([(0, 2, HoldInterpolationStrategy()), (1, 2, HoldInterpolationStrategy())], + instantiated_entries) def test_get_entries_instantiated_one_entry_float_declaration(self) -> None: - table = TablePulseTemplate({0: [(0, 'foo')]}) + table = TablePulseTemplate({0: [(1, 'foo')]}) instantiated_entries = table.get_entries_instantiated({'foo': 2})[0] - self.assertEqual([(0, 2, HoldInterpolationStrategy())], instantiated_entries) + self.assertEqual([(0, 2, HoldInterpolationStrategy()), + (1, 2, HoldInterpolationStrategy())], + instantiated_entries) def test_get_entries_instantiated_two_entries_float_float_declaration_float(self) -> None: table = TablePulseTemplate({0: [('foo', -2.)]}) From 65d10f9ac75d5f9de19c684de04f007ba942ee78 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 10 Feb 2023 18:28:22 +0100 Subject: [PATCH 037/441] Add way to associate a hardware channel with a channel mask --- qupulse/hardware/dacs/alazar2.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/qupulse/hardware/dacs/alazar2.py b/qupulse/hardware/dacs/alazar2.py index 641606d91..15a881cf8 100644 --- a/qupulse/hardware/dacs/alazar2.py +++ b/qupulse/hardware/dacs/alazar2.py @@ -1,3 +1,4 @@ +import dataclasses from typing import Union, Iterable, Dict, Tuple, Mapping, Optional from types import MappingProxyType import logging @@ -30,6 +31,13 @@ def __init__(self, atsaverage_card: 'atsaverage.core.AlazarCard'): self._raw_data_mask = None self.default_buffer_strategy: Optional[BufferStrategySettings] = None + # sadly this is required to associate masks with their corresponding channels + # the better place for this would be in the MeasurementMask class but we do not want to touch it + # to avoid breaking experiments + # TODO: possible improvement is wildcard/regex support but this is complicated to maintain + # (competing matches etc) + self._mask_name_to_hw_channel = {} + @property def atsaverage_card(self): return self._atsaverage_card @@ -74,6 +82,13 @@ def _make_scanline_definition(self, program: AlazarProgram): sample_rate_in_hz = int(sample_rate_in_ghz * 10 ** 9) masks = program.masks(make_best_mask) + for mask in masks: + try: + mask.channel = self._mask_name_to_hw_channel[mask.identifier] + except KeyError as err: + raise KeyError(f"There was no hardware channel registered for the mask {mask!r}", + mask.identifier) from err + if sample_rate_in_ghz != program.sample_rate: raise RuntimeError("Masks were registered with a different sample rate") return create_scanline_definition(masks, program.operations, @@ -130,3 +145,14 @@ def get_input_range(operation_id: str): input_range = get_input_range(op_name) data[op_name] = scanline_data.operationResults[op_name].getAsVoltage(input_range) return data + + def register_mask_for_channel(self, mask_id: str, hw_channel: int) -> None: + """ + + Args: + mask_id: Identifier of the measurement windows + hw_channel: Associated hardware channel (0, 1, 2, 3) + """ + if hw_channel not in range(4): + raise ValueError('{} is not a valid hw channel'.format(hw_channel)) + self._mask_name_to_hw_channel[mask_id] = hw_channel From f1185d960dd026d6fe46f867fca89d969c4f94ba Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 17 Feb 2023 11:18:17 +0100 Subject: [PATCH 038/441] Allows str as volatile argument and verify volatile parameters --- qupulse/pulses/pulse_template.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/qupulse/pulses/pulse_template.py b/qupulse/pulses/pulse_template.py index 46dd5294f..025b0ea52 100644 --- a/qupulse/pulses/pulse_template.py +++ b/qupulse/pulses/pulse_template.py @@ -116,7 +116,7 @@ def create_program(self, *, channel_mapping: Optional[Mapping[ChannelID, Optional[ChannelID]]]=None, global_transformation: Optional[Transformation]=None, to_single_waveform: Set[Union[str, 'PulseTemplate']]=None, - volatile: Set[str] = None) -> Optional['Loop']: + volatile: Union[Set[str], str] = None) -> Optional['Loop']: """Translates this PulseTemplate into a program Loop. The returned Loop represents the PulseTemplate with all parameter values instantiated provided as dictated by @@ -144,6 +144,10 @@ def create_program(self, *, to_single_waveform = set() if volatile is None: volatile = set() + elif isinstance(volatile, str): + volatile = {volatile} + else: + volatile = set(volatile) # make sure all channels are mapped complete_channel_mapping = {channel: channel for channel in self.defined_channels} @@ -167,6 +171,12 @@ def create_program(self, *, scope = DictScope(values=FrozenDict(parameters), volatile=volatile) + for volatile_name in scope.get_volatile_parameters(): + if volatile_name not in scope: + warnings.warn(f"The volatile parameter {volatile_name!r} is not in the given parameters.", + category=UnknownVolatileParameter, + stacklevel=2) + root_loop = Loop() # call subclass specific implementation @@ -531,3 +541,6 @@ def __str__(self) -> str: self.templateA, self.templateB, ', '.join(self.names) ) + +class UnknownVolatileParameter(RuntimeWarning): + pass From cf9292ae29af2c279732c1da29dc6cb2ce1253ba Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 17 Feb 2023 11:25:18 +0100 Subject: [PATCH 039/441] Add a test --- qupulse/pulses/pulse_template.py | 3 ++- tests/pulses/pulse_template_tests.py | 34 +++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/qupulse/pulses/pulse_template.py b/qupulse/pulses/pulse_template.py index 025b0ea52..c8b598e83 100644 --- a/qupulse/pulses/pulse_template.py +++ b/qupulse/pulses/pulse_template.py @@ -26,7 +26,8 @@ from qupulse.pulses.measurement import MeasurementDefiner, MeasurementDeclaration from qupulse.parameter_scope import Scope, DictScope -__all__ = ["PulseTemplate", "AtomicPulseTemplate", "DoubleParameterNameException", "MappingTuple"] +__all__ = ["PulseTemplate", "AtomicPulseTemplate", "DoubleParameterNameException", "MappingTuple", + "UnknownVolatileParameter"] MappingTuple = Union[Tuple['PulseTemplate'], diff --git a/tests/pulses/pulse_template_tests.py b/tests/pulses/pulse_template_tests.py index d9919b10c..64e5451d6 100644 --- a/tests/pulses/pulse_template_tests.py +++ b/tests/pulses/pulse_template_tests.py @@ -10,7 +10,7 @@ from qupulse.expressions import Expression, ExpressionScalar from qupulse.pulses import ConstantPT, FunctionPT, RepetitionPT, ForLoopPT, ParallelChannelPT, MappingPT,\ TimeReversalPT, AtomicMultiChannelPT -from qupulse.pulses.pulse_template import AtomicPulseTemplate, PulseTemplate +from qupulse.pulses.pulse_template import AtomicPulseTemplate, PulseTemplate, UnknownVolatileParameter from qupulse.pulses.multi_channel_pulse_template import MultiChannelWaveform from qupulse._program._loop import Loop @@ -312,6 +312,38 @@ def test_create_program_channel_mapping(self): _internal_create_program.assert_called_once_with(**expected_internal_kwargs, parent_loop=Loop()) + def test_create_program_volatile(self): + template = PulseTemplateStub(defined_channels={'A', 'B'}) + + parameters = {'abc': 1.} + + expected_internal_kwargs = dict(scope=DictScope.from_kwargs(volatile={'abc'}, **parameters), + measurement_mapping=dict(), + channel_mapping={'A': 'A', 'B': 'B'}, + global_transformation=None, + to_single_waveform=set()) + + with mock.patch.object(template, '_internal_create_program') as _internal_create_program: + template.create_program(parameters=parameters, volatile='abc') + + _internal_create_program.assert_called_once_with(**expected_internal_kwargs, parent_loop=Loop()) + with mock.patch.object(template, '_internal_create_program') as _internal_create_program: + template.create_program(parameters=parameters, volatile={'abc'}) + + _internal_create_program.assert_called_once_with(**expected_internal_kwargs, parent_loop=Loop()) + + expected_internal_kwargs = dict(scope=DictScope.from_kwargs(volatile={'abc', 'dfg'}, **parameters), + measurement_mapping=dict(), + channel_mapping={'A': 'A', 'B': 'B'}, + global_transformation=None, + to_single_waveform=set()) + with mock.patch.object(template, '_internal_create_program') as _internal_create_program: + with self.assertWarns(UnknownVolatileParameter): + template.create_program(parameters=parameters, volatile={'abc', 'dfg'}) + + _internal_create_program.assert_called_once_with(**expected_internal_kwargs, parent_loop=Loop()) + + def test_create_program_none(self) -> None: template = PulseTemplateStub(defined_channels={'A'}, parameter_names={'foo'}) parameters = {'foo': 2.126, 'bar': -26.2, 'hugo': 'exp(sin(pi/2))'} From 27cd6bef3a0d774a179d7d5758d1e5fcdb6281af Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 17 Feb 2023 11:40:07 +0100 Subject: [PATCH 040/441] Change lcm implementation to map math.lcm and use it if available --- qupulse/utils/numeric.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/qupulse/utils/numeric.py b/qupulse/utils/numeric.py index b7b505f9d..5a263a39a 100644 --- a/qupulse/utils/numeric.py +++ b/qupulse/utils/numeric.py @@ -7,9 +7,16 @@ import sympy -def lcm(a: int, b: int): - """least common multiple""" - return a * b // gcd(a, b) +try: + from math import lcm +except ImportError: + # python version < 3.9 + def lcm(*integers: int): + """Re-implementation of the least common multiple function that is in the standard library since python 3.9""" + result = 1 + for value in integers: + result = result * value // gcd(value, result) + return result def smallest_factor_ge(n: int, min_factor: int, brute_force: int = 5): From 2775c201e8a8cd4590de869e008e47f59cdc306a Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 17 Feb 2023 12:15:29 +0100 Subject: [PATCH 041/441] Replace plt.get_cmap with colormaps.get_cmap if available --- qupulse/plotting.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/qupulse/plotting.py b/qupulse/plotting.py index cfe8d8c1e..06f4d0227 100644 --- a/qupulse/plotting.py +++ b/qupulse/plotting.py @@ -16,6 +16,13 @@ import itertools import functools +try: + from matplotlib import colormaps + get_cmap = colormaps.get_cmap +except (ImportError, AttributeError): # pragma: no cover + # was deprecated in matplotlib 3.7, but we keep it around to allow this code to work with older versions + get_cmap = plt.get_cmap + from qupulse._program import waveforms from qupulse.utils.types import ChannelID, MeasurementWindow, has_type_interface from qupulse.pulses.pulse_template import PulseTemplate @@ -222,7 +229,7 @@ def plot(pulse: PulseTemplate, if name in plot_measurements: measurement_dict.setdefault(name, []).append((begin, begin+length)) - color_map = plt.cm.get_cmap('plasma') + color_map = get_cmap('plasma') meas_colors = {name: color_map(i/len(measurement_dict)) for i, name in enumerate(measurement_dict.keys())} for name, begin_end_list in measurement_dict.items(): From 28bc4cdd19499544ba2c9b0234b3de276c433728 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 17 Feb 2023 12:30:37 +0100 Subject: [PATCH 042/441] Use typing-extensions in python version < 3.8 --- qupulse/expressions/protocol.py | 7 ++++++- setup.cfg | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/qupulse/expressions/protocol.py b/qupulse/expressions/protocol.py index 58fbd0803..0280747d3 100644 --- a/qupulse/expressions/protocol.py +++ b/qupulse/expressions/protocol.py @@ -1,6 +1,11 @@ """This module contains the interface / protocol descriptions.""" -from typing import Protocol +try: + from typing import Protocol +except ImportError: + # python version < 3.8 + from typing_extensions import Protocol + from typing import Mapping, Union, Sequence, Hashable, Any from numbers import Real diff --git a/setup.cfg b/setup.cfg index 68647e6f9..17185b685 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,6 +26,7 @@ install_requires = sympy>=1.1.1 numpy cached_property;python_version<'3.8' + typing-extensions;python_version<'3.8' frozendict lazy_loader test_suite = tests From 9ef4a7ce5a4a8b9ccfdd3cf4c9a52580bcb68cb9 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 17 Feb 2023 12:51:33 +0100 Subject: [PATCH 043/441] Add test --- tests/utils/numeric_tests.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/utils/numeric_tests.py b/tests/utils/numeric_tests.py index b632f058b..38410c682 100644 --- a/tests/utils/numeric_tests.py +++ b/tests/utils/numeric_tests.py @@ -5,7 +5,7 @@ from collections import deque from itertools import islice -from qupulse.utils.numeric import approximate_rational, approximate_double, smallest_factor_ge +from qupulse.utils.numeric import approximate_rational, approximate_double, smallest_factor_ge, lcm def stern_brocot_sequence() -> Iterator[int]: @@ -120,3 +120,14 @@ def test_smallest_factor_ge(self): self.assertEqual(smallest_factor_ge(45, 4), 5, brute_force) self.assertEqual(smallest_factor_ge(45, 5), 5, brute_force) self.assertEqual(smallest_factor_ge(36, 8), 9, brute_force) + + +class LeastCommonMultipleTests(unittest.TestCase): + def test_few_args(self): + self.assertEqual(1, lcm()) + self.assertEqual(5, lcm(5)) + + def test_multi_args(self): + self.assertEqual(15, lcm(3, 5)) + self.assertEqual(0, lcm(3, 0)) + self.assertEqual(20, lcm(2, 5, 4, 10)) From 5b9ee515dbf97e6e4844579b75dec877b4481da4 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 20 Mar 2023 09:28:48 +0100 Subject: [PATCH 044/441] Add wrapper --- qupulse/expressions/__init__.py | 12 +- qupulse/expressions/wrapper.py | 101 +++ qupulse/pulses/measurement.py | 10 +- tests/expressions/__init__.py | 0 tests/{ => expressions}/expression_tests.py | 929 ++++++++++---------- 5 files changed, 582 insertions(+), 470 deletions(-) create mode 100644 qupulse/expressions/wrapper.py create mode 100644 tests/expressions/__init__.py rename tests/{ => expressions}/expression_tests.py (96%) diff --git a/qupulse/expressions/__init__.py b/qupulse/expressions/__init__.py index d2edd8a77..391f98f72 100644 --- a/qupulse/expressions/__init__.py +++ b/qupulse/expressions/__init__.py @@ -1,10 +1,15 @@ +"""This subpackage contains qupulse's expression logic. The submodule :py:`protocol` defines the :py:`typing.Protocol` +that expression functionality providers must implement. This allows to substitute the powerful and expressive but slow +default implementation with a faster less expressive backend. +""" + from typing import Type, TypeVar from numbers import Real import numpy as np import sympy -from . import legacy, protocol +from . import legacy, protocol, wrapper __all__ = ["Expression", "ExpressionVector", "ExpressionScalar", @@ -16,6 +21,11 @@ ExpressionVector: Type[protocol.ExpressionVector] = legacy.ExpressionVector +Expression, ExpressionScalar, ExpressionVector = wrapper.make_wrappers(legacy.Expression, + legacy.ExpressionScalar, + legacy.ExpressionVector) + + ExpressionLike = TypeVar('ExpressionLike', str, Real, sympy.Expr, ExpressionScalar) diff --git a/qupulse/expressions/wrapper.py b/qupulse/expressions/wrapper.py new file mode 100644 index 000000000..eab995fa1 --- /dev/null +++ b/qupulse/expressions/wrapper.py @@ -0,0 +1,101 @@ +import functools +import inspect +import math +from typing import Sequence, Any, Mapping, Union +from numbers import Real + +import numpy as np + +from qupulse.expressions import protocol, legacy + + +def make_wrappers(expr, expr_scalar, expr_vector): + class ExpressionWrapper(protocol.Expression): + def __init__(self, x): + self._wrapped: protocol.Expression = expr(x) + + @classmethod + def make(cls, expression_or_dict, numpy_evaluation=None) -> 'Expression': + return cls(expression_or_dict) + + @property + def underlying_expression(self) -> Any: + return self._wrapped.underlying_expression + + def __hash__(self) -> int: + return hash(self._wrapped) + + def __eq__(self, other): + return self._wrapped == getattr(other, '_wrapped', other) + + @property + def variables(self) -> Sequence[str]: + return self._wrapped.variables + + def evaluate_in_scope(self, scope: Mapping) -> Union[Real, np.ndarray]: + return self._wrapped.evaluate_in_scope(scope) + + def evaluate_symbolic(self, substitutions: Mapping[str, Any]) -> 'ExpressionWrapper': + """Substitute a part of the expression for another""" + return ExpressionWrapper(self._wrapped.evaluate_symbolic(substitutions)) + + def evaluate_time_dependent(self, scope: Mapping) -> Union['Expression', Real, np.ndarray]: + """Evaluate to a time dependent expression or a constant.""" + return self._wrapped.evaluate_time_dependent(scope) + + def get_serialization_data(self): + return self._wrapped.get_serialization_data() + + class ExpressionScalarWrapper(ExpressionWrapper, protocol.ExpressionScalar): + def __init__(self, x): + ExpressionWrapper.__init__(self, 0) + self._wrapped: protocol.ExpressionScalar = expr_scalar(x) + + # Scalar + def __add__(self, other): + return ExpressionScalarWrapper(self._wrapped + getattr(other, '_wrapped', other)) + + def __sub__(self, other): + return ExpressionScalarWrapper(self._wrapped - getattr(other, '_wrapped', other)) + + def __mul__(self, other): + return ExpressionScalarWrapper(self._wrapped * getattr(other, '_wrapped', other)) + + def __truediv__(self, other): + return ExpressionScalarWrapper(self._wrapped / getattr(other, '_wrapped', other)) + + def __floordiv__(self, other): + return ExpressionScalarWrapper(self._wrapped // getattr(other, '_wrapped', other)) + + def __ceil__(self): + return ExpressionScalarWrapper(math.ceil(self._wrapped)) + + def __floor__(self): + return ExpressionScalarWrapper(math.floor(self._wrapped)) + + def __float__(self): + return float(self._wrapped) + + def __int__(self): + return int(self._wrapped) + + def __abs__(self): + return ExpressionScalarWrapper(abs(self._wrapped)) + + # Ordered + def __lt__(self, other): + return self._wrapped < getattr(other, '_wrapped', other) + + def __le__(self, other): + return self._wrapped <= getattr(other, '_wrapped', other) + + def __gt__(self, other): + return self._wrapped > getattr(other, '_wrapped', other) + + def __ge__(self, other): + return self._wrapped >= getattr(other, '_wrapped', other) + + class ExpressionVectorWrapper(ExpressionWrapper): + pass + + return ExpressionWrapper, ExpressionScalarWrapper, ExpressionVectorWrapper diff --git a/qupulse/pulses/measurement.py b/qupulse/pulses/measurement.py index c9d1f9ba2..2a12575f9 100644 --- a/qupulse/pulses/measurement.py +++ b/qupulse/pulses/measurement.py @@ -2,7 +2,7 @@ from numbers import Real import itertools -from qupulse.expressions import Expression +from qupulse.expressions import Expression, ExpressionScalar from qupulse.utils.types import MeasurementWindow from qupulse.parameter_scope import Scope @@ -15,8 +15,8 @@ def __init__(self, measurements: Optional[List[MeasurementDeclaration]]): self._measurement_windows = [] else: self._measurement_windows = [(name, - begin if isinstance(begin, Expression) else Expression(begin), - length if isinstance(length, Expression) else Expression(length)) + begin if isinstance(begin, Expression) else ExpressionScalar(begin), + length if isinstance(length, Expression) else ExpressionScalar(length)) for name, begin, length in measurements] for _, _, length in self._measurement_windows: if (length < 0) is True: @@ -73,8 +73,8 @@ def measurement_parameters(self) -> AbstractSet[str]: def measurement_declarations(self) -> List[MeasurementDeclaration]: """Return the measurements that are directly declared on `self`. Does _not_ visit eventual child objects.""" return [(name, - begin.original_expression, - length.original_expression) + begin, + length) for name, begin, length in self._measurement_windows] @property diff --git a/tests/expressions/__init__.py b/tests/expressions/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/expression_tests.py b/tests/expressions/expression_tests.py similarity index 96% rename from tests/expression_tests.py rename to tests/expressions/expression_tests.py index 26693821c..756be0442 100644 --- a/tests/expression_tests.py +++ b/tests/expressions/expression_tests.py @@ -1,464 +1,465 @@ -import pickle -import unittest -import sys - -import numpy as np -import sympy.abc -from sympy import sympify, Eq - -from qupulse.expressions import Expression, ExpressionVariableMissingException, NonNumericEvaluation, ExpressionScalar, ExpressionVector -from qupulse.utils.types import TimeType - -class ExpressionTests(unittest.TestCase): - def test_make(self): - self.assertTrue(Expression.make('a') == 'a') - self.assertTrue(Expression.make('a + b') == 'a + b') - self.assertTrue(Expression.make(9) == 9) - - self.assertIsInstance(Expression.make([1, 'a']), ExpressionVector) - - self.assertIsInstance(ExpressionScalar.make('a'), ExpressionScalar) - self.assertIsInstance(ExpressionVector.make(['a']), ExpressionVector) - - -class ExpressionVectorTests(unittest.TestCase): - def test_evaluate_numeric(self) -> None: - e = ExpressionVector(['a * b + c', 'a + d']) - params = { - 'a': 2, - 'b': 1.5, - 'c': -7, - 'd': 9 - } - np.testing.assert_equal(np.array([2 * 1.5 - 7, 2 + 9]), - e.evaluate_numeric(**params)) - - with self.assertRaises(NonNumericEvaluation): - params['a'] = sympify('h') - e.evaluate_numeric(**params) - - def test_evaluate_numeric_2d(self) -> None: - e = ExpressionVector([['a * b + c', 'a + d'], ['a', 3]]) - params = { - 'a': 2, - 'b': 1.5, - 'c': -7, - 'd': 9 - } - np.testing.assert_equal(np.array([[2 * 1.5 - 7, 2 + 9], [2, 3]]), - e.evaluate_numeric(**params)) - - with self.assertRaises(NonNumericEvaluation): - params['a'] = sympify('h') - e.evaluate_numeric(**params) - - def test_partial_evaluation(self): - e = ExpressionVector(['a * b + c', 'a + d']) - - params = { - 'a': 2, - 'b': 1.5, - 'c': -7 - } - - expected = ExpressionVector([2 * 1.5 - 7, '2 + d']) - evaluated = e.evaluate_symbolic(params) - - np.testing.assert_equal(evaluated.underlying_expression, expected.underlying_expression) - - def test_symbolic_evaluation(self): - e = ExpressionVector([['a * b + c', 'a + d'], ['a', 3]]) - params = { - 'a': 2, - 'b': 1.5, - 'c': -7, - 'd': 9 - } - - expected = ExpressionVector([[2 * 1.5 - 7, 2 + 9], [2, 3]]) - evaluated = e.evaluate_symbolic(params) - - np.testing.assert_equal(evaluated.underlying_expression, expected.underlying_expression) - - def test_numeric_expression(self): - numbers = np.linspace(1, 2, num=5) - - e = ExpressionVector(numbers) - - np.testing.assert_equal(e.underlying_expression, numbers) - - def test_eq(self): - e1 = ExpressionVector([1, 2]) - e2 = ExpressionVector(['1', '2']) - e3 = ExpressionVector(['1', 'a']) - e4 = ExpressionVector([1, 'a']) - e5 = ExpressionVector([1, 'a', 3]) - e6 = ExpressionVector([1, 1, '1']) - e7 = ExpressionVector(['a']) - - self.assertEqual(e1, e2) - self.assertEqual(e3, e4) - self.assertNotEqual(e4, e5) - - self.assertEqual(e1, [1, 2]) - self.assertNotEqual(e6, 1) - self.assertEqual(e7, ExpressionScalar('a')) - - def test_hash(self): - e1 = ExpressionVector([1, 2]) - e2 = ExpressionVector(['1', '2']) - e7 = ExpressionVector(['a']) - - s = ExpressionScalar('a') - self.assertEqual({e1, e7}, {e1, e2, e7, s}) - - def test_pickle(self): - expr = ExpressionVector([1, 'a + 5', 3]) - # populate lambdified - expr.evaluate_in_scope({'a': 3}) - dumped = pickle.dumps(expr) - loaded = pickle.loads(dumped) - self.assertEqual(expr, loaded) - - -class ExpressionScalarTests(unittest.TestCase): - def test_format(self): - expr = ExpressionScalar('17') - e_format = '{:.4e}'.format(expr) - self.assertEqual(e_format, "1.7000e+01") - - empty_format = "{}".format(expr) - self.assertEqual(empty_format, '17') - - expr_with_var = ExpressionScalar('17*a') - with self.assertRaises(TypeError): - # throw error on implicit float cast - '{:.4e}'.format(expr_with_var) - - empty_format = "{}".format(expr_with_var) - self.assertEqual(empty_format, '17*a') - - @unittest.skipIf(sys.version_info < (3, 6), "format string literals require 3.6 or higher") - def test_fstring(self) -> None: - src_code = """e = ExpressionScalar('2.0'); \ - self.assertEqual( f'{e}', str(e) ); \ - self.assertEqual( f'{e:.2f}', '%.2f' % e) - """ - exec(src_code) - - def test_evaluate_numeric(self) -> None: - e = ExpressionScalar('a * b + c') - params = { - 'a': 2, - 'b': 1.5, - 'c': -7 - } - self.assertEqual(2 * 1.5 - 7, e.evaluate_numeric(**params)) - - with self.assertRaises(NonNumericEvaluation): - params['a'] = sympify('h') - e.evaluate_numeric(**params) - - def test_evaluate_numpy(self): - e = ExpressionScalar('a * b + c') - params = { - 'a': 2*np.ones(4), - 'b': 1.5*np.ones(4), - 'c': -7*np.ones(4) - } - np.testing.assert_equal((2 * 1.5 - 7) * np.ones(4), e.evaluate_numeric(**params)) - - e = ExpressionScalar('a * b + c') - params = { - 'a': np.array(2), - 'b': np.array(1.5), - 'c': np.array(-7) - } - np.testing.assert_equal((2 * 1.5 - 7), e.evaluate_numeric(**params)) - - def test_indexing(self): - e = ExpressionScalar('a[i] * c') - - params = { - 'a': np.array([1, 2, 3]), - 'i': 1, - 'c': 2 - } - - self.assertEqual(e.evaluate_numeric(**params), 2 * 2) - params['a'] = [1, 2, 3] - self.assertEqual(e.evaluate_numeric(**params), 2 * 2) - params['a'] = np.array([[1, 2, 3], [4, 5, 6]]) - np.testing.assert_equal(e.evaluate_numeric(**params), 2 * np.array([4, 5, 6])) - - def test_partial_evaluation(self) -> None: - e = ExpressionScalar('a * c') - params = {'c': 5.5} - evaluated = e.evaluate_symbolic(params) - expected = ExpressionScalar('a * 5.5') - self.assertEqual(expected.underlying_expression, evaluated.underlying_expression) - - def test_partial_evaluation_vectorized(self) -> None: - e = ExpressionScalar('a[i] * c') - - params = { - 'c': np.array([[1, 2], [3, 4]]) - } - - evaluated = e.evaluate_symbolic(params) - expected = ExpressionVector([['a[i] * 1', 'a[i] * 2'], ['a[i] * 3', 'a[i] * 4']]) - - np.testing.assert_equal(evaluated.underlying_expression, expected.underlying_expression) - - def test_evaluate_numeric_without_numpy(self): - e = Expression('a * b + c') - - params = { - 'a': 2, - 'b': 1.5, - 'c': -7 - } - self.assertEqual(2 * 1.5 - 7, e.evaluate_numeric(**params)) - - params = { - 'a': 2j, - 'b': 1.5, - 'c': -7 - } - self.assertEqual(2j * 1.5 - 7, e.evaluate_numeric(**params)) - - params = { - 'a': 2, - 'b': 6, - 'c': -7 - } - self.assertEqual(2 * 6 - 7, e.evaluate_numeric(**params)) - - params = { - 'a': 2, - 'b': sympify('k'), - 'c': -7 - } - with self.assertRaises(NonNumericEvaluation): - e.evaluate_numeric(**params) - - def test_evaluate_symbolic(self): - e = ExpressionScalar('a * b + c') - params = { - 'a': 'd', - 'c': -7 - } - result = e.evaluate_symbolic(params) - expected = ExpressionScalar('d*b-7') - self.assertEqual(result, expected) - - def test_variables(self) -> None: - e = ExpressionScalar('4 ** pi + x * foo') - expected = sorted(['foo', 'x']) - received = sorted(e.variables) - self.assertEqual(expected, received) - - def test_variables_indexed(self): - e = ExpressionScalar('a[i] * c') - expected = sorted(['a', 'i', 'c']) - received = sorted(e.variables) - self.assertEqual(expected, received) - - def test_evaluate_variable_missing(self) -> None: - e = ExpressionScalar('a * b + c') - params = { - 'b': 1.5 - } - with self.assertRaises(ExpressionVariableMissingException): - e.evaluate_numeric(**params) - - def test_repr(self): - s = 'a * b' - e = ExpressionScalar(s) - self.assertEqual("ExpressionScalar('a * b')", repr(e)) - - def test_repr_original_expression_is_sympy(self): - # in this case we test that we get the original expression back if we do - # eval(repr(e)) - - org = sympy.sympify(3.1415) - e = ExpressionScalar(org) - self.assertEqual(e, eval(repr(e))) - - org = sympy.abc.a * sympy.abc.b - e = ExpressionScalar(org) - self.assertEqual(e, eval(repr(e))) - - org = sympy.sympify('3/17') - e = ExpressionScalar(org) - self.assertEqual(e, eval(repr(e))) - - def test_str(self): - s = 'a * b' - e = ExpressionScalar(s) - self.assertEqual('a*b', str(e)) - - def test_original_expression(self): - s = 'a * b' - self.assertEqual(ExpressionScalar(s).original_expression, s) - - def test_hash(self): - expected = {ExpressionScalar(2), ExpressionScalar('a')} - sequence = [ExpressionScalar(2), ExpressionScalar('a'), ExpressionScalar(2), ExpressionScalar('a')] - self.assertEqual(expected, set(sequence)) - - def test_undefined_comparison(self): - valued = ExpressionScalar(2) - unknown = ExpressionScalar('a') - - self.assertIsNone(unknown < 0) - self.assertIsNone(unknown > 0) - self.assertIsNone(unknown >= 0) - self.assertIsNone(unknown <= 0) - self.assertFalse(unknown == 0) - - self.assertIsNone(0 < unknown) - self.assertIsNone(0 > unknown) - self.assertIsNone(0 <= unknown) - self.assertIsNone(0 >= unknown) - self.assertFalse(0 == unknown) - - self.assertIsNone(unknown < valued) - self.assertIsNone(unknown > valued) - self.assertIsNone(unknown >= valued) - self.assertIsNone(unknown <= valued) - self.assertFalse(unknown == valued) - - valued, unknown = unknown, valued - self.assertIsNone(unknown < valued) - self.assertIsNone(unknown > valued) - self.assertIsNone(unknown >= valued) - self.assertIsNone(unknown <= valued) - self.assertFalse(unknown == valued) - valued, unknown = unknown, valued - - self.assertFalse(unknown == valued) - - def test_defined_comparison(self): - small = ExpressionScalar(2) - large = ExpressionScalar(3) - - self.assertIs(small < small, False) - self.assertIs(small > small, False) - self.assertIs(small <= small, True) - self.assertIs(small >= small, True) - self.assertIs(small == small, True) - - self.assertIs(small < large, True) - self.assertIs(small > large, False) - self.assertIs(small <= large, True) - self.assertIs(small >= large, False) - self.assertIs(small == large, False) - - self.assertIs(large < small, False) - self.assertIs(large > small, True) - self.assertIs(large <= small, False) - self.assertIs(large >= small, True) - self.assertIs(large == small, False) - - def test_number_comparison(self): - valued = ExpressionScalar(2) - - self.assertIs(valued < 3, True) - self.assertIs(valued > 3, False) - self.assertIs(valued <= 3, True) - self.assertIs(valued >= 3, False) - - self.assertIs(valued == 3, False) - self.assertIs(valued == 2, True) - self.assertIs(3 == valued, False) - self.assertIs(2 == valued, True) - - self.assertIs(3 < valued, False) - self.assertIs(3 > valued, True) - self.assertIs(3 <= valued, False) - self.assertIs(3 >= valued, True) - - def assertExpressionEqual(self, lhs: Expression, rhs: Expression): - self.assertTrue(bool(Eq(lhs, rhs)), '{} and {} are not equal'.format(lhs, rhs)) - - def test_number_math(self): - a = ExpressionScalar('a') - b = 3.3 - - self.assertExpressionEqual(a + b, b + a) - self.assertExpressionEqual(a - b, -(b - a)) - self.assertExpressionEqual(a * b, b * a) - self.assertExpressionEqual(a / b, 1 / (b / a)) - - def test_symbolic_math(self): - a = ExpressionScalar('a') - b = ExpressionScalar('b') - - self.assertExpressionEqual(a + b, b + a) - self.assertExpressionEqual(a - b, -(b - a)) - self.assertExpressionEqual(a * b, b * a) - self.assertExpressionEqual(a / b, 1 / (b / a)) - - def test_sympy_math(self): - a = ExpressionScalar('a') - b = sympify('b') - - self.assertExpressionEqual(a + b, b + a) - self.assertExpressionEqual(a - b, -(b - a)) - self.assertExpressionEqual(a * b, b * a) - self.assertExpressionEqual(a / b, 1 / (b / a)) - - def test_is_nan(self): - self.assertTrue(ExpressionScalar('nan').is_nan()) - self.assertTrue(ExpressionScalar('0./0.').is_nan()) - - self.assertFalse(ExpressionScalar(456).is_nan()) - - def test_special_function_numeric_evaluation(self): - expr = Expression('erfc(t)') - data = [-1., 0., 1.] - expected = np.array([1.84270079, 1., 0.15729921]) - result = expr.evaluate_numeric(t=data) - - np.testing.assert_allclose(expected, result) - - def test_evaluate_with_exact_rationals(self): - expr = ExpressionScalar('1 / 3') - self.assertEqual(TimeType.from_fraction(1, 3), expr.evaluate_with_exact_rationals({})) - - expr = ExpressionScalar('a * (1 / 3)') - self.assertEqual(TimeType.from_fraction(2, 3), expr.evaluate_with_exact_rationals({'a': 2})) - - expr = ExpressionScalar('dot(a, b) * (1 / 3)') - self.assertEqual(TimeType.from_fraction(10, 3), - expr.evaluate_with_exact_rationals({'a': [2, 2], 'b': [1, 4]})) - - def test_pickle(self): - expr = ExpressionScalar('1 / a') - # populate lambdified - expr.evaluate_in_scope({'a': 7}) - dumped = pickle.dumps(expr) - loaded = pickle.loads(dumped) - self.assertEqual(expr, loaded) - - -class ExpressionExceptionTests(unittest.TestCase): - def test_expression_variable_missing(self): - variable = 's' - expression = ExpressionScalar('s*t') - - self.assertEqual(str(ExpressionVariableMissingException(variable, expression)), - "Could not evaluate : A value for variable is missing!") - - def test_non_numeric_evaluation(self): - expression = ExpressionScalar('a*b') - call_arguments = dict() - - expected = "The result of evaluate_numeric is of type {} " \ - "which is not a number".format(float) - self.assertEqual(str(NonNumericEvaluation(expression, 1., call_arguments)), expected) - - expected = "The result of evaluate_numeric is of type {} " \ - "which is not a number".format(np.zeros(1).dtype) - self.assertEqual(str(NonNumericEvaluation(expression, np.zeros(1), call_arguments)), expected) +import pickle +import unittest +import sys + +import numpy as np +import sympy.abc +from sympy import sympify, Eq + +from qupulse.expressions.legacy import Expression, ExpressionScalar, ExpressionVector +from qupulse.expressions import ExpressionVariableMissingException, NonNumericEvaluation +from qupulse.utils.types import TimeType + +class ExpressionTests(unittest.TestCase): + def test_make(self): + self.assertTrue(Expression.make('a') == 'a') + self.assertTrue(Expression.make('a + b') == 'a + b') + self.assertTrue(Expression.make(9) == 9) + + self.assertIsInstance(Expression.make([1, 'a']), ExpressionVector) + + self.assertIsInstance(ExpressionScalar.make('a'), ExpressionScalar) + self.assertIsInstance(ExpressionVector.make(['a']), ExpressionVector) + + +class ExpressionVectorTests(unittest.TestCase): + def test_evaluate_numeric(self) -> None: + e = ExpressionVector(['a * b + c', 'a + d']) + params = { + 'a': 2, + 'b': 1.5, + 'c': -7, + 'd': 9 + } + np.testing.assert_equal(np.array([2 * 1.5 - 7, 2 + 9]), + e.evaluate_numeric(**params)) + + with self.assertRaises(NonNumericEvaluation): + params['a'] = sympify('h') + e.evaluate_numeric(**params) + + def test_evaluate_numeric_2d(self) -> None: + e = ExpressionVector([['a * b + c', 'a + d'], ['a', 3]]) + params = { + 'a': 2, + 'b': 1.5, + 'c': -7, + 'd': 9 + } + np.testing.assert_equal(np.array([[2 * 1.5 - 7, 2 + 9], [2, 3]]), + e.evaluate_numeric(**params)) + + with self.assertRaises(NonNumericEvaluation): + params['a'] = sympify('h') + e.evaluate_numeric(**params) + + def test_partial_evaluation(self): + e = ExpressionVector(['a * b + c', 'a + d']) + + params = { + 'a': 2, + 'b': 1.5, + 'c': -7 + } + + expected = ExpressionVector([2 * 1.5 - 7, '2 + d']) + evaluated = e.evaluate_symbolic(params) + + np.testing.assert_equal(evaluated.underlying_expression, expected.underlying_expression) + + def test_symbolic_evaluation(self): + e = ExpressionVector([['a * b + c', 'a + d'], ['a', 3]]) + params = { + 'a': 2, + 'b': 1.5, + 'c': -7, + 'd': 9 + } + + expected = ExpressionVector([[2 * 1.5 - 7, 2 + 9], [2, 3]]) + evaluated = e.evaluate_symbolic(params) + + np.testing.assert_equal(evaluated.underlying_expression, expected.underlying_expression) + + def test_numeric_expression(self): + numbers = np.linspace(1, 2, num=5) + + e = ExpressionVector(numbers) + + np.testing.assert_equal(e.underlying_expression, numbers) + + def test_eq(self): + e1 = ExpressionVector([1, 2]) + e2 = ExpressionVector(['1', '2']) + e3 = ExpressionVector(['1', 'a']) + e4 = ExpressionVector([1, 'a']) + e5 = ExpressionVector([1, 'a', 3]) + e6 = ExpressionVector([1, 1, '1']) + e7 = ExpressionVector(['a']) + + self.assertEqual(e1, e2) + self.assertEqual(e3, e4) + self.assertNotEqual(e4, e5) + + self.assertEqual(e1, [1, 2]) + self.assertNotEqual(e6, 1) + self.assertEqual(e7, ExpressionScalar('a')) + + def test_hash(self): + e1 = ExpressionVector([1, 2]) + e2 = ExpressionVector(['1', '2']) + e7 = ExpressionVector(['a']) + + s = ExpressionScalar('a') + self.assertEqual({e1, e7}, {e1, e2, e7, s}) + + def test_pickle(self): + expr = ExpressionVector([1, 'a + 5', 3]) + # populate lambdified + expr.evaluate_in_scope({'a': 3}) + dumped = pickle.dumps(expr) + loaded = pickle.loads(dumped) + self.assertEqual(expr, loaded) + + +class ExpressionScalarTests(unittest.TestCase): + def test_format(self): + expr = ExpressionScalar('17') + e_format = '{:.4e}'.format(expr) + self.assertEqual(e_format, "1.7000e+01") + + empty_format = "{}".format(expr) + self.assertEqual(empty_format, '17') + + expr_with_var = ExpressionScalar('17*a') + with self.assertRaises(TypeError): + # throw error on implicit float cast + '{:.4e}'.format(expr_with_var) + + empty_format = "{}".format(expr_with_var) + self.assertEqual(empty_format, '17*a') + + @unittest.skipIf(sys.version_info < (3, 6), "format string literals require 3.6 or higher") + def test_fstring(self) -> None: + src_code = """e = ExpressionScalar('2.0'); \ + self.assertEqual( f'{e}', str(e) ); \ + self.assertEqual( f'{e:.2f}', '%.2f' % e) + """ + exec(src_code) + + def test_evaluate_numeric(self) -> None: + e = ExpressionScalar('a * b + c') + params = { + 'a': 2, + 'b': 1.5, + 'c': -7 + } + self.assertEqual(2 * 1.5 - 7, e.evaluate_numeric(**params)) + + with self.assertRaises(NonNumericEvaluation): + params['a'] = sympify('h') + e.evaluate_numeric(**params) + + def test_evaluate_numpy(self): + e = ExpressionScalar('a * b + c') + params = { + 'a': 2*np.ones(4), + 'b': 1.5*np.ones(4), + 'c': -7*np.ones(4) + } + np.testing.assert_equal((2 * 1.5 - 7) * np.ones(4), e.evaluate_numeric(**params)) + + e = ExpressionScalar('a * b + c') + params = { + 'a': np.array(2), + 'b': np.array(1.5), + 'c': np.array(-7) + } + np.testing.assert_equal((2 * 1.5 - 7), e.evaluate_numeric(**params)) + + def test_indexing(self): + e = ExpressionScalar('a[i] * c') + + params = { + 'a': np.array([1, 2, 3]), + 'i': 1, + 'c': 2 + } + + self.assertEqual(e.evaluate_numeric(**params), 2 * 2) + params['a'] = [1, 2, 3] + self.assertEqual(e.evaluate_numeric(**params), 2 * 2) + params['a'] = np.array([[1, 2, 3], [4, 5, 6]]) + np.testing.assert_equal(e.evaluate_numeric(**params), 2 * np.array([4, 5, 6])) + + def test_partial_evaluation(self) -> None: + e = ExpressionScalar('a * c') + params = {'c': 5.5} + evaluated = e.evaluate_symbolic(params) + expected = ExpressionScalar('a * 5.5') + self.assertEqual(expected.underlying_expression, evaluated.underlying_expression) + + def test_partial_evaluation_vectorized(self) -> None: + e = ExpressionScalar('a[i] * c') + + params = { + 'c': np.array([[1, 2], [3, 4]]) + } + + evaluated = e.evaluate_symbolic(params) + expected = ExpressionVector([['a[i] * 1', 'a[i] * 2'], ['a[i] * 3', 'a[i] * 4']]) + + np.testing.assert_equal(evaluated.underlying_expression, expected.underlying_expression) + + def test_evaluate_numeric_without_numpy(self): + e = Expression('a * b + c') + + params = { + 'a': 2, + 'b': 1.5, + 'c': -7 + } + self.assertEqual(2 * 1.5 - 7, e.evaluate_numeric(**params)) + + params = { + 'a': 2j, + 'b': 1.5, + 'c': -7 + } + self.assertEqual(2j * 1.5 - 7, e.evaluate_numeric(**params)) + + params = { + 'a': 2, + 'b': 6, + 'c': -7 + } + self.assertEqual(2 * 6 - 7, e.evaluate_numeric(**params)) + + params = { + 'a': 2, + 'b': sympify('k'), + 'c': -7 + } + with self.assertRaises(NonNumericEvaluation): + e.evaluate_numeric(**params) + + def test_evaluate_symbolic(self): + e = ExpressionScalar('a * b + c') + params = { + 'a': 'd', + 'c': -7 + } + result = e.evaluate_symbolic(params) + expected = ExpressionScalar('d*b-7') + self.assertEqual(result, expected) + + def test_variables(self) -> None: + e = ExpressionScalar('4 ** pi + x * foo') + expected = sorted(['foo', 'x']) + received = sorted(e.variables) + self.assertEqual(expected, received) + + def test_variables_indexed(self): + e = ExpressionScalar('a[i] * c') + expected = sorted(['a', 'i', 'c']) + received = sorted(e.variables) + self.assertEqual(expected, received) + + def test_evaluate_variable_missing(self) -> None: + e = ExpressionScalar('a * b + c') + params = { + 'b': 1.5 + } + with self.assertRaises(ExpressionVariableMissingException): + e.evaluate_numeric(**params) + + def test_repr(self): + s = 'a * b' + e = ExpressionScalar(s) + self.assertEqual("ExpressionScalar('a * b')", repr(e)) + + def test_repr_original_expression_is_sympy(self): + # in this case we test that we get the original expression back if we do + # eval(repr(e)) + + org = sympy.sympify(3.1415) + e = ExpressionScalar(org) + self.assertEqual(e, eval(repr(e))) + + org = sympy.abc.a * sympy.abc.b + e = ExpressionScalar(org) + self.assertEqual(e, eval(repr(e))) + + org = sympy.sympify('3/17') + e = ExpressionScalar(org) + self.assertEqual(e, eval(repr(e))) + + def test_str(self): + s = 'a * b' + e = ExpressionScalar(s) + self.assertEqual('a*b', str(e)) + + def test_original_expression(self): + s = 'a * b' + self.assertEqual(ExpressionScalar(s).original_expression, s) + + def test_hash(self): + expected = {ExpressionScalar(2), ExpressionScalar('a')} + sequence = [ExpressionScalar(2), ExpressionScalar('a'), ExpressionScalar(2), ExpressionScalar('a')] + self.assertEqual(expected, set(sequence)) + + def test_undefined_comparison(self): + valued = ExpressionScalar(2) + unknown = ExpressionScalar('a') + + self.assertIsNone(unknown < 0) + self.assertIsNone(unknown > 0) + self.assertIsNone(unknown >= 0) + self.assertIsNone(unknown <= 0) + self.assertFalse(unknown == 0) + + self.assertIsNone(0 < unknown) + self.assertIsNone(0 > unknown) + self.assertIsNone(0 <= unknown) + self.assertIsNone(0 >= unknown) + self.assertFalse(0 == unknown) + + self.assertIsNone(unknown < valued) + self.assertIsNone(unknown > valued) + self.assertIsNone(unknown >= valued) + self.assertIsNone(unknown <= valued) + self.assertFalse(unknown == valued) + + valued, unknown = unknown, valued + self.assertIsNone(unknown < valued) + self.assertIsNone(unknown > valued) + self.assertIsNone(unknown >= valued) + self.assertIsNone(unknown <= valued) + self.assertFalse(unknown == valued) + valued, unknown = unknown, valued + + self.assertFalse(unknown == valued) + + def test_defined_comparison(self): + small = ExpressionScalar(2) + large = ExpressionScalar(3) + + self.assertIs(small < small, False) + self.assertIs(small > small, False) + self.assertIs(small <= small, True) + self.assertIs(small >= small, True) + self.assertIs(small == small, True) + + self.assertIs(small < large, True) + self.assertIs(small > large, False) + self.assertIs(small <= large, True) + self.assertIs(small >= large, False) + self.assertIs(small == large, False) + + self.assertIs(large < small, False) + self.assertIs(large > small, True) + self.assertIs(large <= small, False) + self.assertIs(large >= small, True) + self.assertIs(large == small, False) + + def test_number_comparison(self): + valued = ExpressionScalar(2) + + self.assertIs(valued < 3, True) + self.assertIs(valued > 3, False) + self.assertIs(valued <= 3, True) + self.assertIs(valued >= 3, False) + + self.assertIs(valued == 3, False) + self.assertIs(valued == 2, True) + self.assertIs(3 == valued, False) + self.assertIs(2 == valued, True) + + self.assertIs(3 < valued, False) + self.assertIs(3 > valued, True) + self.assertIs(3 <= valued, False) + self.assertIs(3 >= valued, True) + + def assertExpressionEqual(self, lhs: Expression, rhs: Expression): + self.assertTrue(bool(Eq(lhs, rhs)), '{} and {} are not equal'.format(lhs, rhs)) + + def test_number_math(self): + a = ExpressionScalar('a') + b = 3.3 + + self.assertExpressionEqual(a + b, b + a) + self.assertExpressionEqual(a - b, -(b - a)) + self.assertExpressionEqual(a * b, b * a) + self.assertExpressionEqual(a / b, 1 / (b / a)) + + def test_symbolic_math(self): + a = ExpressionScalar('a') + b = ExpressionScalar('b') + + self.assertExpressionEqual(a + b, b + a) + self.assertExpressionEqual(a - b, -(b - a)) + self.assertExpressionEqual(a * b, b * a) + self.assertExpressionEqual(a / b, 1 / (b / a)) + + def test_sympy_math(self): + a = ExpressionScalar('a') + b = sympify('b') + + self.assertExpressionEqual(a + b, b + a) + self.assertExpressionEqual(a - b, -(b - a)) + self.assertExpressionEqual(a * b, b * a) + self.assertExpressionEqual(a / b, 1 / (b / a)) + + def test_is_nan(self): + self.assertTrue(ExpressionScalar('nan').is_nan()) + self.assertTrue(ExpressionScalar('0./0.').is_nan()) + + self.assertFalse(ExpressionScalar(456).is_nan()) + + def test_special_function_numeric_evaluation(self): + expr = Expression('erfc(t)') + data = [-1., 0., 1.] + expected = np.array([1.84270079, 1., 0.15729921]) + result = expr.evaluate_numeric(t=data) + + np.testing.assert_allclose(expected, result) + + def test_evaluate_with_exact_rationals(self): + expr = ExpressionScalar('1 / 3') + self.assertEqual(TimeType.from_fraction(1, 3), expr.evaluate_with_exact_rationals({})) + + expr = ExpressionScalar('a * (1 / 3)') + self.assertEqual(TimeType.from_fraction(2, 3), expr.evaluate_with_exact_rationals({'a': 2})) + + expr = ExpressionScalar('dot(a, b) * (1 / 3)') + self.assertEqual(TimeType.from_fraction(10, 3), + expr.evaluate_with_exact_rationals({'a': [2, 2], 'b': [1, 4]})) + + def test_pickle(self): + expr = ExpressionScalar('1 / a') + # populate lambdified + expr.evaluate_in_scope({'a': 7}) + dumped = pickle.dumps(expr) + loaded = pickle.loads(dumped) + self.assertEqual(expr, loaded) + + +class ExpressionExceptionTests(unittest.TestCase): + def test_expression_variable_missing(self): + variable = 's' + expression = ExpressionScalar('s*t') + + self.assertEqual(str(ExpressionVariableMissingException(variable, expression)), + "Could not evaluate : A value for variable is missing!") + + def test_non_numeric_evaluation(self): + expression = ExpressionScalar('a*b') + call_arguments = dict() + + expected = "The result of evaluate_numeric is of type {} " \ + "which is not a number".format(float) + self.assertEqual(str(NonNumericEvaluation(expression, 1., call_arguments)), expected) + + expected = "The result of evaluate_numeric is of type {} " \ + "which is not a number".format(np.zeros(1).dtype) + self.assertEqual(str(NonNumericEvaluation(expression, np.zeros(1), call_arguments)), expected) From c2daff8774e498b68a8807cb9dc0f66c2a10595e Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 28 Mar 2023 09:09:57 +0200 Subject: [PATCH 045/441] Fix formatting in newspieces --- changes.d/512.removal | 2 +- changes.d/696.fix | 2 +- changes.d/709.feature | 6 +++--- changes.d/710.feature | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/changes.d/512.removal b/changes.d/512.removal index 83a9d441b..78cafc9e3 100644 --- a/changes.d/512.removal +++ b/changes.d/512.removal @@ -1 +1 @@ -Remove the `Parameter`, `MappedParameter` and `ConstantParameter` class that where deprecated in version 0.5. +Remove the ``Parameter``, ``MappedParameter`` and ``ConstantParameter`` classes that where deprecated in version 0.5. diff --git a/changes.d/696.fix b/changes.d/696.fix index 9df69bd0b..b93008fd5 100644 --- a/changes.d/696.fix +++ b/changes.d/696.fix @@ -1 +1 @@ -`ConstantPulseTemplate`s from all versions can now be deserialized. \ No newline at end of file +``ConstantPulseTemplate``s from all versions can now be deserialized. \ No newline at end of file diff --git a/changes.d/709.feature b/changes.d/709.feature index 9bdf45b12..e19751033 100644 --- a/changes.d/709.feature +++ b/changes.d/709.feature @@ -1,3 +1,3 @@ -Add support for time dependent expressions for arithmetics with atomic pulse templates i.e. ParallelChannelPT and -ArithmeticPT support time dependent expressions if used with atomic pulse templates. -Rename `ParallelConstantChannelPT` to `ParallelChannelPT` to reflect this change. +Add support for time dependent expressions for arithmetics with atomic pulse templates i.e. ``ParallelChannelPT`` and +``ArithmeticPT`` support time dependent expressions if used with atomic pulse templates. +Rename ``ParallelConstantChannelPT`` to ``ParallelChannelPT`` to reflect this change. diff --git a/changes.d/710.feature b/changes.d/710.feature index 650482cc2..db4b9ea80 100644 --- a/changes.d/710.feature +++ b/changes.d/710.feature @@ -1,2 +1,2 @@ -Add `with_` family of helper methods to `PulseTemplate` to allow convinient and easily discoverable pulse template +Add ``with_`` family of helper methods to ``PulseTemplate`` to allow convinient and easily discoverable pulse template combination. From cc60572108ebe37b2afe037617bfac46d05951d9 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 28 Mar 2023 09:10:44 +0200 Subject: [PATCH 046/441] Push version to 0.8 --- qupulse/__init__.py | 2 +- setup.cfg | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/qupulse/__init__.py b/qupulse/__init__.py index 6cae787b1..d7b836bbc 100644 --- a/qupulse/__init__.py +++ b/qupulse/__init__.py @@ -2,7 +2,7 @@ import lazy_loader as lazy -__version__ = '0.7' +__version__ = '0.8' __getattr__, __dir__, __all__ = lazy.attach_stub(__name__, __file__) diff --git a/setup.cfg b/setup.cfg index 68647e6f9..fbec16147 100644 --- a/setup.cfg +++ b/setup.cfg @@ -88,8 +88,8 @@ filterwarnings = [build_sphinx] project = 'qupulse' -version = 0.5 -release = 0.5rc +version = 0.8 +release = 0.8 source-dir = ./doc/source build-dir = ./doc/build fresh-env = 1 From 9957c7100ce5484e8c314f6c4b40f2c396bbb927 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 28 Mar 2023 09:29:05 +0200 Subject: [PATCH 047/441] Rename legacy to sympy --- qupulse/expressions/__init__.py | 18 +++++++++--------- qupulse/expressions/{legacy.py => sympy.py} | 0 qupulse/expressions/wrapper.py | 2 +- tests/expressions/expression_tests.py | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) rename qupulse/expressions/{legacy.py => sympy.py} (100%) diff --git a/qupulse/expressions/__init__.py b/qupulse/expressions/__init__.py index 391f98f72..d9823bc75 100644 --- a/qupulse/expressions/__init__.py +++ b/qupulse/expressions/__init__.py @@ -7,26 +7,26 @@ from numbers import Real import numpy as np -import sympy +import sympy as sp -from . import legacy, protocol, wrapper +from . import sympy, protocol, wrapper __all__ = ["Expression", "ExpressionVector", "ExpressionScalar", "NonNumericEvaluation", "ExpressionVariableMissingException"] -Expression: Type[protocol.Expression] = legacy.Expression -ExpressionScalar: Type[protocol.ExpressionScalar] = legacy.ExpressionScalar -ExpressionVector: Type[protocol.ExpressionVector] = legacy.ExpressionVector +Expression: Type[protocol.Expression] = sympy.Expression +ExpressionScalar: Type[protocol.ExpressionScalar] = sympy.ExpressionScalar +ExpressionVector: Type[protocol.ExpressionVector] = sympy.ExpressionVector -Expression, ExpressionScalar, ExpressionVector = wrapper.make_wrappers(legacy.Expression, - legacy.ExpressionScalar, - legacy.ExpressionVector) +Expression, ExpressionScalar, ExpressionVector = wrapper.make_wrappers(sympy.Expression, + sympy.ExpressionScalar, + sympy.ExpressionVector) -ExpressionLike = TypeVar('ExpressionLike', str, Real, sympy.Expr, ExpressionScalar) +ExpressionLike = TypeVar('ExpressionLike', str, Real, sp.Expr, ExpressionScalar) class ExpressionVariableMissingException(Exception): diff --git a/qupulse/expressions/legacy.py b/qupulse/expressions/sympy.py similarity index 100% rename from qupulse/expressions/legacy.py rename to qupulse/expressions/sympy.py diff --git a/qupulse/expressions/wrapper.py b/qupulse/expressions/wrapper.py index eab995fa1..91340caaa 100644 --- a/qupulse/expressions/wrapper.py +++ b/qupulse/expressions/wrapper.py @@ -6,7 +6,7 @@ import numpy as np -from qupulse.expressions import protocol, legacy +from qupulse.expressions import protocol, sympy def make_wrappers(expr, expr_scalar, expr_vector): diff --git a/tests/expressions/expression_tests.py b/tests/expressions/expression_tests.py index 756be0442..194434935 100644 --- a/tests/expressions/expression_tests.py +++ b/tests/expressions/expression_tests.py @@ -6,7 +6,7 @@ import sympy.abc from sympy import sympify, Eq -from qupulse.expressions.legacy import Expression, ExpressionScalar, ExpressionVector +from qupulse.expressions.sympy import Expression, ExpressionScalar, ExpressionVector from qupulse.expressions import ExpressionVariableMissingException, NonNumericEvaluation from qupulse.utils.types import TimeType From 52bc6eb658a9e53a8b9fc8db5d44dc1f343ccbc2 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 28 Mar 2023 10:15:52 +0200 Subject: [PATCH 048/441] Push minimal python version to 3.8 and add python 3.10 to test matrix --- .github/workflows/pythontest.yaml | 2 +- README.md | 2 +- setup.cfg | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pythontest.yaml b/.github/workflows/pythontest.yaml index cacee5647..6f7f0b745 100644 --- a/.github/workflows/pythontest.yaml +++ b/.github/workflows/pythontest.yaml @@ -22,7 +22,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9"] + python-version: ["3.8", "3.9", "3.10"] time-type: ["fractions", "gmpy2"] env: INSTALL_EXTRAS: tests,plotting,zurich-instruments,tektronix,tabor-instruments diff --git a/README.md b/README.md index 38509803a..d47d1e09f 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ python -m pip install -e git+https://github.com/qutech/qupulse.git#egg=qupulse[d which will clone the github repository to `./src/qupulse` and do an editable/development install. ### Requirements and dependencies -qupulse requires at least Python 3.7 and is tested on 3.7, 3.8 and 3.9. It relies on some external Python packages as dependencies. +qupulse requires at least Python 3.8 and is tested on 3.8, 3.9 and 3.10. It relies on some external Python packages as dependencies. We intentionally did not restrict versions of dependencies in the install scripts to not unnecessarily prevent usage of newer releases of dependencies that might be compatible. However, if qupulse does encounter problems with a particular dependency version please file an issue. The backend for TaborAWGs requires packages that can be found [here](https://git.rwth-aachen.de/qutech/python-TaborDriver). As a shortcut you can install it from the python interpreter via `qupulse.hardware.awgs.install_requirements('tabor')`. diff --git a/setup.cfg b/setup.cfg index 68647e6f9..ba52ee6a2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,7 +21,7 @@ packages = find: package_dir = qupulse=qupulse qctoolkit=qctoolkit -python_requires = >=3.7 +python_requires = >=3.8 install_requires = sympy>=1.1.1 numpy From e0c292bd4518b3adb8ccc6bc862253daf2592ae5 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 28 Mar 2023 10:28:01 +0200 Subject: [PATCH 049/441] Add newspiece --- changes.d/760.removal | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes.d/760.removal diff --git a/changes.d/760.removal b/changes.d/760.removal new file mode 100644 index 000000000..a59d6a296 --- /dev/null +++ b/changes.d/760.removal @@ -0,0 +1 @@ +Drop support for python version 3.7. From 60d6ccfd0d7e98d633765b13ccb819f1a1498b11 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 28 Mar 2023 10:35:56 +0200 Subject: [PATCH 050/441] Add newspiece --- changes.d/750.feature | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changes.d/750.feature diff --git a/changes.d/750.feature b/changes.d/750.feature new file mode 100644 index 000000000..9e745d690 --- /dev/null +++ b/changes.d/750.feature @@ -0,0 +1,5 @@ +Promote ``qupulse.expression`` to a subpackage and create ``qupulse.expression.protocol`` with protocol classes that define the expression interface that is supposed to be used by qupulse. +The ```sympy`` based implementation is moved to ``qupulse.expressions.sympy`` and imported in ``qupulse.expressions``. + +The intended use is to be able to use less powerful but faster implementations of the ``Expression`` protocol where appropriate. +In this first iteration, qupulse still relies on internals of the ``sympy`` based implementation in many places which is to be removed in the future. From 76d641ce751330edf99e30da20ab635ac9156137 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 28 Mar 2023 10:42:46 +0200 Subject: [PATCH 051/441] Add docstring to wrapper module --- qupulse/expressions/wrapper.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qupulse/expressions/wrapper.py b/qupulse/expressions/wrapper.py index 91340caaa..8ea9dfc18 100644 --- a/qupulse/expressions/wrapper.py +++ b/qupulse/expressions/wrapper.py @@ -1,5 +1,6 @@ -import functools -import inspect +"""This module contains wrapper classes for expression protocol implementations which only implements methods of +the protocol. It is used for finding code that relies on expression implementation details.""" + import math from typing import Sequence, Any, Mapping, Union from numbers import Real From f416ced9c0fdbdf85ff6247728a8d84c61ac376b Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 28 Mar 2023 10:43:32 +0200 Subject: [PATCH 052/441] Drop python 3.7 support --- qupulse/expressions/protocol.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/qupulse/expressions/protocol.py b/qupulse/expressions/protocol.py index 0280747d3..b177f501c 100644 --- a/qupulse/expressions/protocol.py +++ b/qupulse/expressions/protocol.py @@ -1,13 +1,7 @@ -"""This module contains the interface / protocol descriptions.""" - -try: - from typing import Protocol -except ImportError: - # python version < 3.8 - from typing_extensions import Protocol - -from typing import Mapping, Union, Sequence, Hashable, Any +"""This module contains the interface / protocol descriptions of ``Expression``, ``ExpressionScalar`` and +``ExpressionVector``.""" +from typing import Mapping, Union, Sequence, Hashable, Any, Protocol from numbers import Real import numpy as np From 7054d7eeeb3f45c4cbcd469b9bf080e32c7bc40b Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 28 Mar 2023 11:11:31 +0200 Subject: [PATCH 053/441] Add more documentation --- qupulse/expressions/__init__.py | 15 ++++++++++++--- qupulse/expressions/protocol.py | 2 -- qupulse/expressions/wrapper.py | 25 ++++++++++++++++++++----- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/qupulse/expressions/__init__.py b/qupulse/expressions/__init__.py index d9823bc75..acbf29431 100644 --- a/qupulse/expressions/__init__.py +++ b/qupulse/expressions/__init__.py @@ -1,10 +1,18 @@ """This subpackage contains qupulse's expression logic. The submodule :py:`protocol` defines the :py:`typing.Protocol` that expression functionality providers must implement. This allows to substitute the powerful and expressive but slow default implementation with a faster less expressive backend. + +Currently, the + +The default implementation is in :py:``qupulse.expressions.sympy``. + +There is are wrapper classes for finding non-protocol uses of expression in :py:``qupulse.expressions.wrapper``. Define +``QUPULSE_EXPRESSION_WRAPPER`` environment variable when running python to wrap all expression usages. """ from typing import Type, TypeVar from numbers import Real +import os import numpy as np import sympy as sp @@ -21,9 +29,10 @@ ExpressionVector: Type[protocol.ExpressionVector] = sympy.ExpressionVector -Expression, ExpressionScalar, ExpressionVector = wrapper.make_wrappers(sympy.Expression, - sympy.ExpressionScalar, - sympy.ExpressionVector) +if os.environ.get('QUPULSE_EXPRESSION_WRAPPER', None): # pragma: no cover + Expression, ExpressionScalar, ExpressionVector = wrapper.make_wrappers(sympy.Expression, + sympy.ExpressionScalar, + sympy.ExpressionVector) ExpressionLike = TypeVar('ExpressionLike', str, Real, sp.Expr, ExpressionScalar) diff --git a/qupulse/expressions/protocol.py b/qupulse/expressions/protocol.py index b177f501c..11f267790 100644 --- a/qupulse/expressions/protocol.py +++ b/qupulse/expressions/protocol.py @@ -53,8 +53,6 @@ def __abs__(self): pass - - class Expression(Hashable, Protocol): def evaluate_in_scope(self, scope: Mapping) -> Union[Real, np.ndarray]: """Evaluate the expression by taking the variables from the given scope (typically of type Scope but it can be diff --git a/qupulse/expressions/wrapper.py b/qupulse/expressions/wrapper.py index 8ea9dfc18..1052c1174 100644 --- a/qupulse/expressions/wrapper.py +++ b/qupulse/expressions/wrapper.py @@ -1,8 +1,9 @@ -"""This module contains wrapper classes for expression protocol implementations which only implements methods of -the protocol. It is used for finding code that relies on expression implementation details.""" +"""This module contains the function :py:``make_wrappers`` to define wrapper classes for expression protocol implementations +which only implements methods of the protocol. +It is used for finding code that relies on expression implementation details.""" import math -from typing import Sequence, Any, Mapping, Union +from typing import Sequence, Any, Mapping, Union, Tuple from numbers import Real import numpy as np @@ -10,13 +11,27 @@ from qupulse.expressions import protocol, sympy -def make_wrappers(expr, expr_scalar, expr_vector): +def make_wrappers(expr: type, expr_scalar: type, expr_vector: type) -> Tuple[type, type, type]: + """Create wrappers for expression base, scalar and vector types that only expose the methods defined in the + corresponding expression protocol classes. + + The vector is currently not implemented. + + Args: + expr: Expression base type of the implementation + expr_scalar: Expression scalar type of the implementation + expr_vector: Expression vector type of the implementation + + Returns: + A tuple of (base, scalar, vector) types that wrap the given types. + """ + class ExpressionWrapper(protocol.Expression): def __init__(self, x): self._wrapped: protocol.Expression = expr(x) @classmethod - def make(cls, expression_or_dict, numpy_evaluation=None) -> 'Expression': + def make(cls, expression_or_dict, numpy_evaluation=None) -> 'ExpressionWrapper': return cls(expression_or_dict) @property From 48f4ece4c2270eb1660e16ac1e839847987a74a4 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 28 Mar 2023 12:07:31 +0200 Subject: [PATCH 054/441] Compile release notes --- RELEASE_NOTES.rst | 22 ++++++++++++++++++++++ changes.d/512.removal | 1 - changes.d/703.feature | 1 - changes.d/709.feature | 3 --- changes.d/710.feature | 2 -- changes.d/735.feature | 1 - changes.d/760.removal | 1 - 7 files changed, 22 insertions(+), 9 deletions(-) delete mode 100644 changes.d/512.removal delete mode 100644 changes.d/703.feature delete mode 100644 changes.d/709.feature delete mode 100644 changes.d/710.feature delete mode 100644 changes.d/735.feature delete mode 100644 changes.d/760.removal diff --git a/RELEASE_NOTES.rst b/RELEASE_NOTES.rst index c61c8b8c0..51c5c0b07 100644 --- a/RELEASE_NOTES.rst +++ b/RELEASE_NOTES.rst @@ -2,6 +2,28 @@ .. towncrier release notes start +qupulse 0.8 (2023-03-28) +======================== + +Features +-------- + +- New two dimensional plotting function ``qupulse.pulses.plotting.plot_2d``. (`#703 `_) +- Add support for time dependent expressions for arithmetics with atomic pulse templates i.e. ``ParallelChannelPT`` and + ``ArithmeticPT`` support time dependent expressions if used with atomic pulse templates. + Rename ``ParallelConstantChannelPT`` to ``ParallelChannelPT`` to reflect this change. (`#709 `_) +- Add ``with_`` family of helper methods to ``PulseTemplate`` to allow convinient and easily discoverable pulse template + combination. (`#710 `_) +- The plotting module is now located at `qupulse.plotting`. There is a legacy alias at `qupulse.pulses.plotting`. (`#735 `_) + + +Deprecations and Removals +------------------------- + +- Remove the ``Parameter``, ``MappedParameter`` and ``ConstantParameter`` classes that where deprecated in version 0.5. (`#512 `_) +- Drop support for python version 3.7. (`#760 `_) + + qupulse 0.7 (2022-10-05) ======================== diff --git a/changes.d/512.removal b/changes.d/512.removal deleted file mode 100644 index 78cafc9e3..000000000 --- a/changes.d/512.removal +++ /dev/null @@ -1 +0,0 @@ -Remove the ``Parameter``, ``MappedParameter`` and ``ConstantParameter`` classes that where deprecated in version 0.5. diff --git a/changes.d/703.feature b/changes.d/703.feature deleted file mode 100644 index af8aeb0b4..000000000 --- a/changes.d/703.feature +++ /dev/null @@ -1 +0,0 @@ -New two dimensional plotting function ``qupulse.pulses.plotting.plot_2d``. diff --git a/changes.d/709.feature b/changes.d/709.feature deleted file mode 100644 index e19751033..000000000 --- a/changes.d/709.feature +++ /dev/null @@ -1,3 +0,0 @@ -Add support for time dependent expressions for arithmetics with atomic pulse templates i.e. ``ParallelChannelPT`` and -``ArithmeticPT`` support time dependent expressions if used with atomic pulse templates. -Rename ``ParallelConstantChannelPT`` to ``ParallelChannelPT`` to reflect this change. diff --git a/changes.d/710.feature b/changes.d/710.feature deleted file mode 100644 index db4b9ea80..000000000 --- a/changes.d/710.feature +++ /dev/null @@ -1,2 +0,0 @@ -Add ``with_`` family of helper methods to ``PulseTemplate`` to allow convinient and easily discoverable pulse template -combination. diff --git a/changes.d/735.feature b/changes.d/735.feature deleted file mode 100644 index 827aca302..000000000 --- a/changes.d/735.feature +++ /dev/null @@ -1 +0,0 @@ -The plotting module is now located at `qupulse.plotting`. There is a legacy alias at `qupulse.pulses.plotting`. diff --git a/changes.d/760.removal b/changes.d/760.removal deleted file mode 100644 index a59d6a296..000000000 --- a/changes.d/760.removal +++ /dev/null @@ -1 +0,0 @@ -Drop support for python version 3.7. From 0ed9a24889e5217aef6626bde2c78ca08d898e23 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 18 Apr 2023 18:49:14 +0200 Subject: [PATCH 055/441] Use custom sympify hack in ParameterConstraint init --- qupulse/pulses/parameters.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qupulse/pulses/parameters.py b/qupulse/pulses/parameters.py index 51b20e05d..da381b053 100644 --- a/qupulse/pulses/parameters.py +++ b/qupulse/pulses/parameters.py @@ -12,6 +12,7 @@ from qupulse.serialization import AnonymousSerializable from qupulse.expressions import Expression from qupulse.parameter_scope import Scope, ParameterNotProvidedException +from qupulse.utils.sympy import sympify __all__ = ["ParameterNotProvidedException", "ParameterConstraintViolation", "ParameterConstraint"] @@ -22,9 +23,9 @@ def __init__(self, relation: Union[str, sympy.Expr]): super().__init__() if isinstance(relation, str) and '==' in relation: # The '==' operator is interpreted by sympy as exactly, however we need a symbolical evaluation - self._expression = sympy.Eq(*sympy.sympify(relation.split('=='))) + self._expression = sympy.Eq(*sympify(relation.split('=='))) else: - self._expression = sympy.sympify(relation) + self._expression = sympify(relation) if not isinstance(self._expression, sympy.logic.boolalg.Boolean): raise ValueError('Constraint is not boolean') self._expression = Expression(self._expression) From 8d8dab041b896b0e7c429169205df232e5e8aa04 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 5 Jun 2023 11:37:03 +0200 Subject: [PATCH 056/441] Combine default extra dependencies from other extras --- setup.cfg | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/setup.cfg b/setup.cfg index f264461ba..8c68a1adf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -52,15 +52,7 @@ autologging = autologging faster-sampling = numba # Everything besides awg drivers default = - pytest - pytest_benchmark - sphinx>=4 - ipykernel - pyvisa - matplotlib - gmpy2 - autologging - numba + qupulse[tests,docs,plotting,Faster-fractions,autologging,faster-sampling] pandas [options.packages.find] From a7224af7b469e957b03333214e52f4c8675fc11c Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 5 Jun 2023 11:47:57 +0200 Subject: [PATCH 057/441] Fix typo and formatting --- qupulse/expressions/__init__.py | 2 -- qupulse/expressions/protocol.py | 5 +++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/qupulse/expressions/__init__.py b/qupulse/expressions/__init__.py index acbf29431..61224f2fd 100644 --- a/qupulse/expressions/__init__.py +++ b/qupulse/expressions/__init__.py @@ -2,8 +2,6 @@ that expression functionality providers must implement. This allows to substitute the powerful and expressive but slow default implementation with a faster less expressive backend. -Currently, the - The default implementation is in :py:``qupulse.expressions.sympy``. There is are wrapper classes for finding non-protocol uses of expression in :py:``qupulse.expressions.wrapper``. Define diff --git a/qupulse/expressions/protocol.py b/qupulse/expressions/protocol.py index 11f267790..f6f90d459 100644 --- a/qupulse/expressions/protocol.py +++ b/qupulse/expressions/protocol.py @@ -55,7 +55,7 @@ def __abs__(self): class Expression(Hashable, Protocol): def evaluate_in_scope(self, scope: Mapping) -> Union[Real, np.ndarray]: - """Evaluate the expression by taking the variables from the given scope (typically of type Scope but it can be + """Evaluate the expression by taking the variables from the given scope (typically of type Scope, but it can be any mapping.) Args: scope: @@ -69,6 +69,7 @@ def evaluate_symbolic(self, substitutions: Mapping[str, Any]) -> 'Expression': def evaluate_time_dependent(self, scope: Mapping) -> Union['Expression', Real, np.ndarray]: """Evaluate to a time dependent expression or a constant.""" + @property def variables(self) -> Sequence[str]: """ Get all free variables in the expression. @@ -91,7 +92,7 @@ def underlying_expression(self) -> Any: raise NotImplementedError() def get_serialization_data(self): - pass + raise NotImplementedError() class ExpressionScalar(Expression, Scalar, Ordered, Protocol): From a56708fbf19e4b4a9e98052ad2cfad4fff6f31af Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 23 Jun 2023 12:38:44 +0200 Subject: [PATCH 058/441] Fixes repr of Loop to return an evaluatable object if possible and use str for prev human readable output. Use reprlib to detect recursive repr calls --- qupulse/_program/_loop.py | 55 ++++++++++++++++++++++++++++++------ tests/_program/loop_tests.py | 31 ++++++++++++-------- 2 files changed, 67 insertions(+), 19 deletions(-) diff --git a/qupulse/_program/_loop.py b/qupulse/_program/_loop.py index 92d3f7eb2..20ec1204c 100644 --- a/qupulse/_program/_loop.py +++ b/qupulse/_program/_loop.py @@ -3,6 +3,7 @@ from enum import Enum import warnings import bisect +import reprlib import numpy as np import sympy.ntheory @@ -198,23 +199,20 @@ def encapsulate(self) -> None: self._measurements = None self.assert_tree_integrity() - def _get_repr(self, first_prefix, other_prefixes) -> Generator[str, None, None]: + def _get_str(self, first_prefix, other_prefixes) -> Generator[str, None, None]: if self.is_leaf(): yield '%sEXEC %r %d times' % (first_prefix, self._waveform, self.repetition_count) else: yield '%sLOOP %d times:' % (first_prefix, self.repetition_count) for elem in self: - yield from cast(Loop, elem)._get_repr(other_prefixes + ' ->', other_prefixes + ' ') - - def __repr__(self) -> str: - is_circular = is_tree_circular(self) - if is_circular: - return '{}: Circ {}'.format(id(self), is_circular) + yield from cast(Loop, elem)._get_str(other_prefixes + ' ->', other_prefixes + ' ') + @reprlib.recursive_repr() + def __str__(self) -> str: str_len = 0 repr_list = [] - for sub_repr in self._get_repr('', ''): + for sub_repr in self._get_str('', ''): str_len += len(sub_repr) if self.MAX_REPR_SIZE and str_len > self.MAX_REPR_SIZE: @@ -224,6 +222,47 @@ def __repr__(self) -> str: repr_list.append(sub_repr) return '\n'.join(repr_list) + @reprlib.recursive_repr() + def __repr__(self): + children = self.children + waveform = self.waveform + measurements = self._measurements + repetition_count = self.repetition_count + + max_repr_size = self.MAX_REPR_SIZE + + reprs = {} + if children: + if len(children) < max_repr_size // len('Loop(...)'): + reprs['children'] = repr(list(children)) + else: + reprs['children'] = '[...]' + + if waveform: + waveform_repr = repr(waveform) + if len(waveform_repr) >= max_repr_size: + waveform_repr = '...' + reprs['waveform'] = waveform_repr + + if measurements: + meas_repr = repr(measurements) + if len(meas_repr) >= max_repr_size: + meas_repr = '[...]' + reprs['measurements'] = meas_repr + + if repetition_count != 1: + reprs['repetition_count'] = repr(repetition_count) + + kwargs = ', '.join(f'{attr}={val}' for attr, val in reprs.items()) + + type_name = type(self).__name__ + max_kwargs = max(self.MAX_REPR_SIZE - len(type_name) - 2, 0) + + if len(kwargs) > max_kwargs: + kwargs = '...' + + return f'{type(self).__name__}({kwargs})' + def copy_tree_structure(self, new_parent: Union['Loop', bool]=False) -> 'Loop': return type(self)(parent=self.parent if new_parent is False else new_parent, waveform=self._waveform, diff --git a/tests/_program/loop_tests.py b/tests/_program/loop_tests.py index 3bd811cd6..a3ff48aa3 100644 --- a/tests/_program/loop_tests.py +++ b/tests/_program/loop_tests.py @@ -142,7 +142,7 @@ def test_get_measurement_windows(self): # no measurements left self.assertEqual({}, prog.get_measurement_windows()) - def test_repr(self): + def test_str(self): wf_gen = WaveformGenerator(num_channels=1) wfs = [wf_gen() for _ in range(11)] @@ -154,10 +154,19 @@ def test_repr(self): loop.waveform = wfs.pop(0) self.assertEqual(len(wfs), 0) - self.assertEqual(repr(tree), expected) + self.assertEqual(str(tree), expected) with mock.patch.object(Loop, 'MAX_REPR_SIZE', 1): - self.assertEqual(repr(tree), '...') + self.assertEqual(str(tree), '...') + + def test_repr(self): + root_loop = self.get_test_loop() + + root_repr = repr(root_loop) + + root_eval = eval(root_repr) + + self.assertEqual(root_loop, root_eval) def test_is_leaf(self): root_loop = self.get_test_loop(waveform_generator=WaveformGenerator(1)) @@ -195,12 +204,12 @@ def test_flatten_and_balance(self): after = before.copy_tree_structure() after.flatten_and_balance(2) - wf_reprs = dict(zip(ascii_uppercase, - (repr(loop.waveform) + wf_strs = dict(zip(ascii_uppercase, + (str(loop.waveform) for loop in before.get_depth_first_iterator() if loop.is_leaf()))) - before_repr = """\ + before_str = """\ LOOP 1 times: ->EXEC {A} 1 times ->LOOP 10 times: @@ -220,10 +229,10 @@ def test_flatten_and_balance(self): ->EXEC {I} 8 times ->LOOP 9 times: ->EXEC {J} 10 times - ->EXEC {K} 11 times""".format(**wf_reprs) - self.assertEqual(repr(before), before_repr) + ->EXEC {K} 11 times""".format(**wf_strs) + self.assertEqual(str(before), before_str) - expected_after_repr = """\ + expected_after_str = """\ LOOP 1 times: ->LOOP 1 times: ->EXEC {A} 1 times @@ -261,9 +270,9 @@ def test_flatten_and_balance(self): ->EXEC {I} 8 times ->LOOP 9 times: ->EXEC {J} 10 times - ->EXEC {K} 11 times""".format(**wf_reprs) + ->EXEC {K} 11 times""".format(**wf_strs) - self.assertEqual(expected_after_repr, repr(after)) + self.assertEqual(expected_after_str, str(after)) def test_flatten_and_balance_comparison_based(self): wfs = [DummyWaveform(duration=i) for i in range(2)] From d393532109d2b9a7f1c6900775c81821bb34400b Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 23 Jun 2023 12:52:26 +0200 Subject: [PATCH 059/441] Add a simple implementation of the ELFManager that is used by default --- qupulse/hardware/awgs/zihdawg.py | 96 +++++++++++++++++++++++++++++--- 1 file changed, 87 insertions(+), 9 deletions(-) diff --git a/qupulse/hardware/awgs/zihdawg.py b/qupulse/hardware/awgs/zihdawg.py index 6d6972a90..9bcf1cf29 100644 --- a/qupulse/hardware/awgs/zihdawg.py +++ b/qupulse/hardware/awgs/zihdawg.py @@ -10,7 +10,7 @@ import hashlib import argparse import re -from abc import abstractmethod +from abc import abstractmethod, ABC try: # zhinst fires a DeprecationWarning from its own code in some versions... @@ -394,7 +394,7 @@ def _initialize_awg_module(self): self._awg_module.set('awgModule/device', self.master_device.serial) self._awg_module.set('awgModule/index', self.awg_group_index) self._awg_module.execute() - self._elf_manager = ELFManager(self._awg_module) + self._elf_manager = ELFManager.DEFAULT_CLS(self._awg_module) self._upload_generator = () @property @@ -501,6 +501,7 @@ def upload(self, name: str, def _start_compile_and_upload(self): self._uploaded_seqc_source = None self._upload_generator = self._elf_manager.compile_and_upload(self._required_seqc_source) + logger.debug(f"_start_compile_and_upload: %r", next(self._upload_generator, "Finished")) def _wait_for_compile_and_upload(self): for state in self._upload_generator: @@ -560,6 +561,8 @@ def arm(self, name: Optional[str]) -> None: if self._required_seqc_source != self._uploaded_seqc_source: self._wait_for_compile_and_upload() + assert self._required_seqc_source == self._uploaded_seqc_source, "_wait_for_compile_and_upload did not work " \ + "as expected." self.user_register(self._program_manager.Constants.TRIGGER_REGISTER, 0) @@ -581,10 +584,8 @@ def arm(self, name: Optional[str]) -> None: self.user_register(self._program_manager.Constants.PROG_SEL_REGISTER, self._program_manager.name_to_index(name) | int(self._program_manager.Constants.NO_RESET_MASK, 2)) - # this was a workaround for problems in the past and I totally forgot why it was here - # for ch_pair in self.master.channel_tuples: - # ch_pair._wait_for_compile_and_upload() - self.enable(True) + if name is not None: + self.enable(True) def run_current_program(self) -> None: """Run armed program.""" @@ -802,7 +803,9 @@ def offsets(self) -> Tuple[float, ...]: return tuple(map(self.master_device.offset, self._channels())) -class ELFManager: +class ELFManager(ABC): + DEFAULT_CLS = None + class AWGModule: def __init__(self, awg_module: zhinst_core.AwgModule): """Provide an easily mockable interface to the zhinst AwgModule object""" @@ -838,6 +841,14 @@ def compiler_source_file(self) -> str: def compiler_source_file(self, source_file: str): self._module.set('compiler/sourcefile', source_file) + @property + def compiler_source_string(self) -> str: + return self._module.getString('compiler/sourcestring') + + @compiler_source_string.setter + def compiler_source_string(self, source_string: str): + self._module.set('compiler/sourcestring', source_string) + @property def compiler_upload(self) -> bool: """auto upload after compiling""" @@ -912,6 +923,74 @@ def _source_hash(source_string: str) -> str: # use utf-16 because str is UTF16 on most relevant machines (Windows) return hashlib.sha512(bytes(source_string, 'utf-16')).hexdigest() + @abstractmethod + def compile_and_upload(self, source_string: str) -> Generator[str, str, None]: + """The function returns a generator that yields the current state of the progress. The generator is empty iff + the upload is complete. An exception is raised if there is an error. + + To abort send 'abort' to the generator. (not implemented :P) + + Example: + >>> my_source = 'playWave("my_wave");' + >>> for state in elf_manager.compile_and_upload(my_source): + ... print('Current state:', state) + ... time.sleep(1) + + Args: + source_string: Source code to compile + + Returns: + Generator object that needs to be consumed + """ + + +class SimpleELFManager(ELFManager): + def __init__(self, awg_module: zhinst.ziPython.AwgModule): + """This implementation does not attempt to do something clever like caching.""" + super().__init__(awg_module) + + def compile_and_upload(self, source_string: str) -> Generator[str, str, None]: + self.awg_module.compiler_upload = True + self.awg_module.compiler_source_string = source_string + + while True: + status, msg = self.awg_module.compiler_status + if status == - 1: + yield 'compiling' + elif status == 0: + break + elif status == 1: + raise HDAWGCompilationException(msg) + elif status == 2: + logger.warning("Compiler warings: %s", msg) + break + else: + raise RuntimeError("Unexpected status", status, msg) + + while True: + status_int, progress = self.awg_module.elf_status + if progress == 1.0: + break + elif status_int == 1: + HDAWGUploadException(self.awg_module.compiler_status) + else: + yield 'uploading @ %d%%' % (100*progress) + + +ELFManager.DEFAULT_CLS = SimpleELFManager + + +class CachingELFManager(ELFManager): + def __init__(self, awg_module: zhinst.ziPython.AwgModule): + """FAILS TO UPLOAD THE CORRECT ELF FOR SOME REASON""" + super().__init__(awg_module) + + # automatically upload after successful compilation + self.awg_module.compiler_upload = True + + self._compile_job = None # type: Optional[Union[str, Tuple[str, int, str]]] + self._upload_job = None # type: Optional[Union[Tuple[str, float], Tuple[str, int]]] + def _update_compile_job_status(self): """Store current compile status in self._compile_job.""" compiler_start = self.awg_module.compiler_start @@ -920,8 +999,7 @@ def _update_compile_job_status(self): elif isinstance(self._compile_job, str): if compiler_start: - # compilation is running - pass + logger.debug("Compiler is running.") else: compiler_status, status_string = self.awg_module.compiler_status From 3ce1b0b351f659751cac57ebe16cc384c0e4fa29 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 23 Jun 2023 13:06:55 +0200 Subject: [PATCH 060/441] Only attempt to vectorize callables --- qupulse/utils/sympy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qupulse/utils/sympy.py b/qupulse/utils/sympy.py index b4d7286f2..66e830767 100644 --- a/qupulse/utils/sympy.py +++ b/qupulse/utils/sympy.py @@ -22,7 +22,7 @@ except ImportError: _special_functions = {fname: numpy.vectorize(fobject) for fname, fobject in math.__dict__.items() - if not fname.startswith('_') and fname not in numpy.__dict__} + if callable(fobject) and not fname.startswith('_') and fname not in numpy.__dict__} warnings.warn('scipy is not installed. This reduces the set of available functions to those present in numpy + ' 'manually vectorized functions in math.') From 2936ceceae03f133409b1f17c638564816d5eed1 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 23 Jun 2023 14:31:18 +0200 Subject: [PATCH 061/441] Better sympy test error message --- tests/utils/sympy_tests.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/utils/sympy_tests.py b/tests/utils/sympy_tests.py index 871ac2443..b1ff6b1e6 100644 --- a/tests/utils/sympy_tests.py +++ b/tests/utils/sympy_tests.py @@ -345,7 +345,11 @@ def evaluate(self, expression: Union[sympy.Expr, np.ndarray], parameters): if isinstance(expression, np.ndarray): return self.evaluate(sympy.Array(expression), parameters) - result, _ = evaluate_compiled(expression, parameters, compiled=None) + try: + result, _ = evaluate_compiled(expression, parameters, compiled=None) + except Exception as err: + raise AssertionError(f"Compiled evaluation of {expression!r} with {parameters!r} failed: {err!r}", + expression, parameters) from err if isinstance(result, (list, tuple)): return np.array(result) From ceb66efca3344820706ed9a26bcc7aa1b8a99a15 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 23 Jun 2023 14:36:57 +0200 Subject: [PATCH 062/441] Add math to base environment (WHY?) --- qupulse/utils/sympy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qupulse/utils/sympy.py b/qupulse/utils/sympy.py index 66e830767..7a04d53f6 100644 --- a/qupulse/utils/sympy.py +++ b/qupulse/utils/sympy.py @@ -363,7 +363,7 @@ def recursive_substitution(expression: sympy.Expr, return _recursive_substitution(expression, substitutions) -_base_environment = {'builtins': builtins, '__builtins__': builtins} +_base_environment = {'builtins': builtins, '__builtins__': builtins, 'math': math} _math_environment = {**_base_environment, **math.__dict__} _numpy_environment = {**_base_environment, **numpy.__dict__} _sympy_environment = {**_base_environment, **sympy.__dict__} From 0515ced80823eca68afec11216401bbec99d3a51 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 23 Jun 2023 15:04:15 +0200 Subject: [PATCH 063/441] Move tabor segment analysis to _program subpackage --- qupulse/_program/tabor.py | 88 ++++++++++++++++++++- qupulse/hardware/awgs/tabor.py | 94 +++-------------------- tests/_program/tabor_tests.py | 89 ++++++++++++++++++++- tests/hardware/tabor_dummy_based_tests.py | 91 ---------------------- 4 files changed, 186 insertions(+), 176 deletions(-) diff --git a/qupulse/_program/tabor.py b/qupulse/_program/tabor.py index 64002d969..546e94c9a 100644 --- a/qupulse/_program/tabor.py +++ b/qupulse/_program/tabor.py @@ -10,7 +10,7 @@ from qupulse.utils.types import ChannelID, TimeType from qupulse.hardware.awgs.base import ProgramEntry -from qupulse.hardware.util import get_sample_times, voltage_to_uint16 +from qupulse.hardware.util import get_sample_times, voltage_to_uint16, find_positions from qupulse._program.waveforms import Waveform from qupulse._program._loop import Loop from qupulse._program.volatile import VolatileRepetitionCount, VolatileProperty @@ -726,3 +726,89 @@ def parse_single_seq_program(program: Loop, used_channels: FrozenSet[ChannelID]) waveforms=tuple(waveforms.keys()), volatile_parameter_positions=volatile_parameter_positions ) + + +def find_place_for_segments_in_memory( + current_segment_hashes: np.ndarray, + current_segment_references: np.ndarray, + current_segment_capacities: np.ndarray, + total_capacity: int, + segments: Sequence, segment_lengths: Sequence) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + """ + 1. Find known segments + 2. Find empty spaces with fitting length + 3. Find empty spaces with bigger length + 4. Amend remaining segments + :param segments: + :param segment_lengths: + :return: + """ + segment_hashes = np.fromiter((hash(segment) for segment in segments), count=len(segments), dtype=np.int64) + + waveform_to_segment = find_positions(current_segment_hashes, segment_hashes) + + # separate into known and unknown + unknown = waveform_to_segment == -1 + known = ~unknown + + known_pos_in_memory = waveform_to_segment[known] + + assert len(known_pos_in_memory) == 0 or np.all(current_segment_hashes[known_pos_in_memory] == segment_hashes[known]) + + new_reference_counter = current_segment_references.copy() + new_reference_counter[known_pos_in_memory] += 1 + + to_upload_size = np.sum(segment_lengths[unknown] + 16) + free_points_in_total = total_capacity - np.sum(current_segment_capacities[current_segment_references > 0]) + if free_points_in_total < to_upload_size: + raise RuntimeError(f'Not enough free memory. Required {to_upload_size}. Available: {free_points_in_total}') + + to_amend = cast(np.ndarray, unknown) + to_insert = np.full(len(segments), fill_value=-1, dtype=np.int64) + + reserved_indices = np.flatnonzero(new_reference_counter > 0) + first_free = reserved_indices[-1] + 1 if len(reserved_indices) else 0 + + free_segments = new_reference_counter[:first_free] == 0 + free_segment_count = np.sum(free_segments) + + # look for a free segment place with the same length + for segment_idx in np.flatnonzero(to_amend): + if free_segment_count == 0: + break + + pos_of_same_length = np.logical_and(free_segments, + segment_lengths[segment_idx] == current_segment_capacities[:first_free]) + idx_same_length = np.argmax(pos_of_same_length) + if pos_of_same_length[idx_same_length]: + free_segments[idx_same_length] = False + free_segment_count -= 1 + + to_amend[segment_idx] = False + to_insert[segment_idx] = idx_same_length + + # try to find places that are larger than the segments to fit in starting with the large segments and large + # free spaces + segment_indices = np.flatnonzero(to_amend)[np.argsort(segment_lengths[to_amend])[::-1]] + capacities = current_segment_capacities[:first_free] + for segment_idx in segment_indices: + free_capacities = capacities[free_segments] + free_segments_indices = np.flatnonzero(free_segments)[np.argsort(free_capacities)[::-1]] + + if len(free_segments_indices) == 0: + break + + fitting_segment = np.argmax((free_capacities >= segment_lengths[segment_idx])[::-1]) + fitting_segment = free_segments_indices[fitting_segment] + if current_segment_capacities[fitting_segment] >= segment_lengths[segment_idx]: + free_segments[fitting_segment] = False + to_amend[segment_idx] = False + to_insert[segment_idx] = fitting_segment + + free_points_at_end = total_capacity - np.sum(current_segment_capacities[:first_free]) + if np.sum(segment_lengths[to_amend] + 16) > free_points_at_end: + raise RuntimeError('Fragmentation does not allow upload.', + np.sum(segment_lengths[to_amend] + 16), + free_points_at_end) + + return waveform_to_segment, to_amend, to_insert diff --git a/qupulse/hardware/awgs/tabor.py b/qupulse/hardware/awgs/tabor.py index 6e661bc0c..cb2278999 100644 --- a/qupulse/hardware/awgs/tabor.py +++ b/qupulse/hardware/awgs/tabor.py @@ -12,10 +12,10 @@ from qupulse.utils.types import ChannelID from qupulse._program._loop import Loop, make_compatible -from qupulse.hardware.util import voltage_to_uint16, find_positions, traced +from qupulse.hardware.util import voltage_to_uint16, traced from qupulse.hardware.awgs.base import AWG, AWGAmplitudeOffsetHandling from qupulse._program.tabor import TaborSegment, TaborException, TaborProgram, PlottableProgram, TaborSequencing,\ - make_combined_wave + make_combined_wave, find_place_for_segments_in_memory __all__ = ['TaborAWGRepresentation', 'TaborChannelPair'] @@ -543,87 +543,14 @@ def clear(self) -> None: self.change_armed_program(None) def _find_place_for_segments_in_memory(self, segments: Sequence, segment_lengths: Sequence) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: - """ - 1. Find known segments - 2. Find empty spaces with fitting length - 3. Find empty spaces with bigger length - 4. Amend remaining segments - :param segments: - :param segment_lengths: - :return: - """ - segment_hashes = np.fromiter((hash(segment) for segment in segments), count=len(segments), dtype=np.int64) - - waveform_to_segment = find_positions(self._segment_hashes, segment_hashes) - - # separate into known and unknown - unknown = (waveform_to_segment == -1) - known = ~unknown - - known_pos_in_memory = waveform_to_segment[known] - - assert len(known_pos_in_memory) == 0 or np.all(self._segment_hashes[known_pos_in_memory] == segment_hashes[known]) - - new_reference_counter = self._segment_references.copy() - new_reference_counter[known_pos_in_memory] += 1 - - to_upload_size = np.sum(segment_lengths[unknown] + 16) - free_points_in_total = self.total_capacity - np.sum(self._segment_capacity[self._segment_references > 0]) - if free_points_in_total < to_upload_size: - raise MemoryError('Not enough free memory', - free_points_in_total, - to_upload_size, - self._free_points_in_total) - - to_amend = cast(np.ndarray, unknown) - to_insert = np.full(len(segments), fill_value=-1, dtype=np.int64) - - reserved_indices = np.flatnonzero(new_reference_counter > 0) - first_free = reserved_indices[-1] + 1 if len(reserved_indices) else 0 - - free_segments = new_reference_counter[:first_free] == 0 - free_segment_count = np.sum(free_segments) - - # look for a free segment place with the same length - for segment_idx in np.flatnonzero(to_amend): - if free_segment_count == 0: - break - - pos_of_same_length = np.logical_and(free_segments, segment_lengths[segment_idx] == self._segment_capacity[:first_free]) - idx_same_length = np.argmax(pos_of_same_length) - if pos_of_same_length[idx_same_length]: - free_segments[idx_same_length] = False - free_segment_count -= 1 - - to_amend[segment_idx] = False - to_insert[segment_idx] = idx_same_length - - # try to find places that are larger than the segments to fit in starting with the large segments and large - # free spaces - segment_indices = np.flatnonzero(to_amend)[np.argsort(segment_lengths[to_amend])[::-1]] - capacities = self._segment_capacity[:first_free] - for segment_idx in segment_indices: - free_capacities = capacities[free_segments] - free_segments_indices = np.flatnonzero(free_segments)[np.argsort(free_capacities)[::-1]] - - if len(free_segments_indices) == 0: - break - - fitting_segment = np.argmax((free_capacities >= segment_lengths[segment_idx])[::-1]) - fitting_segment = free_segments_indices[fitting_segment] - if self._segment_capacity[fitting_segment] >= segment_lengths[segment_idx]: - free_segments[fitting_segment] = False - to_amend[segment_idx] = False - to_insert[segment_idx] = fitting_segment - - free_points_at_end = self.total_capacity - np.sum(self._segment_capacity[:first_free]) - if np.sum(segment_lengths[to_amend] + 16) > free_points_at_end: - raise MemoryError('Fragmentation does not allow upload.', - np.sum(segment_lengths[to_amend] + 16), - free_points_at_end, - self._free_points_at_end) - - return waveform_to_segment, to_amend, to_insert + return find_place_for_segments_in_memory( + current_segment_hashes=self._segment_hashes, + current_segment_capacities=self._segment_capacity, + current_segment_references=self._segment_references, + total_capacity=self.total_capacity, + segments=segments, + segment_lengths=segment_lengths + ) @with_select @with_configuration_guard @@ -953,3 +880,4 @@ def reset_device(self): self.device.reset() elif isinstance(self.device, TaborChannelPair): self.device.clear() + diff --git a/tests/_program/tabor_tests.py b/tests/_program/tabor_tests.py index ab8cddd30..8f847ae36 100644 --- a/tests/_program/tabor_tests.py +++ b/tests/_program/tabor_tests.py @@ -1,6 +1,7 @@ import unittest import itertools import numpy as np +from copy import deepcopy from qupulse.utils.types import FrozenDict from unittest import mock @@ -15,7 +16,7 @@ except ImportError: pytabor = None -from qupulse._program.tabor import TaborException, TaborProgram, \ +from qupulse._program.tabor import TaborException, TaborProgram, find_place_for_segments_in_memory,\ TaborSegment, TaborSequencing, PlottableProgram, TableDescription, make_combined_wave, TableEntry from qupulse._program._loop import Loop from qupulse._program.volatile import VolatileRepetitionCount @@ -704,3 +705,89 @@ def exec_general(self, data_1, data_2, fill_value=None): with self.assertRaises(ValueError): make_combined_wave(tabor_segments, destination_array=np.ones(16)) + + +class TaborMemoryManagementTests(unittest.TestCase): + def test_find_place_for_segments_in_memory(self): + # empty + kwargs = dict( + total_capacity=2**20, + current_segment_capacities=np.asarray([], dtype=np.uint32), + current_segment_hashes=np.asarray([], dtype=np.int64), + current_segment_references=np.asarray([], dtype=np.int32), + ) + prev_kwargs = deepcopy(kwargs) + + segments = np.asarray([-5, -6, -7, -8, -9]) + segment_lengths = 192 + np.asarray([32, 16, 64, 32, 16]) + + w2s, ta, ti = find_place_for_segments_in_memory( + **kwargs, + segments=segments, segment_lengths=segment_lengths) + self.assertEqual(w2s.tolist(), [-1, -1, -1, -1, -1]) + self.assertEqual(ta.tolist(), [True, True, True, True, True]) + self.assertEqual(ti.tolist(), [-1, -1, -1, -1, -1]) + np.testing.assert_equal(kwargs, prev_kwargs) + + # all new segments + kwargs['current_segment_capacities'] = 192 + np.asarray([0, 16, 32, 16, 0], dtype=np.uint32) + kwargs['current_segment_hashes'] = np.asarray([1, 2, 3, 4, 5], dtype=np.int64) + kwargs['current_segment_references'] = np.asarray([1, 1, 1, 2, 1], dtype=np.int32) + prev_kwargs = deepcopy(kwargs) + + w2s, ta, ti = find_place_for_segments_in_memory(segments=segments, segment_lengths=segment_lengths, **kwargs) + self.assertEqual(w2s.tolist(), [-1, -1, -1, -1, -1]) + self.assertEqual(ta.tolist(), [True, True, True, True, True]) + self.assertEqual(ti.tolist(), [-1, -1, -1, -1, -1]) + np.testing.assert_equal(kwargs, prev_kwargs) + + # some known segments + kwargs['current_segment_capacities'] = 192 + np.asarray([0, 16, 32, 64, 0, 16], dtype=np.uint32) + kwargs['current_segment_hashes'] = np.asarray([1, 2, 3, -7, 5, -9], dtype=np.int64) + kwargs['current_segment_references'] = np.asarray([1, 1, 1, 2, 1, 3], dtype=np.int32) + prev_kwargs = deepcopy(kwargs) + + w2s, ta, ti = find_place_for_segments_in_memory(segments=segments, segment_lengths=segment_lengths, **kwargs) + self.assertEqual(w2s.tolist(), [-1, -1, 3, -1, 5]) + self.assertEqual(ta.tolist(), [True, True, False, True, False]) + self.assertEqual(ti.tolist(), [-1, -1, -1, -1, -1]) + np.testing.assert_equal(kwargs, prev_kwargs) + + # insert some segments with same length + kwargs['current_segment_capacities'] = 192 + np.asarray([0, 16, 32, 64, 0, 16], dtype=np.uint32) + kwargs['current_segment_hashes'] = np.asarray([1, 2, 3, 4, 5, 6], dtype=np.int64) + kwargs['current_segment_references'] = np.asarray([1, 0, 1, 0, 1, 3], dtype=np.int32) + prev_kwargs = deepcopy(kwargs) + + w2s, ta, ti = find_place_for_segments_in_memory(segments=segments, segment_lengths=segment_lengths, **kwargs) + self.assertEqual(w2s.tolist(), [-1, -1, -1, -1, -1]) + self.assertEqual(ta.tolist(), [True, False, False, True, True]) + self.assertEqual(ti.tolist(), [-1, 1, 3, -1, -1]) + np.testing.assert_equal(kwargs, prev_kwargs) + + # insert some segments with smaller length + kwargs['current_segment_capacities'] = 192 + np.asarray([0, 80, 32, 64, 96, 16], dtype=np.uint32) + kwargs['current_segment_hashes'] = np.asarray([1, 2, 3, 4, 5, 6], dtype=np.int64) + kwargs['current_segment_references'] = np.asarray([1, 0, 1, 1, 0, 3], dtype=np.int32) + prev_kwargs = deepcopy(kwargs) + + w2s, ta, ti = find_place_for_segments_in_memory(segments=segments, segment_lengths=segment_lengths, **kwargs) + self.assertEqual(w2s.tolist(), [-1, -1, -1, -1, -1]) + self.assertEqual(ta.tolist(), [True, True, False, False, True]) + self.assertEqual(ti.tolist(), [-1, -1, 4, 1, -1]) + np.testing.assert_equal(kwargs, prev_kwargs) + + # mix everything + segments = np.asarray([-5, -6, -7, -8, -9, -10, -11]) + segment_lengths = 192 + np.asarray([32, 16, 64, 32, 16, 0, 0]) + + kwargs['current_segment_capacities'] = 192 + np.asarray([0, 80, 32, 64, 32, 16], dtype=np.uint32) + kwargs['current_segment_hashes'] = np.asarray([1, 2, 3, 4, -8, 6], dtype=np.int64) + kwargs['current_segment_references'] = np.asarray([1, 0, 1, 0, 1, 0], dtype=np.int32) + prev_kwargs = deepcopy(kwargs) + + w2s, ta, ti = find_place_for_segments_in_memory(segments=segments, segment_lengths=segment_lengths, **kwargs) + self.assertEqual(w2s.tolist(), [-1, -1, -1, 4, -1, -1, -1]) + self.assertEqual(ta.tolist(), [False, True, False, False, True, True, True]) + self.assertEqual(ti.tolist(), [1, -1, 3, -1, -1, -1, -1]) + np.testing.assert_equal(kwargs, prev_kwargs) diff --git a/tests/hardware/tabor_dummy_based_tests.py b/tests/hardware/tabor_dummy_based_tests.py index 4bc3f84fa..7a49e0b64 100644 --- a/tests/hardware/tabor_dummy_based_tests.py +++ b/tests/hardware/tabor_dummy_based_tests.py @@ -461,97 +461,6 @@ def test_upload_offset_handling(self): voltage_transformations=test_transform) self.assertEqual([], offset_mock.call_args_list) - def test_find_place_for_segments_in_memory(self): - def hash_based_on_dir(ch): - hash_list = [] - for d in dir(ch): - o = getattr(ch, d) - if isinstance(o, np.ndarray): - hash_list.append(hash(o.tobytes())) - else: - try: - hash_list.append(hash(o)) - except TypeError: - pass - return hash(tuple(hash_list)) - - channel_pair = TaborChannelPair(self.instrument, identifier='asd', channels=(1, 2)) - - # empty - segments = np.asarray([-5, -6, -7, -8, -9]) - segment_lengths = 192 + np.asarray([32, 16, 64, 32, 16]) - - hash_before = hash_based_on_dir(channel_pair) - - w2s, ta, ti = channel_pair._find_place_for_segments_in_memory(segments, segment_lengths) - self.assertEqual(w2s.tolist(), [-1, -1, -1, -1, -1]) - self.assertEqual(ta.tolist(), [True, True, True, True, True]) - self.assertEqual(ti.tolist(), [-1, -1, -1, -1, -1]) - self.assertEqual(hash_before, hash_based_on_dir(channel_pair)) - - # all new segments - channel_pair._segment_capacity = 192 + np.asarray([0, 16, 32, 16, 0], dtype=np.uint32) - channel_pair._segment_hashes = np.asarray([1, 2, 3, 4, 5], dtype=np.int64) - channel_pair._segment_references = np.asarray([1, 1, 1, 2, 1], dtype=np.int32) - hash_before = hash_based_on_dir(channel_pair) - - w2s, ta, ti = channel_pair._find_place_for_segments_in_memory(segments, segment_lengths) - self.assertEqual(w2s.tolist(), [-1, -1, -1, -1, -1]) - self.assertEqual(ta.tolist(), [True, True, True, True, True]) - self.assertEqual(ti.tolist(), [-1, -1, -1, -1, -1]) - self.assertEqual(hash_before, hash_based_on_dir(channel_pair)) - - # some known segments - channel_pair._segment_capacity = 192 + np.asarray([0, 16, 32, 64, 0, 16], dtype=np.uint32) - channel_pair._segment_hashes = np.asarray([1, 2, 3, -7, 5, -9], dtype=np.int64) - channel_pair._segment_references = np.asarray([1, 1, 1, 2, 1, 3], dtype=np.int32) - hash_before = hash_based_on_dir(channel_pair) - - w2s, ta, ti = channel_pair._find_place_for_segments_in_memory(segments, segment_lengths) - self.assertEqual(w2s.tolist(), [-1, -1, 3, -1, 5]) - self.assertEqual(ta.tolist(), [True, True, False, True, False]) - self.assertEqual(ti.tolist(), [-1, -1, -1, -1, -1]) - self.assertEqual(hash_before, hash_based_on_dir(channel_pair)) - - # insert some segments with same length - channel_pair._segment_capacity = 192 + np.asarray([0, 16, 32, 64, 0, 16], dtype=np.uint32) - channel_pair._segment_hashes = np.asarray([1, 2, 3, 4, 5, 6], dtype=np.int64) - channel_pair._segment_references = np.asarray([1, 0, 1, 0, 1, 3], dtype=np.int32) - hash_before = hash_based_on_dir(channel_pair) - - w2s, ta, ti = channel_pair._find_place_for_segments_in_memory(segments, segment_lengths) - self.assertEqual(w2s.tolist(), [-1, -1, -1, -1, -1]) - self.assertEqual(ta.tolist(), [True, False, False, True, True]) - self.assertEqual(ti.tolist(), [-1, 1, 3, -1, -1]) - self.assertEqual(hash_before, hash_based_on_dir(channel_pair)) - - # insert some segments with smaller length - channel_pair._segment_capacity = 192 + np.asarray([0, 80, 32, 64, 96, 16], dtype=np.uint32) - channel_pair._segment_hashes = np.asarray([1, 2, 3, 4, 5, 6], dtype=np.int64) - channel_pair._segment_references = np.asarray([1, 0, 1, 1, 0, 3], dtype=np.int32) - hash_before = hash_based_on_dir(channel_pair) - - w2s, ta, ti = channel_pair._find_place_for_segments_in_memory(segments, segment_lengths) - self.assertEqual(w2s.tolist(), [-1, -1, -1, -1, -1]) - self.assertEqual(ta.tolist(), [True, True, False, False, True]) - self.assertEqual(ti.tolist(), [-1, -1, 4, 1, -1]) - self.assertEqual(hash_before, hash_based_on_dir(channel_pair)) - - # mix everything - segments = np.asarray([-5, -6, -7, -8, -9, -10, -11]) - segment_lengths = 192 + np.asarray([32, 16, 64, 32, 16, 0, 0]) - - channel_pair._segment_capacity = 192 + np.asarray([0, 80, 32, 64, 32, 16], dtype=np.uint32) - channel_pair._segment_hashes = np.asarray([1, 2, 3, 4, -8, 6], dtype=np.int64) - channel_pair._segment_references = np.asarray([1, 0, 1, 0, 1, 0], dtype=np.int32) - hash_before = hash_based_on_dir(channel_pair) - - w2s, ta, ti = channel_pair._find_place_for_segments_in_memory(segments, segment_lengths) - self.assertEqual(w2s.tolist(), [-1, -1, -1, 4, -1, -1, -1]) - self.assertEqual(ta.tolist(), [False, True, False, False, True, True, True]) - self.assertEqual(ti.tolist(), [1, -1, 3, -1, -1, -1, -1]) - self.assertEqual(hash_before, hash_based_on_dir(channel_pair)) - def test_upload_segment(self): channel_pair = TaborChannelPair(self.instrument, identifier='asd', channels=(1, 2)) From 02ed0a4ecc239e5c7475380063c7541d4f186861 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 23 Jun 2023 15:21:40 +0200 Subject: [PATCH 064/441] Make find_place_for_segments_in_memory a deterministic function by factoring out the hashing --- qupulse/_program/tabor.py | 29 ++++++++++++++++------------- qupulse/hardware/awgs/tabor.py | 6 ++++-- tests/_program/tabor_tests.py | 12 ++++++------ 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/qupulse/_program/tabor.py b/qupulse/_program/tabor.py index 546e94c9a..738522b7d 100644 --- a/qupulse/_program/tabor.py +++ b/qupulse/_program/tabor.py @@ -1,5 +1,6 @@ import sys -from typing import NamedTuple, Optional, List, Generator, Tuple, Sequence, Mapping, Union, Dict, FrozenSet, cast +from typing import NamedTuple, Optional, List, Generator, Tuple, Sequence, Mapping, Union, Dict, FrozenSet, cast,\ + Hashable from enum import Enum import operator from collections import OrderedDict @@ -733,7 +734,8 @@ def find_place_for_segments_in_memory( current_segment_references: np.ndarray, current_segment_capacities: np.ndarray, total_capacity: int, - segments: Sequence, segment_lengths: Sequence) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + new_segment_hashes: Sequence[int], + new_segment_lengths: Sequence[int]) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: """ 1. Find known segments 2. Find empty spaces with fitting length @@ -743,9 +745,10 @@ def find_place_for_segments_in_memory( :param segment_lengths: :return: """ - segment_hashes = np.fromiter((hash(segment) for segment in segments), count=len(segments), dtype=np.int64) + new_segment_hashes = np.asarray(new_segment_hashes) + new_segment_lengths = np.asarray(new_segment_lengths) - waveform_to_segment = find_positions(current_segment_hashes, segment_hashes) + waveform_to_segment = find_positions(current_segment_hashes, new_segment_hashes) # separate into known and unknown unknown = waveform_to_segment == -1 @@ -753,18 +756,18 @@ def find_place_for_segments_in_memory( known_pos_in_memory = waveform_to_segment[known] - assert len(known_pos_in_memory) == 0 or np.all(current_segment_hashes[known_pos_in_memory] == segment_hashes[known]) + assert len(known_pos_in_memory) == 0 or np.all(current_segment_hashes[known_pos_in_memory] == new_segment_hashes[known]) new_reference_counter = current_segment_references.copy() new_reference_counter[known_pos_in_memory] += 1 - to_upload_size = np.sum(segment_lengths[unknown] + 16) + to_upload_size = np.sum(new_segment_lengths[unknown] + 16) free_points_in_total = total_capacity - np.sum(current_segment_capacities[current_segment_references > 0]) if free_points_in_total < to_upload_size: raise RuntimeError(f'Not enough free memory. Required {to_upload_size}. Available: {free_points_in_total}') to_amend = cast(np.ndarray, unknown) - to_insert = np.full(len(segments), fill_value=-1, dtype=np.int64) + to_insert = np.full(len(new_segment_hashes), fill_value=-1, dtype=np.int64) reserved_indices = np.flatnonzero(new_reference_counter > 0) first_free = reserved_indices[-1] + 1 if len(reserved_indices) else 0 @@ -778,7 +781,7 @@ def find_place_for_segments_in_memory( break pos_of_same_length = np.logical_and(free_segments, - segment_lengths[segment_idx] == current_segment_capacities[:first_free]) + new_segment_lengths[segment_idx] == current_segment_capacities[:first_free]) idx_same_length = np.argmax(pos_of_same_length) if pos_of_same_length[idx_same_length]: free_segments[idx_same_length] = False @@ -789,7 +792,7 @@ def find_place_for_segments_in_memory( # try to find places that are larger than the segments to fit in starting with the large segments and large # free spaces - segment_indices = np.flatnonzero(to_amend)[np.argsort(segment_lengths[to_amend])[::-1]] + segment_indices = np.flatnonzero(to_amend)[np.argsort(new_segment_lengths[to_amend])[::-1]] capacities = current_segment_capacities[:first_free] for segment_idx in segment_indices: free_capacities = capacities[free_segments] @@ -798,17 +801,17 @@ def find_place_for_segments_in_memory( if len(free_segments_indices) == 0: break - fitting_segment = np.argmax((free_capacities >= segment_lengths[segment_idx])[::-1]) + fitting_segment = np.argmax((free_capacities >= new_segment_lengths[segment_idx])[::-1]) fitting_segment = free_segments_indices[fitting_segment] - if current_segment_capacities[fitting_segment] >= segment_lengths[segment_idx]: + if current_segment_capacities[fitting_segment] >= new_segment_lengths[segment_idx]: free_segments[fitting_segment] = False to_amend[segment_idx] = False to_insert[segment_idx] = fitting_segment free_points_at_end = total_capacity - np.sum(current_segment_capacities[:first_free]) - if np.sum(segment_lengths[to_amend] + 16) > free_points_at_end: + if np.sum(new_segment_lengths[to_amend] + 16) > free_points_at_end: raise RuntimeError('Fragmentation does not allow upload.', - np.sum(segment_lengths[to_amend] + 16), + np.sum(new_segment_lengths[to_amend] + 16), free_points_at_end) return waveform_to_segment, to_amend, to_insert diff --git a/qupulse/hardware/awgs/tabor.py b/qupulse/hardware/awgs/tabor.py index cb2278999..da878624b 100644 --- a/qupulse/hardware/awgs/tabor.py +++ b/qupulse/hardware/awgs/tabor.py @@ -543,13 +543,15 @@ def clear(self) -> None: self.change_armed_program(None) def _find_place_for_segments_in_memory(self, segments: Sequence, segment_lengths: Sequence) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + segment_hashes = np.fromiter((hash(segment) for segment in segments), dtype=np.int64, count=len(segments)) + return find_place_for_segments_in_memory( current_segment_hashes=self._segment_hashes, current_segment_capacities=self._segment_capacity, current_segment_references=self._segment_references, total_capacity=self.total_capacity, - segments=segments, - segment_lengths=segment_lengths + segment_lengths=segment_lengths, + new_segment_hashes=segment_hashes ) @with_select diff --git a/tests/_program/tabor_tests.py b/tests/_program/tabor_tests.py index 8f847ae36..9021d99a2 100644 --- a/tests/_program/tabor_tests.py +++ b/tests/_program/tabor_tests.py @@ -723,7 +723,7 @@ def test_find_place_for_segments_in_memory(self): w2s, ta, ti = find_place_for_segments_in_memory( **kwargs, - segments=segments, segment_lengths=segment_lengths) + new_segment_hashes=segments, new_segment_lengths=segment_lengths) self.assertEqual(w2s.tolist(), [-1, -1, -1, -1, -1]) self.assertEqual(ta.tolist(), [True, True, True, True, True]) self.assertEqual(ti.tolist(), [-1, -1, -1, -1, -1]) @@ -735,7 +735,7 @@ def test_find_place_for_segments_in_memory(self): kwargs['current_segment_references'] = np.asarray([1, 1, 1, 2, 1], dtype=np.int32) prev_kwargs = deepcopy(kwargs) - w2s, ta, ti = find_place_for_segments_in_memory(segments=segments, segment_lengths=segment_lengths, **kwargs) + w2s, ta, ti = find_place_for_segments_in_memory(new_segment_hashes=segments, new_segment_lengths=segment_lengths, **kwargs) self.assertEqual(w2s.tolist(), [-1, -1, -1, -1, -1]) self.assertEqual(ta.tolist(), [True, True, True, True, True]) self.assertEqual(ti.tolist(), [-1, -1, -1, -1, -1]) @@ -747,7 +747,7 @@ def test_find_place_for_segments_in_memory(self): kwargs['current_segment_references'] = np.asarray([1, 1, 1, 2, 1, 3], dtype=np.int32) prev_kwargs = deepcopy(kwargs) - w2s, ta, ti = find_place_for_segments_in_memory(segments=segments, segment_lengths=segment_lengths, **kwargs) + w2s, ta, ti = find_place_for_segments_in_memory(new_segment_hashes=segments, new_segment_lengths=segment_lengths, **kwargs) self.assertEqual(w2s.tolist(), [-1, -1, 3, -1, 5]) self.assertEqual(ta.tolist(), [True, True, False, True, False]) self.assertEqual(ti.tolist(), [-1, -1, -1, -1, -1]) @@ -759,7 +759,7 @@ def test_find_place_for_segments_in_memory(self): kwargs['current_segment_references'] = np.asarray([1, 0, 1, 0, 1, 3], dtype=np.int32) prev_kwargs = deepcopy(kwargs) - w2s, ta, ti = find_place_for_segments_in_memory(segments=segments, segment_lengths=segment_lengths, **kwargs) + w2s, ta, ti = find_place_for_segments_in_memory(new_segment_hashes=segments, new_segment_lengths=segment_lengths, **kwargs) self.assertEqual(w2s.tolist(), [-1, -1, -1, -1, -1]) self.assertEqual(ta.tolist(), [True, False, False, True, True]) self.assertEqual(ti.tolist(), [-1, 1, 3, -1, -1]) @@ -771,7 +771,7 @@ def test_find_place_for_segments_in_memory(self): kwargs['current_segment_references'] = np.asarray([1, 0, 1, 1, 0, 3], dtype=np.int32) prev_kwargs = deepcopy(kwargs) - w2s, ta, ti = find_place_for_segments_in_memory(segments=segments, segment_lengths=segment_lengths, **kwargs) + w2s, ta, ti = find_place_for_segments_in_memory(new_segment_hashes=segments, new_segment_lengths=segment_lengths, **kwargs) self.assertEqual(w2s.tolist(), [-1, -1, -1, -1, -1]) self.assertEqual(ta.tolist(), [True, True, False, False, True]) self.assertEqual(ti.tolist(), [-1, -1, 4, 1, -1]) @@ -786,7 +786,7 @@ def test_find_place_for_segments_in_memory(self): kwargs['current_segment_references'] = np.asarray([1, 0, 1, 0, 1, 0], dtype=np.int32) prev_kwargs = deepcopy(kwargs) - w2s, ta, ti = find_place_for_segments_in_memory(segments=segments, segment_lengths=segment_lengths, **kwargs) + w2s, ta, ti = find_place_for_segments_in_memory(new_segment_hashes=segments, new_segment_lengths=segment_lengths, **kwargs) self.assertEqual(w2s.tolist(), [-1, -1, -1, 4, -1, -1, -1]) self.assertEqual(ta.tolist(), [False, True, False, False, True, True, True]) self.assertEqual(ti.tolist(), [1, -1, 3, -1, -1, -1, -1]) From 15e790900592aecad48e380f7c0046970bce74f1 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 23 Jun 2023 15:25:19 +0200 Subject: [PATCH 065/441] Make find position deterministic by enforcing stable sort --- qupulse/hardware/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qupulse/hardware/util.py b/qupulse/hardware/util.py index 30a9aae34..f2c60dbac 100644 --- a/qupulse/hardware/util.py +++ b/qupulse/hardware/util.py @@ -99,7 +99,7 @@ def voltage_to_uint16(voltage: np.ndarray, output_amplitude: float, output_offse def find_positions(data: Sequence, to_find: Sequence) -> np.ndarray: """Find indices of the first occurrence of the elements of to_find in data. Elements that are not in data result in -1""" - data_sorter = np.argsort(data) + data_sorter = np.argsort(data, kind='stable') pos_left = np.searchsorted(data, to_find, side='left', sorter=data_sorter) pos_right = np.searchsorted(data, to_find, side='right', sorter=data_sorter) From fab2a827ae39825b283b90b3dac03e912441da9e Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 23 Jun 2023 15:29:11 +0200 Subject: [PATCH 066/441] Fix typo in find_place_for_segments_in_memory kwargs --- qupulse/hardware/awgs/tabor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qupulse/hardware/awgs/tabor.py b/qupulse/hardware/awgs/tabor.py index da878624b..d964a566c 100644 --- a/qupulse/hardware/awgs/tabor.py +++ b/qupulse/hardware/awgs/tabor.py @@ -550,7 +550,7 @@ def _find_place_for_segments_in_memory(self, segments: Sequence, segment_lengths current_segment_capacities=self._segment_capacity, current_segment_references=self._segment_references, total_capacity=self.total_capacity, - segment_lengths=segment_lengths, + new_segment_lengths=segment_lengths, new_segment_hashes=segment_hashes ) From 8b42a27f19406d9965ef5120e125ce2044dfb7ac Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 23 Jun 2023 15:43:50 +0200 Subject: [PATCH 067/441] Specify stable sort in find_place_for_segments_in_memory --- qupulse/_program/tabor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qupulse/_program/tabor.py b/qupulse/_program/tabor.py index 738522b7d..71ffe4390 100644 --- a/qupulse/_program/tabor.py +++ b/qupulse/_program/tabor.py @@ -792,11 +792,11 @@ def find_place_for_segments_in_memory( # try to find places that are larger than the segments to fit in starting with the large segments and large # free spaces - segment_indices = np.flatnonzero(to_amend)[np.argsort(new_segment_lengths[to_amend])[::-1]] + segment_indices = np.flatnonzero(to_amend)[np.argsort(new_segment_lengths[to_amend], kind='stable')[::-1]] capacities = current_segment_capacities[:first_free] for segment_idx in segment_indices: free_capacities = capacities[free_segments] - free_segments_indices = np.flatnonzero(free_segments)[np.argsort(free_capacities)[::-1]] + free_segments_indices = np.flatnonzero(free_segments)[np.argsort(free_capacities, kind='stable')[::-1]] if len(free_segments_indices) == 0: break From 4bd685512ed4628a9be3b57050729aefe7bd6eb6 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 23 Jun 2023 15:59:28 +0200 Subject: [PATCH 068/441] specify zhinst max version for python<3.9 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 8c68a1adf..fbc526a2a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -42,7 +42,7 @@ docs = plotting = matplotlib tabor-instruments = tabor_control>=0.1.1 -zurich-instruments = zhinst +zurich-instruments = zhinst<=20.7.2701;python_version<'3.9' Faster-fractions = gmpy2 tektronix = tek_awg>=0.2.1 autologging = autologging From e2d838ef37d2f630010091953a55224db7786b73 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 23 Jun 2023 16:32:50 +0200 Subject: [PATCH 069/441] Fix requirement marker --- setup.cfg | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index fbc526a2a..6ca9c3e05 100644 --- a/setup.cfg +++ b/setup.cfg @@ -42,7 +42,8 @@ docs = plotting = matplotlib tabor-instruments = tabor_control>=0.1.1 -zurich-instruments = zhinst<=20.7.2701;python_version<'3.9' +zurich-instruments = + zhinst<=20.7.2701;python_version<'3.9' Faster-fractions = gmpy2 tektronix = tek_awg>=0.2.1 autologging = autologging From 0b311abaa99880d9e803127f10513452464b03f7 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 23 Jun 2023 16:38:49 +0200 Subject: [PATCH 070/441] Make Test use default class --- tests/hardware/zihdawg_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/hardware/zihdawg_tests.py b/tests/hardware/zihdawg_tests.py index 9fe675eab..cc86ad151 100644 --- a/tests/hardware/zihdawg_tests.py +++ b/tests/hardware/zihdawg_tests.py @@ -199,7 +199,7 @@ def test_upload(self): @mock.patch('qupulse.hardware.awgs.zihdawg.ELFManager.AWGModule.compiler_upload', new_callable=mock.PropertyMock) class ELFManagerTests(unittest.TestCase): def test_init(self, compiler_upload): - manager = ELFManager(None) + manager = ELFManager.DEFAULT_CLS(None) compiler_upload.assert_called_once_with(True) self.assertIsNone(manager._compile_job) self.assertIsNone(manager._upload_job) From 264448fe8782d0a54cb45f82f26961bafbec1948 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 26 Jun 2023 08:51:51 +0200 Subject: [PATCH 071/441] Add pulse registry support for ArithmeticPT --- qupulse/pulses/arithmetic_pulse_template.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/qupulse/pulses/arithmetic_pulse_template.py b/qupulse/pulses/arithmetic_pulse_template.py index 7a2735ee2..dc9066f85 100644 --- a/qupulse/pulses/arithmetic_pulse_template.py +++ b/qupulse/pulses/arithmetic_pulse_template.py @@ -197,7 +197,8 @@ def __init__(self, arithmetic_operator: str, rhs: Union[PulseTemplate, ExpressionLike, Mapping[ChannelID, ExpressionLike]], *, - identifier: Optional[str] = None): + identifier: Optional[str] = None, + registry: PulseRegistryType = None): """Implements the arithmetics between an aribrary pulse template and scalar values. The values can be the same for all channels, channel specific or only for a subset of the inner pulse templates defined channels. The expression may be time dependent if the pulse template is atomic. @@ -263,6 +264,8 @@ def __init__(self, # this is a hack so we can use the AtomicPulseTemplate.integral default implementation self._AS_EXPRESSION_TIME = AtomicPulseTemplate._AS_EXPRESSION_TIME + self._register(registry=registry) + @staticmethod def _parse_operand(operand: Union[ExpressionLike, Mapping[ChannelID, ExpressionLike]], channels: Set[ChannelID]) -> Union[ExpressionScalar, Mapping[ChannelID, ExpressionScalar]]: From 03aa3d2587e30971df5ca2e28476a2394c1eb733 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 26 Jun 2023 08:56:11 +0200 Subject: [PATCH 072/441] Add niewspiece --- changes.d/771.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes.d/771.misc diff --git a/changes.d/771.misc b/changes.d/771.misc new file mode 100644 index 000000000..168460257 --- /dev/null +++ b/changes.d/771.misc @@ -0,0 +1 @@ +The `repr` implementation of `Loop` will now return an evaluable python expression. The old behaviour moved to the `str` implementation. From 9960a5774e5f332ef3584f50fa5d53c19aa4763c Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 26 Jun 2023 09:04:53 +0200 Subject: [PATCH 073/441] Add newspiece --- changes.d/775.fix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes.d/775.fix diff --git a/changes.d/775.fix b/changes.d/775.fix new file mode 100644 index 000000000..acbe9a605 --- /dev/null +++ b/changes.d/775.fix @@ -0,0 +1 @@ +Add missing pulse registry support to `ArithmeticPT`. From d9e49837587d0928b39f65d831b3f345b3afdda7 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 26 Jun 2023 10:25:13 +0200 Subject: [PATCH 074/441] Right align channel 2 marker data in binary wavform and add test --- qupulse/_program/seqc.py | 2 +- tests/_program/seqc_tests.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/qupulse/_program/seqc.py b/qupulse/_program/seqc.py index 50dd29a7c..2924268b4 100644 --- a/qupulse/_program/seqc.py +++ b/qupulse/_program/seqc.py @@ -106,7 +106,7 @@ def markers_ch1(self): @property def markers_ch2(self): - return np.bitwise_and(self.marker_data, 0b1100) + return np.right_shift(np.bitwise_and(self.marker_data, 0b1100), 2) @classmethod def from_sampled(cls, ch1: Optional[np.ndarray], ch2: Optional[np.ndarray], diff --git a/tests/_program/seqc_tests.py b/tests/_program/seqc_tests.py index 6b2cfb0db..729dacb1a 100644 --- a/tests/_program/seqc_tests.py +++ b/tests/_program/seqc_tests.py @@ -63,6 +63,23 @@ def test_dynamic_rate_reduction(self): self.assertEqual(min(max_rate, n), dyn_n) + def test_marker_data(self): + channel_1_data = np.linspace(-0.3, 0.4, num=192) + channel_2_data = np.linspace(-0.1, 0.1, num=192) + + bit_gen = np.random.PCG64(49174928843) + rng = np.random.Generator(bit_gen) + + m1, m2, m3, m4 = rng.integers(2, size=(4, 192), dtype=np.uint16) + + bwf = BinaryWaveform.from_sampled(channel_1_data, channel_2_data, (m1, m2, m3, m4)) + + ch1_markers = m1 | m2 << 1 + ch2_markers = m3 | m4 << 1 + + np.testing.assert_equal(ch1_markers, bwf.markers_ch1) + np.testing.assert_equal(ch2_markers, bwf.markers_ch2) + def make_binary_waveform(waveform): if zhinst is None: From 04900730c610aba745e00de552ba27a85791cc13 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 6 Jul 2023 12:33:27 +0200 Subject: [PATCH 075/441] Fix reference in learners guide --- doc/source/learners_guide.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/learners_guide.rst b/doc/source/learners_guide.rst index 7ddce71a4..ee728dd2a 100644 --- a/doc/source/learners_guide.rst +++ b/doc/source/learners_guide.rst @@ -91,7 +91,7 @@ This section is incomplete. **Learning Task 1:** -Read :ref:`program` and :ref:`awgs`. +Read :ref:`program` and :ref:`hardware`. Setup an experiment From 052f6df321e599b728002e931909b69597e68010 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Wed, 9 Aug 2023 16:05:09 +0200 Subject: [PATCH 076/441] Add second draft of program builder protocol --- qupulse/program/__init__.py | 87 +++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 qupulse/program/__init__.py diff --git a/qupulse/program/__init__.py b/qupulse/program/__init__.py new file mode 100644 index 000000000..a76395a0b --- /dev/null +++ b/qupulse/program/__init__.py @@ -0,0 +1,87 @@ +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import Optional, Union, Sequence, ContextManager, Mapping, Tuple, Generic, TypeVar +from numbers import Real + +import numpy as np + +from qupulse._program.waveforms import Waveform +from qupulse.utils.types import MeasurementWindow, TimeType +from qupulse._program.volatile import VolatileRepetitionCount + +from typing import Protocol, runtime_checkable + + +NumVal = TypeVar('NumVal', bound=Real) + + +@dataclass +class SimpleExpression(Generic[NumVal]): + base: NumVal + offsets: Sequence[Tuple[str, NumVal]] + + def value(self, scope: Mapping[str, NumVal]) -> NumVal: + value = self.base + for name, factor in self.offsets: + value += scope[name] * factor + return value + + +RepetitionCount = Union[int, VolatileRepetitionCount] +Value = Union[Real, SimpleExpression] + + + + +@runtime_checkable +class Program(Protocol): + """This protocol is used to inspect and or manipulate programs""" + + @property + def duration(self) -> TimeType: + raise NotImplementedError() + + +class ProgramBuilder(Protocol): + """This protocol is used by PulseTemplate to build the program. + + There is a default implementation which is the loop class. + + Other hardware backends can use this protocol to implement easy translation of pulse templates. + + """ + + def hold_voltage(self, duration: Value, voltages: Mapping[str, Value]): + """Supports dynamic i.e. for loop generated offsets and duration""" + + # further specialized commandos like play_harmoic might be added here + + def play_arbitrary_waveform(self, waveform: Waveform): + """""" + + def measure(self, measurements: Optional[Sequence[MeasurementWindow]]): + """Add given measurements at the current position""" + + def with_repetition(self, repetition_count: RepetitionCount) -> ContextManager['ProgramBuilder']: + """Measurements that are added to the new builder are dropped if the builder is empty upon exit""" + + def with_sequence(self) -> ContextManager['ProgramBuilder']: + """ + + Measurements that are added in to the returned program builder are discarded if the sequence is empty on exit. + + Args: + measurements: Measurements to attach to the potential child. Is not repeated with repetition_count. + repetition_count: + Returns: + """ + + def new_subprogram(self) -> ContextManager['ProgramBuilder']: + """Create a context managed program builder whose contents are translated into a single waveform upon exit if + it is not empty.""" + + def with_iteration(self, index_name: str, rng: range) -> ContextManager['ProgramBuilder']: + pass + + def to_program(self) -> Optional[Program]: + """Further addition of new elements might fail after finalizing the program.""" From 00cc8ab190de3c39fc9e86e4298674328d85aa52 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Wed, 9 Aug 2023 16:10:40 +0200 Subject: [PATCH 077/441] Add simple expression docstring --- qupulse/program/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/qupulse/program/__init__.py b/qupulse/program/__init__.py index a76395a0b..9315b8f1a 100644 --- a/qupulse/program/__init__.py +++ b/qupulse/program/__init__.py @@ -17,6 +17,15 @@ @dataclass class SimpleExpression(Generic[NumVal]): + """This is a potential hardware evaluable expression of the form + + C + C1*R1 + C2*R2 + ... + where R1, R2, ... are potential runtime parameters. + + The main use case is the expression of for loop dependent variables where the Rs are loop indices. There the + expressions can be calculated via simple increments. + """ + base: NumVal offsets: Sequence[Tuple[str, NumVal]] From 19e23dfe4efd107e8aae29f39c20e3aad118b036 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Sun, 20 Aug 2023 11:03:19 +0200 Subject: [PATCH 078/441] Add a hardware expression --- qupulse/program/__init__.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/qupulse/program/__init__.py b/qupulse/program/__init__.py index 9315b8f1a..5e058b360 100644 --- a/qupulse/program/__init__.py +++ b/qupulse/program/__init__.py @@ -35,11 +35,23 @@ def value(self, scope: Mapping[str, NumVal]) -> NumVal: value += scope[name] * factor return value + def __add__(self, other): + if isinstance(other, (float, int, TimeType)): + return SimpleExpression(self.base + other, self.offsets) -RepetitionCount = Union[int, VolatileRepetitionCount] -Value = Union[Real, SimpleExpression] + if type(other) == type(self): + return SimpleExpression(self.base + other.base, self.offsets + other.offsets) + return NotImplemented + def __mul__(self, other: NumVal): + return SimpleExpression(self.base * other, tuple((name, value * other) for name, value in self.offsets)) + + + +RepetitionCount = Union[int, VolatileRepetitionCount, SimpleExpression[int]] +HardwareTime = Union[TimeType, SimpleExpression[TimeType]] +HardwareVoltage = Union[float, SimpleExpression[float]] @runtime_checkable @@ -60,7 +72,7 @@ class ProgramBuilder(Protocol): """ - def hold_voltage(self, duration: Value, voltages: Mapping[str, Value]): + def hold_voltage(self, duration: HardwareTime, voltages: Mapping[str, HardwareVoltage]): """Supports dynamic i.e. for loop generated offsets and duration""" # further specialized commandos like play_harmoic might be added here @@ -89,7 +101,7 @@ def new_subprogram(self) -> ContextManager['ProgramBuilder']: """Create a context managed program builder whose contents are translated into a single waveform upon exit if it is not empty.""" - def with_iteration(self, index_name: str, rng: range) -> ContextManager['ProgramBuilder']: + def with_iteration(self, index_name: str, rng: range) -> Tuple[ContextManager['ProgramBuilder'], Any]: pass def to_program(self) -> Optional[Program]: From 8bd76becf88ae6202170115f6b289eccac9447a0 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Sun, 20 Aug 2023 12:11:42 +0200 Subject: [PATCH 079/441] First part of program builder based pt translation --- qupulse/pulses/loop_pulse_template.py | 40 ++++++++------- qupulse/pulses/pulse_template.py | 54 +++++++++------------ qupulse/pulses/repetition_pulse_template.py | 27 +++++------ qupulse/pulses/sequence_pulse_template.py | 13 +++-- 4 files changed, 63 insertions(+), 71 deletions(-) diff --git a/qupulse/pulses/loop_pulse_template.py b/qupulse/pulses/loop_pulse_template.py index 101705ebc..4465145ce 100644 --- a/qupulse/pulses/loop_pulse_template.py +++ b/qupulse/pulses/loop_pulse_template.py @@ -1,5 +1,6 @@ """This module defines LoopPulseTemplate, a higher-order hierarchical pulse template that loops another PulseTemplate based on a condition.""" +import dataclasses import functools import itertools from abc import ABC @@ -13,7 +14,7 @@ from qupulse.parameter_scope import Scope, MappedScope, DictScope from qupulse.utils.types import FrozenDict, FrozenMapping -from qupulse._program._loop import Loop +from qupulse.program import ProgramBuilder from qupulse.expressions import ExpressionScalar, ExpressionVariableMissingException, Expression from qupulse.utils import checked_int_cast, cached_property @@ -149,26 +150,25 @@ def _internal_create_program(self, *, channel_mapping: Dict[ChannelID, Optional[ChannelID]], global_transformation: Optional['Transformation'], to_single_waveform: Set[Union[str, 'PulseTemplate']], - parent_loop: Loop) -> None: + program_builder: ProgramBuilder) -> None: self.validate_scope(scope=scope) - try: - duration = self.duration.evaluate_in_scope(scope) - except ExpressionVariableMissingException as err: - raise ParameterNotProvidedException(err.variable) from err + measurements = self.get_measurement_windows(scope, measurement_mapping) + loop_range = self._loop_range.to_range(scope) + loop_index_name = self._loop_index + + with program_builder.with_iteration(loop_index_name, loop_range) as iteration_program_builder: + iteration_program_builder.measure(measurements) - if duration > 0: - measurements = self.get_measurement_windows(scope, measurement_mapping) - if measurements: - parent_loop.add_measurements(measurements) + # todo: create specialized scope? + inner_scope = MappedScope(scope, {loop_index_name: _ForLoopIndexValue(loop_index_name, loop_range)}) - for local_scope in self._body_scope_generator(scope, forward=True): - self.body._create_program(scope=local_scope, - measurement_mapping=measurement_mapping, - channel_mapping=channel_mapping, - global_transformation=global_transformation, - to_single_waveform=to_single_waveform, - parent_loop=parent_loop) + self.body._create_program(scope=inner_scope, + measurement_mapping=measurement_mapping, + channel_mapping=channel_mapping, + global_transformation=global_transformation, + to_single_waveform=to_single_waveform, + program_builder=iteration_program_builder) def build_waveform(self, parameter_scope: Scope) -> ForLoopWaveform: return ForLoopWaveform([self.body.build_waveform(local_scope) @@ -255,3 +255,9 @@ def __str__(self) -> str: _ForLoopScope = RangeScope + + +@dataclasses.dataclass +class _ForLoopIndexValue: + name: str + rng: range diff --git a/qupulse/pulses/pulse_template.py b/qupulse/pulses/pulse_template.py index c8b598e83..05cecfb76 100644 --- a/qupulse/pulses/pulse_template.py +++ b/qupulse/pulses/pulse_template.py @@ -26,6 +26,8 @@ from qupulse.pulses.measurement import MeasurementDefiner, MeasurementDeclaration from qupulse.parameter_scope import Scope, DictScope +from qupulse.program import ProgramBuilder + __all__ = ["PulseTemplate", "AtomicPulseTemplate", "DoubleParameterNameException", "MappingTuple", "UnknownVolatileParameter"] @@ -117,7 +119,8 @@ def create_program(self, *, channel_mapping: Optional[Mapping[ChannelID, Optional[ChannelID]]]=None, global_transformation: Optional[Transformation]=None, to_single_waveform: Set[Union[str, 'PulseTemplate']]=None, - volatile: Union[Set[str], str] = None) -> Optional['Loop']: + volatile: Union[Set[str], str] = None, + program_builder: ProgramBuilder) -> Optional['Loop']: """Translates this PulseTemplate into a program Loop. The returned Loop represents the PulseTemplate with all parameter values instantiated provided as dictated by @@ -178,7 +181,8 @@ def create_program(self, *, category=UnknownVolatileParameter, stacklevel=2) - root_loop = Loop() + if program_builder is None: + program_builder = Loop() # call subclass specific implementation self._create_program(scope=scope, @@ -186,11 +190,11 @@ def create_program(self, *, channel_mapping=complete_channel_mapping, global_transformation=global_transformation, to_single_waveform=to_single_waveform, - parent_loop=root_loop) + program_builder=program_builder) - if root_loop.waveform is None and len(root_loop.children) == 0: + if program_builder.waveform is None and len(program_builder.children) == 0: return None # return None if no program - return root_loop + return program_builder.to_program() @abstractmethod def _internal_create_program(self, *, @@ -199,7 +203,7 @@ def _internal_create_program(self, *, channel_mapping: Dict[ChannelID, Optional[ChannelID]], global_transformation: Optional[Transformation], to_single_waveform: Set[Union[str, 'PulseTemplate']], - parent_loop: Loop) -> None: + program_builder: ProgramBuilder) -> None: """The subclass specific implementation of create_program(). Receives a Loop instance parent_loop to which it should append measurements and its own Loops as children. @@ -220,36 +224,22 @@ def _create_program(self, *, channel_mapping: Dict[ChannelID, Optional[ChannelID]], global_transformation: Optional[Transformation], to_single_waveform: Set[Union[str, 'PulseTemplate']], - parent_loop: Loop): + program_builder: ProgramBuilder): """Generic part of create program. This method handles to_single_waveform and the configuration of the transformer.""" if self.identifier in to_single_waveform or self in to_single_waveform: - root = Loop() + with program_builder.new_subprogram() as inner_program_builder: - if not scope.get_volatile_parameters().keys().isdisjoint(self.parameter_names): - raise NotImplementedError('A pulse template that has volatile parameters cannot be transformed into a ' - 'single waveform yet.') - - self._internal_create_program(scope=scope, - measurement_mapping=measurement_mapping, - channel_mapping=channel_mapping, - global_transformation=None, - to_single_waveform=to_single_waveform, - parent_loop=root) + if not scope.get_volatile_parameters().keys().isdisjoint(self.parameter_names): + raise NotImplementedError('A pulse template that has volatile parameters cannot be transformed into a ' + 'single waveform yet.') - waveform = to_waveform(root) - - if global_transformation: - waveform = TransformingWaveform.from_transformation(waveform, global_transformation) - - # convert the nicely formatted measurement windows back into the old format again :( - measurements = root.get_measurement_windows() - measurement_window_list = [] - for measurement_name, (begins, lengths) in measurements.items(): - measurement_window_list.extend(zip(itertools.repeat(measurement_name), begins, lengths)) - - parent_loop.add_measurements(measurement_window_list) - parent_loop.append_child(waveform=waveform) + self._internal_create_program(scope=scope, + measurement_mapping=measurement_mapping, + channel_mapping=channel_mapping, + global_transformation=global_transformation, + to_single_waveform=to_single_waveform, + program_builder=inner_program_builder) else: self._internal_create_program(scope=scope, @@ -257,7 +247,7 @@ def _create_program(self, *, channel_mapping=channel_mapping, to_single_waveform=to_single_waveform, global_transformation=global_transformation, - parent_loop=parent_loop) + program_builder=program_builder) def with_parallel_channels(self, values: Mapping[ChannelID, ExpressionLike]) -> 'PulseTemplate': """Create a new pulse template that sets the given channels to the corresponding values. diff --git a/qupulse/pulses/repetition_pulse_template.py b/qupulse/pulses/repetition_pulse_template.py index 809a91ce0..1c54729f1 100644 --- a/qupulse/pulses/repetition_pulse_template.py +++ b/qupulse/pulses/repetition_pulse_template.py @@ -9,6 +9,7 @@ from qupulse.serialization import Serializer, PulseRegistryType from qupulse._program._loop import Loop, VolatileRepetitionCount +from qupulse.program import ProgramBuilder from qupulse.parameter_scope import Scope from qupulse.utils.types import ChannelID @@ -118,13 +119,11 @@ def _internal_create_program(self, *, channel_mapping: Dict[ChannelID, Optional[ChannelID]], global_transformation: Optional['Transformation'], to_single_waveform: AbstractSet[Union[str, 'PulseTemplate']], - parent_loop: Loop) -> None: + program_builder: ProgramBuilder) -> None: self.validate_scope(scope) repetition_count = max(0, self.get_repetition_count_value(scope)) - # todo (2018-07-19): could in some circumstances possibly just multiply subprogram repetition count? - # could be tricky if any repetition count is volatile ? check later and optimize if necessary if repetition_count > 0: if scope.get_volatile_parameters().keys() & self.repetition_count.variables: repetition_definition = VolatileRepetitionCount(self.repetition_count, scope) @@ -132,19 +131,17 @@ def _internal_create_program(self, *, else: repetition_definition = repetition_count - repj_loop = Loop(repetition_count=repetition_definition) - self.body._create_program(scope=scope, - measurement_mapping=measurement_mapping, - channel_mapping=channel_mapping, - global_transformation=global_transformation, - to_single_waveform=to_single_waveform, - parent_loop=repj_loop) - if repj_loop.waveform is not None or len(repj_loop.children) > 0: - measurements = self.get_measurement_windows(scope, measurement_mapping) - if measurements: - parent_loop.add_measurements(measurements) + measurements = self.get_measurement_windows(scope, measurement_mapping) - parent_loop.append_child(loop=repj_loop) + with program_builder.with_repetition(repetition_definition) as repetition_program_builder: + if measurements: + repetition_program_builder.measure(measurements) + self.body._create_program(scope=scope, + measurement_mapping=measurement_mapping, + channel_mapping=channel_mapping, + global_transformation=global_transformation, + to_single_waveform=to_single_waveform, + program_builder=repetition_program_builder) def get_serialization_data(self, serializer: Optional[Serializer]=None) -> Dict[str, Any]: data = super().get_serialization_data(serializer) diff --git a/qupulse/pulses/sequence_pulse_template.py b/qupulse/pulses/sequence_pulse_template.py index 1ef1bed0b..3abfa70f7 100644 --- a/qupulse/pulses/sequence_pulse_template.py +++ b/qupulse/pulses/sequence_pulse_template.py @@ -8,7 +8,7 @@ import warnings from qupulse.serialization import Serializer, PulseRegistryType -from qupulse._program._loop import Loop +from qupulse.program import ProgramBuilder from qupulse.parameter_scope import Scope from qupulse.utils import cached_property from qupulse.utils.types import MeasurementWindow, ChannelID, TimeType @@ -133,21 +133,20 @@ def _internal_create_program(self, *, channel_mapping: Dict[ChannelID, Optional[ChannelID]], global_transformation: Optional['Transformation'], to_single_waveform: Set[Union[str, 'PulseTemplate']], - parent_loop: Loop) -> None: + program_builder: ProgramBuilder) -> None: self.validate_scope(scope) - if self.duration.evaluate_in_scope(scope) > 0: - measurements = self.get_measurement_windows(scope, measurement_mapping) + measurements = self.get_measurement_windows(scope, measurement_mapping) + with program_builder.with_sequence() as sequence_program_builder: if measurements: - parent_loop.add_measurements(measurements) - + sequence_program_builder.measure(measurements) for subtemplate in self.subtemplates: subtemplate._create_program(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, global_transformation=global_transformation, to_single_waveform=to_single_waveform, - parent_loop=parent_loop) + program_builder=sequence_program_builder) def get_serialization_data(self, serializer: Optional[Serializer]=None) -> Dict[str, Any]: data = super().get_serialization_data(serializer) From f6a9ca4064c5f8bd60dd056222dea7699d14df4f Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Sun, 20 Aug 2023 12:12:25 +0200 Subject: [PATCH 080/441] Loop based program builder interface --- qupulse/program/__init__.py | 18 +++++++-- qupulse/program/loop.py | 80 +++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 qupulse/program/loop.py diff --git a/qupulse/program/__init__.py b/qupulse/program/__init__.py index 5e058b360..f6a313da0 100644 --- a/qupulse/program/__init__.py +++ b/qupulse/program/__init__.py @@ -1,6 +1,7 @@ +import contextlib from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import Optional, Union, Sequence, ContextManager, Mapping, Tuple, Generic, TypeVar +from typing import Optional, Union, Sequence, ContextManager, Mapping, Tuple, Generic, TypeVar, Iterable from numbers import Real import numpy as np @@ -8,6 +9,7 @@ from qupulse._program.waveforms import Waveform from qupulse.utils.types import MeasurementWindow, TimeType from qupulse._program.volatile import VolatileRepetitionCount +from qupulse.parameter_scope import Scope from typing import Protocol, runtime_checkable @@ -72,6 +74,10 @@ class ProgramBuilder(Protocol): """ + def inner_scope(self, scope: Scope) -> Scope: + """This function is necessary to inject program builder specific parameter implementations into the build + process.""" + def hold_voltage(self, duration: HardwareTime, voltages: Mapping[str, HardwareVoltage]): """Supports dynamic i.e. for loop generated offsets and duration""" @@ -83,7 +89,7 @@ def play_arbitrary_waveform(self, waveform: Waveform): def measure(self, measurements: Optional[Sequence[MeasurementWindow]]): """Add given measurements at the current position""" - def with_repetition(self, repetition_count: RepetitionCount) -> ContextManager['ProgramBuilder']: + def with_repetition(self, repetition_count: RepetitionCount) -> Iterable['ProgramBuilder']: """Measurements that are added to the new builder are dropped if the builder is empty upon exit""" def with_sequence(self) -> ContextManager['ProgramBuilder']: @@ -101,8 +107,14 @@ def new_subprogram(self) -> ContextManager['ProgramBuilder']: """Create a context managed program builder whose contents are translated into a single waveform upon exit if it is not empty.""" - def with_iteration(self, index_name: str, rng: range) -> Tuple[ContextManager['ProgramBuilder'], Any]: + def with_iteration(self, index_name: str, rng: range) -> Iterable['ProgramBuilder']: pass def to_program(self) -> Optional[Program]: """Further addition of new elements might fail after finalizing the program.""" + + +def iterate_context_managers(iterable): + for cm in iterable: + with cm as inner: + yield inner diff --git a/qupulse/program/loop.py b/qupulse/program/loop.py new file mode 100644 index 000000000..cbc9fbb6f --- /dev/null +++ b/qupulse/program/loop.py @@ -0,0 +1,80 @@ +from typing import * +from collections import defaultdict +from contextlib import contextmanager + +from ..utils.types import MeasurementWindow +from . import ProgramBuilder, RepetitionCount, HardwareTime, HardwareVoltage, iterate_context_managers +from .._program._loop import Loop, to_waveform +from .._program.waveforms import ConstantWaveform, Waveform +from ..pulses.range import RangeScope +from ..parameter_scope import Scope + + +class LoopBuilder(ProgramBuilder): + def __init__(self): + self._root = Loop() + self._top = self._root + + self._duration = 0 + self._stack: List[Tuple[Loop, Optional[Tuple[str, int]]]] = [(self._top, None)] + self._measurements = {} + + def inner_scope(self, scope: Scope) -> Scope: + local_vars = self._stack[-1][1] + if local_vars is None: + return scope + else: + return RangeScope(scope, *local_vars) + + def _push(self, loop, index): + self._top = loop + self._stack.append((loop, index)) + + def _pop(self): + stack = self._stack + loop, _ = stack.pop() + parent = self._top = self._stack[-1][0] + if loop.parent is None and (len(loop.children) != 0 or loop.waveform is not None): + parent.append_child(loop=loop) + + def measure(self, measurements: Optional[Sequence[MeasurementWindow]]): + self._top.add_measurements(measurements) + + def with_repetition(self, repetition_count: RepetitionCount) -> Iterable['ProgramBuilder']: + + new_top = Loop(repetition_count=repetition_count) + self._push(new_top, None) + + yield self + + self._pop() + + def with_iteration(self, index_name: str, rng: range) -> Iterable['ProgramBuilder']: + for value in rng: + loop = Loop() + self._push(loop, (index_name, value)) + yield self + self._pop() + + @contextmanager + def with_sequence(self) -> ContextManager['ProgramBuilder']: + loop = Loop() + self._push(loop, None) + yield self + self._pop() + + def hold_voltage(self, duration: HardwareTime, voltages: Mapping[str, HardwareVoltage]): + self.play_arbitrary_waveform(ConstantWaveform.from_mapping(duration=duration, amplitude=voltages)) + + def play_arbitrary_waveform(self, waveform: Waveform): + self._top.append_child(waveform=waveform) + + @contextmanager + def new_subprogram(self) -> ContextManager['ProgramBuilder']: + loop = Loop() + self._push(loop, None) + yield self + if loop.children or (loop.waveform and loop.repetition_count != 1): + loop._waveform = to_waveform(loop) + loop._repetition_definition = 1 + self._pop() From 33c7308d51069b555e6a8c335294884d853cdc26 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Sun, 20 Aug 2023 21:19:01 +0200 Subject: [PATCH 081/441] Make the first tests pass and include conditional measurements in control flow requests --- qupulse/program/__init__.py | 18 ++- qupulse/program/loop.py | 139 ++++++++++++------ qupulse/pulses/arithmetic_pulse_template.py | 4 +- qupulse/pulses/loop_pulse_template.py | 11 +- qupulse/pulses/mapping_pulse_template.py | 6 +- qupulse/pulses/pulse_template.py | 22 +-- qupulse/pulses/repetition_pulse_template.py | 7 +- qupulse/pulses/sequence_pulse_template.py | 4 +- .../pulses/arithmetic_pulse_template_tests.py | 8 +- tests/pulses/loop_pulse_template_tests.py | 58 ++++---- tests/pulses/pulse_template_tests.py | 17 ++- .../pulses/repetition_pulse_template_tests.py | 137 ++++++++--------- tests/pulses/sequencing_dummies.py | 15 +- 13 files changed, 257 insertions(+), 189 deletions(-) diff --git a/qupulse/program/__init__.py b/qupulse/program/__init__.py index f6a313da0..e5a5d36d0 100644 --- a/qupulse/program/__init__.py +++ b/qupulse/program/__init__.py @@ -87,12 +87,14 @@ def play_arbitrary_waveform(self, waveform: Waveform): """""" def measure(self, measurements: Optional[Sequence[MeasurementWindow]]): - """Add given measurements at the current position""" + """Unconditionally add given measurements relative to the current position.""" - def with_repetition(self, repetition_count: RepetitionCount) -> Iterable['ProgramBuilder']: + def with_repetition(self, repetition_count: RepetitionCount, + measurements: Optional[Sequence[MeasurementWindow]] = None) -> Iterable['ProgramBuilder']: """Measurements that are added to the new builder are dropped if the builder is empty upon exit""" - def with_sequence(self) -> ContextManager['ProgramBuilder']: + def with_sequence(self, + measurements: Optional[Sequence[MeasurementWindow]] = None) -> ContextManager['ProgramBuilder']: """ Measurements that are added in to the returned program builder are discarded if the sequence is empty on exit. @@ -107,14 +109,14 @@ def new_subprogram(self) -> ContextManager['ProgramBuilder']: """Create a context managed program builder whose contents are translated into a single waveform upon exit if it is not empty.""" - def with_iteration(self, index_name: str, rng: range) -> Iterable['ProgramBuilder']: + def with_iteration(self, index_name: str, rng: range, + measurements: Optional[Sequence[MeasurementWindow]] = None) -> Iterable['ProgramBuilder']: pass def to_program(self) -> Optional[Program]: """Further addition of new elements might fail after finalizing the program.""" -def iterate_context_managers(iterable): - for cm in iterable: - with cm as inner: - yield inner +def default_program_builder() -> ProgramBuilder: + from qupulse.program.loop import LoopBuilder + return LoopBuilder() diff --git a/qupulse/program/loop.py b/qupulse/program/loop.py index cbc9fbb6f..2abded03e 100644 --- a/qupulse/program/loop.py +++ b/qupulse/program/loop.py @@ -1,80 +1,137 @@ +import warnings from typing import * from collections import defaultdict +from dataclasses import dataclass from contextlib import contextmanager -from ..utils.types import MeasurementWindow -from . import ProgramBuilder, RepetitionCount, HardwareTime, HardwareVoltage, iterate_context_managers -from .._program._loop import Loop, to_waveform -from .._program.waveforms import ConstantWaveform, Waveform -from ..pulses.range import RangeScope -from ..parameter_scope import Scope +import dataclasses + +from qupulse.utils.types import MeasurementWindow +from qupulse.program import ProgramBuilder, RepetitionCount, HardwareTime, HardwareVoltage, TimeType +from qupulse._program._loop import Loop, to_waveform +from qupulse._program.waveforms import ConstantWaveform, Waveform +from qupulse.pulses.range import RangeScope +from qupulse.parameter_scope import Scope + + +@dataclass +class LoopGuard: + loop: Loop + measurements: Optional[List[MeasurementWindow]] + + def append_child(self, **kwargs): + if self.measurements: + self.loop.add_measurements(self.measurements) + self.measurements = None + self.loop.append_child(**kwargs) + + def add_measurements(self, measurements: List[MeasurementWindow]): + if self.measurements is None: + self.measurements = measurements + else: + self.measurements.extend(measurements) + + +@dataclass +class StackFrame: + loop: Union[Loop, LoopGuard] + + iterating: Optional[Tuple[str, int]] class LoopBuilder(ProgramBuilder): + """ + + Notes fduring implementation: + - This program builder does not use the Loop class to generate the measurements + + """ + def __init__(self): - self._root = Loop() - self._top = self._root + self._root: Loop = Loop() + self._top: Union[Loop, LoopGuard] = self._root - self._duration = 0 - self._stack: List[Tuple[Loop, Optional[Tuple[str, int]]]] = [(self._top, None)] - self._measurements = {} + self._stack: List[StackFrame] = [StackFrame(self._root, None)] def inner_scope(self, scope: Scope) -> Scope: - local_vars = self._stack[-1][1] + local_vars = self._stack[-1].iterating if local_vars is None: return scope else: return RangeScope(scope, *local_vars) - def _push(self, loop, index): - self._top = loop - self._stack.append((loop, index)) + def _push(self, stack_entry: StackFrame): + self._top = stack_entry.loop + self._stack.append(stack_entry) def _pop(self): stack = self._stack - loop, _ = stack.pop() - parent = self._top = self._stack[-1][0] - if loop.parent is None and (len(loop.children) != 0 or loop.waveform is not None): - parent.append_child(loop=loop) + stack.pop() + self._top = stack[-1].loop - def measure(self, measurements: Optional[Sequence[MeasurementWindow]]): - self._top.add_measurements(measurements) + def _try_append(self, loop, measurements): + if loop.waveform or len(loop) != 0: + if measurements is not None: + self._top.add_measurements(measurements) + self._top.append_child(loop=loop) - def with_repetition(self, repetition_count: RepetitionCount) -> Iterable['ProgramBuilder']: - - new_top = Loop(repetition_count=repetition_count) - self._push(new_top, None) + def measure(self, measurements: Optional[Sequence[MeasurementWindow]]): + if measurements: + self._top.add_measurements(measurements) + def with_repetition(self, repetition_count: RepetitionCount, + measurements: Optional[Sequence[MeasurementWindow]] = None) -> Iterable['ProgramBuilder']: + repetition_loop = Loop(repetition_count=repetition_count) + self._push(StackFrame(repetition_loop, None)) yield self - self._pop() + self._try_append(repetition_loop, measurements) - def with_iteration(self, index_name: str, rng: range) -> Iterable['ProgramBuilder']: + def with_iteration(self, index_name: str, rng: range, + measurements: Optional[Sequence[MeasurementWindow]] = None) -> Iterable['ProgramBuilder']: + top_frame = StackFrame(LoopGuard(self._top, measurements), None) + self._push(top_frame) for value in rng: - loop = Loop() - self._push(loop, (index_name, value)) + top_frame.iterating = (index_name, value) yield self - self._pop() + self._pop() @contextmanager - def with_sequence(self) -> ContextManager['ProgramBuilder']: - loop = Loop() - self._push(loop, None) + def with_sequence(self, measurements: Optional[Sequence[MeasurementWindow]] = None) -> ContextManager['ProgramBuilder']: + sequence_loop = Loop(measurements=measurements) + self._push(StackFrame(sequence_loop, None)) yield self self._pop() + self._try_append(sequence_loop, None) def hold_voltage(self, duration: HardwareTime, voltages: Mapping[str, HardwareVoltage]): - self.play_arbitrary_waveform(ConstantWaveform.from_mapping(duration=duration, amplitude=voltages)) + self.play_arbitrary_waveform(ConstantWaveform.from_mapping(duration, voltages)) def play_arbitrary_waveform(self, waveform: Waveform): self._top.append_child(waveform=waveform) @contextmanager def new_subprogram(self) -> ContextManager['ProgramBuilder']: - loop = Loop() - self._push(loop, None) - yield self - if loop.children or (loop.waveform and loop.repetition_count != 1): - loop._waveform = to_waveform(loop) - loop._repetition_definition = 1 - self._pop() + inner_builder = LoopBuilder() + yield inner_builder + inner_program = inner_builder.to_program() + + if inner_program is not None: + for name, (begins, lengths) in inner_program.get_measurement_windows().items(): + for begin, length in zip(begins, lengths): + self._top.add_measurements((name, begin, length)) + self.play_arbitrary_waveform(to_waveform(inner_program)) + + def to_program(self) -> Optional[Loop]: + if len(self._stack) != 1: + warnings.warn("Creating program with active build stack.") + if self._root.waveform or len(self._root.children) != 0: + return self._root + + @classmethod + def _testing_dummy(cls, stack): + builder = cls() + builder._stack = [StackFrame(loop, None) for loop in stack] + builder._root = builder._stack[0].loop + builder._top = builder._stack[-1].loop + return builder diff --git a/qupulse/pulses/arithmetic_pulse_template.py b/qupulse/pulses/arithmetic_pulse_template.py index dc9066f85..f04a7a163 100644 --- a/qupulse/pulses/arithmetic_pulse_template.py +++ b/qupulse/pulses/arithmetic_pulse_template.py @@ -382,7 +382,7 @@ def _internal_create_program(self, *, channel_mapping: Dict[ChannelID, Optional[ChannelID]], global_transformation: Optional[Transformation], to_single_waveform: Set[Union[str, 'PulseTemplate']], - parent_loop: 'Loop'): + program_builder: 'ProgramBuilder'): """The operation is applied by modifying the transformation the pulse template operand sees.""" if not scope.get_volatile_parameters().keys().isdisjoint(self._scalar_operand_parameters): raise NotImplementedError('The scalar operand of arithmetic pulse template cannot be volatile') @@ -398,7 +398,7 @@ def _internal_create_program(self, *, channel_mapping=channel_mapping, global_transformation=transformation, to_single_waveform=to_single_waveform, - parent_loop=parent_loop) + program_builder=program_builder) def build_waveform(self, parameters: Dict[str, Real], diff --git a/qupulse/pulses/loop_pulse_template.py b/qupulse/pulses/loop_pulse_template.py index 4465145ce..e122b82e2 100644 --- a/qupulse/pulses/loop_pulse_template.py +++ b/qupulse/pulses/loop_pulse_template.py @@ -153,17 +153,14 @@ def _internal_create_program(self, *, program_builder: ProgramBuilder) -> None: self.validate_scope(scope=scope) - measurements = self.get_measurement_windows(scope, measurement_mapping) loop_range = self._loop_range.to_range(scope) loop_index_name = self._loop_index - with program_builder.with_iteration(loop_index_name, loop_range) as iteration_program_builder: - iteration_program_builder.measure(measurements) - - # todo: create specialized scope? - inner_scope = MappedScope(scope, {loop_index_name: _ForLoopIndexValue(loop_index_name, loop_range)}) + measurements = self.get_measurement_windows(scope, measurement_mapping) - self.body._create_program(scope=inner_scope, + for iteration_program_builder in program_builder.with_iteration(loop_index_name, loop_range, + measurements=measurements): + self.body._create_program(scope=iteration_program_builder.inner_scope(scope), measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, global_transformation=global_transformation, diff --git a/qupulse/pulses/mapping_pulse_template.py b/qupulse/pulses/mapping_pulse_template.py index 9134da251..670cd285b 100644 --- a/qupulse/pulses/mapping_pulse_template.py +++ b/qupulse/pulses/mapping_pulse_template.py @@ -9,7 +9,7 @@ from qupulse.pulses.pulse_template import PulseTemplate, MappingTuple from qupulse.pulses.parameters import ParameterNotProvidedException, ParameterConstrainer from qupulse._program.waveforms import Waveform -from qupulse._program._loop import Loop +from qupulse.program import ProgramBuilder from qupulse.serialization import Serializer, PulseRegistryType __all__ = [ @@ -301,7 +301,7 @@ def _internal_create_program(self, *, channel_mapping: Dict[ChannelID, Optional[ChannelID]], global_transformation: Optional['Transformation'], to_single_waveform: Set[Union[str, 'PulseTemplate']], - parent_loop: Loop) -> None: + program_builder: ProgramBuilder) -> None: self.validate_scope(scope) # parameters are validated in map_parameters() call, no need to do it here again explicitly @@ -310,7 +310,7 @@ def _internal_create_program(self, *, channel_mapping=self.get_updated_channel_mapping(channel_mapping), global_transformation=global_transformation, to_single_waveform=to_single_waveform, - parent_loop=parent_loop) + program_builder=program_builder) def build_waveform(self, parameters: Dict[str, numbers.Real], diff --git a/qupulse/pulses/pulse_template.py b/qupulse/pulses/pulse_template.py index 05cecfb76..6f01c7350 100644 --- a/qupulse/pulses/pulse_template.py +++ b/qupulse/pulses/pulse_template.py @@ -26,7 +26,7 @@ from qupulse.pulses.measurement import MeasurementDefiner, MeasurementDeclaration from qupulse.parameter_scope import Scope, DictScope -from qupulse.program import ProgramBuilder +from qupulse.program import ProgramBuilder, default_program_builder __all__ = ["PulseTemplate", "AtomicPulseTemplate", "DoubleParameterNameException", "MappingTuple", "UnknownVolatileParameter"] @@ -120,7 +120,7 @@ def create_program(self, *, global_transformation: Optional[Transformation]=None, to_single_waveform: Set[Union[str, 'PulseTemplate']]=None, volatile: Union[Set[str], str] = None, - program_builder: ProgramBuilder) -> Optional['Loop']: + program_builder: ProgramBuilder = None) -> Optional['Loop']: """Translates this PulseTemplate into a program Loop. The returned Loop represents the PulseTemplate with all parameter values instantiated provided as dictated by @@ -152,6 +152,8 @@ def create_program(self, *, volatile = {volatile} else: volatile = set(volatile) + if program_builder is None: + program_builder = default_program_builder() # make sure all channels are mapped complete_channel_mapping = {channel: channel for channel in self.defined_channels} @@ -181,9 +183,6 @@ def create_program(self, *, category=UnknownVolatileParameter, stacklevel=2) - if program_builder is None: - program_builder = Loop() - # call subclass specific implementation self._create_program(scope=scope, measurement_mapping=measurement_mapping, @@ -192,8 +191,6 @@ def create_program(self, *, to_single_waveform=to_single_waveform, program_builder=program_builder) - if program_builder.waveform is None and len(program_builder.children) == 0: - return None # return None if no program return program_builder.to_program() @abstractmethod @@ -453,7 +450,7 @@ def _internal_create_program(self, *, channel_mapping: Dict[ChannelID, Optional[ChannelID]], global_transformation: Optional[Transformation], to_single_waveform: Set[Union[str, 'PulseTemplate']], - parent_loop: Loop) -> None: + program_builder: ProgramBuilder) -> None: """Parameter constraints are validated in build_waveform because build_waveform is guaranteed to be called during sequencing""" ### current behavior (same as previously): only adds EXEC Loop and measurements if a waveform exists. @@ -465,12 +462,17 @@ def _internal_create_program(self, *, if waveform: measurements = self.get_measurement_windows(parameters=scope, measurement_mapping=measurement_mapping) + program_builder.measure(measurements) if global_transformation: waveform = TransformingWaveform.from_transformation(waveform, global_transformation) - parent_loop.add_measurements(measurements=measurements) - parent_loop.append_child(waveform=waveform) + constant_values = waveform.constant_value_dict() + if constant_values is None: + program_builder.play_arbitrary_waveform(waveform) + else: + program_builder.hold_voltage(waveform.duration, constant_values) + @abstractmethod def build_waveform(self, diff --git a/qupulse/pulses/repetition_pulse_template.py b/qupulse/pulses/repetition_pulse_template.py index 1c54729f1..b40464bdd 100644 --- a/qupulse/pulses/repetition_pulse_template.py +++ b/qupulse/pulses/repetition_pulse_template.py @@ -133,10 +133,9 @@ def _internal_create_program(self, *, measurements = self.get_measurement_windows(scope, measurement_mapping) - with program_builder.with_repetition(repetition_definition) as repetition_program_builder: - if measurements: - repetition_program_builder.measure(measurements) - self.body._create_program(scope=scope, + for repetition_program_builder in program_builder.with_repetition(repetition_definition, + measurements=measurements): + self.body._create_program(scope=repetition_program_builder.inner_scope(scope), measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, global_transformation=global_transformation, diff --git a/qupulse/pulses/sequence_pulse_template.py b/qupulse/pulses/sequence_pulse_template.py index 3abfa70f7..bf4822a44 100644 --- a/qupulse/pulses/sequence_pulse_template.py +++ b/qupulse/pulses/sequence_pulse_template.py @@ -137,9 +137,7 @@ def _internal_create_program(self, *, self.validate_scope(scope) measurements = self.get_measurement_windows(scope, measurement_mapping) - with program_builder.with_sequence() as sequence_program_builder: - if measurements: - sequence_program_builder.measure(measurements) + with program_builder.with_sequence(measurements=measurements) as sequence_program_builder: for subtemplate in self.subtemplates: subtemplate._create_program(scope=scope, measurement_mapping=measurement_mapping, diff --git a/tests/pulses/arithmetic_pulse_template_tests.py b/tests/pulses/arithmetic_pulse_template_tests.py index b4d821684..66ff074c8 100644 --- a/tests/pulses/arithmetic_pulse_template_tests.py +++ b/tests/pulses/arithmetic_pulse_template_tests.py @@ -433,7 +433,7 @@ def test_internal_create_program(self): measurement_mapping = dict(m1='m2') global_transformation = OffsetTransformation({'unrelated': 1.}) to_single_waveform = {'something_else'} - parent_loop = mock.Mock() + program_builder = mock.Mock() expected_transformation = mock.Mock(spec=IdentityTransformation()) @@ -448,7 +448,7 @@ def test_internal_create_program(self): channel_mapping=channel_mapping, global_transformation=global_transformation, to_single_waveform=to_single_waveform, - parent_loop=parent_loop + program_builder=program_builder ) get_transformation.assert_called_once_with(parameters=scope, channel_mapping=channel_mapping) @@ -459,7 +459,7 @@ def test_internal_create_program(self): channel_mapping=channel_mapping, global_transformation=expected_transformation, to_single_waveform=to_single_waveform, - parent_loop=parent_loop + program_builder=program_builder ) with self.assertRaisesRegex(NotImplementedError, 'volatile'): @@ -469,7 +469,7 @@ def test_internal_create_program(self): channel_mapping=channel_mapping, global_transformation=global_transformation, to_single_waveform=to_single_waveform, - parent_loop=parent_loop + program_builder=program_builder ) def test_integral(self): diff --git a/tests/pulses/loop_pulse_template_tests.py b/tests/pulses/loop_pulse_template_tests.py index 0df6a2a04..91d85f7d6 100644 --- a/tests/pulses/loop_pulse_template_tests.py +++ b/tests/pulses/loop_pulse_template_tests.py @@ -10,7 +10,7 @@ from qupulse.pulses.parameters import InvalidParameterNameException, ParameterConstraintViolation,\ ParameterNotProvidedException, ParameterConstraint -from qupulse._program._loop import Loop +from qupulse.program.loop import LoopBuilder, Loop from tests.pulses.sequencing_dummies import DummyPulseTemplate, MeasurementWindowTestCase, DummyWaveform from tests.serialization_dummies import DummySerializer @@ -209,12 +209,13 @@ def test_create_program_constraint_on_loop_var_exception(self): # loop index not accessible in current build_sequence -> Exception children = [Loop(waveform=DummyWaveform(duration=2.0))] program = Loop(children=children) + program_builder = LoopBuilder._testing_dummy([program]) with self.assertRaises(ParameterNotProvidedException): flt._internal_create_program(scope=scope, measurement_mapping=dict(), channel_mapping=dict(), - parent_loop=program, + program_builder=program_builder, to_single_waveform=set(), global_transformation=None) self.assertEqual(children, list(program.children)) @@ -234,11 +235,13 @@ def test_create_program_invalid_params(self) -> None: children = [Loop(waveform=DummyWaveform(duration=2.0))] program = Loop(children=children) + program_builder = LoopBuilder._testing_dummy([program]) + with self.assertRaises(ParameterConstraintViolation): flt._internal_create_program(scope=invalid_scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, - parent_loop=program, + program_builder=program_builder, to_single_waveform=set(), global_transformation=None) @@ -259,11 +262,13 @@ def test_create_program_invalid_measurement_mapping(self) -> None: children = [Loop(waveform=DummyWaveform(duration=2.0))] program = Loop(children=children) + program_builder = LoopBuilder._testing_dummy([program]) + with self.assertRaises(KeyError): flt._internal_create_program(scope=invalid_scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, - parent_loop=program, + program_builder=program_builder, to_single_waveform=set(), global_transformation=None) @@ -278,12 +283,12 @@ def test_create_program_invalid_measurement_mapping(self) -> None: flt._internal_create_program(scope=invalid_scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, - parent_loop=program, + program_builder=program_builder, to_single_waveform=set(), global_transformation=None) def test_create_program_missing_params(self) -> None: - dt = DummyPulseTemplate(parameter_names={'i'}, waveform=DummyWaveform(duration=4.0), duration='t', measurements=[('b', 2, 1)]) + dt = DummyPulseTemplate(parameter_names={'i'}, waveform=DummyWaveform(duration=4.0), measurements=[('b', 2, 1)]) flt = ForLoopPulseTemplate(body=dt, loop_index='i', loop_range=('a', 'b', 'c'), measurements=[('A', 'alph', 1)], parameter_constraints=['c > 1']) @@ -293,13 +298,14 @@ def test_create_program_missing_params(self) -> None: children = [Loop(waveform=DummyWaveform(duration=2.0))] program = Loop(children=children) + program_builder = LoopBuilder._testing_dummy([program]) # test parameter in constraints with self.assertRaises(ParameterNotProvidedException): flt._internal_create_program(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, - parent_loop=program, + program_builder=program_builder, to_single_waveform=set(), global_transformation=None) @@ -309,17 +315,7 @@ def test_create_program_missing_params(self) -> None: flt._internal_create_program(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, - parent_loop=program, - to_single_waveform=set(), - global_transformation=None) - - # test parameter in duration - scope = DictScope.from_kwargs(a=1, b=4, c=2, alph=0) - with self.assertRaises(ParameterNotProvidedException): - flt._internal_create_program(scope=scope, - measurement_mapping=measurement_mapping, - channel_mapping=channel_mapping, - parent_loop=program, + program_builder=program_builder, to_single_waveform=set(), global_transformation=None) @@ -338,17 +334,17 @@ def test_create_program_body_none(self) -> None: measurement_mapping = dict(A='B', b='b') channel_mapping = dict(C='D') - program = Loop() + program_builder = LoopBuilder() + flt._internal_create_program(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, - parent_loop=program, + program_builder=program_builder, to_single_waveform=set(), global_transformation=None) - self.assertEqual(0, len(program.children)) - self.assertEqual(1, program.repetition_count) - self.assertEqual([], list(program.children)) + program = program_builder.to_program() + self.assertIsNone(program) def test_create_program(self) -> None: dt = DummyPulseTemplate(parameter_names={'i'}, @@ -365,7 +361,7 @@ def test_create_program(self) -> None: to_single_waveform = {'tom', 'jerry'} global_transformation = TransformationStub() - program = Loop() + program_builder = LoopBuilder() # inner _create_program does nothing expected_program = Loop(measurements=[('B', .1, 1)]) @@ -374,7 +370,7 @@ def test_create_program(self) -> None: channel_mapping=channel_mapping, global_transformation=global_transformation, to_single_waveform=to_single_waveform, - parent_loop=program) + program_builder=program_builder) expected_create_program_calls = [mock.call(**expected_create_program_kwargs, scope=_ForLoopScope(scope, 'i', i)) for i in (1, 3)] @@ -386,7 +382,7 @@ def test_create_program(self) -> None: flt._internal_create_program(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, - parent_loop=program, + program_builder=program_builder, to_single_waveform=to_single_waveform, global_transformation=global_transformation) @@ -394,7 +390,6 @@ def test_create_program(self) -> None: get_measurement_windows.assert_called_once_with(scope, measurement_mapping) self.assertEqual(body_create_program.call_args_list, expected_create_program_calls) - self.assertEqual(expected_program, program) def test_create_program_append(self) -> None: dt = DummyPulseTemplate(parameter_names={'i'}, waveform=DummyWaveform(duration=4.0), duration=4, @@ -408,12 +403,17 @@ def test_create_program_append(self) -> None: children = [Loop(waveform=DummyWaveform(duration=2.0))] program = Loop(children=children) + + program_builder = LoopBuilder._testing_dummy([program]) + flt._internal_create_program(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, - parent_loop=program, to_single_waveform=set(), - global_transformation=None) + global_transformation=None, + program_builder=program_builder) + + program_builder.to_program() self.assertEqual(3, len(program.children)) self.assertIs(children[0], program.children[0]) diff --git a/tests/pulses/pulse_template_tests.py b/tests/pulses/pulse_template_tests.py index 64e5451d6..1f5bd81bb 100644 --- a/tests/pulses/pulse_template_tests.py +++ b/tests/pulses/pulse_template_tests.py @@ -13,6 +13,7 @@ from qupulse.pulses.pulse_template import AtomicPulseTemplate, PulseTemplate, UnknownVolatileParameter from qupulse.pulses.multi_channel_pulse_template import MultiChannelWaveform from qupulse._program._loop import Loop +from qupulse.program import ProgramBuilder from qupulse._program.transformation import Transformation from qupulse._program.waveforms import TransformingWaveform @@ -20,6 +21,8 @@ from tests.pulses.sequencing_dummies import DummyWaveform from tests._program.transformation_tests import TransformationStub +from qupulse.program.loop import LoopBuilder + class PulseTemplateStub(PulseTemplate): """All abstract methods are stubs that raise NotImplementedError to catch unexpected calls. If a method is needed in @@ -95,11 +98,11 @@ def final_values(self) -> Dict[ChannelID, ExpressionScalar]: def get_appending_internal_create_program(waveform=DummyWaveform(), always_append=False, measurements: list=None): - def internal_create_program(*, scope, parent_loop: Loop, **_): + def internal_create_program(*, scope, program_builder: ProgramBuilder, **_): if always_append or 'append_a_child' in scope: if measurements is not None: - parent_loop.add_measurements(measurements=measurements) - parent_loop.append_child(waveform=waveform) + program_builder.measure(measurements=measurements) + program_builder.play_arbitrary_waveform(waveform=waveform) return internal_create_program @@ -196,7 +199,7 @@ def test__create_program(self): channel_mapping = {'B': 'A'} global_transformation = TransformationStub() to_single_waveform = {'voll', 'toggo'} - parent_loop = Loop() + program_builder = LoopBuilder() template = PulseTemplateStub() with mock.patch.object(template, '_internal_create_program') as _internal_create_program: @@ -205,7 +208,7 @@ def test__create_program(self): channel_mapping=channel_mapping, global_transformation=global_transformation, to_single_waveform=to_single_waveform, - parent_loop=parent_loop) + program_builder=program_builder) _internal_create_program.assert_called_once_with( scope=scope, @@ -213,9 +216,9 @@ def test__create_program(self): channel_mapping=channel_mapping, global_transformation=global_transformation, to_single_waveform=to_single_waveform, - parent_loop=parent_loop) + program_builder=program_builder) - self.assertEqual(parent_loop, Loop()) + self.assertEqual(program_builder.to_program(), Loop()) with self.assertRaisesRegex(NotImplementedError, "volatile"): template._parameter_names = {'c'} diff --git a/tests/pulses/repetition_pulse_template_tests.py b/tests/pulses/repetition_pulse_template_tests.py index b59cef820..ff399682d 100644 --- a/tests/pulses/repetition_pulse_template_tests.py +++ b/tests/pulses/repetition_pulse_template_tests.py @@ -5,7 +5,8 @@ from qupulse.parameter_scope import Scope, DictScope from qupulse.utils.types import FrozenDict -from qupulse._program._loop import Loop +from qupulse.program import default_program_builder +from qupulse.program.loop import Loop, LoopBuilder from qupulse.expressions import Expression, ExpressionScalar from qupulse.pulses import ConstantPT from qupulse.pulses.repetition_pulse_template import RepetitionPulseTemplate,ParameterNotIntegerException @@ -118,7 +119,7 @@ def test_internal_create_program(self): global_transformation = TransformationStub() to_single_waveform = {'to', 'single', 'waveform'} - program = Loop() + program_builder = LoopBuilder() expected_program = Loop(children=[Loop(children=[Loop(waveform=wf)], repetition_count=6)], measurements=[('l', .1, .2)]) @@ -134,7 +135,8 @@ def test_internal_create_program(self): channel_mapping=channel_mapping, global_transformation=global_transformation, to_single_waveform=to_single_waveform, - parent_loop=program) + program_builder=program_builder) + program = program_builder.to_program() self.assertEqual(program, expected_program) body_create_program.assert_called_once_with(scope=scope, @@ -142,7 +144,7 @@ def test_internal_create_program(self): channel_mapping=channel_mapping, global_transformation=global_transformation, to_single_waveform=to_single_waveform, - parent_loop=program.children[0]) + program_builder=program_builder) validate_scope.assert_called_once_with(scope) get_repetition_count_value.assert_called_once_with(scope) get_meas.assert_called_once_with(scope, measurement_mapping) @@ -154,20 +156,22 @@ def test_create_program_constant_success_measurements(self) -> None: scope = DictScope.from_mapping({'foo': 8}) measurement_mapping = {'my': 'thy', 'b': 'b'} channel_mapping = {} - program = Loop() + program_builder = LoopBuilder() t._internal_create_program(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, to_single_waveform=set(), global_transformation=None, - parent_loop=program) + program_builder=program_builder) + + program = program_builder.to_program() self.assertEqual(1, len(program.children)) internal_loop = program[0] # type: Loop self.assertEqual(repetitions, internal_loop.repetition_count) self.assertEqual(1, len(internal_loop)) - self.assertEqual((scope, measurement_mapping, channel_mapping, internal_loop), body.create_program_calls[-1]) + self.assertEqual((scope, measurement_mapping, channel_mapping, program_builder), body.create_program_calls[-1]) self.assertEqual(body.waveform, internal_loop[0].waveform) self.assert_measurement_windows_equal({'b': ([0, 2, 4], [1, 1, 1]), 'thy': ([2], [2])}, program.get_measurement_windows()) @@ -185,13 +189,14 @@ def test_create_program_declaration_success(self) -> None: scope = DictScope.from_kwargs(foo=3) measurement_mapping = dict(moth='fire') channel_mapping = dict(asd='f') - program = Loop() + program_builder = LoopBuilder() t._internal_create_program(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, to_single_waveform=set(), global_transformation=None, - parent_loop=program) + program_builder=program_builder) + program = program_builder.to_program() self.assertEqual(1, program.repetition_count) self.assertEqual(1, len(program.children)) @@ -199,7 +204,7 @@ def test_create_program_declaration_success(self) -> None: self.assertEqual(scope[repetitions], internal_loop.repetition_count) self.assertEqual(1, len(internal_loop)) - self.assertEqual((scope, measurement_mapping, channel_mapping, internal_loop), + self.assertEqual((scope, measurement_mapping, channel_mapping, program_builder), body.create_program_calls[-1]) self.assertEqual(body.waveform, internal_loop[0].waveform) @@ -219,13 +224,14 @@ def test_create_program_declaration_success_appended_measurements(self) -> None: channel_mapping = dict(asd='f') children = [Loop(waveform=DummyWaveform(duration=0))] program = Loop(children=children, measurements=[('a', [0], [1])], repetition_count=2) + program_builder = LoopBuilder._testing_dummy([program]) t._internal_create_program(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, to_single_waveform=set(), global_transformation=None, - parent_loop=program) + program_builder=program_builder) self.assertEqual(2, program.repetition_count) self.assertEqual(2, len(program.children)) @@ -234,7 +240,7 @@ def test_create_program_declaration_success_appended_measurements(self) -> None: self.assertEqual(scope[repetitions], internal_loop.repetition_count) self.assertEqual(1, len(internal_loop)) - self.assertEqual((scope, measurement_mapping, channel_mapping, internal_loop), body.create_program_calls[-1]) + self.assertEqual((scope, measurement_mapping, channel_mapping, program_builder), body.create_program_calls[-1]) self.assertEqual(body.waveform, internal_loop[0].waveform) self.assert_measurement_windows_equal({'fire': ([0, 6], [7.1, 7.1]), @@ -251,13 +257,14 @@ def test_create_program_declaration_success_measurements(self) -> None: scope = DictScope.from_kwargs(foo=3, meas_end=7.1) measurement_mapping = dict(moth='fire', b='b') channel_mapping = dict(asd='f') - program = Loop() + program_builder = LoopBuilder() t._internal_create_program(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, to_single_waveform=set(), global_transformation=None, - parent_loop=program) + program_builder=program_builder) + program = program_builder.to_program() self.assertEqual(1, program.repetition_count) self.assertEqual(1, len(program.children)) @@ -265,7 +272,7 @@ def test_create_program_declaration_success_measurements(self) -> None: self.assertEqual(scope[repetitions], internal_loop.repetition_count) self.assertEqual(1, len(internal_loop)) - self.assertEqual((scope, measurement_mapping, channel_mapping, internal_loop), body.create_program_calls[-1]) + self.assertEqual((scope, measurement_mapping, channel_mapping, program_builder), body.create_program_calls[-1]) self.assertEqual(body.waveform, internal_loop[0].waveform) self.assert_measurement_windows_equal({'fire': ([0], [7.1]), 'b': ([0, 2, 4], [1, 1, 1])}, program.get_measurement_windows()) @@ -281,13 +288,14 @@ def test_create_program_declaration_exceeds_bounds(self) -> None: children = [Loop(waveform=DummyWaveform(duration=0))] program = Loop(children=children) + program_builder = LoopBuilder._testing_dummy([program]) with self.assertRaises(ParameterConstraintViolation): t._internal_create_program(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, to_single_waveform=set(), global_transformation=None, - parent_loop=program) + program_builder=program_builder) self.assertFalse(body.create_program_calls) self.assertEqual(1, program.repetition_count) self.assertEqual(children, list(program.children)) @@ -303,21 +311,22 @@ def test_create_program_declaration_parameter_not_provided(self) -> None: channel_mapping = dict(asd='f') children = [Loop(waveform=DummyWaveform(duration=0))] program = Loop(children=children) + program_builder = LoopBuilder._testing_dummy([program]) with self.assertRaises(ParameterNotProvidedException): t._internal_create_program(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, - to_single_waveform=set(), - global_transformation=None, - parent_loop=program) + to_single_waveform=set(), + global_transformation=None, + program_builder=program_builder) with self.assertRaises(ParameterNotProvidedException): t._internal_create_program(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, - to_single_waveform=set(), - global_transformation=None, - parent_loop=program) + to_single_waveform=set(), + global_transformation=None, + program_builder=program_builder) self.assertFalse(body.create_program_calls) self.assertEqual(1, program.repetition_count) @@ -334,13 +343,15 @@ def test_create_program_declaration_parameter_value_not_whole(self) -> None: channel_mapping = dict(asd='f') children = [Loop(waveform=DummyWaveform(duration=0))] program = Loop(children=children) + program_builder = LoopBuilder._testing_dummy([program]) + with self.assertRaises(ParameterNotIntegerException): t._internal_create_program(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, - to_single_waveform=set(), - global_transformation=None, - parent_loop=program) + to_single_waveform=set(), + global_transformation=None, + program_builder=program_builder) self.assertFalse(body.create_program_calls) self.assertEqual(1, program.repetition_count) self.assertEqual(children, list(program.children)) @@ -356,13 +367,15 @@ def test_create_program_constant_measurement_mapping_failure(self) -> None: channel_mapping = dict(asd='f') children = [Loop(waveform=DummyWaveform(duration=0))] program = Loop(children=children) + program_builder = LoopBuilder._testing_dummy([program]) + with self.assertRaises(KeyError): t._internal_create_program(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, to_single_waveform=set(), global_transformation=None, - parent_loop=program) + program_builder=program_builder) # test for failure on child level measurement_mapping = dict(a='a') @@ -372,7 +385,7 @@ def test_create_program_constant_measurement_mapping_failure(self) -> None: channel_mapping=channel_mapping, to_single_waveform=set(), global_transformation=None, - parent_loop=program) + program_builder=program_builder) self.assertFalse(body.create_program_calls) self.assertEqual(1, program.repetition_count) self.assertEqual(children, list(program.children)) @@ -392,17 +405,16 @@ def test_create_program_rep_count_zero_constant(self) -> None: measurement_mapping = dict(moth='fire') channel_mapping = dict(asd='f') - program = Loop() + program_builder = LoopBuilder() t._internal_create_program(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, to_single_waveform=set(), global_transformation=None, - parent_loop=program) + program_builder=program_builder) + program = program_builder.to_program() self.assertFalse(body.create_program_calls) - self.assertFalse(program.children) - self.assertEqual(1, program.repetition_count) - self.assertEqual(None, program._measurements) + self.assertIsNone(program) def test_create_program_rep_count_zero_constant_with_measurement(self) -> None: repetitions = 0 @@ -417,17 +429,16 @@ def test_create_program_rep_count_zero_constant_with_measurement(self) -> None: measurement_mapping = dict(moth='fire') channel_mapping = dict(asd='f') - program = Loop() + program_builder = default_program_builder() t._internal_create_program(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, to_single_waveform=set(), global_transformation=None, - parent_loop=program) + program_builder=program_builder) + program = program_builder.to_program() self.assertFalse(body.create_program_calls) - self.assertFalse(program.children) - self.assertEqual(1, program.repetition_count) - self.assertEqual(None, program._measurements) + self.assertIsNone(program) def test_create_program_rep_count_zero_declaration(self) -> None: repetitions = "foo" @@ -442,17 +453,16 @@ def test_create_program_rep_count_zero_declaration(self) -> None: measurement_mapping = dict(moth='fire') channel_mapping = dict(asd='f') - program = Loop() + program_builder = default_program_builder() t._internal_create_program(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, to_single_waveform=set(), global_transformation=None, - parent_loop=program) + program_builder=program_builder) + program = program_builder.to_program() self.assertFalse(body.create_program_calls) - self.assertFalse(program.children) - self.assertEqual(1, program.repetition_count) - self.assertEqual(None, program._measurements) + self.assertIsNone(program) def test_create_program_rep_count_zero_declaration_with_measurement(self) -> None: repetitions = "foo" @@ -467,17 +477,16 @@ def test_create_program_rep_count_zero_declaration_with_measurement(self) -> Non measurement_mapping = dict(moth='fire') channel_mapping = dict(asd='f') - program = Loop() + program_builder = default_program_builder() t._internal_create_program(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, to_single_waveform=set(), global_transformation=None, - parent_loop=program) + program_builder=program_builder) + program = program_builder.to_program() self.assertFalse(body.create_program_calls) - self.assertFalse(program.children) - self.assertEqual(1, program.repetition_count) - self.assertEqual(None, program._measurements) + self.assertIsNone(program) def test_create_program_rep_count_neg_declaration(self) -> None: repetitions = "foo" @@ -492,17 +501,16 @@ def test_create_program_rep_count_neg_declaration(self) -> None: measurement_mapping = dict(moth='fire') channel_mapping = dict(asd='f') - program = Loop() + program_builder = default_program_builder() t._internal_create_program(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, to_single_waveform=set(), global_transformation=None, - parent_loop=program) + program_builder=program_builder) + program = program_builder.to_program() self.assertFalse(body.create_program_calls) - self.assertFalse(program.children) - self.assertEqual(1, program.repetition_count) - self.assertEqual(None, program._measurements) + self.assertIsNone(program) def test_create_program_rep_count_neg_declaration_with_measurements(self) -> None: repetitions = "foo" @@ -517,17 +525,16 @@ def test_create_program_rep_count_neg_declaration_with_measurements(self) -> Non measurement_mapping = dict(moth='fire') channel_mapping = dict(asd='f') - program = Loop() + program_builder = default_program_builder() t._internal_create_program(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, to_single_waveform=set(), global_transformation=None, - parent_loop=program) + program_builder=program_builder) + program = program_builder.to_program() self.assertFalse(body.create_program_calls) - self.assertFalse(program.children) - self.assertEqual(1, program.repetition_count) - self.assertEqual(None, program._measurements) + self.assertIsNone(program) def test_create_program_none_subprogram(self) -> None: repetitions = "foo" @@ -536,16 +543,14 @@ def test_create_program_none_subprogram(self) -> None: scope = DictScope.from_kwargs(foo=3) measurement_mapping = dict(moth='fire') channel_mapping = dict(asd='f') - program = Loop() + program_builder = default_program_builder() t._internal_create_program(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, to_single_waveform=set(), global_transformation=None, - parent_loop=program) - self.assertFalse(program.children) - self.assertEqual(1, program.repetition_count) - self.assertEqual(None, program._measurements) + program_builder=program_builder) + self.assertIsNone(program_builder.to_program()) def test_create_program_none_subprogram_with_measurement(self) -> None: repetitions = "foo" @@ -555,17 +560,15 @@ def test_create_program_none_subprogram_with_measurement(self) -> None: scope = DictScope.from_kwargs(foo=3, meas_end=7.1) measurement_mapping = dict(moth='fire', b='b') channel_mapping = dict(asd='f') - program = Loop() + program_builder = default_program_builder() t._internal_create_program(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, to_single_waveform=set(), global_transformation=None, - parent_loop=program) - self.assertFalse(program.children) - self.assertEqual(1, program.repetition_count) - self.assertEqual(None, program._measurements) + program_builder=program_builder) + self.assertIsNone(program_builder.to_program()) class RepetitionPulseTemplateSerializationTests(SerializableTests, unittest.TestCase): diff --git a/tests/pulses/sequencing_dummies.py b/tests/pulses/sequencing_dummies.py index 889aea575..8b49eccab 100644 --- a/tests/pulses/sequencing_dummies.py +++ b/tests/pulses/sequencing_dummies.py @@ -1,17 +1,21 @@ """STANDARD LIBRARY IMPORTS""" import numbers +import typing from typing import Tuple, List, Dict, Optional, Set, Any, Union, Mapping import copy import numpy import unittest +import qupulse.program.loop + """LOCAL IMPORTS""" from qupulse.parameter_scope import Scope from qupulse._program._loop import Loop from qupulse.utils.types import MeasurementWindow, ChannelID, TimeType, time_from_float from qupulse.serialization import Serializer from qupulse._program.waveforms import Waveform +from qupulse.program import ProgramBuilder from qupulse.pulses.pulse_template import AtomicPulseTemplate from qupulse.pulses.interpolation import InterpolationStrategy from qupulse.expressions import Expression, ExpressionScalar @@ -198,15 +202,18 @@ def _internal_create_program(self, *, channel_mapping: Dict[ChannelID, Optional[ChannelID]], global_transformation: Optional['Transformation'], to_single_waveform: Set[Union[str, 'PulseTemplate']], - parent_loop: Loop) -> None: + program_builder: ProgramBuilder) -> None: measurements = self.get_measurement_windows(scope, measurement_mapping) - self.create_program_calls.append((scope, measurement_mapping, channel_mapping, parent_loop)) + self.create_program_calls.append((scope, measurement_mapping, channel_mapping, program_builder)) if self._program: + program_builder = typing.cast(program_builder, qupulse.program.loop.LoopBuilder) + parent_loop = program_builder._top + parent_loop.add_measurements(measurements) parent_loop.append_child(waveform=self._program.waveform, children=self._program.children) elif self.waveform: - parent_loop.add_measurements(measurements) - parent_loop.append_child(waveform=self.waveform) + program_builder.measure(measurements) + program_builder.play_arbitrary_waveform(waveform=self.waveform) def build_waveform(self, parameters: Dict[str, numbers.Real], From b12195db8557062dfa32750cb8beae2b6dbe72df Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Sun, 20 Aug 2023 21:23:06 +0200 Subject: [PATCH 082/441] make mappingpt tests pass --- tests/pulses/mapping_pulse_template_tests.py | 31 +++++++++----------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/tests/pulses/mapping_pulse_template_tests.py b/tests/pulses/mapping_pulse_template_tests.py index 366360d97..5ebd6a960 100644 --- a/tests/pulses/mapping_pulse_template_tests.py +++ b/tests/pulses/mapping_pulse_template_tests.py @@ -8,6 +8,8 @@ AmbiguousMappingException, MappingCollisionException from qupulse.pulses.parameters import ParameterConstraintViolation, ParameterConstraint, ParameterNotProvidedException from qupulse.expressions import Expression +from qupulse.program import default_program_builder +from qupulse.program.loop import Loop, LoopBuilder from qupulse._program._loop import Loop from tests.pulses.sequencing_dummies import DummyPulseTemplate, MeasurementWindowTestCase, DummyWaveform @@ -140,9 +142,6 @@ def test_from_tuple_partial_mappings(self): measurement_mapping={'m1': 'n1'}, channel_mapping={'c1': 'd1'}) - - - def test_external_params(self): template = DummyPulseTemplate(parameter_names={'foo', 'bar'}) st = MappingPulseTemplate(template, parameter_mapping={'foo': 't*k', 'bar': 't*l'}) @@ -315,13 +314,13 @@ def test_create_program(self) -> None: pre_measurement_mapping = {'meas2': 'meas3'} pre_channel_mapping = {'default': 'A'} - program = Loop() + program_builder = default_program_builder() expected_inner_args = dict(scope=st.map_scope(pre_scope), measurement_mapping=st.get_updated_measurement_mapping(pre_measurement_mapping), channel_mapping=st.get_updated_channel_mapping(pre_channel_mapping), to_single_waveform=to_single_waveform, global_transformation=global_transformation, - parent_loop=program) + program_builder=program_builder) with mock.patch.object(template, '_create_program') as inner_create_program: st._internal_create_program(scope=pre_scope, @@ -329,11 +328,11 @@ def test_create_program(self) -> None: channel_mapping=pre_channel_mapping, to_single_waveform=to_single_waveform, global_transformation=global_transformation, - parent_loop=program) + program_builder=program_builder) inner_create_program.assert_called_once_with(**expected_inner_args) # as we mock the inner function there shouldnt be any changes - self.assertEqual(program, Loop()) + self.assertIsNone(program_builder.to_program()) def test_create_program_invalid_measurement_mapping(self) -> None: measurement_mapping = {'meas1': 'meas2'} @@ -352,14 +351,14 @@ def test_create_program_invalid_measurement_mapping(self) -> None: pre_measurement_mapping = {} pre_channel_mapping = {'default': 'A'} - program = Loop() + program_builder = default_program_builder() with self.assertRaises(KeyError): st._internal_create_program(scope=pre_scope, measurement_mapping=pre_measurement_mapping, channel_mapping=pre_channel_mapping, to_single_waveform=set(), global_transformation=None, - parent_loop=program) + program_builder=program_builder) def test_create_program_parameter_constraint_violation(self) -> None: measurement_mapping = {'meas1': 'meas2'} @@ -379,14 +378,14 @@ def test_create_program_parameter_constraint_violation(self) -> None: pre_measurement_mapping = {'meas2': 'meas3'} pre_channel_mapping = {'default': 'A'} - program = Loop() + program_builder = default_program_builder() with self.assertRaises(ParameterConstraintViolation): st._internal_create_program(scope=pre_scope, measurement_mapping=pre_measurement_mapping, channel_mapping=pre_channel_mapping, to_single_waveform=set(), global_transformation=None, - parent_loop=program) + program_builder=program_builder) def test_create_program_subtemplate_none(self) -> None: measurement_mapping = {'meas1': 'meas2'} @@ -406,24 +405,22 @@ def test_create_program_subtemplate_none(self) -> None: pre_measurement_mapping = {'meas2': 'meas3'} pre_channel_mapping = {'default': 'A'} - program = Loop() + program_builder = LoopBuilder() st._internal_create_program(scope=pre_scope, measurement_mapping=pre_measurement_mapping, channel_mapping=pre_channel_mapping, to_single_waveform=set(), global_transformation=None, - parent_loop=program) + program_builder=program_builder) self.assertEqual(1, len(template.create_program_calls)) self.assertEqual((st.map_scope(pre_scope), st.get_updated_measurement_mapping(pre_measurement_mapping), st.get_updated_channel_mapping(pre_channel_mapping), - program), + program_builder), template.create_program_calls[-1]) - self.assertEqual(1, program.repetition_count) - self.assertEqual(0, len(program.children)) - self.assertIsNone(program._measurements) + self.assertIsNone(program_builder.to_program()) def test_same_channel_error(self): From 625dcbfd454af2a10978169d066399a60eb202ce Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Sun, 20 Aug 2023 21:37:48 +0200 Subject: [PATCH 083/441] Make sequencept tests pass --- qupulse/program/loop.py | 16 ++-- tests/pulses/sequence_pulse_template_tests.py | 77 +++++++++---------- 2 files changed, 44 insertions(+), 49 deletions(-) diff --git a/qupulse/program/loop.py b/qupulse/program/loop.py index 2abded03e..e607d2743 100644 --- a/qupulse/program/loop.py +++ b/qupulse/program/loop.py @@ -89,20 +89,18 @@ def with_repetition(self, repetition_count: RepetitionCount, def with_iteration(self, index_name: str, rng: range, measurements: Optional[Sequence[MeasurementWindow]] = None) -> Iterable['ProgramBuilder']: - top_frame = StackFrame(LoopGuard(self._top, measurements), None) - self._push(top_frame) - for value in rng: - top_frame.iterating = (index_name, value) - yield self - self._pop() + with self.with_sequence(): + top_frame = self._stack[-1] + for value in rng: + top_frame.iterating = (index_name, value) + yield self @contextmanager def with_sequence(self, measurements: Optional[Sequence[MeasurementWindow]] = None) -> ContextManager['ProgramBuilder']: - sequence_loop = Loop(measurements=measurements) - self._push(StackFrame(sequence_loop, None)) + top_frame = StackFrame(LoopGuard(self._top, measurements), None) + self._push(top_frame) yield self self._pop() - self._try_append(sequence_loop, None) def hold_voltage(self, duration: HardwareTime, voltages: Mapping[str, HardwareVoltage]): self.play_arbitrary_waveform(ConstantWaveform.from_mapping(duration, voltages)) diff --git a/tests/pulses/sequence_pulse_template_tests.py b/tests/pulses/sequence_pulse_template_tests.py index 4985fb8de..8e6a128ce 100644 --- a/tests/pulses/sequence_pulse_template_tests.py +++ b/tests/pulses/sequence_pulse_template_tests.py @@ -8,6 +8,8 @@ from qupulse.pulses.mapping_pulse_template import MappingPulseTemplate from qupulse.pulses.parameters import ParameterConstraint, ParameterConstraintViolation, ParameterNotProvidedException from qupulse._program._loop import Loop +from qupulse.program import default_program_builder +from qupulse.program.loop import LoopBuilder from tests.pulses.sequencing_dummies import DummyPulseTemplate, DummyWaveform, MeasurementWindowTestCase from tests.serialization_dummies import DummySerializer @@ -237,7 +239,7 @@ def test_internal_create_program(self): global_transformation=TransformationStub(), to_single_waveform={'to', 'single', 'waveform'}) - program = Loop() + program_builder = LoopBuilder() expected_program = Loop(children=[Loop(waveform=wfs[0]), Loop(waveform=wfs[1])], @@ -251,14 +253,16 @@ def test_internal_create_program(self): mock.patch.object(sub_templates[1], '_create_program', wraps=get_appending_internal_create_program(wfs[1], True)) as create_1: - spt._internal_create_program(**kwargs, parent_loop=program) + spt._internal_create_program(**kwargs, program_builder=program_builder) + + program = program_builder.to_program() self.assertEqual(expected_program, program) validate_scope.assert_called_once_with(kwargs['scope']) get_measurement_windows.assert_called_once_with(kwargs['scope'], kwargs['measurement_mapping']) - create_0.assert_called_once_with(**kwargs, parent_loop=program) - create_1.assert_called_once_with(**kwargs, parent_loop=program) + create_0.assert_called_once_with(**kwargs, program_builder=program_builder) + create_1.assert_called_once_with(**kwargs, program_builder=program_builder) def test_create_program_internal(self) -> None: sub1 = DummyPulseTemplate(duration=3, waveform=DummyWaveform(duration=3), measurements=[('b', 1, 2)], defined_channels={'A'}) @@ -267,13 +271,15 @@ def test_create_program_internal(self) -> None: measurement_mapping = {'a': 'a', 'b': 'b'} channel_mapping = dict() seq = SequencePulseTemplate(sub1, sub2, measurements=[('a', 0, 1)]) - loop = Loop() + program_builder = LoopBuilder() + seq._internal_create_program(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, global_transformation=None, to_single_waveform=set(), - parent_loop=loop) + program_builder=program_builder) + loop = program_builder.to_program() self.assertEqual(1, loop.repetition_count) self.assertIsNone(loop.waveform) self.assertEqual([Loop(repetition_count=1, waveform=sub1.waveform), @@ -283,13 +289,14 @@ def test_create_program_internal(self) -> None: ### test again with inverted sequence seq = SequencePulseTemplate(sub2, sub1, measurements=[('a', 0, 1)]) - loop = Loop() + program_builder = LoopBuilder() seq._internal_create_program(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, global_transformation=None, to_single_waveform=set(), - parent_loop=loop) + program_builder=program_builder) + loop = program_builder.to_program() self.assertEqual(1, loop.repetition_count) self.assertIsNone(loop.waveform) self.assertEqual([Loop(repetition_count=1, waveform=sub2.waveform), @@ -304,6 +311,7 @@ def test_internal_create_program_no_measurement_mapping(self) -> None: seq = SequencePulseTemplate(sub1, sub2, measurements=[('a', 0, 1)]) children = [Loop(waveform=DummyWaveform())] loop = Loop(measurements=[], children=children) + program_builder = LoopBuilder._testing_dummy([loop]) with self.assertRaises(KeyError): seq._internal_create_program(scope=scope, @@ -311,9 +319,7 @@ def test_internal_create_program_no_measurement_mapping(self) -> None: channel_mapping=dict(), global_transformation=None, to_single_waveform=set(), - - parent_loop=loop) - + program_builder=program_builder) self.assertFalse(sub1.create_program_calls) self.assertFalse(sub2.create_program_calls) self.assertEqual(children, list(loop.children)) @@ -328,8 +334,7 @@ def test_internal_create_program_no_measurement_mapping(self) -> None: channel_mapping=dict(), global_transformation=None, to_single_waveform=set(), - - parent_loop=loop) + program_builder=program_builder) def test_internal_create_program_one_child_no_duration(self) -> None: sub1 = DummyPulseTemplate(duration=0, waveform=None, measurements=[('b', 1, 2)], defined_channels={'A'}) @@ -338,13 +343,14 @@ def test_internal_create_program_one_child_no_duration(self) -> None: measurement_mapping = {'a': 'a', 'b': 'b'} channel_mapping = dict() seq = SequencePulseTemplate(sub1, sub2, measurements=[('a', 0, 1)]) - loop = Loop() + program_builder = LoopBuilder() seq._internal_create_program(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, global_transformation=None, to_single_waveform=set(), - parent_loop=loop) + program_builder=program_builder) + loop = program_builder.to_program() self.assertEqual(1, loop.repetition_count) self.assertIsNone(loop.waveform) self.assertEqual([Loop(repetition_count=1, waveform=sub2.waveform)], @@ -357,13 +363,14 @@ def test_internal_create_program_one_child_no_duration(self) -> None: ### test again with inverted sequence seq = SequencePulseTemplate(sub2, sub1, measurements=[('a', 0, 1)]) - loop = Loop() + program_builder = LoopBuilder() seq._internal_create_program(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, global_transformation=None, to_single_waveform=set(), - parent_loop=loop) + program_builder=program_builder) + loop = program_builder.to_program() self.assertEqual(1, loop.repetition_count) self.assertIsNone(loop.waveform) self.assertEqual([Loop(repetition_count=1, waveform=sub2.waveform)], @@ -382,68 +389,58 @@ def test_internal_create_program_both_children_no_duration(self) -> None: channel_mapping = dict() seq = SequencePulseTemplate(sub1, sub2, measurements=[('a', 0, 1)]) - loop = Loop(measurements=None) + program_builder = default_program_builder() seq._internal_create_program(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, global_transformation=None, to_single_waveform=set(), - parent_loop=loop) - self.assertEqual(1, loop.repetition_count) - self.assertIsNone(loop.waveform) - self.assertEqual([], list(loop.children)) - self.assertIsNone(loop._measurements) + program_builder=program_builder) + self.assertIsNone(program_builder.to_program()) def test_internal_create_program_parameter_constraint_violations(self) -> None: sub1 = DummyPulseTemplate(duration=3, waveform=DummyWaveform(duration=3), measurements=[('b', 1, 2)]) sub2 = DummyPulseTemplate(duration=2, waveform=DummyWaveform(duration=2), parameter_names={'foo'}) scope = DictScope.from_kwargs(foo=7) seq = SequencePulseTemplate(sub1, sub2, measurements=[('a', 0, 1)], parameter_constraints={'foo < 2'}) - loop = Loop() + program_builder = default_program_builder() with self.assertRaises(ParameterConstraintViolation): seq._internal_create_program(scope=scope, measurement_mapping={'a': 'a', 'b': 'b'}, channel_mapping=dict(), global_transformation=None, to_single_waveform=set(), - - parent_loop=loop) + program_builder=program_builder) + self.assertIsNone(program_builder.to_program()) def test_internal_create_program_parameter_missing(self) -> None: sub1 = DummyPulseTemplate(duration=3, waveform=DummyWaveform(duration=3), measurements=[('b', 1, 2)]) - sub2 = DummyPulseTemplate(duration='d', waveform=DummyWaveform(duration=2), parameter_names={'foo'}) + sub2 = DummyPulseTemplate(duration=2, waveform=DummyWaveform(duration=2), parameter_names={'foo'}) seq = SequencePulseTemplate(sub1, sub2, measurements=[('a', 'bar', 1)], parameter_constraints={'foo < 2'}) - loop = Loop() # test parameter from constraints scope = DictScope.from_kwargs() + program_builder = default_program_builder() with self.assertRaises(ParameterNotProvidedException): seq._internal_create_program(scope=scope, measurement_mapping={'a': 'a', 'b': 'b'}, channel_mapping=dict(), global_transformation=None, to_single_waveform=set(), - parent_loop=loop) + program_builder=program_builder) + self.assertIsNone(program_builder.to_program()) # test parameter from measurements scope = DictScope.from_mapping({'foo': 1}) + program_builder = default_program_builder() with self.assertRaises(ParameterNotProvidedException): seq._internal_create_program(scope=scope, measurement_mapping={'a': 'a', 'b': 'b'}, channel_mapping=dict(), global_transformation=None, to_single_waveform=set(), - parent_loop=loop) - - # test parameter from duration - scope = DictScope.from_mapping({'foo': 1, 'bar': 0}) - with self.assertRaises(ParameterNotProvidedException): - seq._internal_create_program(scope=scope, - measurement_mapping={'a': 'a', 'b': 'b'}, - channel_mapping=dict(), - global_transformation=None, - to_single_waveform=set(), - parent_loop=loop) + program_builder=program_builder) + self.assertIsNone(program_builder.to_program()) class SequencePulseTemplateTestProperties(SequencePulseTemplateTest): From 86cbf4b6b58281722748a0429a5c6be0a23eed23 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 21 Aug 2023 08:43:54 +0200 Subject: [PATCH 084/441] Move loop to public program --- qupulse/{_program/_loop.py => program/loop.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename qupulse/{_program/_loop.py => program/loop.py} (100%) diff --git a/qupulse/_program/_loop.py b/qupulse/program/loop.py similarity index 100% rename from qupulse/_program/_loop.py rename to qupulse/program/loop.py From ffa2e43739e842cde9def61bac05b8af43947d4d Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 21 Aug 2023 08:44:24 +0200 Subject: [PATCH 085/441] Add backwards compatibility link to new location --- qupulse/_program/_loop.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 qupulse/_program/_loop.py diff --git a/qupulse/_program/_loop.py b/qupulse/_program/_loop.py new file mode 100644 index 000000000..b5ad53deb --- /dev/null +++ b/qupulse/_program/_loop.py @@ -0,0 +1,9 @@ +"""Backwards compatibility link to qupulse.program.loop""" + +from qupulse.program.loop import * + +import qupulse.program.loop + +__all__ = qupulse.program.loop.__all__ + +del qupulse From aa12727f0016724ef79c92243607c1c9c0412754 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 21 Aug 2023 08:56:37 +0200 Subject: [PATCH 086/441] Move waveforms to public program interface --- qupulse/{_program => program}/waveforms.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename qupulse/{_program => program}/waveforms.py (100%) diff --git a/qupulse/_program/waveforms.py b/qupulse/program/waveforms.py similarity index 100% rename from qupulse/_program/waveforms.py rename to qupulse/program/waveforms.py From 40c298dbc7aaa47137d4a2a523be8b3aa61f6b1e Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 21 Aug 2023 08:57:46 +0200 Subject: [PATCH 087/441] Add link from old to new waveform location --- qupulse/_program/waveforms.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 qupulse/_program/waveforms.py diff --git a/qupulse/_program/waveforms.py b/qupulse/_program/waveforms.py new file mode 100644 index 000000000..5038c3988 --- /dev/null +++ b/qupulse/_program/waveforms.py @@ -0,0 +1,9 @@ +"""Backwards compatibility link to qupulse.program.waveforms""" + +from qupulse.program.waveforms import * + +import qupulse.program.waveforms + +__all__ = qupulse.program.waveforms.__all__ + +del qupulse From 57d16962336920ffcc4ae0e8d89785b7ea5ec974 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 21 Aug 2023 09:02:10 +0200 Subject: [PATCH 088/441] Move volatile and transformation to public interface --- qupulse/{_program => program}/transformation.py | 0 qupulse/{_program => program}/volatile.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename qupulse/{_program => program}/transformation.py (100%) rename qupulse/{_program => program}/volatile.py (100%) diff --git a/qupulse/_program/transformation.py b/qupulse/program/transformation.py similarity index 100% rename from qupulse/_program/transformation.py rename to qupulse/program/transformation.py diff --git a/qupulse/_program/volatile.py b/qupulse/program/volatile.py similarity index 100% rename from qupulse/_program/volatile.py rename to qupulse/program/volatile.py From 64ddf0b606a97362a8f038855599c344dbf3afb3 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 21 Aug 2023 09:06:48 +0200 Subject: [PATCH 089/441] Add backward compatibility links --- qupulse/_program/transformation.py | 7 +++++++ qupulse/_program/volatile.py | 7 +++++++ 2 files changed, 14 insertions(+) create mode 100644 qupulse/_program/transformation.py create mode 100644 qupulse/_program/volatile.py diff --git a/qupulse/_program/transformation.py b/qupulse/_program/transformation.py new file mode 100644 index 000000000..c6a0d0def --- /dev/null +++ b/qupulse/_program/transformation.py @@ -0,0 +1,7 @@ +from qupulse.program.transformation import * + +import qupulse.program.transformation + +__all__ = qupulse.program.transformation.__all__ + +del qupulse diff --git a/qupulse/_program/volatile.py b/qupulse/_program/volatile.py new file mode 100644 index 000000000..ddfe2aa16 --- /dev/null +++ b/qupulse/_program/volatile.py @@ -0,0 +1,7 @@ +from qupulse.program.volatile import * + +import qupulse.program.volatile + +__all__ = qupulse.program.volatile.__all__ + +del qupulse From 09a30089df157f2f0d699f5a7a27add6e31571a8 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 21 Aug 2023 10:39:54 +0200 Subject: [PATCH 090/441] Change imports to use new location --- qupulse/_program/seqc.py | 6 +++--- qupulse/_program/tabor.py | 6 +++--- qupulse/hardware/awgs/base.py | 4 ++-- qupulse/hardware/awgs/tabor.py | 2 +- qupulse/hardware/awgs/tektronix.py | 4 ++-- qupulse/hardware/awgs/zihdawg.py | 2 +- .../feature_awg/channel_tuple_wrapper.py | 2 +- qupulse/hardware/feature_awg/features.py | 2 +- qupulse/hardware/feature_awg/tabor.py | 2 +- qupulse/hardware/setup.py | 2 +- qupulse/hardware/util.py | 2 +- qupulse/plotting.py | 6 +++--- qupulse/pulses/arithmetic_pulse_template.py | 4 ++-- qupulse/pulses/constant_pulse_template.py | 2 +- qupulse/pulses/function_pulse_template.py | 2 +- qupulse/pulses/loop_pulse_template.py | 4 ++-- qupulse/pulses/mapping_pulse_template.py | 4 ++-- qupulse/pulses/multi_channel_pulse_template.py | 4 ++-- qupulse/pulses/point_pulse_template.py | 2 +- qupulse/pulses/pulse_template.py | 6 +++--- qupulse/pulses/repetition_pulse_template.py | 3 ++- qupulse/pulses/sequence_pulse_template.py | 4 ++-- qupulse/pulses/table_pulse_template.py | 2 +- qupulse/pulses/time_reversal_pulse_template.py | 4 ++-- tests/_program/loop_tests.py | 17 ++++++++--------- tests/_program/seqc_tests.py | 2 +- tests/_program/tabor_tests.py | 2 +- tests/_program/transformation_tests.py | 6 +++--- tests/_program/waveforms_tests.py | 4 ++-- tests/hardware/base_tests.py | 2 +- .../feature_awg/awg_new_driver_base_tests.py | 2 +- tests/hardware/setup_tests.py | 2 +- tests/hardware/tabor_dummy_based_tests.py | 2 +- tests/hardware/tabor_tests.py | 2 +- tests/hardware/tektronix_tests.py | 2 +- tests/hardware/zihdawg_tests.py | 2 +- tests/pulses/bug_tests.py | 4 ++-- tests/pulses/constant_pulse_template_tests.py | 18 +++++++++--------- tests/pulses/loop_pulse_template_tests.py | 2 +- tests/pulses/mapping_pulse_template_tests.py | 2 +- tests/pulses/plotting_tests.py | 2 +- tests/pulses/pulse_template_tests.py | 2 +- .../pulses/repetition_pulse_template_tests.py | 2 +- tests/pulses/sequence_pulse_template_tests.py | 2 +- tests/pulses/sequencing_dummies.py | 2 +- tests/qctoolkit_alias_tests.py | 4 ++-- 46 files changed, 83 insertions(+), 83 deletions(-) diff --git a/qupulse/_program/seqc.py b/qupulse/_program/seqc.py index 2924268b4..989ffdc52 100644 --- a/qupulse/_program/seqc.py +++ b/qupulse/_program/seqc.py @@ -33,9 +33,9 @@ from qupulse.utils.types import ChannelID, TimeType from qupulse.utils import replace_multiple, grouper -from qupulse._program.waveforms import Waveform -from qupulse._program._loop import Loop -from qupulse._program.volatile import VolatileRepetitionCount, VolatileProperty +from qupulse.program.waveforms import Waveform +from qupulse.program.loop import Loop +from qupulse.program.volatile import VolatileRepetitionCount, VolatileProperty from qupulse.hardware.awgs.base import ProgramEntry from qupulse.hardware.util import zhinst_voltage_to_uint16 diff --git a/qupulse/_program/tabor.py b/qupulse/_program/tabor.py index 71ffe4390..b92c76f16 100644 --- a/qupulse/_program/tabor.py +++ b/qupulse/_program/tabor.py @@ -12,9 +12,9 @@ from qupulse.utils.types import ChannelID, TimeType from qupulse.hardware.awgs.base import ProgramEntry from qupulse.hardware.util import get_sample_times, voltage_to_uint16, find_positions -from qupulse._program.waveforms import Waveform -from qupulse._program._loop import Loop -from qupulse._program.volatile import VolatileRepetitionCount, VolatileProperty +from qupulse.program.waveforms import Waveform +from qupulse.program.loop import Loop +from qupulse.program.volatile import VolatileRepetitionCount, VolatileProperty assert(sys.byteorder == 'little') diff --git a/qupulse/hardware/awgs/base.py b/qupulse/hardware/awgs/base.py index 498788038..108230179 100644 --- a/qupulse/hardware/awgs/base.py +++ b/qupulse/hardware/awgs/base.py @@ -14,8 +14,8 @@ from qupulse.hardware.util import get_sample_times, not_none_indices from qupulse.utils.types import ChannelID -from qupulse._program._loop import Loop -from qupulse._program.waveforms import Waveform +from qupulse.program.loop import Loop +from qupulse.program.waveforms import Waveform from qupulse.comparable import Comparable from qupulse.utils.types import TimeType diff --git a/qupulse/hardware/awgs/tabor.py b/qupulse/hardware/awgs/tabor.py index d964a566c..90e03866e 100644 --- a/qupulse/hardware/awgs/tabor.py +++ b/qupulse/hardware/awgs/tabor.py @@ -11,7 +11,7 @@ import numpy as np from qupulse.utils.types import ChannelID -from qupulse._program._loop import Loop, make_compatible +from qupulse.program.loop import Loop, make_compatible from qupulse.hardware.util import voltage_to_uint16, traced from qupulse.hardware.awgs.base import AWG, AWGAmplitudeOffsetHandling from qupulse._program.tabor import TaborSegment, TaborException, TaborProgram, PlottableProgram, TaborSequencing,\ diff --git a/qupulse/hardware/awgs/tektronix.py b/qupulse/hardware/awgs/tektronix.py index 5865388e6..eaaa7a360 100644 --- a/qupulse/hardware/awgs/tektronix.py +++ b/qupulse/hardware/awgs/tektronix.py @@ -11,8 +11,8 @@ from qupulse.hardware.awgs.base import AWG, AWGAmplitudeOffsetHandling, ProgramOverwriteException from qupulse import ChannelID -from qupulse._program._loop import Loop, make_compatible -from qupulse._program.waveforms import Waveform as QuPulseWaveform +from qupulse.program.loop import Loop, make_compatible +from qupulse.program.waveforms import Waveform as QuPulseWaveform from qupulse.utils.types import TimeType from qupulse.hardware.util import voltage_to_uint16, get_sample_times, traced from qupulse.utils import pairwise diff --git a/qupulse/hardware/awgs/zihdawg.py b/qupulse/hardware/awgs/zihdawg.py index 6d6972a90..c6ec80f21 100644 --- a/qupulse/hardware/awgs/zihdawg.py +++ b/qupulse/hardware/awgs/zihdawg.py @@ -30,7 +30,7 @@ import time from qupulse.utils.types import ChannelID, TimeType, time_from_float -from qupulse._program._loop import Loop, make_compatible +from qupulse.program.loop import Loop, make_compatible from qupulse._program.seqc import HDAWGProgramManager, UserRegister, WaveformFileSystem from qupulse.hardware.awgs.base import AWG, ChannelNotFoundException, AWGAmplitudeOffsetHandling from qupulse.hardware.util import traced diff --git a/qupulse/hardware/feature_awg/channel_tuple_wrapper.py b/qupulse/hardware/feature_awg/channel_tuple_wrapper.py index 10212153b..a840ce9c9 100644 --- a/qupulse/hardware/feature_awg/channel_tuple_wrapper.py +++ b/qupulse/hardware/feature_awg/channel_tuple_wrapper.py @@ -1,7 +1,7 @@ from typing import Tuple, Optional, Callable, Set from qupulse import ChannelID -from qupulse._program._loop import Loop +from qupulse.program.loop import Loop from qupulse.hardware.feature_awg.base import AWGChannelTuple from qupulse.hardware.feature_awg.features import ProgramManagement, VolatileParameters from qupulse.hardware.awgs.base import AWG diff --git a/qupulse/hardware/feature_awg/features.py b/qupulse/hardware/feature_awg/features.py index 1b82189cc..29439f70a 100644 --- a/qupulse/hardware/feature_awg/features.py +++ b/qupulse/hardware/feature_awg/features.py @@ -3,7 +3,7 @@ from numbers import Real from enum import Enum -from qupulse._program._loop import Loop +from qupulse.program.loop import Loop from qupulse.hardware.feature_awg.base import AWGDeviceFeature, AWGChannelFeature, AWGChannelTupleFeature,\ AWGChannelTuple from qupulse.utils.types import ChannelID diff --git a/qupulse/hardware/feature_awg/tabor.py b/qupulse/hardware/feature_awg/tabor.py index 4a08ab8b1..554939719 100644 --- a/qupulse/hardware/feature_awg/tabor.py +++ b/qupulse/hardware/feature_awg/tabor.py @@ -11,7 +11,7 @@ import numpy as np from qupulse import ChannelID -from qupulse._program._loop import Loop, make_compatible +from qupulse.program.loop import Loop, make_compatible from qupulse.hardware.feature_awg.channel_tuple_wrapper import ChannelTupleAdapter from qupulse.hardware.feature_awg.features import ChannelSynchronization, AmplitudeOffsetHandling, VoltageRange, \ diff --git a/qupulse/hardware/setup.py b/qupulse/hardware/setup.py index 646815742..d034e3b26 100644 --- a/qupulse/hardware/setup.py +++ b/qupulse/hardware/setup.py @@ -5,7 +5,7 @@ from qupulse.hardware.awgs.base import AWG from qupulse.hardware.dacs import DAC -from qupulse._program._loop import Loop +from qupulse.program.loop import Loop from qupulse.utils.types import ChannelID diff --git a/qupulse/hardware/util.py b/qupulse/hardware/util.py index f2c60dbac..f7d173572 100644 --- a/qupulse/hardware/util.py +++ b/qupulse/hardware/util.py @@ -10,7 +10,7 @@ def traced(obj): """Noop traced that is used if autologging package is not available""" return obj -from qupulse._program.waveforms import Waveform +from qupulse.program.waveforms import Waveform from qupulse.utils.types import TimeType from qupulse.utils import pairwise diff --git a/qupulse/plotting.py b/qupulse/plotting.py index 06f4d0227..9810fb6ae 100644 --- a/qupulse/plotting.py +++ b/qupulse/plotting.py @@ -23,11 +23,11 @@ # was deprecated in matplotlib 3.7, but we keep it around to allow this code to work with older versions get_cmap = plt.get_cmap -from qupulse._program import waveforms +from qupulse.program import waveforms from qupulse.utils.types import ChannelID, MeasurementWindow, has_type_interface from qupulse.pulses.pulse_template import PulseTemplate -from qupulse._program.waveforms import Waveform -from qupulse._program._loop import Loop, to_waveform +from qupulse.program.waveforms import Waveform +from qupulse.program.loop import Loop, to_waveform __all__ = ["render", "plot", "PlottingNotPossibleException"] diff --git a/qupulse/pulses/arithmetic_pulse_template.py b/qupulse/pulses/arithmetic_pulse_template.py index dc9066f85..46b61df4c 100644 --- a/qupulse/pulses/arithmetic_pulse_template.py +++ b/qupulse/pulses/arithmetic_pulse_template.py @@ -14,8 +14,8 @@ from qupulse.utils.types import ChannelID from qupulse.pulses.measurement import MeasurementWindow from qupulse.pulses.pulse_template import AtomicPulseTemplate, PulseTemplate -from qupulse._program.waveforms import Waveform, ArithmeticWaveform, TransformingWaveform -from qupulse._program.transformation import Transformation, ScalingTransformation, OffsetTransformation,\ +from qupulse.program.waveforms import Waveform, ArithmeticWaveform, TransformingWaveform +from qupulse.program.transformation import Transformation, ScalingTransformation, OffsetTransformation,\ IdentityTransformation diff --git a/qupulse/pulses/constant_pulse_template.py b/qupulse/pulses/constant_pulse_template.py index 24ae16847..192ee82b1 100644 --- a/qupulse/pulses/constant_pulse_template.py +++ b/qupulse/pulses/constant_pulse_template.py @@ -10,7 +10,7 @@ import numbers from typing import Any, Dict, List, Optional, Union, Mapping, AbstractSet -from qupulse._program.waveforms import ConstantWaveform +from qupulse.program.waveforms import ConstantWaveform from qupulse.utils.types import TimeType, ChannelID from qupulse.utils import cached_property from qupulse.expressions import ExpressionScalar, ExpressionLike diff --git a/qupulse/pulses/function_pulse_template.py b/qupulse/pulses/function_pulse_template.py index fa02feaff..24d98fbe2 100644 --- a/qupulse/pulses/function_pulse_template.py +++ b/qupulse/pulses/function_pulse_template.py @@ -17,7 +17,7 @@ from qupulse.utils.types import ChannelID, TimeType, time_from_float from qupulse.pulses.parameters import ParameterConstrainer, ParameterConstraint from qupulse.pulses.pulse_template import AtomicPulseTemplate, MeasurementDeclaration -from qupulse._program.waveforms import FunctionWaveform +from qupulse.program.waveforms import FunctionWaveform __all__ = ["FunctionPulseTemplate"] diff --git a/qupulse/pulses/loop_pulse_template.py b/qupulse/pulses/loop_pulse_template.py index 101705ebc..72e8a6022 100644 --- a/qupulse/pulses/loop_pulse_template.py +++ b/qupulse/pulses/loop_pulse_template.py @@ -13,13 +13,13 @@ from qupulse.parameter_scope import Scope, MappedScope, DictScope from qupulse.utils.types import FrozenDict, FrozenMapping -from qupulse._program._loop import Loop +from qupulse.program.loop import Loop from qupulse.expressions import ExpressionScalar, ExpressionVariableMissingException, Expression from qupulse.utils import checked_int_cast, cached_property from qupulse.pulses.parameters import InvalidParameterNameException, ParameterConstrainer, ParameterNotProvidedException from qupulse.pulses.pulse_template import PulseTemplate, ChannelID, AtomicPulseTemplate -from qupulse._program.waveforms import SequenceWaveform as ForLoopWaveform +from qupulse.program.waveforms import SequenceWaveform as ForLoopWaveform from qupulse.pulses.measurement import MeasurementDefiner, MeasurementDeclaration from qupulse.pulses.range import ParametrizedRange, RangeScope diff --git a/qupulse/pulses/mapping_pulse_template.py b/qupulse/pulses/mapping_pulse_template.py index 9134da251..c39aa846e 100644 --- a/qupulse/pulses/mapping_pulse_template.py +++ b/qupulse/pulses/mapping_pulse_template.py @@ -8,8 +8,8 @@ from qupulse.parameter_scope import Scope, MappedScope from qupulse.pulses.pulse_template import PulseTemplate, MappingTuple from qupulse.pulses.parameters import ParameterNotProvidedException, ParameterConstrainer -from qupulse._program.waveforms import Waveform -from qupulse._program._loop import Loop +from qupulse.program.waveforms import Waveform +from qupulse.program.loop import Loop from qupulse.serialization import Serializer, PulseRegistryType __all__ = [ diff --git a/qupulse/pulses/multi_channel_pulse_template.py b/qupulse/pulses/multi_channel_pulse_template.py index beb12b68e..8b19d152a 100644 --- a/qupulse/pulses/multi_channel_pulse_template.py +++ b/qupulse/pulses/multi_channel_pulse_template.py @@ -17,8 +17,8 @@ from qupulse.utils import isclose from qupulse.utils.sympy import almost_equal, Sympifyable from qupulse.utils.types import ChannelID, TimeType -from qupulse._program.waveforms import MultiChannelWaveform, Waveform, TransformingWaveform -from qupulse._program.transformation import ParallelChannelTransformation, Transformation, chain_transformations +from qupulse.program.waveforms import MultiChannelWaveform, Waveform, TransformingWaveform +from qupulse.program.transformation import ParallelChannelTransformation, Transformation, chain_transformations from qupulse.pulses.pulse_template import PulseTemplate, AtomicPulseTemplate from qupulse.pulses.mapping_pulse_template import MappingPulseTemplate, MappingTuple from qupulse.pulses.parameters import ParameterConstrainer diff --git a/qupulse/pulses/point_pulse_template.py b/qupulse/pulses/point_pulse_template.py index b5c5cfa03..5972e4740 100644 --- a/qupulse/pulses/point_pulse_template.py +++ b/qupulse/pulses/point_pulse_template.py @@ -9,7 +9,7 @@ from qupulse.utils.sympy import IndexedBroadcast from qupulse.utils.types import ChannelID from qupulse.expressions import Expression, ExpressionScalar -from qupulse._program.waveforms import TableWaveform, TableWaveformEntry +from qupulse.program.waveforms import TableWaveform, TableWaveformEntry from qupulse.pulses.parameters import ParameterConstraint, ParameterConstrainer from qupulse.pulses.pulse_template import AtomicPulseTemplate, MeasurementDeclaration from qupulse.pulses.table_pulse_template import TableEntry, EntryInInit diff --git a/qupulse/pulses/pulse_template.py b/qupulse/pulses/pulse_template.py index c8b598e83..57f756e26 100644 --- a/qupulse/pulses/pulse_template.py +++ b/qupulse/pulses/pulse_template.py @@ -19,10 +19,10 @@ from qupulse.utils import forced_hash from qupulse.serialization import Serializable from qupulse.expressions import ExpressionScalar, Expression, ExpressionLike -from qupulse._program._loop import Loop, to_waveform -from qupulse._program.transformation import Transformation, IdentityTransformation, ChainedTransformation, chain_transformations +from qupulse.program.loop import Loop, to_waveform +from qupulse.program.transformation import Transformation, IdentityTransformation, ChainedTransformation, chain_transformations -from qupulse._program.waveforms import Waveform, TransformingWaveform +from qupulse.program.waveforms import Waveform, TransformingWaveform from qupulse.pulses.measurement import MeasurementDefiner, MeasurementDeclaration from qupulse.parameter_scope import Scope, DictScope diff --git a/qupulse/pulses/repetition_pulse_template.py b/qupulse/pulses/repetition_pulse_template.py index 809a91ce0..674e4cdd7 100644 --- a/qupulse/pulses/repetition_pulse_template.py +++ b/qupulse/pulses/repetition_pulse_template.py @@ -8,7 +8,8 @@ import numpy as np from qupulse.serialization import Serializer, PulseRegistryType -from qupulse._program._loop import Loop, VolatileRepetitionCount +from qupulse.program.loop import Loop +from qupulse.program.volatile import VolatileRepetitionCount from qupulse.parameter_scope import Scope from qupulse.utils.types import ChannelID diff --git a/qupulse/pulses/sequence_pulse_template.py b/qupulse/pulses/sequence_pulse_template.py index 1ef1bed0b..7a8eafeef 100644 --- a/qupulse/pulses/sequence_pulse_template.py +++ b/qupulse/pulses/sequence_pulse_template.py @@ -8,14 +8,14 @@ import warnings from qupulse.serialization import Serializer, PulseRegistryType -from qupulse._program._loop import Loop +from qupulse.program.loop import Loop from qupulse.parameter_scope import Scope from qupulse.utils import cached_property from qupulse.utils.types import MeasurementWindow, ChannelID, TimeType from qupulse.pulses.pulse_template import PulseTemplate, AtomicPulseTemplate from qupulse.pulses.parameters import ConstraintLike, ParameterConstrainer from qupulse.pulses.mapping_pulse_template import MappingPulseTemplate, MappingTuple -from qupulse._program.waveforms import SequenceWaveform +from qupulse.program.waveforms import SequenceWaveform from qupulse.pulses.measurement import MeasurementDeclaration, MeasurementDefiner from qupulse.expressions import Expression, ExpressionScalar diff --git a/qupulse/pulses/table_pulse_template.py b/qupulse/pulses/table_pulse_template.py index 873bbdf93..973536e81 100644 --- a/qupulse/pulses/table_pulse_template.py +++ b/qupulse/pulses/table_pulse_template.py @@ -23,7 +23,7 @@ from qupulse.pulses.pulse_template import AtomicPulseTemplate, MeasurementDeclaration from qupulse.pulses.interpolation import InterpolationStrategy, LinearInterpolationStrategy, \ HoldInterpolationStrategy, JumpInterpolationStrategy -from qupulse._program.waveforms import TableWaveform, TableWaveformEntry +from qupulse.program.waveforms import TableWaveform, TableWaveformEntry from qupulse.expressions import ExpressionScalar, Expression from qupulse.pulses.multi_channel_pulse_template import MultiChannelWaveform diff --git a/qupulse/pulses/time_reversal_pulse_template.py b/qupulse/pulses/time_reversal_pulse_template.py index a4758d1a7..35a1884be 100644 --- a/qupulse/pulses/time_reversal_pulse_template.py +++ b/qupulse/pulses/time_reversal_pulse_template.py @@ -1,8 +1,8 @@ from typing import Optional, Set, Dict, Union from qupulse import ChannelID -from qupulse._program._loop import Loop -from qupulse._program.waveforms import Waveform +from qupulse.program.loop import Loop +from qupulse.program.waveforms import Waveform from qupulse.serialization import PulseRegistryType from qupulse.expressions import ExpressionScalar diff --git a/tests/_program/loop_tests.py b/tests/_program/loop_tests.py index a3ff48aa3..55d4e465f 100644 --- a/tests/_program/loop_tests.py +++ b/tests/_program/loop_tests.py @@ -11,12 +11,11 @@ from qupulse.parameter_scope import DictScope from qupulse.utils.types import TimeType, time_from_float -from qupulse._program.volatile import VolatileRepetitionCount -from qupulse._program._loop import Loop, _make_compatible, _is_compatible, _CompatibilityLevel,\ +from qupulse.program.volatile import VolatileRepetitionCount +from qupulse.program.loop import Loop, _make_compatible, _is_compatible, _CompatibilityLevel,\ RepetitionWaveform, SequenceWaveform, make_compatible, MakeCompatibleWarning, DroppedMeasurementWarning,\ VolatileModificationWarning, roll_constant_waveforms -from qupulse._program._loop import Loop, _make_compatible, _is_compatible, _CompatibilityLevel,\ - RepetitionWaveform, SequenceWaveform, make_compatible, MakeCompatibleWarning, ConstantWaveform +from qupulse.program.waveforms import * from tests.pulses.sequencing_dummies import DummyWaveform from qupulse.pulses.multi_channel_pulse_template import MultiChannelWaveform @@ -488,27 +487,27 @@ def test_make_compatible(self): sample_rate=TimeType.from_float(1.)) priv_kwargs = dict(min_len=5, quantum=10, sample_rate=TimeType.from_float(1.)) - with mock.patch('qupulse._program._loop._is_compatible', + with mock.patch('qupulse.program.loop._is_compatible', return_value=_CompatibilityLevel.incompatible_too_short) as mocked: with self.assertRaisesRegex(ValueError, 'too short'): make_compatible(program, **pub_kwargs) mocked.assert_called_once_with(program, **priv_kwargs) - with mock.patch('qupulse._program._loop._is_compatible', + with mock.patch('qupulse.program.loop._is_compatible', return_value=_CompatibilityLevel.incompatible_fraction) as mocked: with self.assertRaisesRegex(ValueError, 'not an integer'): make_compatible(program, **pub_kwargs) mocked.assert_called_once_with(program, **priv_kwargs) - with mock.patch('qupulse._program._loop._is_compatible', + with mock.patch('qupulse.program.loop._is_compatible', return_value=_CompatibilityLevel.incompatible_quantum) as mocked: with self.assertRaisesRegex(ValueError, 'not a multiple of quantum'): make_compatible(program, **pub_kwargs) mocked.assert_called_once_with(program, **priv_kwargs) - with mock.patch('qupulse._program._loop._is_compatible', + with mock.patch('qupulse.program.loop._is_compatible', return_value=_CompatibilityLevel.action_required) as is_compat: - with mock.patch('qupulse._program._loop._make_compatible') as make_compat: + with mock.patch('qupulse.program.loop._make_compatible') as make_compat: make_compatible(program, **pub_kwargs) is_compat.assert_called_once_with(program, **priv_kwargs) diff --git a/tests/_program/seqc_tests.py b/tests/_program/seqc_tests.py index 729dacb1a..75644fdfd 100644 --- a/tests/_program/seqc_tests.py +++ b/tests/_program/seqc_tests.py @@ -13,7 +13,7 @@ from qupulse.expressions import ExpressionScalar from qupulse.parameter_scope import DictScope -from qupulse._program._loop import Loop +from qupulse.program.loop import Loop from qupulse._program.waveforms import ConstantWaveform from qupulse._program.seqc import BinaryWaveform, loop_to_seqc, WaveformPlayback, Repeat, SteppingRepeat, Scope,\ to_node_clusters, find_sharable_waveforms, mark_sharable_waveforms, UserRegisterManager, HDAWGProgramManager,\ diff --git a/tests/_program/tabor_tests.py b/tests/_program/tabor_tests.py index 9021d99a2..919147ec5 100644 --- a/tests/_program/tabor_tests.py +++ b/tests/_program/tabor_tests.py @@ -18,7 +18,7 @@ from qupulse._program.tabor import TaborException, TaborProgram, find_place_for_segments_in_memory,\ TaborSegment, TaborSequencing, PlottableProgram, TableDescription, make_combined_wave, TableEntry -from qupulse._program._loop import Loop +from qupulse.program.loop import Loop from qupulse._program.volatile import VolatileRepetitionCount from qupulse.hardware.util import voltage_to_uint16 from qupulse.utils.types import TimeType diff --git a/tests/_program/transformation_tests.py b/tests/_program/transformation_tests.py index 3f3664822..f9422f1f4 100644 --- a/tests/_program/transformation_tests.py +++ b/tests/_program/transformation_tests.py @@ -5,7 +5,7 @@ from qupulse.expressions import ExpressionScalar -from qupulse._program.transformation import LinearTransformation, Transformation, IdentityTransformation,\ +from qupulse.program.transformation import LinearTransformation, Transformation, IdentityTransformation,\ ChainedTransformation, ParallelChannelTransformation, chain_transformations, OffsetTransformation,\ ScalingTransformation @@ -43,7 +43,7 @@ def test_chain(self): self.assertIs(trafo.chain(IdentityTransformation()), trafo) - with mock.patch('qupulse._program.transformation.chain_transformations', + with mock.patch('qupulse.program.transformation.chain_transformations', return_value='asd') as chain_transformations: self.assertEqual(trafo.chain(trafo), 'asd') chain_transformations.assert_called_once_with(trafo, trafo) @@ -277,7 +277,7 @@ def test_chain(self): trafo = TransformationStub() chained = ChainedTransformation(*trafos) - with mock.patch('qupulse._program.transformation.chain_transformations', + with mock.patch('qupulse.program.transformation.chain_transformations', return_value='asd') as chain_transformations: self.assertEqual(chained.chain(trafo), 'asd') chain_transformations.assert_called_once_with(*trafos, trafo) diff --git a/tests/_program/waveforms_tests.py b/tests/_program/waveforms_tests.py index d84b2c763..c62fceb3a 100644 --- a/tests/_program/waveforms_tests.py +++ b/tests/_program/waveforms_tests.py @@ -7,10 +7,10 @@ from qupulse.utils.types import TimeType from qupulse.pulses.interpolation import HoldInterpolationStrategy, LinearInterpolationStrategy,\ JumpInterpolationStrategy -from qupulse._program.waveforms import MultiChannelWaveform, RepetitionWaveform, SequenceWaveform,\ +from qupulse.program.waveforms import MultiChannelWaveform, RepetitionWaveform, SequenceWaveform,\ TableWaveformEntry, TableWaveform, TransformingWaveform, SubsetWaveform, ArithmeticWaveform, ConstantWaveform,\ Waveform, FunctorWaveform, FunctionWaveform, ReversedWaveform -from qupulse._program.transformation import LinearTransformation +from qupulse.program.transformation import LinearTransformation from qupulse.expressions import ExpressionScalar, Expression from tests.pulses.sequencing_dummies import DummyWaveform, DummyInterpolationStrategy diff --git a/tests/hardware/base_tests.py b/tests/hardware/base_tests.py index a3f029618..db6585737 100644 --- a/tests/hardware/base_tests.py +++ b/tests/hardware/base_tests.py @@ -5,7 +5,7 @@ import numpy as np from qupulse.utils.types import TimeType -from qupulse._program._loop import Loop +from qupulse.program.loop import Loop from qupulse.hardware.awgs.base import ProgramEntry from tests.pulses.sequencing_dummies import DummyWaveform diff --git a/tests/hardware/feature_awg/awg_new_driver_base_tests.py b/tests/hardware/feature_awg/awg_new_driver_base_tests.py index 12070e1c1..1845ab1e0 100644 --- a/tests/hardware/feature_awg/awg_new_driver_base_tests.py +++ b/tests/hardware/feature_awg/awg_new_driver_base_tests.py @@ -3,7 +3,7 @@ import warnings from qupulse import ChannelID -from qupulse._program._loop import Loop +from qupulse.program.loop import Loop from qupulse.hardware.feature_awg import channel_tuple_wrapper from qupulse.hardware.feature_awg.base import AWGDevice, AWGChannel, AWGChannelTuple, AWGMarkerChannel from qupulse.hardware.feature_awg.features import ChannelSynchronization, ProgramManagement, VoltageRange, \ diff --git a/tests/hardware/setup_tests.py b/tests/hardware/setup_tests.py index d36467e23..fd3ba3c17 100644 --- a/tests/hardware/setup_tests.py +++ b/tests/hardware/setup_tests.py @@ -4,7 +4,7 @@ import numpy as np from qupulse.hardware.setup import HardwareSetup, PlaybackChannel, MarkerChannel, MeasurementMask -from qupulse._program._loop import Loop +from qupulse.program.loop import Loop from tests.pulses.sequencing_dummies import DummyWaveform diff --git a/tests/hardware/tabor_dummy_based_tests.py b/tests/hardware/tabor_dummy_based_tests.py index 7a49e0b64..98588b5ed 100644 --- a/tests/hardware/tabor_dummy_based_tests.py +++ b/tests/hardware/tabor_dummy_based_tests.py @@ -10,7 +10,7 @@ from qupulse.hardware.awgs.base import AWGAmplitudeOffsetHandling from qupulse.utils.types import TimeType -from qupulse._program._loop import Loop +from qupulse.program.loop import Loop from qupulse._program.tabor import TableDescription, TimeType, TableEntry, TaborSegment, TaborProgram,\ make_combined_wave, TaborSequencing from qupulse._program.waveforms import ConstantWaveform diff --git a/tests/hardware/tabor_tests.py b/tests/hardware/tabor_tests.py index 6226b2c63..e9bf6e5bf 100644 --- a/tests/hardware/tabor_tests.py +++ b/tests/hardware/tabor_tests.py @@ -9,7 +9,7 @@ from qupulse.hardware.awgs.tabor import TaborException, TaborProgram, \ TaborSegment, TaborSequencing, with_configuration_guard, PlottableProgram -from qupulse._program._loop import Loop +from qupulse.program.loop import Loop from qupulse.hardware.util import voltage_to_uint16 from tests.pulses.sequencing_dummies import DummyWaveform diff --git a/tests/hardware/tektronix_tests.py b/tests/hardware/tektronix_tests.py index 773876e2e..aed67d79e 100644 --- a/tests/hardware/tektronix_tests.py +++ b/tests/hardware/tektronix_tests.py @@ -13,7 +13,7 @@ from qupulse.hardware.awgs.tektronix import TektronixAWG, TektronixProgram, parse_program, _make_binary_waveform,\ voltage_to_uint16, WaveformEntry, WaveformStorage -from qupulse._program._loop import Loop +from qupulse.program.loop import Loop from qupulse.utils.types import TimeType from tests.pulses.sequencing_dummies import DummyWaveform from qupulse._program.waveforms import MultiChannelWaveform diff --git a/tests/hardware/zihdawg_tests.py b/tests/hardware/zihdawg_tests.py index 9fe675eab..2cb6bbafe 100644 --- a/tests/hardware/zihdawg_tests.py +++ b/tests/hardware/zihdawg_tests.py @@ -26,7 +26,7 @@ raise unittest.SkipTest("zhinst not present") from err from qupulse.utils.types import TimeType -from qupulse._program._loop import Loop +from qupulse.program.loop import Loop from tests.pulses.sequencing_dummies import DummyWaveform from qupulse.hardware.awgs.zihdawg import HDAWGChannelGroup, HDAWGRepresentation, HDAWGValueError, UserRegister,\ ELFManager, HDAWGChannelGrouping, SingleDeviceChannelGroup diff --git a/tests/pulses/bug_tests.py b/tests/pulses/bug_tests.py index 6be11fcf2..44fd49677 100644 --- a/tests/pulses/bug_tests.py +++ b/tests/pulses/bug_tests.py @@ -13,7 +13,7 @@ from qupulse.plotting import plot -from qupulse._program._loop import to_waveform +from qupulse.program.loop import to_waveform from qupulse.utils import isclose class BugTests(unittest.TestCase): @@ -77,7 +77,7 @@ def test_issue_584_uninitialized_table_sample(self): """issue 584""" d = 598.3333333333334 - 480 tpt = TablePulseTemplate(entries={'P': [(0, 1.0, 'hold'), (d, 1.0, 'hold')]}) - with mock.patch('qupulse._program.waveforms.PULSE_TO_WAVEFORM_ERROR', 1e-6): + with mock.patch('qupulse.program.waveforms.PULSE_TO_WAVEFORM_ERROR', 1e-6): wf = to_waveform(tpt.create_program()) self.assertTrue(isclose(d, wf.duration, abs_tol=1e-6)) diff --git a/tests/pulses/constant_pulse_template_tests.py b/tests/pulses/constant_pulse_template_tests.py index f0e374869..197aa1017 100644 --- a/tests/pulses/constant_pulse_template_tests.py +++ b/tests/pulses/constant_pulse_template_tests.py @@ -1,14 +1,14 @@ import unittest import qupulse.plotting -import qupulse._program.waveforms +import qupulse.program.waveforms import qupulse.utils.sympy from qupulse.pulses import TablePT, FunctionPT, AtomicMultiChannelPT, MappingPT from qupulse.pulses.multi_channel_pulse_template import AtomicMultiChannelPulseTemplate from qupulse.plotting import plot from qupulse.pulses.sequence_pulse_template import SequencePulseTemplate -from qupulse._program._loop import make_compatible -from qupulse._program.waveforms import ConstantWaveform +from qupulse.program.loop import make_compatible +from qupulse.program.waveforms import ConstantWaveform from qupulse.serialization import DictBackend, PulseStorage from qupulse.pulses.constant_pulse_template import ConstantPulseTemplate, ExpressionScalar, TimeType @@ -44,10 +44,10 @@ def test_zero_duration(self): self.assertEqual(pulse.duration, 12) def test_regression_duration_conversion(self): - old_value = qupulse._program.waveforms.PULSE_TO_WAVEFORM_ERROR + old_value = qupulse.program.waveforms.PULSE_TO_WAVEFORM_ERROR try: - qupulse._program.waveforms.PULSE_TO_WAVEFORM_ERROR = 1e-6 + qupulse.program.waveforms.PULSE_TO_WAVEFORM_ERROR = 1e-6 for duration_in_samples in [64, 936320, 24615392]: p = ConstantPulseTemplate(duration_in_samples / 2.4, {'a': 0}) number_of_samples = p.create_program().duration * 2.4 @@ -57,19 +57,19 @@ def test_regression_duration_conversion(self): p2 = ConstantPulseTemplate((duration_in_samples + 1) / 2.4, {'a': 0}) self.assertNotEqual(p.create_program().duration, p2.create_program().duration) finally: - qupulse._program.waveforms.PULSE_TO_WAVEFORM_ERROR = old_value + qupulse.program.waveforms.PULSE_TO_WAVEFORM_ERROR = old_value def test_regression_duration_conversion_functionpt(self): - old_value = qupulse._program.waveforms.PULSE_TO_WAVEFORM_ERROR + old_value = qupulse.program.waveforms.PULSE_TO_WAVEFORM_ERROR try: - qupulse._program.waveforms.PULSE_TO_WAVEFORM_ERROR = 1e-6 + qupulse.program.waveforms.PULSE_TO_WAVEFORM_ERROR = 1e-6 for duration_in_samples in [64, 2000, 936320]: p = FunctionPT('1', duration_expression=duration_in_samples / 2.4, channel='a') number_of_samples = p.create_program().duration * 2.4 self.assertEqual(number_of_samples.denominator, 1) finally: - qupulse._program.waveforms.PULSE_TO_WAVEFORM_ERROR = old_value + qupulse.program.waveforms.PULSE_TO_WAVEFORM_ERROR = old_value def test_regression_template_combination(self): old_value = qupulse.utils.sympy.SYMPY_DURATION_ERROR_MARGIN diff --git a/tests/pulses/loop_pulse_template_tests.py b/tests/pulses/loop_pulse_template_tests.py index 0df6a2a04..e5380607f 100644 --- a/tests/pulses/loop_pulse_template_tests.py +++ b/tests/pulses/loop_pulse_template_tests.py @@ -10,7 +10,7 @@ from qupulse.pulses.parameters import InvalidParameterNameException, ParameterConstraintViolation,\ ParameterNotProvidedException, ParameterConstraint -from qupulse._program._loop import Loop +from qupulse.program.loop import Loop from tests.pulses.sequencing_dummies import DummyPulseTemplate, MeasurementWindowTestCase, DummyWaveform from tests.serialization_dummies import DummySerializer diff --git a/tests/pulses/mapping_pulse_template_tests.py b/tests/pulses/mapping_pulse_template_tests.py index 366360d97..b3da11749 100644 --- a/tests/pulses/mapping_pulse_template_tests.py +++ b/tests/pulses/mapping_pulse_template_tests.py @@ -8,7 +8,7 @@ AmbiguousMappingException, MappingCollisionException from qupulse.pulses.parameters import ParameterConstraintViolation, ParameterConstraint, ParameterNotProvidedException from qupulse.expressions import Expression -from qupulse._program._loop import Loop +from qupulse.program.loop import Loop from tests.pulses.sequencing_dummies import DummyPulseTemplate, MeasurementWindowTestCase, DummyWaveform from tests.serialization_tests import SerializableTests diff --git a/tests/pulses/plotting_tests.py b/tests/pulses/plotting_tests.py index 0e96a2c90..fdceac210 100644 --- a/tests/pulses/plotting_tests.py +++ b/tests/pulses/plotting_tests.py @@ -9,7 +9,7 @@ from qupulse.plotting import PlottingNotPossibleException, render, plot from qupulse.pulses.table_pulse_template import TablePulseTemplate from qupulse.pulses.sequence_pulse_template import SequencePulseTemplate -from qupulse._program._loop import Loop +from qupulse.program.loop import Loop from tests.pulses.sequencing_dummies import DummyWaveform, DummyPulseTemplate diff --git a/tests/pulses/pulse_template_tests.py b/tests/pulses/pulse_template_tests.py index 64e5451d6..f8c0e241a 100644 --- a/tests/pulses/pulse_template_tests.py +++ b/tests/pulses/pulse_template_tests.py @@ -12,7 +12,7 @@ TimeReversalPT, AtomicMultiChannelPT from qupulse.pulses.pulse_template import AtomicPulseTemplate, PulseTemplate, UnknownVolatileParameter from qupulse.pulses.multi_channel_pulse_template import MultiChannelWaveform -from qupulse._program._loop import Loop +from qupulse.program.loop import Loop from qupulse._program.transformation import Transformation from qupulse._program.waveforms import TransformingWaveform diff --git a/tests/pulses/repetition_pulse_template_tests.py b/tests/pulses/repetition_pulse_template_tests.py index b59cef820..5f83fc9da 100644 --- a/tests/pulses/repetition_pulse_template_tests.py +++ b/tests/pulses/repetition_pulse_template_tests.py @@ -5,7 +5,7 @@ from qupulse.parameter_scope import Scope, DictScope from qupulse.utils.types import FrozenDict -from qupulse._program._loop import Loop +from qupulse.program.loop import Loop from qupulse.expressions import Expression, ExpressionScalar from qupulse.pulses import ConstantPT from qupulse.pulses.repetition_pulse_template import RepetitionPulseTemplate,ParameterNotIntegerException diff --git a/tests/pulses/sequence_pulse_template_tests.py b/tests/pulses/sequence_pulse_template_tests.py index 4985fb8de..3a625eb3a 100644 --- a/tests/pulses/sequence_pulse_template_tests.py +++ b/tests/pulses/sequence_pulse_template_tests.py @@ -7,7 +7,7 @@ from qupulse.pulses.sequence_pulse_template import SequencePulseTemplate, SequenceWaveform from qupulse.pulses.mapping_pulse_template import MappingPulseTemplate from qupulse.pulses.parameters import ParameterConstraint, ParameterConstraintViolation, ParameterNotProvidedException -from qupulse._program._loop import Loop +from qupulse.program.loop import Loop from tests.pulses.sequencing_dummies import DummyPulseTemplate, DummyWaveform, MeasurementWindowTestCase from tests.serialization_dummies import DummySerializer diff --git a/tests/pulses/sequencing_dummies.py b/tests/pulses/sequencing_dummies.py index 889aea575..18ca487ce 100644 --- a/tests/pulses/sequencing_dummies.py +++ b/tests/pulses/sequencing_dummies.py @@ -8,7 +8,7 @@ """LOCAL IMPORTS""" from qupulse.parameter_scope import Scope -from qupulse._program._loop import Loop +from qupulse.program.loop import Loop from qupulse.utils.types import MeasurementWindow, ChannelID, TimeType, time_from_float from qupulse.serialization import Serializer from qupulse._program.waveforms import Waveform diff --git a/tests/qctoolkit_alias_tests.py b/tests/qctoolkit_alias_tests.py index 5c7a5d834..1ae446776 100644 --- a/tests/qctoolkit_alias_tests.py +++ b/tests/qctoolkit_alias_tests.py @@ -10,7 +10,7 @@ def test_alias(self): self.assertIs(qctoolkit.pulses.TablePT, qupulse.pulses.TablePT) def test_class_identity(self): - from qupulse._program._loop import Loop as Loop_qu - from qctoolkit._program._loop import Loop as Loop_qc + from qupulse.program.loop import Loop as Loop_qu + from qctoolkit.program.loop import Loop as Loop_qc self.assertIs(Loop_qc, Loop_qu) From 6b099faa5ac5619c6d8796fd028099d8f42219f3 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 21 Aug 2023 10:40:33 +0200 Subject: [PATCH 091/441] Add missing init --- qupulse/program/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 qupulse/program/__init__.py diff --git a/qupulse/program/__init__.py b/qupulse/program/__init__.py new file mode 100644 index 000000000..e69de29bb From 6e465f8dae8cfb6dde2471aaa46f0e742e8e3b5d Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 21 Aug 2023 10:40:58 +0200 Subject: [PATCH 092/441] Update __all__ and change missed imports --- qupulse/program/loop.py | 8 ++++---- qupulse/program/transformation.py | 6 ++++++ qupulse/program/volatile.py | 3 +++ qupulse/program/waveforms.py | 7 ++++--- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/qupulse/program/loop.py b/qupulse/program/loop.py index 20ec1204c..044de8f03 100644 --- a/qupulse/program/loop.py +++ b/qupulse/program/loop.py @@ -8,17 +8,17 @@ import numpy as np import sympy.ntheory -from qupulse._program.waveforms import Waveform, ConstantWaveform -from qupulse._program.volatile import VolatileRepetitionCount, VolatileProperty +from qupulse.program.waveforms import Waveform, ConstantWaveform +from qupulse.program.volatile import VolatileRepetitionCount, VolatileProperty from qupulse.utils import is_integer from qupulse.utils.types import TimeType, MeasurementWindow from qupulse.utils.tree import Node, is_tree_circular from qupulse.utils.numeric import smallest_factor_ge -from qupulse._program.waveforms import SequenceWaveform, RepetitionWaveform +from qupulse.program.waveforms import SequenceWaveform, RepetitionWaveform -__all__ = ['Loop', 'make_compatible', 'MakeCompatibleWarning'] +__all__ = ['Loop', 'make_compatible', 'MakeCompatibleWarning', 'to_waveform'] class Loop(Node): diff --git a/qupulse/program/transformation.py b/qupulse/program/transformation.py index 66a1641f9..784e8e193 100644 --- a/qupulse/program/transformation.py +++ b/qupulse/program/transformation.py @@ -13,6 +13,11 @@ _TrafoValue = Union[Real, ExpressionScalar] +__all__ = ['Transformation', 'IdentityTransformation', 'LinearTransformation', 'ScalingTransformation', + 'OffsetTransformation', 'ParallelChannelTransformation', 'ChainedTransformation', + 'chain_transformations'] + + class Transformation(Comparable): _identity_singleton = None """Transforms numeric time-voltage values for multiple channels to other time-voltage values. The number and names @@ -394,6 +399,7 @@ def _get_constant_output_channels(expressions: Mapping[ChannelID, _TrafoValue], for ch in constant_input_channels if not hasattr(expressions.get(ch, None), 'variables')} + def _are_valid_transformation_expressions(expressions: Mapping[ChannelID, _TrafoValue]) -> bool: return all(expr.variables == ('t',) for expr in expressions.values() diff --git a/qupulse/program/volatile.py b/qupulse/program/volatile.py index ab76e8ecc..afd4692ff 100644 --- a/qupulse/program/volatile.py +++ b/qupulse/program/volatile.py @@ -9,6 +9,9 @@ from qupulse.utils import is_integer +__all__ = ['VolatileProperty', 'VolatileValue', 'VolatileRepetitionCount'] + + VolatileProperty = NamedTuple('VolatileProperty', [('expression', Expression), ('dependencies', FrozenMapping[str, Expression])]) VolatileProperty.__doc__ = """Hashable representation of a volatile program property. It does not contain the concrete diff --git a/qupulse/program/waveforms.py b/qupulse/program/waveforms.py index a173f3bf3..9cdcbf4ff 100644 --- a/qupulse/program/waveforms.py +++ b/qupulse/program/waveforms.py @@ -17,7 +17,7 @@ import numpy as np from qupulse import ChannelID -from qupulse._program.transformation import Transformation +from qupulse.program.transformation import Transformation from qupulse.utils import checked_int_cast, isclose from qupulse.utils.types import TimeType, time_from_float from qupulse.utils.performance import is_monotonic @@ -26,16 +26,17 @@ from qupulse.pulses.interpolation import InterpolationStrategy from qupulse.utils import checked_int_cast, isclose from qupulse.utils.types import TimeType, time_from_float, FrozenDict -from qupulse._program.transformation import Transformation +from qupulse.program.transformation import Transformation from qupulse.utils import pairwise class ConstantFunctionPulseTemplateWarning(UserWarning): """ This warning indicates a constant waveform is constructed from a FunctionPulseTemplate """ pass + __all__ = ["Waveform", "TableWaveform", "TableWaveformEntry", "FunctionWaveform", "SequenceWaveform", "MultiChannelWaveform", "RepetitionWaveform", "TransformingWaveform", "ArithmeticWaveform", - "ConstantFunctionPulseTemplateWarning"] + "ConstantFunctionPulseTemplateWarning", "ConstantWaveform"] PULSE_TO_WAVEFORM_ERROR = None # error margin in pulse template to waveform conversion From 280ac0854b9ecdd37c6343893d0e9a26951f04b4 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 21 Aug 2023 10:57:17 +0200 Subject: [PATCH 093/441] Fix atomic stub tests --- tests/pulses/pulse_template_tests.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/tests/pulses/pulse_template_tests.py b/tests/pulses/pulse_template_tests.py index c2677f389..87f6564aa 100644 --- a/tests/pulses/pulse_template_tests.py +++ b/tests/pulses/pulse_template_tests.py @@ -453,7 +453,7 @@ def test_internal_create_program(self) -> None: scope = DictScope.from_kwargs(foo=7.2, volatile={'gutes_zeuch'}) measurement_mapping = {'M': 'N'} channel_mapping = {'B': 'A'} - program = Loop() + program_builder = LoopBuilder() expected_program = Loop(children=[Loop(waveform=wf)], measurements=[('N', 0, 5)]) @@ -462,20 +462,17 @@ def test_internal_create_program(self) -> None: template._internal_create_program(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, - parent_loop=program, + program_builder=program_builder, to_single_waveform=set(), global_transformation=None) build_waveform.assert_called_once_with(parameters=scope, channel_mapping=channel_mapping) - + program = program_builder.to_program() self.assertEqual(expected_program, program) - # MultiChannelProgram calls cleanup - program.cleanup() - def test_internal_create_program_transformation(self): inner_wf = DummyWaveform() template = AtomicPulseTemplateStub(parameter_names=set()) - program = Loop() + program_builder = LoopBuilder() global_transformation = TransformationStub() scope = DictScope.from_kwargs() expected_program = Loop(children=[Loop(waveform=TransformingWaveform(inner_wf, global_transformation))]) @@ -484,10 +481,10 @@ def test_internal_create_program_transformation(self): template._internal_create_program(scope=scope, measurement_mapping={}, channel_mapping={}, - parent_loop=program, + program_builder=program_builder, to_single_waveform=set(), global_transformation=global_transformation) - + program = program_builder.to_program() self.assertEqual(expected_program, program) def test_internal_create_program_no_waveform(self) -> None: @@ -497,7 +494,7 @@ def test_internal_create_program_no_waveform(self) -> None: scope = DictScope.from_kwargs(foo=3.5, bar=3, volatile={'bar'}) measurement_mapping = {'M': 'N'} channel_mapping = {'B': 'A'} - program = Loop() + program_builder = LoopBuilder() expected_program = Loop() @@ -508,13 +505,13 @@ def test_internal_create_program_no_waveform(self) -> None: template._internal_create_program(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, - parent_loop=program, + program_builder=program_builder, to_single_waveform=set(), global_transformation=None) build_waveform.assert_called_once_with(parameters=scope, channel_mapping=channel_mapping) get_meas_windows.assert_not_called() - self.assertEqual(expected_program, program) + self.assertIsNone(program_builder.to_program()) def test_internal_create_program_volatile(self): template = AtomicPulseTemplateStub(parameter_names={'foo'}) @@ -522,13 +519,13 @@ def test_internal_create_program_volatile(self): measurement_mapping = {'M': 'N'} channel_mapping = {'B': 'A'} - program = Loop() + program_builder = LoopBuilder() with self.assertRaisesRegex(AssertionError, "volatile"): template._internal_create_program(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, - parent_loop=program, + program_builder=program_builder, to_single_waveform=set(), global_transformation=None) - self.assertEqual(Loop(), program) + self.assertIsNone(program_builder.to_program()) From 37681d81f98bcadd4a0094bac4864e623381b789 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 21 Aug 2023 11:26:29 +0200 Subject: [PATCH 094/441] Add global transformation to new subprogram request --- qupulse/program/__init__.py | 2 +- qupulse/program/loop.py | 17 +++++++++++------ qupulse/pulses/pulse_template.py | 4 ++-- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/qupulse/program/__init__.py b/qupulse/program/__init__.py index e5a5d36d0..4747672a3 100644 --- a/qupulse/program/__init__.py +++ b/qupulse/program/__init__.py @@ -105,7 +105,7 @@ def with_sequence(self, Returns: """ - def new_subprogram(self) -> ContextManager['ProgramBuilder']: + def new_subprogram(self, global_transformation: 'Transformation' = None) -> ContextManager['ProgramBuilder']: """Create a context managed program builder whose contents are translated into a single waveform upon exit if it is not empty.""" diff --git a/qupulse/program/loop.py b/qupulse/program/loop.py index 1e29a6522..46a1f16e8 100644 --- a/qupulse/program/loop.py +++ b/qupulse/program/loop.py @@ -15,8 +15,9 @@ from qupulse.program.waveforms import ConstantWaveform, Waveform from qupulse.pulses.range import RangeScope from qupulse.parameter_scope import Scope -from qupulse.program.waveforms import Waveform, ConstantWaveform +from qupulse.program.waveforms import Waveform, ConstantWaveform, TransformingWaveform from qupulse.program.volatile import VolatileRepetitionCount, VolatileProperty +from qupulse.program.transformation import Transformation from qupulse.utils import is_integer from qupulse.utils.types import TimeType, MeasurementWindow @@ -826,16 +827,20 @@ def play_arbitrary_waveform(self, waveform: Waveform): self._top.append_child(waveform=waveform) @contextmanager - def new_subprogram(self) -> ContextManager['ProgramBuilder']: + def new_subprogram(self, global_transformation: Transformation = None) -> ContextManager['ProgramBuilder']: inner_builder = LoopBuilder() yield inner_builder inner_program = inner_builder.to_program() if inner_program is not None: - for name, (begins, lengths) in inner_program.get_measurement_windows().items(): - for begin, length in zip(begins, lengths): - self._top.add_measurements((name, begin, length)) - self.play_arbitrary_waveform(to_waveform(inner_program)) + measurements = [(name, begin, length) + for name, (begins, lengths) in inner_program.get_measurement_windows().items() + for begin, length in zip(begins, lengths)] + self._top.add_measurements(measurements) + waveform = to_waveform(inner_program) + if global_transformation is not None: + waveform = TransformingWaveform.from_transformation(waveform, global_transformation) + self.play_arbitrary_waveform(waveform) def to_program(self) -> Optional[Loop]: if len(self._stack) != 1: diff --git a/qupulse/pulses/pulse_template.py b/qupulse/pulses/pulse_template.py index 30fdc4392..97c64feaf 100644 --- a/qupulse/pulses/pulse_template.py +++ b/qupulse/pulses/pulse_template.py @@ -225,7 +225,7 @@ def _create_program(self, *, """Generic part of create program. This method handles to_single_waveform and the configuration of the transformer.""" if self.identifier in to_single_waveform or self in to_single_waveform: - with program_builder.new_subprogram() as inner_program_builder: + with program_builder.new_subprogram(global_transformation=global_transformation) as inner_program_builder: if not scope.get_volatile_parameters().keys().isdisjoint(self.parameter_names): raise NotImplementedError('A pulse template that has volatile parameters cannot be transformed into a ' @@ -234,7 +234,7 @@ def _create_program(self, *, self._internal_create_program(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, - global_transformation=global_transformation, + global_transformation=None, to_single_waveform=to_single_waveform, program_builder=inner_program_builder) From f3c2e80ebe201a7cb6670bff905bd893fbe97bd8 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 21 Aug 2023 11:26:52 +0200 Subject: [PATCH 095/441] Fix bare pulse template tests --- tests/pulses/pulse_template_tests.py | 98 +++++++++++++++++----------- 1 file changed, 59 insertions(+), 39 deletions(-) diff --git a/tests/pulses/pulse_template_tests.py b/tests/pulses/pulse_template_tests.py index 87f6564aa..a846ae74e 100644 --- a/tests/pulses/pulse_template_tests.py +++ b/tests/pulses/pulse_template_tests.py @@ -13,7 +13,7 @@ from qupulse.pulses.pulse_template import AtomicPulseTemplate, PulseTemplate, UnknownVolatileParameter from qupulse.pulses.multi_channel_pulse_template import MultiChannelWaveform from qupulse.program.loop import Loop -from qupulse.program import ProgramBuilder +from qupulse.program import ProgramBuilder, default_program_builder from qupulse._program.transformation import Transformation from qupulse._program.waveforms import TransformingWaveform @@ -178,16 +178,19 @@ def test_create_program(self) -> None: dummy_waveform = DummyWaveform() expected_program = Loop(children=[Loop(waveform=dummy_waveform)]) + program_builder = LoopBuilder() + with mock.patch.object(template, '_create_program', wraps=get_appending_internal_create_program(dummy_waveform)) as _create_program: - program = template.create_program(parameters=parameters, - measurement_mapping=measurement_mapping, - channel_mapping=channel_mapping, - to_single_waveform=to_single_waveform, - global_transformation=global_transformation, - volatile=volatile) - _create_program.assert_called_once_with(**expected_internal_kwargs, parent_loop=program) + with mock.patch('qupulse.pulses.pulse_template.default_program_builder', return_value=program_builder): + program = template.create_program(parameters=parameters, + measurement_mapping=measurement_mapping, + channel_mapping=channel_mapping, + to_single_waveform=to_single_waveform, + global_transformation=global_transformation, + volatile=volatile) + _create_program.assert_called_once_with(**expected_internal_kwargs, program_builder=program_builder) self.assertEqual(expected_program, program) self.assertEqual(previos_measurement_mapping, measurement_mapping) self.assertEqual(previous_channel_mapping, channel_mapping) @@ -218,7 +221,7 @@ def test__create_program(self): to_single_waveform=to_single_waveform, program_builder=program_builder) - self.assertEqual(program_builder.to_program(), Loop()) + self.assertIsNone(program_builder.to_program()) with self.assertRaisesRegex(NotImplementedError, "volatile"): template._parameter_names = {'c'} @@ -227,7 +230,7 @@ def test__create_program(self): channel_mapping=channel_mapping, global_transformation=global_transformation, to_single_waveform={template}, - parent_loop=parent_loop) + program_builder=program_builder) def test__create_program_single_waveform(self): template = PulseTemplateStub(identifier='pt_identifier', parameter_names={'alpha'}) @@ -237,7 +240,9 @@ def test__create_program_single_waveform(self): scope = DictScope.from_kwargs(a=1., b=2., volatile={'a'}) measurement_mapping = {'M': 'N'} channel_mapping = {'B': 'A'} - parent_loop = Loop() + + program_builder = LoopBuilder() + inner_program_builder = LoopBuilder() wf = DummyWaveform() single_waveform = DummyWaveform() @@ -259,28 +264,30 @@ def test__create_program_single_waveform(self): with mock.patch.object(template, '_internal_create_program', wraps=appending_create_program) as _internal_create_program: - with mock.patch('qupulse.pulses.pulse_template.to_waveform', + with mock.patch('qupulse.program.loop.to_waveform', return_value=single_waveform) as to_waveform: - template._create_program(scope=scope, - measurement_mapping=measurement_mapping, - channel_mapping=channel_mapping, - global_transformation=global_transformation, - to_single_waveform=to_single_waveform, - parent_loop=parent_loop) + with mock.patch('qupulse.program.loop.LoopBuilder', return_value=inner_program_builder): + template._create_program(scope=scope, + measurement_mapping=measurement_mapping, + channel_mapping=channel_mapping, + global_transformation=global_transformation, + to_single_waveform=to_single_waveform, + program_builder=program_builder) _internal_create_program.assert_called_once_with(scope=scope, measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, global_transformation=None, to_single_waveform=to_single_waveform, - parent_loop=expected_inner_program) + program_builder=inner_program_builder) to_waveform.assert_called_once_with(expected_inner_program) - expected_program._measurements = set(expected_program._measurements) - parent_loop._measurements = set(parent_loop._measurements) + program = program_builder.to_program() - self.assertEqual(expected_program, parent_loop) + expected_program._measurements = set(expected_program._measurements) + program._measurements = set(program._measurements) + self.assertEqual(expected_program, program) def test_create_program_defaults(self) -> None: template = PulseTemplateStub(defined_channels={'A', 'B'}, parameter_names={'foo'}, measurement_names={'hugo', 'foo'}) @@ -293,12 +300,15 @@ def test_create_program_defaults(self) -> None: dummy_waveform = DummyWaveform() expected_program = Loop(children=[Loop(waveform=dummy_waveform)]) + program_builder = LoopBuilder() with mock.patch.object(template, '_internal_create_program', wraps=get_appending_internal_create_program(dummy_waveform, True)) as _internal_create_program: - program = template.create_program() - _internal_create_program.assert_called_once_with(**expected_internal_kwargs, parent_loop=program) + with mock.patch('qupulse.pulses.pulse_template.default_program_builder', return_value=program_builder) as pb: + program = template.create_program() + pb.assert_called_once_with() + _internal_create_program.assert_called_once_with(**expected_internal_kwargs, program_builder=program_builder) self.assertEqual(expected_program, program) def test_create_program_channel_mapping(self): @@ -310,10 +320,11 @@ def test_create_program_channel_mapping(self): global_transformation=None, to_single_waveform=set()) - with mock.patch.object(template, '_internal_create_program') as _internal_create_program: - template.create_program(channel_mapping={'A': 'C'}) - - _internal_create_program.assert_called_once_with(**expected_internal_kwargs, parent_loop=Loop()) + with mock.patch('qupulse.pulses.pulse_template.default_program_builder') as pb: + with mock.patch.object(template, '_internal_create_program') as _internal_create_program: + template.create_program(channel_mapping={'A': 'C'}) + pb.assert_called_once_with() + _internal_create_program.assert_called_once_with(**expected_internal_kwargs, program_builder=pb.return_value) def test_create_program_volatile(self): template = PulseTemplateStub(defined_channels={'A', 'B'}) @@ -327,27 +338,33 @@ def test_create_program_volatile(self): to_single_waveform=set()) with mock.patch.object(template, '_internal_create_program') as _internal_create_program: - template.create_program(parameters=parameters, volatile='abc') + program_builder = default_program_builder() + with mock.patch('qupulse.pulses.pulse_template.default_program_builder', return_value=program_builder): + template.create_program(parameters=parameters, volatile='abc') - _internal_create_program.assert_called_once_with(**expected_internal_kwargs, parent_loop=Loop()) + _internal_create_program.assert_called_once_with(**expected_internal_kwargs, program_builder=program_builder) with mock.patch.object(template, '_internal_create_program') as _internal_create_program: - template.create_program(parameters=parameters, volatile={'abc'}) + program_builder = default_program_builder() + with mock.patch('qupulse.pulses.pulse_template.default_program_builder', return_value=program_builder): + template.create_program(parameters=parameters, volatile={'abc'}) - _internal_create_program.assert_called_once_with(**expected_internal_kwargs, parent_loop=Loop()) + _internal_create_program.assert_called_once_with(**expected_internal_kwargs, program_builder=program_builder) expected_internal_kwargs = dict(scope=DictScope.from_kwargs(volatile={'abc', 'dfg'}, **parameters), measurement_mapping=dict(), channel_mapping={'A': 'A', 'B': 'B'}, global_transformation=None, to_single_waveform=set()) - with mock.patch.object(template, '_internal_create_program') as _internal_create_program: - with self.assertWarns(UnknownVolatileParameter): - template.create_program(parameters=parameters, volatile={'abc', 'dfg'}) - - _internal_create_program.assert_called_once_with(**expected_internal_kwargs, parent_loop=Loop()) + program_builder = default_program_builder() + with mock.patch('qupulse.pulses.pulse_template.default_program_builder', return_value=program_builder): + with mock.patch.object(template, '_internal_create_program') as _internal_create_program: + with self.assertWarns(UnknownVolatileParameter): + template.create_program(parameters=parameters, volatile={'abc', 'dfg'}) + _internal_create_program.assert_called_once_with(**expected_internal_kwargs, program_builder=program_builder) - def test_create_program_none(self) -> None: + @mock.patch('qupulse.pulses.pulse_template.default_program_builder') + def test_create_program_none(self, pb_mock) -> None: template = PulseTemplateStub(defined_channels={'A'}, parameter_names={'foo'}) parameters = {'foo': 2.126, 'bar': -26.2, 'hugo': 'exp(sin(pi/2))'} measurement_mapping = {'M': 'N'} @@ -360,6 +377,7 @@ def test_create_program_none(self) -> None: channel_mapping=channel_mapping, global_transformation=None, to_single_waveform=set()) + pb_mock.return_value = LoopBuilder() with mock.patch.object(template, '_internal_create_program') as _internal_create_program: @@ -367,7 +385,9 @@ def test_create_program_none(self) -> None: measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, volatile=volatile) - _internal_create_program.assert_called_once_with(**expected_internal_kwargs, parent_loop=Loop()) + pb_mock.assert_called_once_with() + _internal_create_program.assert_called_once_with(**expected_internal_kwargs, + program_builder=pb_mock.return_value) self.assertIsNone(program) def test_matmul(self): From d7dfc0bd766de1c5f0479f12924e36d80b5aaec6 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 21 Aug 2023 11:36:58 +0200 Subject: [PATCH 096/441] Fix missing measurement handling in LoopBuilder for loop creation --- qupulse/program/loop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qupulse/program/loop.py b/qupulse/program/loop.py index 46a1f16e8..61c79d92b 100644 --- a/qupulse/program/loop.py +++ b/qupulse/program/loop.py @@ -807,7 +807,7 @@ def with_repetition(self, repetition_count: RepetitionCount, def with_iteration(self, index_name: str, rng: range, measurements: Optional[Sequence[MeasurementWindow]] = None) -> Iterable['ProgramBuilder']: - with self.with_sequence(): + with self.with_sequence(measurements): top_frame = self._stack[-1] for value in rng: top_frame.iterating = (index_name, value) From cd70a4a0d6cfb89df270f3c6db4694ad210ce1c3 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 21 Aug 2023 11:37:25 +0200 Subject: [PATCH 097/441] Better measurement window comparison --- tests/pulses/sequencing_dummies.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/pulses/sequencing_dummies.py b/tests/pulses/sequencing_dummies.py index b06d32653..f101d8799 100644 --- a/tests/pulses/sequencing_dummies.py +++ b/tests/pulses/sequencing_dummies.py @@ -24,10 +24,13 @@ class MeasurementWindowTestCase(unittest.TestCase): def assert_measurement_windows_equal(self, expected, actual) -> bool: - self.assertEqual(expected.keys(), actual.keys()) - for k in expected: - self.assertEqual(list(expected[k][0]), list(actual[k][0])) - self.assertEqual(list(expected[k][1]), list(actual[k][1])) + def normalize_measurement_windows(mw): + return {name: ([bs[idx] for idx in numpy.argsort(bs)], [ls[idx] for idx in numpy.argsort(bs)]) + for name, (bs, ls) in mw.items()} + + expected = normalize_measurement_windows(expected) + actual = normalize_measurement_windows(actual) + self.assertEqual(expected, actual) class DummyWaveform(Waveform): From 8c20bae6b3d64dca69dd0bf0718fab4cde88289b Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 21 Aug 2023 11:39:40 +0200 Subject: [PATCH 098/441] Fix last test --- qupulse/pulses/loop_pulse_template.py | 2 +- tests/pulses/loop_pulse_template_tests.py | 1 - tests/pulses/multi_channel_pulse_template_tests.py | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/qupulse/pulses/loop_pulse_template.py b/qupulse/pulses/loop_pulse_template.py index 457a5cdee..0f458c687 100644 --- a/qupulse/pulses/loop_pulse_template.py +++ b/qupulse/pulses/loop_pulse_template.py @@ -159,7 +159,7 @@ def _internal_create_program(self, *, measurements = self.get_measurement_windows(scope, measurement_mapping) for iteration_program_builder in program_builder.with_iteration(loop_index_name, loop_range, - measurements=measurements): + measurements=measurements): self.body._create_program(scope=iteration_program_builder.inner_scope(scope), measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, diff --git a/tests/pulses/loop_pulse_template_tests.py b/tests/pulses/loop_pulse_template_tests.py index 91d85f7d6..c6a658bf8 100644 --- a/tests/pulses/loop_pulse_template_tests.py +++ b/tests/pulses/loop_pulse_template_tests.py @@ -390,7 +390,6 @@ def test_create_program(self) -> None: get_measurement_windows.assert_called_once_with(scope, measurement_mapping) self.assertEqual(body_create_program.call_args_list, expected_create_program_calls) - def test_create_program_append(self) -> None: dt = DummyPulseTemplate(parameter_names={'i'}, waveform=DummyWaveform(duration=4.0), duration=4, measurements=[('b', 2, 1)]) diff --git a/tests/pulses/multi_channel_pulse_template_tests.py b/tests/pulses/multi_channel_pulse_template_tests.py index 87ae72575..617ccb935 100644 --- a/tests/pulses/multi_channel_pulse_template_tests.py +++ b/tests/pulses/multi_channel_pulse_template_tests.py @@ -414,7 +414,7 @@ def test_internal_create_program(self): measurement_names={'M'}, waveform=DummyWaveform()) overwritten_channels = {'Y': 'c', 'Z': 'a', 'ToNone': 'foo'} - parent_loop = object() + program_builder = object() measurement_mapping = object() channel_mapping = {'Y': 'O', 'Z': 'Z', 'X': 'X', 'ToNone': None} to_single_waveform = object() @@ -422,7 +422,7 @@ def test_internal_create_program(self): other_kwargs = dict(measurement_mapping=measurement_mapping, channel_mapping=channel_mapping, to_single_waveform=to_single_waveform, - parent_loop=parent_loop) + program_builder=program_builder) pccpt = ParallelChannelPulseTemplate(template, overwritten_channels) scope = DictScope.from_kwargs(c=1.2, a=3.4) From bfd9150e958bb3b4ec07882e982308603ba54232 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 22 Aug 2023 09:23:13 +0200 Subject: [PATCH 099/441] Update program concept description --- doc/source/concepts/program.rst | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/doc/source/concepts/program.rst b/doc/source/concepts/program.rst index 0b34df0ae..3d4172bc5 100644 --- a/doc/source/concepts/program.rst +++ b/doc/source/concepts/program.rst @@ -4,14 +4,19 @@ Instantiated Pulse: Program --------------------------- In qupulse an instantiated pulse template is called a program as it is something that an arbitrary waveform generator -(AWG) can execute/playback. It is created by the `create_program` method of the pulse template which returns a hardware -independent representation which is currently of type ``Loop``. Opposed to the `PulseTemplate` interfaces the interface of the program is currently not covered by the qupulse backward compatibility and stability guarantee. -This is reflected by the fact that it lives in the private module ``qupulse._program._loop``. +(AWG) can execute/playback. +It is created by the ``create_program`` method of the pulse template which returns a hardware +independent representation which is of type ``Loop``. +This ``Loop`` object is the root node of a tree ``Loop``s of arbitrary depth. +Each node consists of a repetition count and either a waveform or a sequence of nodes which are repeated that many times. +Iterations like the ```ForLoopPT`` cannot be represented natively but are unrolled into a sequence of items. +The repetition count is currently the only property of a program that can be defined as volatile. This means that the AWG driver tries to upload the program in a way, where the repetition count can quickly be changed. This is implemented via the ```VolatileRepetitionCount`` class. -There is no description of the details of the program object here to avoid outdated documentation. +There is no description of the details of the program object here to avoid duplicated and outdated documentation. The documentation is in the docstrings of the source code. The program can be thought of as compact representation of a mapping :math:`\{t | 0 \le t \le t_{\texttt{duration}}} \rightarrow \mathbb{R}^n` from the time while the program lasts :math:´t´ to an n-dimensional voltage space :math:´\mathbb{R}^n´. The dimensions are named by the channel names. -The subpackage ``_qupulse._program`` also contains hardware specific translations of the programs for example a -transpiler to Zurich Instruments sequencing C in ``_qupulse._program.seqc``. +The ``Loop`` class and its constituents ``Waveform`` and ``VolatileRepetitionCount`` are defined in the ``qupulse.program`` subpackage and it's submodules. +The private subpackage ``qupulse._program`` contains AWG driver internals that can change with any release, for example a +transpiler to Zurich Instruments sequencing C in ``qupulse._program.seqc``. From 2864d6222fbf486bdfe9ba23b3cd09dfe3df5c1b Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 22 Aug 2023 09:33:58 +0200 Subject: [PATCH 100/441] Optimize imports --- qupulse/program/loop.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/qupulse/program/loop.py b/qupulse/program/loop.py index 044de8f03..6e929d5eb 100644 --- a/qupulse/program/loop.py +++ b/qupulse/program/loop.py @@ -1,22 +1,18 @@ -from typing import Union, Dict, Iterable, Tuple, cast, List, Optional, Generator, Mapping +import reprlib +import warnings from collections import defaultdict from enum import Enum -import warnings -import bisect -import reprlib +from typing import Union, Dict, Iterable, Tuple, cast, List, Optional, Generator, Mapping import numpy as np -import sympy.ntheory -from qupulse.program.waveforms import Waveform, ConstantWaveform from qupulse.program.volatile import VolatileRepetitionCount, VolatileProperty - +from qupulse.program.waveforms import SequenceWaveform, RepetitionWaveform +from qupulse.program.waveforms import Waveform, ConstantWaveform from qupulse.utils import is_integer -from qupulse.utils.types import TimeType, MeasurementWindow -from qupulse.utils.tree import Node, is_tree_circular from qupulse.utils.numeric import smallest_factor_ge - -from qupulse.program.waveforms import SequenceWaveform, RepetitionWaveform +from qupulse.utils.tree import Node +from qupulse.utils.types import TimeType, MeasurementWindow __all__ = ['Loop', 'make_compatible', 'MakeCompatibleWarning', 'to_waveform'] From cb9356cb309c73e8fe9799419002c2d77c862ef6 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 22 Aug 2023 09:34:21 +0200 Subject: [PATCH 101/441] Remove dead code --- qupulse/program/loop.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/qupulse/program/loop.py b/qupulse/program/loop.py index 6e929d5eb..69d03c89d 100644 --- a/qupulse/program/loop.py +++ b/qupulse/program/loop.py @@ -501,11 +501,6 @@ def reverse_inplace(self): ] -class ChannelSplit(Exception): - def __init__(self, channel_sets): - self.channel_sets = channel_sets - - def to_waveform(program: Loop) -> Waveform: if program.is_leaf(): if program.repetition_count == 1: From 548dcb087c7e9a53fff4be84741eb879b22c4df2 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 22 Aug 2023 09:34:42 +0200 Subject: [PATCH 102/441] Add a few docstrings and correct type annotation --- qupulse/program/loop.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/qupulse/program/loop.py b/qupulse/program/loop.py index 69d03c89d..a11826762 100644 --- a/qupulse/program/loop.py +++ b/qupulse/program/loop.py @@ -17,6 +17,9 @@ __all__ = ['Loop', 'make_compatible', 'MakeCompatibleWarning', 'to_waveform'] +DurationStructure = Tuple[int, Union[TimeType, 'DurationStructure']] + + class Loop(Node): MAX_REPR_SIZE = 2000 __slots__ = ('_waveform', '_measurements', '_repetition_definition', '_cached_body_duration') @@ -393,6 +396,8 @@ def flatten_and_balance(self, depth: int) -> None: i += 1 def _has_single_child_that_can_be_merged(self) -> bool: + """Check if self has only once child which can cheaply be merged into self by multiplying the repetition counts. + """ if len(self) == 1: child = cast(Loop, self[0]) return not self._measurements or (child.repetition_count == 1 and not child.volatile_repetition) @@ -480,7 +485,12 @@ def cleanup(self, actions=('remove_empty_loops', 'merge_single_child')): if 'merge_single_child' in actions and self._has_single_child_that_can_be_merged(): self._merge_single_child() - def get_duration_structure(self) -> Tuple[int, Union[TimeType, tuple]]: + def get_duration_structure(self) -> DurationStructure: + """Returns a tuple that fingerprints the structure of waveform durations and repetitions of self. + + One possible use case is to identify repeated duration structures and reuse the same control flow with + differing data. + """ if self.is_leaf(): return self.repetition_count, self.waveform.duration else: From 45877ebbe90d9ed9959c3654c063693940c0b564 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 22 Aug 2023 10:49:41 +0200 Subject: [PATCH 103/441] Add newspiece --- changes.d/779.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes.d/779.feature diff --git a/changes.d/779.feature b/changes.d/779.feature new file mode 100644 index 000000000..edae2b25c --- /dev/null +++ b/changes.d/779.feature @@ -0,0 +1 @@ +Promote parts of the private subpackage `qupulse._program` to the public subpackage `qupulse.program`, i.e. `loop`, `volatile`, `transformation` and `waveforms`. This allows external packages/drivers to rely on stability of the `Loop` class. From 37b02f9014e992aa9a8c5c6c266b9f06de6953d7 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 22 Aug 2023 11:08:59 +0200 Subject: [PATCH 104/441] Update, fix and slightly extend documentation regarding expressions --- doc/source/concepts/pulsetemplates.rst | 2 +- qupulse/expressions/__init__.py | 6 +++--- qupulse/expressions/protocol.py | 2 ++ 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/doc/source/concepts/pulsetemplates.rst b/doc/source/concepts/pulsetemplates.rst index 9553f0e3f..9c1463917 100644 --- a/doc/source/concepts/pulsetemplates.rst +++ b/doc/source/concepts/pulsetemplates.rst @@ -33,7 +33,7 @@ Parameters As mentioned above, all pulse templates may depend on parameters. During pulse template initialization the parameters simply are the free variables of expressions that occur in the pulse template. For example the :class:`.FunctionPulseTemplate` has expressions for its duration and the voltage time dependency i.e. the underlying function. Some pulse templates provided means to constrain parameters by accepting a list of :class:`.ParameterConstraint` which encapsulate comparative expressions that must evaluate to true for a given parameter set to successfully instantiate a pulse from the pulse template. This can be used to encode physical or logical parameter boundaries at pulse level. -The mathematical expressions (for parameter transformation or as the function of the :class:`.FunctionPulseTemplate`) are encapsulated into an :class:`.Expression` class which wraps `sympy `_ for string evaluation. +The mathematical expressions (for parameter transformation or as the function of the :class:`.FunctionPulseTemplate`) are encapsulated into an :class:`.sympy.Expression` class which wraps `sympy `_ for string evaluation by default. Other more performant or secure backends can potentially be implemented by conforming to the :class:`.protocol.Expression`. Parameters can be mapped to arbitrary expressions via :class:`.mapping_pulse_template.MappingPulseTemplate`. One use case can be deriving pulse parameters from physical quantities. diff --git a/qupulse/expressions/__init__.py b/qupulse/expressions/__init__.py index 61224f2fd..52aa5f325 100644 --- a/qupulse/expressions/__init__.py +++ b/qupulse/expressions/__init__.py @@ -1,10 +1,10 @@ -"""This subpackage contains qupulse's expression logic. The submodule :py:`protocol` defines the :py:`typing.Protocol` +"""This subpackage contains qupulse's expression logic. The submodule :py:mod:`.expressions.protocol` defines the :py:class:`typing.Protocol` that expression functionality providers must implement. This allows to substitute the powerful and expressive but slow default implementation with a faster less expressive backend. -The default implementation is in :py:``qupulse.expressions.sympy``. +The default implementation is in :py:mod:`.expressions.sympy`. -There is are wrapper classes for finding non-protocol uses of expression in :py:``qupulse.expressions.wrapper``. Define +There is are wrapper classes for finding non-protocol uses of expression in :py:mod:`.expressions.wrapper`. Define ``QUPULSE_EXPRESSION_WRAPPER`` environment variable when running python to wrap all expression usages. """ diff --git a/qupulse/expressions/protocol.py b/qupulse/expressions/protocol.py index f6f90d459..667337c3d 100644 --- a/qupulse/expressions/protocol.py +++ b/qupulse/expressions/protocol.py @@ -54,6 +54,8 @@ def __abs__(self): class Expression(Hashable, Protocol): + """This protocol defines how Expressions are allowed to be used in qupulse.""" + def evaluate_in_scope(self, scope: Mapping) -> Union[Real, np.ndarray]: """Evaluate the expression by taking the variables from the given scope (typically of type Scope, but it can be any mapping.) From 58a3471085de95753aeaf215beb5f232c08f5eeb Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Wed, 23 Aug 2023 21:51:44 +0200 Subject: [PATCH 105/441] optimize unsafe_sample --- qupulse/program/waveforms.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qupulse/program/waveforms.py b/qupulse/program/waveforms.py index 9cdcbf4ff..e08e410f4 100644 --- a/qupulse/program/waveforms.py +++ b/qupulse/program/waveforms.py @@ -369,8 +369,8 @@ def unsafe_sample(self, entries = self._table for entry1, entry2 in pairwise(entries): - indices = slice(np.searchsorted(sample_times, entry1.t, 'left'), - np.searchsorted(sample_times, entry2.t, 'right')) + indices = slice(sample_times.searchsorted(entry1.t, 'left'), + sample_times.searchsorted(entry2.t, 'right')) output_array[indices] = \ entry2.interp((float(entry1.t), entry1.v), (float(entry2.t), entry2.v), @@ -626,7 +626,7 @@ def unsafe_sample(self, # indexing in numpy and their copy/reference behaviour end = time + subwaveform.duration - indices = slice(*np.searchsorted(sample_times, (float(time), float(end)), 'left')) + indices = slice(*sample_times.searchsorted((float(time), float(end)), 'left')) subwaveform.unsafe_sample(channel=channel, sample_times=sample_times[indices]-np.float64(time), output_array=output_array[indices]) @@ -843,7 +843,7 @@ def unsafe_sample(self, time = 0 for _ in range(self._repetition_count): end = time + body_duration - indices = slice(*np.searchsorted(sample_times, (float(time), float(end)), 'left')) + indices = slice(*sample_times.searchsorted((float(time), float(end)), 'left')) self._body.unsafe_sample(channel=channel, sample_times=sample_times[indices] - float(time), output_array=output_array[indices]) From 935dd30e95de44cabedb1b8529c13d9c19812030 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 25 Aug 2023 12:12:51 +0200 Subject: [PATCH 106/441] Add __pow__ convenience repetition operation. --- qupulse/pulses/pulse_template.py | 4 ++++ tests/pulses/pulse_template_tests.py | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/qupulse/pulses/pulse_template.py b/qupulse/pulses/pulse_template.py index 57f756e26..f685bd088 100644 --- a/qupulse/pulses/pulse_template.py +++ b/qupulse/pulses/pulse_template.py @@ -96,6 +96,10 @@ def __rmatmul__(self, other: MappingTuple) -> 'SequencePulseTemplate': return SequencePulseTemplate.concatenate(other, self) + def __pow__(self, power: ExpressionLike): + """This is a convenience wrapper for :func:`.with_repetition`.""" + return self.with_repetition(power) + @property @abstractmethod def integral(self) -> Dict[ChannelID, ExpressionScalar]: diff --git a/tests/pulses/pulse_template_tests.py b/tests/pulses/pulse_template_tests.py index f8c0e241a..8d4a58716 100644 --- a/tests/pulses/pulse_template_tests.py +++ b/tests/pulses/pulse_template_tests.py @@ -376,6 +376,11 @@ def test_matmul(self): self.assertEqual(a @ b, 'concat') mock_concatenate.assert_called_once_with(a, b) + def test_pow(self): + pt = PulseTemplateStub() + pow_pt = pt ** 5 + self.assertEqual(pow_pt, pt.with_repetition(5)) + def test_rmatmul(self): a = PulseTemplateStub() b = (1, 2, 3) From 1b0c991a286a32ff992a88dc7ec0e72f12d51f44 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 25 Aug 2023 12:17:45 +0200 Subject: [PATCH 107/441] Add newspiece --- changes.d/692.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes.d/692.feature diff --git a/changes.d/692.feature b/changes.d/692.feature new file mode 100644 index 000000000..7cbdb46cb --- /dev/null +++ b/changes.d/692.feature @@ -0,0 +1 @@ +Add `__pow__` as a repetition shortcut. This means you can do `my_pulse_template ** 5` or `my_pulse_template ** 'my_repetition_count'`. \ No newline at end of file From 8f101897fb0083867b9bbac8c86466d26a72d0ef Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 25 Aug 2023 14:08:34 +0200 Subject: [PATCH 108/441] Give an exception with more context if deserialization fails --- qupulse/serialization.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/qupulse/serialization.py b/qupulse/serialization.py index 3825057ed..389748666 100644 --- a/qupulse/serialization.py +++ b/qupulse/serialization.py @@ -1024,7 +1024,11 @@ def filter_serializables(self, obj_dict) -> Any: if get_default_pulse_registry() is self.storage: registry = dict() - return deserialization_callback(identifier=obj_identifier, registry=registry, **obj_dict) + try: + return deserialization_callback(identifier=obj_identifier, registry=registry, **obj_dict) + except Exception as err: + raise ValueError(f"Unable to deserialize {type_identifier} from {obj_dict}", + type_identifier, obj_dict) from err return obj_dict From ffbaf89c73560f5a75b7f3d29960343d21c55821 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 25 Aug 2023 14:34:33 +0200 Subject: [PATCH 109/441] Add automated mask shrinking --- qupulse/hardware/dacs/alazar.py | 5 ++- qupulse/utils/performance.py | 62 ++++++++++++++++++++++++++++++-- tests/utils/performance_tests.py | 17 ++++++++- 3 files changed, 77 insertions(+), 7 deletions(-) diff --git a/qupulse/hardware/dacs/alazar.py b/qupulse/hardware/dacs/alazar.py index 7398e2115..c694c4be6 100644 --- a/qupulse/hardware/dacs/alazar.py +++ b/qupulse/hardware/dacs/alazar.py @@ -16,7 +16,7 @@ from qupulse.utils.types import TimeType from qupulse.hardware.dacs.dac_base import DAC from qupulse.hardware.util import traced -from qupulse.utils.performance import time_windows_to_samples +from qupulse.utils.performance import time_windows_to_samples, shrink_overlapping_windows logger = logging.getLogger(__name__) @@ -283,8 +283,7 @@ def _make_mask(self, mask_id: str, begins, lengths) -> Mask: if mask_type not in ('auto', 'cross_buffer', None): warnings.warn("Currently only CrossBufferMask is implemented.") - if np.any(begins[:-1]+lengths[:-1] > begins[1:]): - raise ValueError('Found overlapping windows in begins') + begins, lengths = shrink_overlapping_windows(begins, lengths) mask = CrossBufferMask() mask.identifier = mask_id diff --git a/qupulse/utils/performance.py b/qupulse/utils/performance.py index 4076b664c..b68bc2929 100644 --- a/qupulse/utils/performance.py +++ b/qupulse/utils/performance.py @@ -1,3 +1,4 @@ +import warnings from typing import Tuple import numpy as np @@ -24,6 +25,64 @@ def _is_monotonic_numpy(arr: np.ndarray) -> bool: return np.all(arr[1:] >= arr[:-1]) +def _shrink_overlapping_windows_numpy(begins, lengths) -> bool: + ends = begins + lengths + + overlaps = np.zeros_like(ends) + np.maximum(ends[:-1] - begins[1:], 0, out=overlaps[1:]) + + if np.any(overlaps >= lengths): + raise ValueError("Overlap is bigger than measurement window") + if np.any(overlaps > 0): + begins += overlaps + lengths -= overlaps + return True + return False + + +@njit +def _shrink_overlapping_windows_numba(begins, lengths) -> bool: + shrank = False + for idx in range(len(begins) - 1): + end = begins[idx] + lengths[idx] + next_begin = begins[idx + 1] + overlap = end - next_begin + + if overlap > 0: + shrank = True + if lengths[idx + 1] > overlap: + begins[idx + 1] += overlap + lengths[idx + 1] -= overlap + else: + raise ValueError("Overlap is bigger than measurement window") + return shrank + + +class WindowOverlapWarning(RuntimeWarning): + pass + + +def shrink_overlapping_windows(begins, lengths, use_numba: bool = numba is not None) -> Tuple[np.array, np.array]: + """Shrink windows in place if they overlap. Emits WindowOverlapWarning if a window was shrunk. + + Raises: + ValueError: if the overlap is bigger than a window. + + Warnings: + WindowOverlapWarning + """ + if use_numba: + backend = _shrink_overlapping_windows_numba + else: + backend = _shrink_overlapping_windows_numpy + begins = begins.copy() + lengths = lengths.copy() + if backend(begins, lengths): + warnings.warn("Found overlapping measurement windows which are automatically shrunken if possible.", + category=WindowOverlapWarning) + return begins, lengths + + @njit def _time_windows_to_samples_sorted_numba(begins, lengths, sample_rate: float) -> Tuple[np.ndarray, np.ndarray]: @@ -79,6 +138,3 @@ def time_windows_to_samples(begins: np.ndarray, lengths: np.ndarray, is_monotonic = _is_monotonic_numpy else: is_monotonic = _is_monotonic_numba - - - diff --git a/tests/utils/performance_tests.py b/tests/utils/performance_tests.py index d158dce5c..ea07574b8 100644 --- a/tests/utils/performance_tests.py +++ b/tests/utils/performance_tests.py @@ -2,7 +2,8 @@ import numpy as np -from qupulse.utils.performance import _time_windows_to_samples_numba, _time_windows_to_samples_numpy +from qupulse.utils.performance import (_time_windows_to_samples_numba, _time_windows_to_samples_numpy, + shrink_overlapping_windows) class TimeWindowsToSamplesTest(unittest.TestCase): @@ -28,3 +29,17 @@ def test_unsorted(self): self.assert_implementations_equal(begins, lengths, sr) +class TestOverlappingWindowReduction(unittest.TestCase): + def test_shrink_overlapping_windows_numba(self): + np.testing.assert_equal( + (np.array([1, 4, 8]), np.array([3, 4, 4])), + shrink_overlapping_windows(np.array([1, 4, 7]), + np.array([3, 4, 5]), use_numba=True) + ) + + def test_shrink_overlapping_windows_numpy(self): + np.testing.assert_equal( + (np.array([1, 4, 8]), np.array([3, 4, 4])), + shrink_overlapping_windows(np.array([1, 4, 7]), + np.array([3, 4, 5]), use_numba=False) + ) \ No newline at end of file From 6d1e3f93034f2d8acdad72fc45301f33ec079096 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 25 Aug 2023 15:05:49 +0200 Subject: [PATCH 110/441] Do an exception cause check --- tests/serialization_tests.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/serialization_tests.py b/tests/serialization_tests.py index f230bd6da..1ebd93b8e 100644 --- a/tests/serialization_tests.py +++ b/tests/serialization_tests.py @@ -1051,9 +1051,12 @@ def test_deserialize_storage_is_not_default_registry_id_occupied(self) -> None: del pulse_storage pulse_storage = PulseStorage(backend) - with self.assertRaisesRegex(RuntimeError, "Pulse with name already exists"): + with self.assertRaises(ValueError) as cm: pulse_storage['peter'] + # this is shitty + self.assertIsInstance(cm.exception.__cause__, RuntimeError) + def test_deserialize_twice_same_object_storage_is_default_registry(self) -> None: backend = DummyStorageBackend() From 607099147121d0182bdd7b0b799d87ad094b8896 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 25 Aug 2023 15:19:34 +0200 Subject: [PATCH 111/441] Fix unsigned integer overflow error... --- qupulse/utils/performance.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qupulse/utils/performance.py b/qupulse/utils/performance.py index b68bc2929..d1d669f88 100644 --- a/qupulse/utils/performance.py +++ b/qupulse/utils/performance.py @@ -46,9 +46,9 @@ def _shrink_overlapping_windows_numba(begins, lengths) -> bool: for idx in range(len(begins) - 1): end = begins[idx] + lengths[idx] next_begin = begins[idx + 1] - overlap = end - next_begin - if overlap > 0: + if end > next_begin: + overlap = end - next_begin shrank = True if lengths[idx + 1] > overlap: begins[idx + 1] += overlap From dba662cb67716186c8554218de723203f57a3802 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 25 Aug 2023 15:20:41 +0200 Subject: [PATCH 112/441] Test for warning --- tests/hardware/alazar_tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/hardware/alazar_tests.py b/tests/hardware/alazar_tests.py index c6e7d032d..034f05254 100644 --- a/tests/hardware/alazar_tests.py +++ b/tests/hardware/alazar_tests.py @@ -6,7 +6,7 @@ from ..hardware import * from qupulse.hardware.dacs.alazar import AlazarCard, AlazarProgram from qupulse.utils.types import TimeType - +from qupulse.utils.performance import WindowOverlapWarning class AlazarProgramTest(unittest.TestCase): def setUp(self) -> None: @@ -112,7 +112,7 @@ def test_make_mask(self): with self.assertRaises(KeyError): card._make_mask('N', begins, lengths) - with self.assertRaises(ValueError): + with self.assertWarns(WindowOverlapWarning): card._make_mask('M', begins, lengths*3) mask = card._make_mask('M', begins, lengths) From 6072f8aa659b1ab21d8daea216ff23fbf5c07c98 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 28 Aug 2023 21:50:06 +0200 Subject: [PATCH 113/441] Fix overflow due to pyint to int32 conversion --- qupulse/pulses/pulse_template.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/qupulse/pulses/pulse_template.py b/qupulse/pulses/pulse_template.py index 57f756e26..cdc3b42e6 100644 --- a/qupulse/pulses/pulse_template.py +++ b/qupulse/pulses/pulse_template.py @@ -13,6 +13,7 @@ import collections from numbers import Real, Number +import numpy import sympy from qupulse.utils.types import ChannelID, DocStringABCMeta, FrozenDict @@ -50,6 +51,8 @@ class PulseTemplate(Serializable): """This is not stable""" _DEFAULT_FORMAT_SPEC = 'identifier' + _CAST_INT_TO_INT64 = True + def __init__(self, *, identifier: Optional[str]) -> None: super().__init__(identifier=identifier) @@ -166,8 +169,15 @@ def create_program(self, *, scope = parameters else: parameters = dict(parameters) + to_int = numpy.int64 if self._CAST_INT_TO_INT64 else lambda x: x for parameter_name, value in parameters.items(): - if not isinstance(value, Number): + if type(value) is int: + # numpy casts ints to int32 per default on windows + # this can easily lead to overflows when times of the order of seconds + # are represented with integers + parameters[parameter_name] = to_int(value) + + elif not isinstance(value, Number): parameters[parameter_name] = Expression(value).evaluate_numeric() scope = DictScope(values=FrozenDict(parameters), volatile=volatile) From 03deed3ab5a0eb60ea6566172af71903591c0e26 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 29 Aug 2023 09:37:03 +0200 Subject: [PATCH 114/441] Skip seqc marker data test if prerequisites not installed --- tests/_program/seqc_tests.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/_program/seqc_tests.py b/tests/_program/seqc_tests.py index 75644fdfd..16c5b8be3 100644 --- a/tests/_program/seqc_tests.py +++ b/tests/_program/seqc_tests.py @@ -27,6 +27,10 @@ except ImportError: zhinst = None +try: + import numba +except ImportError: + numba = None def take(n, iterable): "Return first n items of the iterable as a list" @@ -63,6 +67,7 @@ def test_dynamic_rate_reduction(self): self.assertEqual(min(max_rate, n), dyn_n) + @unittest.skipIf(zhinst is None and numba is None, "BinaryWaveform.from_sampled backend missing") def test_marker_data(self): channel_1_data = np.linspace(-0.3, 0.4, num=192) channel_2_data = np.linspace(-0.1, 0.1, num=192) From d65a99ce21287262ced2cd8ead0f29ae57c9510b Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 29 Aug 2023 09:44:30 +0200 Subject: [PATCH 115/441] Make test skip dependent on exception instead of hard coding dependencies --- tests/_program/seqc_tests.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/_program/seqc_tests.py b/tests/_program/seqc_tests.py index 16c5b8be3..8761d496a 100644 --- a/tests/_program/seqc_tests.py +++ b/tests/_program/seqc_tests.py @@ -14,6 +14,7 @@ from qupulse.parameter_scope import DictScope from qupulse.program.loop import Loop +from qupulse.hardware.util import zhinst_voltage_to_uint16 from qupulse._program.waveforms import ConstantWaveform from qupulse._program.seqc import BinaryWaveform, loop_to_seqc, WaveformPlayback, Repeat, SteppingRepeat, Scope,\ to_node_clusters, find_sharable_waveforms, mark_sharable_waveforms, UserRegisterManager, HDAWGProgramManager,\ @@ -27,10 +28,15 @@ except ImportError: zhinst = None +# This block checks if zhinst_voltage_to_uint16 works. A failing implementation (due to missing dependencies) +# skips tests further down try: - import numba -except ImportError: - numba = None + zhinst_voltage_to_uint16(np.zeros(16), np.zeros(16), + (np.zeros(16), np.zeros(16), np.zeros(16), np.zeros(16))) +except AttributeError: + # prerequisites not installed + zhinst_voltage_to_uint16 = None + def take(n, iterable): "Return first n items of the iterable as a list" @@ -67,7 +73,7 @@ def test_dynamic_rate_reduction(self): self.assertEqual(min(max_rate, n), dyn_n) - @unittest.skipIf(zhinst is None and numba is None, "BinaryWaveform.from_sampled backend missing") + @unittest.skipIf(zhinst_voltage_to_uint16 is None, "BinaryWaveform.from_sampled backend missing") def test_marker_data(self): channel_1_data = np.linspace(-0.3, 0.4, num=192) channel_2_data = np.linspace(-0.1, 0.1, num=192) From 56ed176c70b4b31a29c8e9e079c1cc4bb5fbcc74 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 29 Aug 2023 09:45:52 +0200 Subject: [PATCH 116/441] Optimize imports --- tests/_program/seqc_tests.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/tests/_program/seqc_tests.py b/tests/_program/seqc_tests.py index 8761d496a..12522ac5c 100644 --- a/tests/_program/seqc_tests.py +++ b/tests/_program/seqc_tests.py @@ -1,26 +1,23 @@ -import unittest -from unittest import TestCase, mock -import time -from itertools import zip_longest, islice +import hashlib +import pathlib import sys import tempfile -import pathlib -import hashlib -import random +import time +import unittest +from itertools import zip_longest, islice +from unittest import TestCase, mock import numpy as np +from qupulse._program.seqc import BinaryWaveform, loop_to_seqc, WaveformPlayback, Repeat, SteppingRepeat, Scope, \ + to_node_clusters, find_sharable_waveforms, mark_sharable_waveforms, UserRegisterManager, HDAWGProgramManager, \ + UserRegister, WaveformFileSystem from qupulse.expressions import ExpressionScalar +from qupulse.hardware.util import zhinst_voltage_to_uint16 from qupulse.parameter_scope import DictScope - from qupulse.program.loop import Loop -from qupulse.hardware.util import zhinst_voltage_to_uint16 -from qupulse._program.waveforms import ConstantWaveform -from qupulse._program.seqc import BinaryWaveform, loop_to_seqc, WaveformPlayback, Repeat, SteppingRepeat, Scope,\ - to_node_clusters, find_sharable_waveforms, mark_sharable_waveforms, UserRegisterManager, HDAWGProgramManager,\ - UserRegister, WaveformFileSystem -from qupulse._program.volatile import VolatileRepetitionCount - +from qupulse.program.volatile import VolatileRepetitionCount +from qupulse.program.waveforms import ConstantWaveform from tests.pulses.sequencing_dummies import DummyWaveform try: From 67f77ba7bf8fc15fcb54f382bd9e88d7a65e4595 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 28 Sep 2023 12:59:46 +0200 Subject: [PATCH 117/441] Replace ParsedProgram with a dataclass for better type checking --- qupulse/_program/tabor.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/qupulse/_program/tabor.py b/qupulse/_program/tabor.py index b92c76f16..bb0b4a980 100644 --- a/qupulse/_program/tabor.py +++ b/qupulse/_program/tabor.py @@ -1,3 +1,4 @@ +import dataclasses import sys from typing import NamedTuple, Optional, List, Generator, Tuple, Sequence, Mapping, Union, Dict, FrozenSet, cast,\ Hashable @@ -645,12 +646,12 @@ def prepare_program_for_advanced_sequence_mode(program: Loop, min_seq_len: int, i += 1 -ParsedProgram = NamedTuple('ParsedProgram', [('advanced_sequencer_table', Sequence[TableEntry]), - ('sequencer_tables', Sequence[Sequence[ - Tuple[TableDescription, Optional[VolatileProperty]]]]), - ('waveforms', Tuple[Waveform, ...]), - ('volatile_parameter_positions', Dict[Union[int, Tuple[int, int]], - VolatileRepetitionCount])]) +@dataclasses.dataclass +class ParsedProgram: + advanced_sequencer_table: Sequence[TableEntry] + sequencer_tables: Sequence[Sequence[Tuple[TableDescription, Optional[VolatileProperty]]]] + waveforms: Tuple[Waveform, ...] + volatile_parameter_positions: Dict[Union[int, Tuple[int, int]], VolatileRepetitionCount] def parse_aseq_program(program: Loop, used_channels: FrozenSet[ChannelID]) -> ParsedProgram: From 1675f67350523a27611c7267031e377bcc7fb171 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 28 Sep 2023 13:00:58 +0200 Subject: [PATCH 118/441] Add segment deduplication to sampling code --- qupulse/_program/tabor.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/qupulse/_program/tabor.py b/qupulse/_program/tabor.py index bb0b4a980..5b96ac7f3 100644 --- a/qupulse/_program/tabor.py +++ b/qupulse/_program/tabor.py @@ -430,7 +430,7 @@ def __init__(self, else: self.setup_advanced_sequence_mode() - self._sampled_segments = self._calc_sampled_segments() + self._sampled_segments, self._waveform_to_segment = self._calc_sampled_segments() @property def markers(self) -> Tuple[Optional[ChannelID], Optional[ChannelID]]: @@ -464,7 +464,7 @@ def _marker_data(self, waveform: Waveform, time: np.ndarray, marker_idx: int): marker = self._markers[marker_idx] return waveform.get_sampled(channel=marker, sample_times=time) != 0 - def _calc_sampled_segments(self) -> Tuple[Sequence[TaborSegment], Sequence[int]]: + def _calc_sampled_segments(self) -> Tuple[Tuple[Sequence[TaborSegment], Sequence[int]], Sequence[int]]: """ Returns: (segments, segment_lengths) @@ -474,7 +474,8 @@ def _calc_sampled_segments(self) -> Tuple[Sequence[TaborSegment], Sequence[int]] if np.any(segment_lengths % 16 > 0) or np.any(segment_lengths < 192): raise TaborException('At least one waveform has a length that is smaller 192 or not a multiple of 16') - segments = [] + segments: Dict[TaborSegment, int] = {} + waveform_to_segment = [] for i, waveform in enumerate(self._parsed_program.waveforms): t = time_array[:segment_lengths[i]] marker_time = t[::2] @@ -488,8 +489,9 @@ def _calc_sampled_segments(self) -> Tuple[Sequence[TaborSegment], Sequence[int]] ch_b=segment_b, marker_a=marker_a, marker_b=marker_b) - segments.append(segment) - return segments, segment_lengths + segment_idx = segments.setdefault(segment, len(segments)) + waveform_to_segment.append(segment_idx) + return (list(segments.keys()), segment_lengths), waveform_to_segment def setup_single_sequence_mode(self) -> None: assert self.program.depth() == 1 @@ -556,10 +558,13 @@ def update_volatile_parameters(self, parameters: Mapping[str, numbers.Number]) - return modifications - def get_sequencer_tables(self): # -> List[List[TableDescription, Optional[MappedParameter]]]: - return self._parsed_program.sequencer_tables + def get_sequencer_tables(self) -> Sequence[Sequence[Tuple[TableDescription, Optional[VolatileProperty]]]]: + wf_to_seq = self._waveform_to_segment + return [[((rep_count, wf_to_seq[elem_idx], jump), volatile) + for (rep_count, elem_idx, jump), volatile in sequencer_table] + for sequencer_table in self._parsed_program.sequencer_tables] - def get_advanced_sequencer_table(self) -> List[TableEntry]: + def get_advanced_sequencer_table(self) -> Sequence[TableEntry]: """Advanced sequencer table that can be used via the download_adv_seq_table pytabor command""" return self._parsed_program.advanced_sequencer_table From def4627f7d4190c7827b94f64a26eaccd091f5ba Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 28 Sep 2023 13:12:20 +0200 Subject: [PATCH 119/441] Fix auto close on backend switch deprecation warning --- tests/pulses/plotting_tests.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/tests/pulses/plotting_tests.py b/tests/pulses/plotting_tests.py index fdceac210..ea3765c89 100644 --- a/tests/pulses/plotting_tests.py +++ b/tests/pulses/plotting_tests.py @@ -14,6 +14,12 @@ from tests.pulses.sequencing_dummies import DummyWaveform, DummyPulseTemplate +def use_svg_backend(): + import matplotlib.pyplot + matplotlib.pyplot.close('all') + matplotlib.use('svg') + + class PlotterTests(unittest.TestCase): def test_render_loop_sliced(self) -> None: wf = DummyWaveform(duration=19) @@ -87,17 +93,18 @@ def integrated_test_with_sequencer_and_pulse_templates(self) -> None: self.assertEqual(expected_voltages, voltages) def test_plot_empty_pulse(self) -> None: - import matplotlib - matplotlib.use('svg') # use non-interactive backend so that test does not fail on travis + # use non-interactive backend so that test does not fail on travis + use_svg_backend() pt = DummyPulseTemplate() with self.assertWarnsRegex(UserWarning, "empty", msg="plot() did not issue a warning for an empty pulse"): plot(pt, dict(), show=False) def test_plot_pulse_automatic_sample_rate(self) -> None: - import matplotlib - matplotlib.use('svg') # use non-interactive backend so that test does not fail on travis - pt=ConstantPT(100, {'a': 1}) + # use non-interactive backend so that test does not fail on travis + use_svg_backend() + + pt = ConstantPT(100, {'a': 1}) plot(pt, sample_rate=None) def test_bug_447(self): @@ -132,8 +139,8 @@ def test(self) -> None: class PlottingIsinstanceTests(unittest.TestCase): @unittest.skip("Breaks other tests") def test_bug_422(self): - import matplotlib - matplotlib.use('svg') # use non-interactive backend so that test does not fail on travis + # use non-interactive backend so that test does not fail on travis + use_svg_backend() to_reload = ['qupulse._program._loop', 'qupulse.pulses.pulse_template', From af83e326e31686a3b143609fd825ca8315bf465e Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 28 Sep 2023 14:20:33 +0200 Subject: [PATCH 120/441] Fix DummyWaveform hash for float outputs --- tests/pulses/sequencing_dummies.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/pulses/sequencing_dummies.py b/tests/pulses/sequencing_dummies.py index 18ca487ce..b94fa1f72 100644 --- a/tests/pulses/sequencing_dummies.py +++ b/tests/pulses/sequencing_dummies.py @@ -47,7 +47,8 @@ def compare_key(self) -> Any: except AttributeError: pass return hash( - tuple(sorted((channel, output.tobytes()) for channel, output in self.sample_output.items())) + tuple(sorted((channel, getattr(output, 'tobytes', lambda: output)()) + for channel, output in self.sample_output.items())) ) else: return id(self) From 10ed70f369b359d05c66bb8a976b19b891e9b929 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 28 Sep 2023 14:21:12 +0200 Subject: [PATCH 121/441] Fix get_sequener_table entry type --- qupulse/_program/tabor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qupulse/_program/tabor.py b/qupulse/_program/tabor.py index 5b96ac7f3..e4e54b725 100644 --- a/qupulse/_program/tabor.py +++ b/qupulse/_program/tabor.py @@ -560,7 +560,7 @@ def update_volatile_parameters(self, parameters: Mapping[str, numbers.Number]) - def get_sequencer_tables(self) -> Sequence[Sequence[Tuple[TableDescription, Optional[VolatileProperty]]]]: wf_to_seq = self._waveform_to_segment - return [[((rep_count, wf_to_seq[elem_idx], jump), volatile) + return [[(TableDescription(rep_count, wf_to_seq[elem_idx], jump), volatile) for (rep_count, elem_idx, jump), volatile in sequencer_table] for sequencer_table in self._parsed_program.sequencer_tables] From 2a9a83beec6a2f1c9245deb78a4a5e23ce710bbc Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 28 Sep 2023 14:22:01 +0200 Subject: [PATCH 122/441] Make differing waveforms produce differing outputs in tabor tests --- tests/_program/tabor_tests.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/_program/tabor_tests.py b/tests/_program/tabor_tests.py index 919147ec5..c90b48aa1 100644 --- a/tests/_program/tabor_tests.py +++ b/tests/_program/tabor_tests.py @@ -232,8 +232,8 @@ def test_depth_1_single_waveform(self): self.assertEqual(t_program.get_advanced_sequencer_table(), [TableDescription(1, 1, 0)]) def test_depth_1_single_sequence(self): - program = Loop(children=[Loop(waveform=DummyWaveform(defined_channels={'A'}, duration=1), repetition_count=3), - Loop(waveform=DummyWaveform(defined_channels={'A'}, duration=1), repetition_count=4)], + program = Loop(children=[Loop(waveform=DummyWaveform(sample_output={'A': 0.1}, duration=1), repetition_count=3), + Loop(waveform=DummyWaveform(sample_output={'A': 0.2}, duration=1), repetition_count=4)], repetition_count=1) t_program = TaborProgram(program, channels=(None, 'A'), markers=(None, None), @@ -247,8 +247,8 @@ def test_depth_1_single_sequence(self): def test_depth_1_single_sequence_2(self): """Use the same wf twice""" - wf_1 = DummyWaveform(defined_channels={'A'}, duration=1) - wf_2 = DummyWaveform(defined_channels={'A'}, duration=1) + wf_1 = DummyWaveform(sample_output={'A': 0.1}, duration=1) + wf_2 = DummyWaveform(sample_output={'A': 0.2}, duration=1) program = Loop(children=[Loop(waveform=wf_1, repetition_count=3), Loop(waveform=wf_2, repetition_count=4), @@ -266,8 +266,8 @@ def test_depth_1_single_sequence_2(self): self.assertEqual(t_program.get_advanced_sequencer_table(), [TableDescription(1, 1, 0)]) def test_depth_1_advanced_sequence_unroll(self): - wf_1 = DummyWaveform(defined_channels={'A'}, duration=1) - wf_2 = DummyWaveform(defined_channels={'A'}, duration=1) + wf_1 = DummyWaveform(sample_output={'A': 0.1}, duration=1) + wf_2 = DummyWaveform(sample_output={'A': 0.2}, duration=1) program = Loop(children=[Loop(waveform=wf_1, repetition_count=3), Loop(waveform=wf_2, repetition_count=4)], @@ -285,8 +285,8 @@ def test_depth_1_advanced_sequence_unroll(self): self.assertEqual(t_program.get_advanced_sequencer_table(), [TableEntry(5, 1, 0)]) def test_depth_1_advanced_sequence(self): - wf_1 = DummyWaveform(defined_channels={'A'}, duration=1) - wf_2 = DummyWaveform(defined_channels={'A'}, duration=1) + wf_1 = DummyWaveform(sample_output={'A': 0.1}, duration=1) + wf_2 = DummyWaveform(sample_output={'A': 0.2}, duration=1) program = Loop(children=[Loop(waveform=wf_1, repetition_count=3), Loop(waveform=wf_2, repetition_count=4), @@ -385,8 +385,8 @@ def test_update_volatile_parameters_with_depth1(self): s = VolatileRepetitionCount(expression=ExpressionScalar('s'), scope=DictScope(values=FrozenDict(s=3), volatile=set('s'))) - wf_1 = DummyWaveform(defined_channels={'A'}, duration=1) - wf_2 = DummyWaveform(defined_channels={'A'}, duration=1) + wf_1 = DummyWaveform(sample_output={'A': 0.1}, duration=1) + wf_2 = DummyWaveform(sample_output={'A': 0.2}, duration=1) program = Loop(children=[Loop(waveform=wf_1, repetition_count=s), Loop(waveform=wf_2, repetition_count=4), @@ -419,8 +419,8 @@ def test_update_volatile_parameters_with_depth2(self): a = VolatileRepetitionCount(expression=ExpressionScalar('a'), scope=DictScope(values=FrozenDict(a=5), volatile=set('a'))) - wf_1 = DummyWaveform(defined_channels={'A'}, duration=1) - wf_2 = DummyWaveform(defined_channels={'A'}, duration=1) + wf_1 = DummyWaveform(sample_output={'A': 0.1}, duration=1) + wf_2 = DummyWaveform(sample_output={'A': 0.2}, duration=1) program = Loop(children=[Loop(children=[Loop(waveform=wf_1, repetition_count=s), Loop(waveform=wf_2, repetition_count=4), From 11b072600629b4f6e38f47c595a2e4f35463240f Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 28 Sep 2023 15:55:45 +0200 Subject: [PATCH 123/441] Remove gmpy build dependencies --- .github/workflows/pythontest.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/pythontest.yaml b/.github/workflows/pythontest.yaml index 6f7f0b745..64a7c27cd 100644 --- a/.github/workflows/pythontest.yaml +++ b/.github/workflows/pythontest.yaml @@ -29,10 +29,9 @@ jobs: steps: - - name: Prepare gmpy2 build dependencies + - name: Add gmpy extra feature if: ${{ matrix.time-type }} == 'gmpy2' run: | - sudo apt-get install -y libgmp-dev libmpfr-dev libmpc-dev echo "INSTALL_EXTRAS=${{ env.INSTALL_EXTRAS }},Faster-fractions" >> $GITHUB_ENV - name: Checkout repository From 2db12a37c0392e9a6db3842815da98718e64127d Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 29 Sep 2023 08:01:29 +0200 Subject: [PATCH 124/441] Trim segement lengths to correct number of elements --- qupulse/_program/tabor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qupulse/_program/tabor.py b/qupulse/_program/tabor.py index e4e54b725..00492c28a 100644 --- a/qupulse/_program/tabor.py +++ b/qupulse/_program/tabor.py @@ -491,7 +491,7 @@ def _calc_sampled_segments(self) -> Tuple[Tuple[Sequence[TaborSegment], Sequence marker_b=marker_b) segment_idx = segments.setdefault(segment, len(segments)) waveform_to_segment.append(segment_idx) - return (list(segments.keys()), segment_lengths), waveform_to_segment + return (list(segments.keys()), segment_lengths[:len(segments)]), waveform_to_segment def setup_single_sequence_mode(self) -> None: assert self.program.depth() == 1 From 5b4d9112620db852700523303f0aafee1f3e34f2 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 29 Sep 2023 13:47:51 +0200 Subject: [PATCH 125/441] Add deduplication test --- tests/_program/tabor_tests.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/_program/tabor_tests.py b/tests/_program/tabor_tests.py index c90b48aa1..cd171e373 100644 --- a/tests/_program/tabor_tests.py +++ b/tests/_program/tabor_tests.py @@ -380,6 +380,23 @@ def my_gen(gen): np.testing.assert_equal(sampled_seg.ch_a, data[0]) np.testing.assert_equal(sampled_seg.ch_b, data[1]) + def test_calc_sampled_segments_deduplication(self): + wf1 = ConstantWaveform(duration=1, amplitude=0.1, channel='A') + wf2 = ConstantWaveform(duration=1, amplitude=0.2, channel='A') + wf3 = SubsetWaveform( + ConstantWaveform.from_mapping(duration=1, constant_values={'A': 0.1, 'B': 0.2}), + {'A'} + ) + loop = Loop(children=[ + Loop(waveform=wf1), + Loop(waveform=wf2), + Loop(waveform=wf3), + ]) + prog = TaborProgram(loop, self.instr_props, ('A', None), (None, None), **self.program_entry_kwargs) + sampled, sampled_length = prog.get_sampled_segments() + self.assertEqual(len(sampled), 2) + self.assertEqual([192, 192], list(sampled_length)) + def test_update_volatile_parameters_with_depth1(self): parameters = {'s': 10, 'not': 13} s = VolatileRepetitionCount(expression=ExpressionScalar('s'), scope=DictScope(values=FrozenDict(s=3), From 8b13e0044aece9b35f0dfa7369ee4b8cb62607b8 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 29 Sep 2023 13:48:02 +0200 Subject: [PATCH 126/441] Adjust docstring --- qupulse/_program/tabor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qupulse/_program/tabor.py b/qupulse/_program/tabor.py index 00492c28a..7ded91977 100644 --- a/qupulse/_program/tabor.py +++ b/qupulse/_program/tabor.py @@ -467,7 +467,7 @@ def _marker_data(self, waveform: Waveform, time: np.ndarray, marker_idx: int): def _calc_sampled_segments(self) -> Tuple[Tuple[Sequence[TaborSegment], Sequence[int]], Sequence[int]]: """ Returns: - (segments, segment_lengths) + ((segments, segment_lengths), waveform_to_segment) """ time_array, segment_lengths = get_sample_times(self._parsed_program.waveforms, self._sample_rate) From 691052eace21617dcb3ded1afbe53a2134b7b16a Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 29 Sep 2023 15:48:12 +0200 Subject: [PATCH 127/441] Test that the correct segment lengths are selected --- tests/_program/tabor_tests.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/_program/tabor_tests.py b/tests/_program/tabor_tests.py index cd171e373..5a5b7b5a3 100644 --- a/tests/_program/tabor_tests.py +++ b/tests/_program/tabor_tests.py @@ -381,12 +381,13 @@ def my_gen(gen): np.testing.assert_equal(sampled_seg.ch_b, data[1]) def test_calc_sampled_segments_deduplication(self): - wf1 = ConstantWaveform(duration=1, amplitude=0.1, channel='A') - wf2 = ConstantWaveform(duration=1, amplitude=0.2, channel='A') - wf3 = SubsetWaveform( - ConstantWaveform.from_mapping(duration=1, constant_values={'A': 0.1, 'B': 0.2}), + wf1 = ConstantWaveform(duration=2, amplitude=0.1, channel='A') + wf2 = SubsetWaveform( + ConstantWaveform.from_mapping(duration=2, constant_values={'A': 0.1, 'B': 0.2}), {'A'} ) + wf3 = ConstantWaveform(duration=1, amplitude=0.2, channel='A') + loop = Loop(children=[ Loop(waveform=wf1), Loop(waveform=wf2), @@ -395,7 +396,7 @@ def test_calc_sampled_segments_deduplication(self): prog = TaborProgram(loop, self.instr_props, ('A', None), (None, None), **self.program_entry_kwargs) sampled, sampled_length = prog.get_sampled_segments() self.assertEqual(len(sampled), 2) - self.assertEqual([192, 192], list(sampled_length)) + self.assertEqual([192 * 2, 192], list(sampled_length)) def test_update_volatile_parameters_with_depth1(self): parameters = {'s': 10, 'not': 13} From d9a48c4803d055ef28c3c6f76a910c9e33db135c Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 29 Sep 2023 16:09:04 +0200 Subject: [PATCH 128/441] Correct segment length tracking for deduplicated segments --- qupulse/_program/tabor.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/qupulse/_program/tabor.py b/qupulse/_program/tabor.py index 7ded91977..235cc945c 100644 --- a/qupulse/_program/tabor.py +++ b/qupulse/_program/tabor.py @@ -469,15 +469,17 @@ def _calc_sampled_segments(self) -> Tuple[Tuple[Sequence[TaborSegment], Sequence Returns: ((segments, segment_lengths), waveform_to_segment) """ - time_array, segment_lengths = get_sample_times(self._parsed_program.waveforms, self._sample_rate) + time_array, waveform_samples = get_sample_times(self._parsed_program.waveforms, self._sample_rate) - if np.any(segment_lengths % 16 > 0) or np.any(segment_lengths < 192): + if np.any(waveform_samples % 16 > 0) or np.any(waveform_samples < 192): raise TaborException('At least one waveform has a length that is smaller 192 or not a multiple of 16') segments: Dict[TaborSegment, int] = {} + segment_lengths = [] + waveform_to_segment = [] - for i, waveform in enumerate(self._parsed_program.waveforms): - t = time_array[:segment_lengths[i]] + for waveform, n_samples in zip(self._parsed_program.waveforms, waveform_samples): + t = time_array[:n_samples] marker_time = t[::2] segment_a = self._channel_data(waveform, t, 0) segment_b = self._channel_data(waveform, t, 1) @@ -489,9 +491,12 @@ def _calc_sampled_segments(self) -> Tuple[Tuple[Sequence[TaborSegment], Sequence ch_b=segment_b, marker_a=marker_a, marker_b=marker_b) - segment_idx = segments.setdefault(segment, len(segments)) + previous_segment_count = len(segments) + segment_idx = segments.setdefault(segment, previous_segment_count) waveform_to_segment.append(segment_idx) - return (list(segments.keys()), segment_lengths[:len(segments)]), waveform_to_segment + if segment_idx == previous_segment_count: + segment_lengths.append(n_samples) + return (list(segments.keys()), np.array(segment_lengths, dtype=np.uint64)), waveform_to_segment def setup_single_sequence_mode(self) -> None: assert self.program.depth() == 1 From c8ef798e541df92c36219520d6fdc1da5b8bfa65 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Sat, 30 Sep 2023 11:33:38 +0200 Subject: [PATCH 129/441] Missing imports in tests --- tests/_program/tabor_tests.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/_program/tabor_tests.py b/tests/_program/tabor_tests.py index 5a5b7b5a3..bb1b7386b 100644 --- a/tests/_program/tabor_tests.py +++ b/tests/_program/tabor_tests.py @@ -19,6 +19,7 @@ from qupulse._program.tabor import TaborException, TaborProgram, find_place_for_segments_in_memory,\ TaborSegment, TaborSequencing, PlottableProgram, TableDescription, make_combined_wave, TableEntry from qupulse.program.loop import Loop +from qupulse.program.waveforms import ConstantWaveform, SubsetWaveform from qupulse._program.volatile import VolatileRepetitionCount from qupulse.hardware.util import voltage_to_uint16 from qupulse.utils.types import TimeType From b07d801dbb54e4a4e24fc1952bab00a06474179e Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Wed, 4 Oct 2023 10:26:44 +0200 Subject: [PATCH 130/441] Add PulseTemplate.pad_to --- qupulse/pulses/pulse_template.py | 37 ++++++++++++++++++++++++++++ tests/pulses/pulse_template_tests.py | 2 ++ 2 files changed, 39 insertions(+) diff --git a/qupulse/pulses/pulse_template.py b/qupulse/pulses/pulse_template.py index 5da9a2b56..a97b2f4b1 100644 --- a/qupulse/pulses/pulse_template.py +++ b/qupulse/pulses/pulse_template.py @@ -383,6 +383,43 @@ def with_appended(self, *appended: 'PulseTemplate'): else: return self + def pad_to(self, to_new_duration: Union[ExpressionLike, Callable[[Expression], Expression]], + pt_kwargs: Mapping[str, Any] = None) -> 'PulseTemplate': + """ + Examples: + # pad to a fixed duration + padded_1 = my_pt.pad_to(1000) + + # pad to a fixed sample coun + padded_2 = my_pt.pad_to('sample_rate * 1000') + + # pad to the next muliple of 16 samples with a symbolic sample rate + padded_3 = my_pt.pad_to(to_next_multiple('sample_rate', 16)) + + # pad to the next muliple of 16 samples with a fixed sample rate of 1 GHz + padded_4 = my_pt.pad_to(to_next_multiple(1, 16)) + Args: + to_new_duration: Duration or callable that maps the current duration to the new duration + pt_kwargs: Keyword arguments for the newly created sequence pulse template. + + Returns: + + """ + from qupulse.pulses import ConstantPT, SequencePT + current_duration = self.duration + if callable(to_new_duration): + new_duration = to_new_duration(current_duration) + else: + new_duration = ExpressionScalar(to_new_duration) + pad_duration = new_duration - current_duration + if not pt_kwargs and pad_duration == 0: + return self + pad_pt = ConstantPT(pad_duration, self.final_values) + if pt_kwargs: + return SequencePT(self, pad_pt, **pt_kwargs) + else: + return self @ pad_pt + def __format__(self, format_spec: str): if format_spec == '': format_spec = self._DEFAULT_FORMAT_SPEC diff --git a/tests/pulses/pulse_template_tests.py b/tests/pulses/pulse_template_tests.py index 8d4a58716..145f726e4 100644 --- a/tests/pulses/pulse_template_tests.py +++ b/tests/pulses/pulse_template_tests.py @@ -343,6 +343,8 @@ def test_create_program_volatile(self): _internal_create_program.assert_called_once_with(**expected_internal_kwargs, parent_loop=Loop()) + def test_pad_to(self): + raise NotImplementedError("TODO") def test_create_program_none(self) -> None: template = PulseTemplateStub(defined_channels={'A'}, parameter_names={'foo'}) From d1477e4c538130c4e1067236140b5bce4183eef1 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Wed, 11 Oct 2023 11:09:32 +0200 Subject: [PATCH 131/441] Add pad_to tests --- tests/pulses/pulse_template_tests.py | 79 ++++++++++++++++++++++++++-- 1 file changed, 76 insertions(+), 3 deletions(-) diff --git a/tests/pulses/pulse_template_tests.py b/tests/pulses/pulse_template_tests.py index 145f726e4..876c47cf7 100644 --- a/tests/pulses/pulse_template_tests.py +++ b/tests/pulses/pulse_template_tests.py @@ -3,6 +3,8 @@ from unittest import mock from typing import Optional, Dict, Set, Any, Union + +import frozendict import sympy from qupulse.parameter_scope import Scope, DictScope @@ -23,12 +25,14 @@ class PulseTemplateStub(PulseTemplate): """All abstract methods are stubs that raise NotImplementedError to catch unexpected calls. If a method is needed in - a test one should use mock.patch or mock.patch.object""" + a test one should use mock.patch or mock.patch.object. + Properties can be passed as init argument because mocking them is a pita.""" def __init__(self, identifier=None, defined_channels=None, duration=None, parameter_names=None, measurement_names=None, + final_values=None, registry=None): super().__init__(identifier=identifier) @@ -36,6 +40,7 @@ def __init__(self, identifier=None, self._duration = duration self._parameter_names = parameter_names self._measurement_names = set() if measurement_names is None else measurement_names + self._final_values = final_values self.internal_create_program_args = [] self._register(registry=registry) @@ -89,7 +94,10 @@ def initial_values(self) -> Dict[ChannelID, ExpressionScalar]: @property def final_values(self) -> Dict[ChannelID, ExpressionScalar]: - raise NotImplementedError() + if self._final_values is None: + raise NotImplementedError() + else: + return self._final_values def get_appending_internal_create_program(waveform=DummyWaveform(), @@ -344,7 +352,72 @@ def test_create_program_volatile(self): _internal_create_program.assert_called_once_with(**expected_internal_kwargs, parent_loop=Loop()) def test_pad_to(self): - raise NotImplementedError("TODO") + from qupulse.pulses import SequencePT + + def to_multiple_of_192(x: Expression) -> Expression: + return (x + 191) // 192 * 192 + + final_values = frozendict.frozendict({'A': ExpressionScalar(0.1), 'B': ExpressionScalar('a')}) + measurements = [('M', 0, 'y')] + + pt = PulseTemplateStub(duration=ExpressionScalar(10)) + padded = pt.pad_to(10) + self.assertIs(pt, padded) + + pt = PulseTemplateStub(duration=ExpressionScalar('duration')) + padded = pt.pad_to('duration') + self.assertIs(pt, padded) + + # padding with numeric durations + + pt = PulseTemplateStub(duration=ExpressionScalar(10), + final_values=final_values, + defined_channels=final_values.keys()) + padded = pt.pad_to(20) + self.assertEqual(padded.duration, 20) + self.assertEqual(padded.final_values, final_values) + self.assertIsInstance(padded, SequencePT) + self.assertIs(padded.subtemplates[0], pt) + + padded = pt.pad_to(20, pt_kwargs=dict(measurements=measurements)) + self.assertEqual(padded.duration, 20) + self.assertEqual(padded.final_values, final_values) + self.assertIsInstance(padded, SequencePT) + self.assertIs(padded.subtemplates[0], pt) + self.assertEqual(measurements, padded.measurement_declarations) + + padded = pt.pad_to(10, pt_kwargs=dict(measurements=measurements)) + self.assertEqual(padded.duration, 10) + self.assertEqual(padded.final_values, final_values) + self.assertIsInstance(padded, SequencePT) + self.assertIs(padded.subtemplates[0], pt) + self.assertEqual(measurements, padded.measurement_declarations) + + # padding with numeric duation and callable + padded = pt.pad_to(to_multiple_of_192) + self.assertEqual(padded.duration, 192) + self.assertEqual(padded.final_values, final_values) + self.assertIsInstance(padded, SequencePT) + self.assertIs(padded.subtemplates[0], pt) + + # padding with symbolic durations + + pt = PulseTemplateStub(duration=ExpressionScalar('duration'), + final_values=final_values, + defined_channels=final_values.keys()) + padded = pt.pad_to('new_duration') + self.assertEqual(padded.duration, 'new_duration') + self.assertEqual(padded.final_values, final_values) + self.assertIsInstance(padded, SequencePT) + self.assertIs(padded.subtemplates[0], pt) + + # padding symbolic durations with callable + + padded = pt.pad_to(to_multiple_of_192) + self.assertEqual(padded.duration, '(duration + 191) // 192 * 192') + self.assertEqual(padded.final_values, final_values) + self.assertIsInstance(padded, SequencePT) + self.assertIs(padded.subtemplates[0], pt) def test_create_program_none(self) -> None: template = PulseTemplateStub(defined_channels={'A'}, parameter_names={'foo'}) From 4290e25b93144a7c41bc6191af151df52c5fb0cc Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Wed, 11 Oct 2023 11:10:45 +0200 Subject: [PATCH 132/441] Add floordiv implementation to sympy expression wrapper --- qupulse/expressions/sympy.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/qupulse/expressions/sympy.py b/qupulse/expressions/sympy.py index eeb64ee97..150a1b6a5 100644 --- a/qupulse/expressions/sympy.py +++ b/qupulse/expressions/sympy.py @@ -401,6 +401,12 @@ def __truediv__(self, other: Union['ExpressionScalar', Number, sympy.Expr]) -> ' def __rtruediv__(self, other: Union['ExpressionScalar', Number, sympy.Expr]) -> 'ExpressionScalar': return self.make(self._sympified_expression.__rtruediv__(self._extract_sympified(other))) + def __floordiv__(self, other: Union['ExpressionScalar', Number, sympy.Expr]) -> 'ExpressionScalar': + return self.make(self._sympified_expression.__floordiv__(self._extract_sympified(other))) + + def __rfloordiv__(self, other: Union['ExpressionScalar', Number, sympy.Expr]) -> 'ExpressionScalar': + return self.make(self._sympified_expression.__rfloordiv__(self._extract_sympified(other))) + def __neg__(self) -> 'ExpressionScalar': return self.make(self._sympified_expression.__neg__()) From 319fd18361676d8dbb18c073b3de45af32b2bf22 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Wed, 11 Oct 2023 11:24:16 +0200 Subject: [PATCH 133/441] Add floordiv tests --- tests/expressions/expression_tests.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/expressions/expression_tests.py b/tests/expressions/expression_tests.py index 194434935..e7fd5110a 100644 --- a/tests/expressions/expression_tests.py +++ b/tests/expressions/expression_tests.py @@ -391,6 +391,9 @@ def test_number_math(self): self.assertExpressionEqual(a - b, -(b - a)) self.assertExpressionEqual(a * b, b * a) self.assertExpressionEqual(a / b, 1 / (b / a)) + self.assertExpressionEqual(a // 3, ExpressionScalar('floor(a / 3)')) + self.assertExpressionEqual(a // 3, ExpressionScalar('a // 3')) + self.assertExpressionEqual(3 // a, ExpressionScalar('floor(3 / a)')) def test_symbolic_math(self): a = ExpressionScalar('a') @@ -400,6 +403,7 @@ def test_symbolic_math(self): self.assertExpressionEqual(a - b, -(b - a)) self.assertExpressionEqual(a * b, b * a) self.assertExpressionEqual(a / b, 1 / (b / a)) + self.assertExpressionEqual(a // b, ExpressionScalar('floor(a / b)')) def test_sympy_math(self): a = ExpressionScalar('a') From 72d6af8532e736bb75d5328fb30fc62a4383aec2 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Wed, 11 Oct 2023 11:53:25 +0200 Subject: [PATCH 134/441] Better docstring --- qupulse/pulses/pulse_template.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/qupulse/pulses/pulse_template.py b/qupulse/pulses/pulse_template.py index a97b2f4b1..91f0f39ac 100644 --- a/qupulse/pulses/pulse_template.py +++ b/qupulse/pulses/pulse_template.py @@ -383,27 +383,31 @@ def with_appended(self, *appended: 'PulseTemplate'): else: return self - def pad_to(self, to_new_duration: Union[ExpressionLike, Callable[[Expression], Expression]], + def pad_to(self, to_new_duration: Union[ExpressionLike, Callable[[Expression], ExpressionLike]], pt_kwargs: Mapping[str, Any] = None) -> 'PulseTemplate': - """ + """Pad this pulse template to the given duration. + The target duration can be numeric, symbolic or a callable that returns a new duration from the current + duration. + Examples: # pad to a fixed duration - padded_1 = my_pt.pad_to(1000) + >>> padded_1 = my_pt.pad_to(1000) # pad to a fixed sample coun - padded_2 = my_pt.pad_to('sample_rate * 1000') + >>> padded_2 = my_pt.pad_to('sample_rate * 1000') # pad to the next muliple of 16 samples with a symbolic sample rate - padded_3 = my_pt.pad_to(to_next_multiple('sample_rate', 16)) + >>> padded_3 = my_pt.pad_to(to_next_multiple('sample_rate', 16)) # pad to the next muliple of 16 samples with a fixed sample rate of 1 GHz - padded_4 = my_pt.pad_to(to_next_multiple(1, 16)) + >>> padded_4 = my_pt.pad_to(to_next_multiple(1, 16)) Args: to_new_duration: Duration or callable that maps the current duration to the new duration pt_kwargs: Keyword arguments for the newly created sequence pulse template. Returns: - + A pulse template that has the duration given by ``to_new_duration``. It can be ``self`` if the duration is + already as required. It is never ``self`` if ``pt_kwargs`` is non-empty. """ from qupulse.pulses import ConstantPT, SequencePT current_duration = self.duration From f06cd64c77ddce223e63c027777ef5928bfbae78 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Wed, 11 Oct 2023 11:56:41 +0200 Subject: [PATCH 135/441] Newspiece --- changes.d/801.feature | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changes.d/801.feature diff --git a/changes.d/801.feature b/changes.d/801.feature new file mode 100644 index 000000000..fa703198f --- /dev/null +++ b/changes.d/801.feature @@ -0,0 +1,2 @@ +Add ``PulseTemplate.pad_to`` method to help padding to minimal lengths or multiples of given durations. + \ No newline at end of file From f84b587d430c9a9322751507c077dc9aae521b5d Mon Sep 17 00:00:00 2001 From: Nomos11 <82180697+Nomos11@users.noreply.github.com> Date: Fri, 13 Oct 2023 15:04:39 +0200 Subject: [PATCH 136/441] Exemplary to_next_multiple could also be put somewhere else --- qupulse/utils/__init__.py | 36 +++++++++++++++++++++++++++++++-- tests/utils/utils_tests.py | 41 +++++++++++++++++++++++++++++++++++++- 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/qupulse/utils/__init__.py b/qupulse/utils/__init__.py index 43de28c1c..8cc9e593b 100644 --- a/qupulse/utils/__init__.py +++ b/qupulse/utils/__init__.py @@ -1,11 +1,14 @@ """This package contains utility functions and classes as well as custom sympy extensions(hacks).""" -from typing import Union, Iterable, Any, Tuple, Mapping, Iterator, TypeVar, Sequence, AbstractSet +from typing import Union, Iterable, Any, Tuple, Mapping, Iterator, TypeVar, Sequence, AbstractSet, Optional, Callable import itertools import re import numbers from collections import OrderedDict from frozendict import frozendict +from qupulse.expressions import ExpressionScalar, ExpressionLike +from qupulse.utils.types import TimeType +from sympy import Max, sign import numpy @@ -25,7 +28,7 @@ __all__ = ["checked_int_cast", "is_integer", "isclose", "pairwise", "replace_multiple", "cached_property", - "forced_hash"] + "forced_hash", "to_next_multiple"] def checked_int_cast(x: Union[float, int, numpy.ndarray], epsilon: float=1e-6) -> int: @@ -122,3 +125,32 @@ def forced_hash(obj) -> int: return hash(tuple(map(forced_hash, obj))) raise + + +def to_next_multiple(sample_rate: ExpressionLike, quantum: int, + min_quanta: Optional[int] = None) -> Callable[[ExpressionLike],ExpressionScalar]: + """Construct a helper function to expand a duration to one corresponding to + valid sample multiples according to the arguments given. + Useful e.g. for PulseTemplate.pad_to's 'to_new_duration'-argument. + + Args: + sample_rate: sample rate with respect to which the duration is evaluated. + quantum: number of samples to whose next integer multiple the duration shall be rounded up to. + min_quanta: number of multiples of quantum not to fall short of. + Returns: + A function that takes a duration (ExpressionLike) as input, and returns + a duration rounded up to the next valid samples count in given sample rate. + The function returns 0 if duration==0, <0 is not checked if min_quanta=1. + + """ + sample_rate = ExpressionScalar(sample_rate) + #is it more efficient to omit the Max call if not necessary? + if min_quanta is None: + #double negative for ceil division. + return lambda duration: -(-(duration*sample_rate)//quantum) * (quantum/sample_rate) + else: + #still return 0 if duration==0 + return lambda duration: ExpressionScalar((quantum/sample_rate)*\ + Max(-(-(ExpressionScalar(duration)*sample_rate)//quantum),min_quanta)\ + *Max(0,sign(ExpressionScalar(duration)))) + \ No newline at end of file diff --git a/tests/utils/utils_tests.py b/tests/utils/utils_tests.py index 6ec75092c..83e1a26aa 100644 --- a/tests/utils/utils_tests.py +++ b/tests/utils/utils_tests.py @@ -2,7 +2,7 @@ from unittest import mock from collections import OrderedDict -from qupulse.utils import checked_int_cast, replace_multiple, _fallback_pairwise +from qupulse.utils import checked_int_cast, replace_multiple, _fallback_pairwise, to_next_multiple class PairWiseTest(unittest.TestCase): @@ -102,3 +102,42 @@ def test_replace_multiple_overlap(self): replacements = OrderedDict(reversed(replacement_list)) result = replace_multiple('asdf', replacements) self.assertEqual(result, '2') + + +class ToNextMultipleTests(unittest.TestCase): + def test_to_next_multiple(self): + from qupulse.utils.types import TimeType + from qupulse.expressions import ExpressionScalar + + duration = TimeType.from_float(47.1415926535) + evaluated = to_next_multiple(sample_rate=TimeType.from_float(2.4),quantum=16)(duration) + expected = ExpressionScalar('160/3') + self.assertEqual(evaluated, expected) + + duration = TimeType.from_float(3.1415926535) + evaluated = to_next_multiple(sample_rate=TimeType.from_float(2.4),quantum=16,min_quanta=13)(duration) + expected = ExpressionScalar('260/3') + self.assertEqual(evaluated, expected) + + duration = 6185240.0000001 + evaluated = to_next_multiple(sample_rate=1.0,quantum=16,min_quanta=13)(duration) + expected = 6185248 + self.assertEqual(evaluated, expected) + + duration = 0. + evaluated = to_next_multiple(sample_rate=1.0,quantum=16,min_quanta=13)(duration) + expected = 0. + self.assertEqual(evaluated, expected) + + duration = ExpressionScalar('abc') + evaluated = to_next_multiple(sample_rate=1.0,quantum=16,min_quanta=13)(duration).evaluate_in_scope(dict(abc=0.)) + expected = 0. + self.assertEqual(evaluated, expected) + + duration = ExpressionScalar('q') + evaluated = to_next_multiple(sample_rate=ExpressionScalar('w'),quantum=16,min_quanta=1)(duration).evaluate_in_scope( + dict(q=3.14159,w=1.0)) + expected = 16. + self.assertEqual(evaluated, expected) + + \ No newline at end of file From 5fc9c64099b15adec60f3bc3794e955599585b11 Mon Sep 17 00:00:00 2001 From: Nomos11 <82180697+Nomos11@users.noreply.github.com> Date: Fri, 13 Oct 2023 15:23:35 +0200 Subject: [PATCH 137/441] typo --- qupulse/utils/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qupulse/utils/__init__.py b/qupulse/utils/__init__.py index 8cc9e593b..ae4aab6df 100644 --- a/qupulse/utils/__init__.py +++ b/qupulse/utils/__init__.py @@ -140,7 +140,7 @@ def to_next_multiple(sample_rate: ExpressionLike, quantum: int, Returns: A function that takes a duration (ExpressionLike) as input, and returns a duration rounded up to the next valid samples count in given sample rate. - The function returns 0 if duration==0, <0 is not checked if min_quanta=1. + The function returns 0 if duration==0, <0 is not checked if min_quanta is None. """ sample_rate = ExpressionScalar(sample_rate) From 86430dd1298582e87839efda5f5f669239b10d57 Mon Sep 17 00:00:00 2001 From: Nomos11 <82180697+Nomos11@users.noreply.github.com> Date: Fri, 13 Oct 2023 15:32:22 +0200 Subject: [PATCH 138/441] Update __init__.py --- qupulse/utils/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qupulse/utils/__init__.py b/qupulse/utils/__init__.py index ae4aab6df..b6b02aba6 100644 --- a/qupulse/utils/__init__.py +++ b/qupulse/utils/__init__.py @@ -150,7 +150,7 @@ def to_next_multiple(sample_rate: ExpressionLike, quantum: int, return lambda duration: -(-(duration*sample_rate)//quantum) * (quantum/sample_rate) else: #still return 0 if duration==0 - return lambda duration: ExpressionScalar((quantum/sample_rate)*\ - Max(-(-(ExpressionScalar(duration)*sample_rate)//quantum),min_quanta)\ + return lambda duration: ExpressionScalar((quantum/sample_rate)\ + *Max(-(-(ExpressionScalar(duration)*sample_rate)//quantum),min_quanta)\ *Max(0,sign(ExpressionScalar(duration)))) \ No newline at end of file From 8e4e159f0e50f372792e77a56e18718c612543ed Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 16 Oct 2023 17:52:07 +0200 Subject: [PATCH 139/441] Add more SimpleExpression arithmetics --- qupulse/program/__init__.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/qupulse/program/__init__.py b/qupulse/program/__init__.py index 4747672a3..dd16cc52e 100644 --- a/qupulse/program/__init__.py +++ b/qupulse/program/__init__.py @@ -46,9 +46,28 @@ def __add__(self, other): return NotImplemented + def __radd__(self, other): + return self.__add__(other) + + def __sub__(self, other): + return self.__add__(-other) + + def __rsub__(self, other): + (-self).__add__(other) + + def __neg__(self): + return SimpleExpression(-self.base, tuple((name, -value) for name, value in self.offsets)) + def __mul__(self, other: NumVal): return SimpleExpression(self.base * other, tuple((name, value * other) for name, value in self.offsets)) + def __rmul__(self, other): + return self.__mul__(other) + + def evaluate_in_scope(self, *args, **kwargs): + # TODO: remove. It is currently required to avoid nesting this class in an expression for the MappedScope + # We can maybe replace is with a HardwareScope or something along those lines + return self RepetitionCount = Union[int, VolatileRepetitionCount, SimpleExpression[int]] From 7c520a76b02c5e73a96026ce21c7e1a9a8b15701 Mon Sep 17 00:00:00 2001 From: Nomos11 <82180697+Nomos11@users.noreply.github.com> Date: Tue, 17 Oct 2023 00:01:28 +0200 Subject: [PATCH 140/441] string representation of sympy functionality --- qupulse/utils/__init__.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/qupulse/utils/__init__.py b/qupulse/utils/__init__.py index b6b02aba6..326072f4b 100644 --- a/qupulse/utils/__init__.py +++ b/qupulse/utils/__init__.py @@ -7,8 +7,6 @@ from collections import OrderedDict from frozendict import frozendict from qupulse.expressions import ExpressionScalar, ExpressionLike -from qupulse.utils.types import TimeType -from sympy import Max, sign import numpy @@ -150,7 +148,5 @@ def to_next_multiple(sample_rate: ExpressionLike, quantum: int, return lambda duration: -(-(duration*sample_rate)//quantum) * (quantum/sample_rate) else: #still return 0 if duration==0 - return lambda duration: ExpressionScalar((quantum/sample_rate)\ - *Max(-(-(ExpressionScalar(duration)*sample_rate)//quantum),min_quanta)\ - *Max(0,sign(ExpressionScalar(duration)))) + return lambda duration: ExpressionScalar(f'{quantum}/({sample_rate})*Max({min_quanta},-(-{duration}*{sample_rate}//{quantum}))*Max(0, sign({duration}))') \ No newline at end of file From 4a542c0cb55abd40e29d8a8096b557466b200e55 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Wed, 18 Oct 2023 12:24:11 +0200 Subject: [PATCH 141/441] Disallow quadratic simple expression terms --- qupulse/program/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qupulse/program/__init__.py b/qupulse/program/__init__.py index dd16cc52e..70257fc32 100644 --- a/qupulse/program/__init__.py +++ b/qupulse/program/__init__.py @@ -59,6 +59,8 @@ def __neg__(self): return SimpleExpression(-self.base, tuple((name, -value) for name, value in self.offsets)) def __mul__(self, other: NumVal): + if isinstance(other, SimpleExpression): + return NotImplemented return SimpleExpression(self.base * other, tuple((name, value * other) for name, value in self.offsets)) def __rmul__(self, other): From f3f97324ac5098e2b5c62f1dee21f1b65d9eddfc Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Wed, 18 Oct 2023 12:24:50 +0200 Subject: [PATCH 142/441] Allow SimpleExpression as evaluation result with a hack --- qupulse/program/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/qupulse/program/__init__.py b/qupulse/program/__init__.py index 70257fc32..117c7301c 100644 --- a/qupulse/program/__init__.py +++ b/qupulse/program/__init__.py @@ -10,6 +10,7 @@ from qupulse.utils.types import MeasurementWindow, TimeType from qupulse._program.volatile import VolatileRepetitionCount from qupulse.parameter_scope import Scope +from qupulse.expressions import sympy as sym_expr from typing import Protocol, runtime_checkable @@ -141,3 +142,7 @@ def to_program(self) -> Optional[Program]: def default_program_builder() -> ProgramBuilder: from qupulse.program.loop import LoopBuilder return LoopBuilder() + + +# TODO: hackedy, hackedy +sym_expr.ALLOWED_NUMERIC_SCALAR_TYPES = sym_expr.ALLOWED_NUMERIC_SCALAR_TYPES + (SimpleExpression,) From 32a69ed02bf6593d9bdc0f48cffa58a71b4292fc Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Wed, 18 Oct 2023 12:33:12 +0200 Subject: [PATCH 143/441] Add an linear space program builder as a basis for decadac and new hdawg --- qupulse/program/linspace.py | 154 ++++++++++++++++++++++++++++++++ tests/program/linspace_tests.py | 25 ++++++ 2 files changed, 179 insertions(+) create mode 100644 qupulse/program/linspace.py create mode 100644 tests/program/linspace_tests.py diff --git a/qupulse/program/linspace.py b/qupulse/program/linspace.py new file mode 100644 index 000000000..80ff6b302 --- /dev/null +++ b/qupulse/program/linspace.py @@ -0,0 +1,154 @@ +import contextlib +from dataclasses import dataclass +from typing import Mapping, Optional, Sequence, ContextManager, Iterable, Tuple, Union + +from qupulse import ChannelID, MeasurementWindow +from qupulse.parameter_scope import Scope, MappedScope, FrozenDict +from qupulse.program import (ProgramBuilder, HardwareTime, HardwareVoltage, Waveform, RepetitionCount, TimeType, + SimpleExpression) +from qupulse.expressions import sympy as sym_expr + + +@dataclass +class LinSpaceNode: + """AST node for a program that supports linear spacing of set points as well as nested sequencing and repetitions""" + + +@dataclass +class LinSpaceSet(LinSpaceNode): + bases: Tuple[float, ...] + factors: Tuple[Optional[Tuple[float, ...]], ...] + + duration_base: TimeType + duration_factors: Optional[Tuple[TimeType, ...]] + + +@dataclass +class LinSpaceRepeat(LinSpaceNode): + body: Tuple[LinSpaceNode, ...] + count: int + + +@dataclass +class LinSpaceIter(LinSpaceNode): + """Iteration in linear space are restricted to range 0 to length. Offsets and spacing are stored in the set node.""" + body: Tuple[LinSpaceNode, ...] + length: int + + +class LinSpaceBuilder(ProgramBuilder): + def __init__(self, channels: Tuple[Optional[ChannelID], ...]): + super().__init__() + self._name_to_idx = {name: idx for idx, name in enumerate(channels) if name is not None} + self._idx_to_name = channels + + self._stack = [[]] + self._ranges = [] + + @classmethod + def from_channel_dict(cls, channels: Mapping[ChannelID, int]): + assert len(set(channels.values())) == len(channels), "no duplicate target channels" + channel_list = [None] * 20 + for ch_name, ch_idx in channels.items(): + channel_list[ch_idx] = ch_name + return cls(tuple(channel_list)) + + def _root(self): + return self._stack[0] + + def _get_rng(self, idx_name: str) -> range: + return self._get_ranges()[idx_name] + + def inner_scope(self, scope: Scope) -> Scope: + """This function is necessary to inject program builder specific parameter implementations into the build + process.""" + if self._ranges: + name, _ = self._ranges[-1] + return MappedScope(scope, FrozenDict({name: SimpleExpression(base=0, offsets=[(name, 1)])})) + else: + return scope + + def _get_ranges(self): + return dict(self._ranges) + + def hold_voltage(self, duration: HardwareTime, voltages: Mapping[ChannelID, HardwareVoltage]): + voltages = sorted((self._name_to_idx[ch_name], value) for ch_name, value in voltages.items()) + voltages = [value for _, value in voltages] + + ranges = self._get_ranges() + factors = [] + bases = [] + for value in voltages: + if isinstance(value, float): + bases.append(value) + factors.append(None) + continue + offsets = value.offsets + base = value.base + incs = [] + for rng_name, rng in ranges.items(): + start = 0. + step = 0. + for off_name, offset in offsets: + if off_name == rng_name: + start += rng.start * offset + step += rng.step * offset + base += start + incs.append(step) + factors.append(tuple(incs)) + bases.append(base) + + if isinstance(duration, SimpleExpression): + duration_factors = duration.offsets + duration_base = duration.base + else: + duration_base = duration + duration_factors = None + + set_cmd = LinSpaceSet(bases=tuple(bases), + factors=tuple(factors), + duration_base=duration_base, + duration_factors=duration_factors) + + self._stack[-1].append(set_cmd) + + def play_arbitrary_waveform(self, waveform: Waveform): + raise NotImplementedError('Not implemented yet (postponed)') + + def measure(self, measurements: Optional[Sequence[MeasurementWindow]]): + """Ignores measurements""" + pass + + def with_repetition(self, repetition_count: RepetitionCount, + measurements: Optional[Sequence[MeasurementWindow]] = None) -> Iterable['ProgramBuilder']: + if repetition_count == 0: + return + self._stack.append([]) + yield self + blocks = self._stack.pop() + if blocks: + self._stack[-1].append(LinSpaceRepeat(body=tuple(blocks), count=repetition_count)) + + @contextlib.contextmanager + def with_sequence(self, + measurements: Optional[Sequence[MeasurementWindow]] = None) -> ContextManager['ProgramBuilder']: + yield self + + def new_subprogram(self, global_transformation: 'Transformation' = None) -> ContextManager['ProgramBuilder']: + raise NotImplementedError('Not implemented yet (postponed)') + + def with_iteration(self, index_name: str, rng: range, + measurements: Optional[Sequence[MeasurementWindow]] = None) -> Iterable['ProgramBuilder']: + if len(rng) == 0: + return + self._stack.append([]) + self._ranges.append((index_name, rng)) + yield self + cmds = self._stack.pop() + self._ranges.pop() + if cmds: + self._stack[-1].append(LinSpaceIter(body=tuple(cmds), length=len(rng))) + + def to_program(self) -> Optional[Sequence[LinSpaceNode]]: + if self._root(): + return self._root() diff --git a/tests/program/linspace_tests.py b/tests/program/linspace_tests.py new file mode 100644 index 000000000..45b63b365 --- /dev/null +++ b/tests/program/linspace_tests.py @@ -0,0 +1,25 @@ +from unittest import TestCase + +from qupulse.pulses import * +from qupulse.program.linspace import * + + +class IdxProgramBuilderTests(TestCase): + def test_single_channel_ramp(self): + hold = ConstantPT(10**6, {'a': '-1. + idx * 0.01'}) + ramp = hold.with_iteration('idx', 200) + + program_builder = LinSpaceBuilder.from_channel_dict({'a': 0}) + program = ramp.create_program(program_builder=program_builder) + + expected = LinSpaceIter( + length=200, + body=(LinSpaceSet( + bases=(-1.,), + factors=((0.01,),), + duration_base=TimeType(10**6), + duration_factors=None + ),) + ) + + self.assertEqual([expected], program) From f9d3810b890532bc95e11e934471c31cbf138898 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 19 Oct 2023 15:45:22 +0200 Subject: [PATCH 144/441] Proper translation of csd and singlet reload scan --- qupulse/program/linspace.py | 247 +++++++++++++++++++++++++++++++- tests/program/linspace_tests.py | 175 +++++++++++++++++++++- 2 files changed, 415 insertions(+), 7 deletions(-) diff --git a/qupulse/program/linspace.py b/qupulse/program/linspace.py index 80ff6b302..0ae2d0b88 100644 --- a/qupulse/program/linspace.py +++ b/qupulse/program/linspace.py @@ -1,6 +1,8 @@ +import abc import contextlib +import dataclasses from dataclasses import dataclass -from typing import Mapping, Optional, Sequence, ContextManager, Iterable, Tuple, Union +from typing import Mapping, Optional, Sequence, ContextManager, Iterable, Tuple, Union, Dict, List from qupulse import ChannelID, MeasurementWindow from qupulse.parameter_scope import Scope, MappedScope, FrozenDict @@ -9,25 +11,88 @@ from qupulse.expressions import sympy as sym_expr +DEFAULT_RESOLUTION: float = 1e-9 + + @dataclass class LinSpaceNode: """AST node for a program that supports linear spacing of set points as well as nested sequencing and repetitions""" + def dependencies(self) -> Mapping[int, set]: + raise NotImplementedError + + +@dataclass +class LinSpaceSet: + channel: int + base: float + factors: Optional[Tuple[float, ...]] + + + +@dataclass +class Wait: + duration: TimeType @dataclass -class LinSpaceSet(LinSpaceNode): +class LinSpaceHold(LinSpaceNode): bases: Tuple[float, ...] factors: Tuple[Optional[Tuple[float, ...]], ...] duration_base: TimeType duration_factors: Optional[Tuple[TimeType, ...]] + def dependencies(self) -> Mapping[int, set]: + return {idx: {factors} + for idx, factors in enumerate(self.factors) + if factors} + + def to_atomic_commands(self): + if self.duration_factors: + raise NotImplementedError('Variable durations are not implemented for commands yet') + return [LinSpaceSet(idx, base, factors) + for idx, (base, factors) in enumerate(zip(self.bases, self.factors))] + [Wait(self.duration_base)] + + def to_increment_commands(self, previous: Tuple[float, ...], iter_advance: Sequence[bool]): + if self.duration_factors: + raise NotImplementedError('Variable durations are not implemented for increment commands yet') + set_vals = [] + inc_vals = [] + for prev, base, factors in zip(previous, self.bases, self.factors): + set_val = base + if set_val == prev: + # TODO: epsilon + set_val = None + + if factors: + inc_val = 0. + for advance, factor in zip(iter_advance, factors): + if advance: + inc_val += factor + + inc_val = None + if base != prev: + + set_vals.append(base) + else: + pass + assert inc_val is None or set_val is None + inc_vals.append(inc_val) + set_vals.append(set_val) + @dataclass class LinSpaceRepeat(LinSpaceNode): body: Tuple[LinSpaceNode, ...] count: int + def dependencies(self): + dependencies = {} + for node in self.body: + for idx, deps in node.dependencies().items(): + dependencies.setdefault(idx, set()).update(deps) + return dependencies + @dataclass class LinSpaceIter(LinSpaceNode): @@ -35,6 +100,15 @@ class LinSpaceIter(LinSpaceNode): body: Tuple[LinSpaceNode, ...] length: int + def dependencies(self): + dependencies = {} + for node in self.body: + for idx, deps in node.dependencies().items(): + shortened = {dep[:-1] for dep in deps} + if shortened != {()}: + dependencies.setdefault(idx, set()).update(deps) + return dependencies + class LinSpaceBuilder(ProgramBuilder): def __init__(self, channels: Tuple[Optional[ChannelID], ...]): @@ -105,10 +179,10 @@ def hold_voltage(self, duration: HardwareTime, voltages: Mapping[ChannelID, Hard duration_base = duration duration_factors = None - set_cmd = LinSpaceSet(bases=tuple(bases), - factors=tuple(factors), - duration_base=duration_base, - duration_factors=duration_factors) + set_cmd = LinSpaceHold(bases=tuple(bases), + factors=tuple(factors), + duration_base=duration_base, + duration_factors=duration_factors) self._stack[-1].append(set_cmd) @@ -152,3 +226,164 @@ def with_iteration(self, index_name: str, rng: range, def to_program(self) -> Optional[Sequence[LinSpaceNode]]: if self._root(): return self._root() + + + +@dataclass +class LoopLabel: + idx: int + count: int + + +@dataclass +class Increment: + channel: int + value: float + dependency_key: 'DepKey' + + +@dataclass +class Set: + channel: int + value: float + key: 'DepKey' = dataclasses.field(default_factory=lambda: DepKey(())) + + +@dataclass +class LoopJmp: + idx: int + + +@dataclass(frozen=True) +class DepState: + base: float + iterations: Tuple[int, ...] + + +@dataclass(frozen=True) +class DepKey: + """The key that identifies how a certain set command depends on iteration indices.""" + factors: Tuple[int, ...] + + @classmethod + def from_voltages(cls, voltages: Sequence[float], resolution: float): + # remove trailing zeros + while voltages and voltages[-1] == 0: + voltages = voltages[:-1] + return cls(tuple(int(round(voltage / resolution)) for voltage in voltages)) + + +@dataclass +class TranslationState: + label_num: int + commands: list + iterations: list + active_dep: Dict[int, DepKey] + dep_states: Dict[int, Dict[DepKey, DepState]] + plain_voltage: Dict[int, float] + resolution: float = dataclasses.field(default_factory=lambda: DEFAULT_RESOLUTION) + + def new_loop(self, count: int): + label = LoopLabel(self.label_num, count) + jmp = LoopJmp(self.label_num) + self.label_num += 1 + return label, jmp + + def get_dependency_state(self, dependencies: Mapping[int, set]): + return { + self.dep_states.get(ch, {}).get(DepKey.from_voltages(dep, self.resolution), None) + for ch, deps in dependencies.items() + for dep in deps + } + + def set_voltage(self, channel: int, value: float): + key = DepKey(()) + if self.active_dep.get(channel, None) != key or self.plain_voltage.get(channel, None) != value: + self.commands.append(Set(channel, value, key)) + self.active_dep[channel] = key + self.plain_voltage[channel] = value + + +def to_atomic_commands(node: Union[LinSpaceNode, Sequence[LinSpaceNode]], state: TranslationState): + """This step replaces iterations with """ + if isinstance(node, Sequence): + for lin_node in node: + to_atomic_commands(lin_node, state) + + if isinstance(node, LinSpaceRepeat): + pre_dep_state = state.get_dependency_state(node.dependencies()) + label, jmp = state.new_loop(node.count) + initial_position = len(state.commands) + state.commands.append(label) + to_atomic_commands(node.body, state) + post_dep_state = state.get_dependency_state(node.dependencies()) + if pre_dep_state != post_dep_state: + # hackedy + state.commands.pop(initial_position) + state.commands.append(label) + label.count -= 1 + to_atomic_commands(node.body, state) + state.commands.append(jmp) + + elif isinstance(node, LinSpaceIter): + state.iterations.append(0) + to_atomic_commands(node.body, state) + + if node.length > 1: + state.iterations[-1] = node.length + label, jmp = state.new_loop(node.length - 1) + state.commands.append(label) + to_atomic_commands(node.body, state) + state.commands.append(jmp) + state.iterations.pop() + + elif isinstance(node, LinSpaceHold): + if node.duration_factors: + raise NotImplementedError("TODO") + + for ch, (base, factors) in enumerate(zip(node.bases, node.factors)): + if factors is None: + state.set_voltage(ch, base) + continue + + dep_key = DepKey.from_voltages(voltages=factors, resolution=state.resolution) + new_dep_state = DepState( + base, + iterations=tuple(state.iterations) + ) + + current_dep_state = state.dep_states.setdefault(ch, {}).get(dep_key, None) + if current_dep_state is None: + assert all(it == 0 for it in state.iterations) + state.commands.append(Set(ch, base, dep_key)) + state.active_dep[ch] = dep_key + + else: + inc = current_dep_state.base - new_dep_state.base + for i, j, factor in zip(current_dep_state.iterations, new_dep_state.iterations, factors): + if i == j: + continue + if i < j: + assert i == 0 + # regular iteration + inc += factor + else: + assert j == 0 + inc -= factor * i + # we insert all inc here (also inc == 0) because it signals to activate this amplitude register + if inc or state.active_dep.get(ch, None) != dep_key: + state.commands.append(Increment(ch, inc, dep_key)) + state.active_dep[ch] = dep_key + state.dep_states[ch][dep_key] = new_dep_state + state.commands.append(Wait(node.duration_base)) + + +def to_increment_commands(linspace_nodes: Sequence[LinSpaceNode]) -> list: + state = TranslationState(0, [], [], active_dep={}, dep_states={}, plain_voltage={}) + to_atomic_commands(linspace_nodes, state) + return state.commands + + + + + diff --git a/tests/program/linspace_tests.py b/tests/program/linspace_tests.py index 45b63b365..98f914d74 100644 --- a/tests/program/linspace_tests.py +++ b/tests/program/linspace_tests.py @@ -14,7 +14,7 @@ def test_single_channel_ramp(self): expected = LinSpaceIter( length=200, - body=(LinSpaceSet( + body=(LinSpaceHold( bases=(-1.,), factors=((0.01,),), duration_base=TimeType(10**6), @@ -23,3 +23,176 @@ def test_single_channel_ramp(self): ) self.assertEqual([expected], program) + + def test_single_ramp_increment_commands(self): + program = LinSpaceIter( + length=200, + body=(LinSpaceHold( + bases=(-1.,), + factors=((0.01,),), + duration_base=TimeType(10 ** 6), + duration_factors=None + ),) + ) + + commands = to_increment_commands([program]) + + expected = [ + Set(0, -1.0), + Wait(TimeType(10 ** 6)), + LoopLabel(0, 199), + Increment(0, 0.01, DepKey.from_voltages((0.01,), DEFAULT_RESOLUTION)), + Wait(TimeType(10 ** 6)), + LoopJmp(0) + ] + self.assertEqual(expected, commands) + + def test_csd_program(self): + hold = ConstantPT(10**6, {'a': '-1. + idx_a * 0.01', 'b': '-.5 + idx_b * 0.02'}) + scan_a = hold.with_iteration('idx_a', 200) + csd = scan_a.with_iteration('idx_b', 100) + + program_builder = LinSpaceBuilder.from_channel_dict({'a': 0, 'b': 1}) + program = csd.create_program(program_builder=program_builder) + + expected = LinSpaceIter(length=100, body=(LinSpaceIter( + length=200, + body=(LinSpaceHold( + bases=(-1., -0.5), + factors=((0.0, 0.01), + (0.02, 0.0)), + duration_base=TimeType(10**6), + duration_factors=None + ),) + ),)) + + self.assertEqual([expected], program) + + def test_csd_increment_commands(self): + program = LinSpaceIter(length=100, body=(LinSpaceIter( + length=200, + body=(LinSpaceHold( + bases=(-1., -0.5), + factors=((0.0, 0.01), + (0.02, 0.0)), + duration_base=TimeType(10 ** 6), + duration_factors=None + ),) + ),)) + + commands = to_increment_commands([program]) + + expected = [ + Set(0, -1.0, DepKey.from_voltages((0, 0.01,), DEFAULT_RESOLUTION)), + Set(1, -0.5, DepKey.from_voltages((0.02,), DEFAULT_RESOLUTION)), + Wait(TimeType(10 ** 6)), + + LoopLabel(0, 199), + Increment(0, 0.01, DepKey.from_voltages((0, 0.01,), DEFAULT_RESOLUTION)), + Wait(TimeType(10 ** 6)), + LoopJmp(0), + + LoopLabel(1, 99), + + Increment(0, -2.0, DepKey.from_voltages((0, 0.01,), DEFAULT_RESOLUTION)), + Increment(1, 0.02, DepKey.from_voltages((0.02,), DEFAULT_RESOLUTION)), + Wait(TimeType(10 ** 6)), + + LoopLabel(2, 199), + Increment(0, 0.01, DepKey.from_voltages((0, 0.01,), DEFAULT_RESOLUTION)), + Wait(TimeType(10 ** 6)), + LoopJmp(2), + + LoopJmp(1), + ] + self.assertEqual(expected, commands) + + +class SingletLoadProcessing(TestCase): + def setUp(self): + wait = ConstantPT(10 ** 6, {'a': '-1. + idx_a * 0.01', 'b': '-.5 + idx_b * 0.02'}) + load_random = ConstantPT(10 ** 5, {'a': -.4, 'b': -.3}) + meas = ConstantPT(10 ** 5, {'a': 0.05, 'b': 0.06}) + + singlet_scan = (load_random @ wait @ meas).with_iteration('idx_a', 200).with_iteration('idx_b', 100) + self.pulse_template = singlet_scan + + self.program = LinSpaceIter(length=100, body=(LinSpaceIter( + length=200, + body=( + LinSpaceHold(bases=(-0.4, -0.3), factors=(None, None), duration_base=TimeType(10 ** 5), + duration_factors=None), + LinSpaceHold(bases=(-1., -0.5), + factors=((0.0, 0.01), + (0.02, 0.0)), + duration_base=TimeType(10 ** 6), + duration_factors=None), + LinSpaceHold(bases=(0.05, 0.06), factors=(None, None), duration_base=TimeType(10 ** 5), + duration_factors=None), + ) + ),)) + + key_0 = DepKey.from_voltages((0, 0.01,), DEFAULT_RESOLUTION) + key_1 = DepKey.from_voltages((0.02,), DEFAULT_RESOLUTION) + + self.commands = [ + Set(0, -0.4), + Set(1, -0.3), + Wait(TimeType(10 ** 5)), + Set(0, -1.0, key_0), + Set(1, -0.5, key_1), + Wait(TimeType(10 ** 6)), + Set(0, 0.05), + Set(1, 0.06), + Wait(TimeType(10 ** 5)), + + LoopLabel(0, 199), + Set(0, -0.4), + Set(1, -0.3), + Wait(TimeType(10 ** 5)), + Increment(0, 0.01, key_0), + Increment(1, 0.00, key_1), + Wait(TimeType(10 ** 6)), + Set(0, 0.05), + Set(1, 0.06), + Wait(TimeType(10 ** 5)), + LoopJmp(0), + + LoopLabel(1, 99), + + Set(0, -0.4), + Set(1, -0.3), + Wait(TimeType(10 ** 5)), + Increment(0, -2.0, key_0), + Increment(1, 0.02, key_1), + Wait(TimeType(10 ** 6)), + Set(0, 0.05), + Set(1, 0.06), + Wait(TimeType(10 ** 5)), + + LoopLabel(2, 199), + + Set(0, -0.4), + Set(1, -0.3), + Wait(TimeType(10 ** 5)), + Increment(0, 0.01, key_0), + Increment(1, 0.00, key_1), + Wait(TimeType(10 ** 6)), + Set(0, 0.05), + Set(1, 0.06), + Wait(TimeType(10 ** 5)), + + LoopJmp(2), + + LoopJmp(1), + ] + + def test_singlet_scan_program(self): + program_builder = LinSpaceBuilder.from_channel_dict({'a': 0, 'b': 1}) + program = self.pulse_template.create_program(program_builder=program_builder) + self.assertEqual([self.program], program) + + def test_singlet_scan_commands(self): + commands = to_increment_commands([self.program]) + self.assertEqual(self.commands, commands) + From ce42fc0a7f4ca49515dd63d82cdaac1615bf7972 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 19 Oct 2023 16:12:09 +0200 Subject: [PATCH 145/441] Cleanup tests --- tests/program/linspace_tests.py | 102 ++++++++++++++------------------ 1 file changed, 44 insertions(+), 58 deletions(-) diff --git a/tests/program/linspace_tests.py b/tests/program/linspace_tests.py index 98f914d74..bc154904d 100644 --- a/tests/program/linspace_tests.py +++ b/tests/program/linspace_tests.py @@ -4,15 +4,12 @@ from qupulse.program.linspace import * -class IdxProgramBuilderTests(TestCase): - def test_single_channel_ramp(self): - hold = ConstantPT(10**6, {'a': '-1. + idx * 0.01'}) - ramp = hold.with_iteration('idx', 200) - - program_builder = LinSpaceBuilder.from_channel_dict({'a': 0}) - program = ramp.create_program(program_builder=program_builder) +class SingleRampTest(TestCase): + def setUp(self): + hold = ConstantPT(10 ** 6, {'a': '-1. + idx * 0.01'}) + self.pulse_template = hold.with_iteration('idx', 200) - expected = LinSpaceIter( + self.program = LinSpaceIter( length=200, body=(LinSpaceHold( bases=(-1.,), @@ -22,90 +19,79 @@ def test_single_channel_ramp(self): ),) ) - self.assertEqual([expected], program) - - def test_single_ramp_increment_commands(self): - program = LinSpaceIter( - length=200, - body=(LinSpaceHold( - bases=(-1.,), - factors=((0.01,),), - duration_base=TimeType(10 ** 6), - duration_factors=None - ),) - ) - - commands = to_increment_commands([program]) + key = DepKey.from_voltages((0.01,), DEFAULT_INCREMENT_RESOLUTION) - expected = [ - Set(0, -1.0), + self.commands = [ + Set(0, -1.0, key), Wait(TimeType(10 ** 6)), LoopLabel(0, 199), - Increment(0, 0.01, DepKey.from_voltages((0.01,), DEFAULT_RESOLUTION)), + Increment(0, 0.01, key), Wait(TimeType(10 ** 6)), LoopJmp(0) ] - self.assertEqual(expected, commands) - def test_csd_program(self): - hold = ConstantPT(10**6, {'a': '-1. + idx_a * 0.01', 'b': '-.5 + idx_b * 0.02'}) - scan_a = hold.with_iteration('idx_a', 200) - csd = scan_a.with_iteration('idx_b', 100) + def test_program(self): + program_builder = LinSpaceBuilder(('a',)) + program = self.pulse_template.create_program(program_builder=program_builder) + self.assertEqual([self.program], program) - program_builder = LinSpaceBuilder.from_channel_dict({'a': 0, 'b': 1}) - program = csd.create_program(program_builder=program_builder) + def test_commands(self): + commands = to_increment_commands([self.program]) + self.assertEqual(self.commands, commands) - expected = LinSpaceIter(length=100, body=(LinSpaceIter( - length=200, - body=(LinSpaceHold( - bases=(-1., -0.5), - factors=((0.0, 0.01), - (0.02, 0.0)), - duration_base=TimeType(10**6), - duration_factors=None - ),) - ),)) - self.assertEqual([expected], program) +class PlainCSDTest(TestCase): + def setUp(self): + hold = ConstantPT(10**6, {'a': '-1. + idx_a * 0.01', 'b': '-.5 + idx_b * 0.02'}) + scan_a = hold.with_iteration('idx_a', 200) + self.pulse_template = scan_a.with_iteration('idx_b', 100) - def test_csd_increment_commands(self): - program = LinSpaceIter(length=100, body=(LinSpaceIter( + self.program = LinSpaceIter(length=100, body=(LinSpaceIter( length=200, body=(LinSpaceHold( bases=(-1., -0.5), factors=((0.0, 0.01), (0.02, 0.0)), - duration_base=TimeType(10 ** 6), + duration_base=TimeType(10**6), duration_factors=None ),) ),)) - commands = to_increment_commands([program]) + key_0 = DepKey.from_voltages((0, 0.01,), DEFAULT_INCREMENT_RESOLUTION) + key_1 = DepKey.from_voltages((0.02,), DEFAULT_INCREMENT_RESOLUTION) - expected = [ - Set(0, -1.0, DepKey.from_voltages((0, 0.01,), DEFAULT_RESOLUTION)), - Set(1, -0.5, DepKey.from_voltages((0.02,), DEFAULT_RESOLUTION)), + self.commands = [ + Set(0, -1.0, key_0), + Set(1, -0.5, key_1), Wait(TimeType(10 ** 6)), LoopLabel(0, 199), - Increment(0, 0.01, DepKey.from_voltages((0, 0.01,), DEFAULT_RESOLUTION)), + Increment(0, 0.01, key_0), Wait(TimeType(10 ** 6)), LoopJmp(0), LoopLabel(1, 99), - Increment(0, -2.0, DepKey.from_voltages((0, 0.01,), DEFAULT_RESOLUTION)), - Increment(1, 0.02, DepKey.from_voltages((0.02,), DEFAULT_RESOLUTION)), + Increment(0, -2.0, key_0), + Increment(1, 0.02, key_1), Wait(TimeType(10 ** 6)), LoopLabel(2, 199), - Increment(0, 0.01, DepKey.from_voltages((0, 0.01,), DEFAULT_RESOLUTION)), + Increment(0, 0.01, key_0), Wait(TimeType(10 ** 6)), LoopJmp(2), LoopJmp(1), ] - self.assertEqual(expected, commands) + + def test_program(self): + program_builder = LinSpaceBuilder(('a', 'b')) + program = self.pulse_template.create_program(program_builder=program_builder) + self.assertEqual([self.program], program) + + def test_increment_commands(self): + commands = to_increment_commands([self.program]) + self.assertEqual(self.commands, commands) class SingletLoadProcessing(TestCase): @@ -132,8 +118,8 @@ def setUp(self): ) ),)) - key_0 = DepKey.from_voltages((0, 0.01,), DEFAULT_RESOLUTION) - key_1 = DepKey.from_voltages((0.02,), DEFAULT_RESOLUTION) + key_0 = DepKey.from_voltages((0, 0.01,), DEFAULT_INCREMENT_RESOLUTION) + key_1 = DepKey.from_voltages((0.02,), DEFAULT_INCREMENT_RESOLUTION) self.commands = [ Set(0, -0.4), @@ -188,7 +174,7 @@ def setUp(self): ] def test_singlet_scan_program(self): - program_builder = LinSpaceBuilder.from_channel_dict({'a': 0, 'b': 1}) + program_builder = LinSpaceBuilder(('a', 'b')) program = self.pulse_template.create_program(program_builder=program_builder) self.assertEqual([self.program], program) From c724725ec3b58f650497e4e7c90e1a370223f2b4 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 19 Oct 2023 16:21:08 +0200 Subject: [PATCH 146/441] Cleanup implementation and add arbitrary waveform forwarding --- qupulse/program/linspace.py | 114 +++++++++++++++++++----------------- 1 file changed, 59 insertions(+), 55 deletions(-) diff --git a/qupulse/program/linspace.py b/qupulse/program/linspace.py index 0ae2d0b88..a2f47ebc8 100644 --- a/qupulse/program/linspace.py +++ b/qupulse/program/linspace.py @@ -8,10 +8,25 @@ from qupulse.parameter_scope import Scope, MappedScope, FrozenDict from qupulse.program import (ProgramBuilder, HardwareTime, HardwareVoltage, Waveform, RepetitionCount, TimeType, SimpleExpression) -from qupulse.expressions import sympy as sym_expr +from qupulse.program.waveforms import MultiChannelWaveform -DEFAULT_RESOLUTION: float = 1e-9 +# this resolution is used to unify increments +# the increments themselves remain floats +DEFAULT_INCREMENT_RESOLUTION: float = 1e-9 + + +@dataclass(frozen=True) +class DepKey: + """The key that identifies how a certain set command depends on iteration indices.""" + factors: Tuple[int, ...] + + @classmethod + def from_voltages(cls, voltages: Sequence[float], resolution: float): + # remove trailing zeros + while voltages and voltages[-1] == 0: + voltages = voltages[:-1] + return cls(tuple(int(round(voltage / resolution)) for voltage in voltages)) @dataclass @@ -21,19 +36,6 @@ def dependencies(self) -> Mapping[int, set]: raise NotImplementedError -@dataclass -class LinSpaceSet: - channel: int - base: float - factors: Optional[Tuple[float, ...]] - - - -@dataclass -class Wait: - duration: TimeType - - @dataclass class LinSpaceHold(LinSpaceNode): bases: Tuple[float, ...] @@ -47,12 +49,6 @@ def dependencies(self) -> Mapping[int, set]: for idx, factors in enumerate(self.factors) if factors} - def to_atomic_commands(self): - if self.duration_factors: - raise NotImplementedError('Variable durations are not implemented for commands yet') - return [LinSpaceSet(idx, base, factors) - for idx, (base, factors) in enumerate(zip(self.bases, self.factors))] + [Wait(self.duration_base)] - def to_increment_commands(self, previous: Tuple[float, ...], iter_advance: Sequence[bool]): if self.duration_factors: raise NotImplementedError('Variable durations are not implemented for increment commands yet') @@ -81,6 +77,12 @@ def to_increment_commands(self, previous: Tuple[float, ...], iter_advance: Seque set_vals.append(set_val) +@dataclass +class LinSpaceArbitraryWaveform(LinSpaceNode): + waveform: Waveform + channels: Tuple[ChannelID, ...] + + @dataclass class LinSpaceRepeat(LinSpaceNode): body: Tuple[LinSpaceNode, ...] @@ -111,22 +113,21 @@ def dependencies(self): class LinSpaceBuilder(ProgramBuilder): - def __init__(self, channels: Tuple[Optional[ChannelID], ...]): + """This program builder supports efficient translation of pulse templates that use symbolic linearly + spaced voltages and durations. + + The channel identifiers are reduced to their index in the given channel tuple. + + Arbitrary waveforms are not implemented yet + """ + def __init__(self, channels: Tuple[ChannelID, ...]): super().__init__() - self._name_to_idx = {name: idx for idx, name in enumerate(channels) if name is not None} + self._name_to_idx = {name: idx for idx, name in enumerate(channels)} self._idx_to_name = channels self._stack = [[]] self._ranges = [] - @classmethod - def from_channel_dict(cls, channels: Mapping[ChannelID, int]): - assert len(set(channels.values())) == len(channels), "no duplicate target channels" - channel_list = [None] * 20 - for ch_name, ch_idx in channels.items(): - channel_list[ch_idx] = ch_name - return cls(tuple(channel_list)) - def _root(self): return self._stack[0] @@ -187,7 +188,7 @@ def hold_voltage(self, duration: HardwareTime, voltages: Mapping[ChannelID, Hard self._stack[-1].append(set_cmd) def play_arbitrary_waveform(self, waveform: Waveform): - raise NotImplementedError('Not implemented yet (postponed)') + return self._stack[-1].append(LinSpaceArbitraryWaveform(waveform, self._idx_to_name)) def measure(self, measurements: Optional[Sequence[MeasurementWindow]]): """Ignores measurements""" @@ -249,39 +250,43 @@ class Set: key: 'DepKey' = dataclasses.field(default_factory=lambda: DepKey(())) +@dataclass +class Wait: + duration: TimeType + + @dataclass class LoopJmp: idx: int +@dataclass +class Play: + waveform: Waveform + channels: Tuple[ChannelID] + + +Command = Increment | Set | LoopLabel | LoopJmp | Wait | Play + + @dataclass(frozen=True) class DepState: base: float iterations: Tuple[int, ...] -@dataclass(frozen=True) -class DepKey: - """The key that identifies how a certain set command depends on iteration indices.""" - factors: Tuple[int, ...] - @classmethod - def from_voltages(cls, voltages: Sequence[float], resolution: float): - # remove trailing zeros - while voltages and voltages[-1] == 0: - voltages = voltages[:-1] - return cls(tuple(int(round(voltage / resolution)) for voltage in voltages)) @dataclass class TranslationState: - label_num: int - commands: list - iterations: list - active_dep: Dict[int, DepKey] - dep_states: Dict[int, Dict[DepKey, DepState]] - plain_voltage: Dict[int, float] - resolution: float = dataclasses.field(default_factory=lambda: DEFAULT_RESOLUTION) + label_num: int = dataclasses.field(default=0) + commands: List[Command] = dataclasses.field(default_factory=list) + iterations: List[int] = dataclasses.field(default_factory=list) + active_dep: Dict[int, DepKey] = dataclasses.field(default_factory=dict) + dep_states: Dict[int, Dict[DepKey, DepState]] = dataclasses.field(default_factory=dict) + plain_voltage: Dict[int, float] = dataclasses.field(default_factory=dict) + resolution: float = dataclasses.field(default_factory=lambda: DEFAULT_INCREMENT_RESOLUTION) def new_loop(self, count: int): label = LoopLabel(self.label_num, count) @@ -376,14 +381,13 @@ def to_atomic_commands(node: Union[LinSpaceNode, Sequence[LinSpaceNode]], state: state.active_dep[ch] = dep_key state.dep_states[ch][dep_key] = new_dep_state state.commands.append(Wait(node.duration_base)) + elif isinstance(node, LinSpaceArbitraryWaveform): + state.commands.append(Play(node.waveform, node.channels)) + else: + raise TypeError("The node type is not handled", type(node), node) -def to_increment_commands(linspace_nodes: Sequence[LinSpaceNode]) -> list: +def to_increment_commands(linspace_nodes: Sequence[LinSpaceNode]) -> List[Command]: state = TranslationState(0, [], [], active_dep={}, dep_states={}, plain_voltage={}) to_atomic_commands(linspace_nodes, state) return state.commands - - - - - From e1cbe89b3e8d5feb5abb4e6645df5e12782a1d24 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 19 Oct 2023 16:34:01 +0200 Subject: [PATCH 147/441] Fix missing elif for type dispatch --- qupulse/program/linspace.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/qupulse/program/linspace.py b/qupulse/program/linspace.py index a2f47ebc8..d99d993a0 100644 --- a/qupulse/program/linspace.py +++ b/qupulse/program/linspace.py @@ -229,7 +229,6 @@ def to_program(self) -> Optional[Sequence[LinSpaceNode]]: return self._root() - @dataclass class LoopLabel: idx: int @@ -315,7 +314,7 @@ def to_atomic_commands(node: Union[LinSpaceNode, Sequence[LinSpaceNode]], state: for lin_node in node: to_atomic_commands(lin_node, state) - if isinstance(node, LinSpaceRepeat): + elif isinstance(node, LinSpaceRepeat): pre_dep_state = state.get_dependency_state(node.dependencies()) label, jmp = state.new_loop(node.count) initial_position = len(state.commands) From 3033aaab8200a990579fa7681d5a2596f3d9ee00 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 19 Oct 2023 16:34:27 +0200 Subject: [PATCH 148/441] Add manually tilted 2d scan test --- tests/program/linspace_tests.py | 56 +++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/tests/program/linspace_tests.py b/tests/program/linspace_tests.py index bc154904d..49dd64647 100644 --- a/tests/program/linspace_tests.py +++ b/tests/program/linspace_tests.py @@ -94,6 +94,62 @@ def test_increment_commands(self): self.assertEqual(self.commands, commands) +class TiltedCSDTest(TestCase): + def setUp(self): + hold = ConstantPT(10**6, {'a': '-1. + idx_a * 0.01 + idx_b * 1e-3', 'b': '-.5 + idx_b * 0.02 - 3e-3 * idx_a'}) + scan_a = hold.with_iteration('idx_a', 200) + self.pulse_template = scan_a.with_iteration('idx_b', 100) + + self.program = LinSpaceIter(length=100, body=(LinSpaceIter( + length=200, + body=(LinSpaceHold( + bases=(-1., -0.5), + factors=((1e-3, 0.01), + (0.02, -3e-3)), + duration_base=TimeType(10**6), + duration_factors=None + ),) + ),)) + + key_0 = DepKey.from_voltages((1e-3, 0.01,), DEFAULT_INCREMENT_RESOLUTION) + key_1 = DepKey.from_voltages((0.02, -3e-3), DEFAULT_INCREMENT_RESOLUTION) + + self.commands = [ + Set(0, -1.0, key_0), + Set(1, -0.5, key_1), + Wait(TimeType(10 ** 6)), + + LoopLabel(0, 199), + Increment(0, 0.01, key_0), + Increment(1, -3e-3, key_1), + Wait(TimeType(10 ** 6)), + LoopJmp(0), + + LoopLabel(1, 99), + + Increment(0, 1e-3 + -200 * 1e-2, key_0), + Increment(1, 0.02 + -200 * -3e-3, key_1), + Wait(TimeType(10 ** 6)), + + LoopLabel(2, 199), + Increment(0, 0.01, key_0), + Increment(1, -3e-3, key_1), + Wait(TimeType(10 ** 6)), + LoopJmp(2), + + LoopJmp(1), + ] + + def test_program(self): + program_builder = LinSpaceBuilder(('a', 'b')) + program = self.pulse_template.create_program(program_builder=program_builder) + self.assertEqual([self.program], program) + + def test_increment_commands(self): + commands = to_increment_commands([self.program]) + self.assertEqual(self.commands, commands) + + class SingletLoadProcessing(TestCase): def setUp(self): wait = ConstantPT(10 ** 6, {'a': '-1. + idx_a * 0.01', 'b': '-.5 + idx_b * 0.02'}) From 0421d2631923a53423afe99ff0cd163a7fe9ec60 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 19 Oct 2023 16:55:35 +0200 Subject: [PATCH 149/441] Add transformation test and make it pass by being less strict about types in transformations --- qupulse/program/waveforms.py | 2 +- tests/program/linspace_tests.py | 34 ++++++++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/qupulse/program/waveforms.py b/qupulse/program/waveforms.py index 9cdcbf4ff..246639b32 100644 --- a/qupulse/program/waveforms.py +++ b/qupulse/program/waveforms.py @@ -892,7 +892,7 @@ def from_transformation(cls, inner_waveform: Waveform, transformation: Transform if constant_values is None or not transformation.is_constant_invariant(): return cls(inner_waveform, transformation) - transformed_constant_values = {key: float(value) for key, value in transformation(0., constant_values).items()} + transformed_constant_values = {key: value for key, value in transformation(0., constant_values).items()} return ConstantWaveform.from_mapping(inner_waveform.duration, transformed_constant_values) def is_constant(self) -> bool: diff --git a/tests/program/linspace_tests.py b/tests/program/linspace_tests.py index 49dd64647..07ed16e3c 100644 --- a/tests/program/linspace_tests.py +++ b/tests/program/linspace_tests.py @@ -1,8 +1,9 @@ +import unittest from unittest import TestCase from qupulse.pulses import * from qupulse.program.linspace import * - +from qupulse.program.transformation import * class SingleRampTest(TestCase): def setUp(self): @@ -238,3 +239,34 @@ def test_singlet_scan_commands(self): commands = to_increment_commands([self.program]) self.assertEqual(self.commands, commands) + +class TransformedRampTest(TestCase): + def setUp(self): + hold = ConstantPT(10 ** 6, {'a': '-1. + idx * 0.01'}) + self.pulse_template = hold.with_iteration('idx', 200) + self.transformation = ScalingTransformation({'a': 2.0}) + + self.program = LinSpaceIter( + length=200, + body=(LinSpaceHold( + bases=(-2.,), + factors=((0.02,),), + duration_base=TimeType(10 ** 6), + duration_factors=None + ),) + ) + + def test_global_trafo_program(self): + program_builder = LinSpaceBuilder(('a',)) + program = self.pulse_template.create_program(program_builder=program_builder, + global_transformation=self.transformation) + self.assertEqual([self.program], program) + + def test_local_trafo_program(self): + program_builder = LinSpaceBuilder(('a',)) + with self.assertRaises(NotImplementedError): + # not implemented yet. This test should work as soon as its implemented + program = self.pulse_template.create_program(program_builder=program_builder, + global_transformation=self.transformation, + to_single_waveform={self.pulse_template}) + self.assertEqual([self.program], program) From 47540e461cc85462c057703d01776ff8cc9df8a4 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 19 Oct 2023 17:25:54 +0200 Subject: [PATCH 150/441] Remove dead code --- qupulse/program/linspace.py | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/qupulse/program/linspace.py b/qupulse/program/linspace.py index d99d993a0..177afa97c 100644 --- a/qupulse/program/linspace.py +++ b/qupulse/program/linspace.py @@ -49,33 +49,6 @@ def dependencies(self) -> Mapping[int, set]: for idx, factors in enumerate(self.factors) if factors} - def to_increment_commands(self, previous: Tuple[float, ...], iter_advance: Sequence[bool]): - if self.duration_factors: - raise NotImplementedError('Variable durations are not implemented for increment commands yet') - set_vals = [] - inc_vals = [] - for prev, base, factors in zip(previous, self.bases, self.factors): - set_val = base - if set_val == prev: - # TODO: epsilon - set_val = None - - if factors: - inc_val = 0. - for advance, factor in zip(iter_advance, factors): - if advance: - inc_val += factor - - inc_val = None - if base != prev: - - set_vals.append(base) - else: - pass - assert inc_val is None or set_val is None - inc_vals.append(inc_val) - set_vals.append(set_val) - @dataclass class LinSpaceArbitraryWaveform(LinSpaceNode): From af7cac5888f08b4b7b72160f12662fd07719477a Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 19 Oct 2023 17:26:47 +0200 Subject: [PATCH 151/441] Make repetitions work and test it --- qupulse/program/linspace.py | 2 +- tests/program/linspace_tests.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/qupulse/program/linspace.py b/qupulse/program/linspace.py index 177afa97c..ed4090b6e 100644 --- a/qupulse/program/linspace.py +++ b/qupulse/program/linspace.py @@ -81,7 +81,7 @@ def dependencies(self): for idx, deps in node.dependencies().items(): shortened = {dep[:-1] for dep in deps} if shortened != {()}: - dependencies.setdefault(idx, set()).update(deps) + dependencies.setdefault(idx, set()).update(shortened) return dependencies diff --git a/tests/program/linspace_tests.py b/tests/program/linspace_tests.py index 07ed16e3c..03a5b2971 100644 --- a/tests/program/linspace_tests.py +++ b/tests/program/linspace_tests.py @@ -1,3 +1,4 @@ +import copy import unittest from unittest import TestCase @@ -100,6 +101,7 @@ def setUp(self): hold = ConstantPT(10**6, {'a': '-1. + idx_a * 0.01 + idx_b * 1e-3', 'b': '-.5 + idx_b * 0.02 - 3e-3 * idx_a'}) scan_a = hold.with_iteration('idx_a', 200) self.pulse_template = scan_a.with_iteration('idx_b', 100) + self.repeated_pt = self.pulse_template.with_repetition(42) self.program = LinSpaceIter(length=100, body=(LinSpaceIter( length=200, @@ -111,6 +113,7 @@ def setUp(self): duration_factors=None ),) ),)) + self.repeated_program = LinSpaceRepeat(body=(self.program,), count=42) key_0 = DepKey.from_voltages((1e-3, 0.01,), DEFAULT_INCREMENT_RESOLUTION) key_1 = DepKey.from_voltages((0.02, -3e-3), DEFAULT_INCREMENT_RESOLUTION) @@ -140,16 +143,30 @@ def setUp(self): LoopJmp(1), ] + inner_commands = copy.deepcopy(self.commands) + for cmd in inner_commands: + if hasattr(cmd, 'idx'): + cmd.idx += 1 + self.repeated_commands = [LoopLabel(0, 42)] + inner_commands + [LoopJmp(0)] def test_program(self): program_builder = LinSpaceBuilder(('a', 'b')) program = self.pulse_template.create_program(program_builder=program_builder) self.assertEqual([self.program], program) + def test_repeated_program(self): + program_builder = LinSpaceBuilder(('a', 'b')) + program = self.repeated_pt.create_program(program_builder=program_builder) + self.assertEqual([self.repeated_program], program) + def test_increment_commands(self): commands = to_increment_commands([self.program]) self.assertEqual(self.commands, commands) + def test_repeated_increment_commands(self): + commands = to_increment_commands([self.repeated_program]) + self.assertEqual(self.repeated_commands, commands) + class SingletLoadProcessing(TestCase): def setUp(self): From bd033e51ea68eb799f68a4d2bc1c08a3ecc7e28f Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 19 Oct 2023 17:35:49 +0200 Subject: [PATCH 152/441] Add more docstrings and mark the translation state as an implementation detail --- qupulse/program/linspace.py | 180 +++++++++++++++++++----------------- 1 file changed, 95 insertions(+), 85 deletions(-) diff --git a/qupulse/program/linspace.py b/qupulse/program/linspace.py index ed4090b6e..2ac6c51a9 100644 --- a/qupulse/program/linspace.py +++ b/qupulse/program/linspace.py @@ -18,7 +18,11 @@ @dataclass(frozen=True) class DepKey: - """The key that identifies how a certain set command depends on iteration indices.""" + """The key that identifies how a certain set command depends on iteration indices. The factors are rounded with a + given resolution to be independent on rounding errors. + + These objects allow backends which support it to track multiple amplitudes at once. + """ factors: Tuple[int, ...] @classmethod @@ -38,6 +42,8 @@ def dependencies(self) -> Mapping[int, set]: @dataclass class LinSpaceHold(LinSpaceNode): + """Hold voltages for a given time. The voltages and the time may depend on the iteration index.""" + bases: Tuple[float, ...] factors: Tuple[Optional[Tuple[float, ...]], ...] @@ -52,12 +58,14 @@ def dependencies(self) -> Mapping[int, set]: @dataclass class LinSpaceArbitraryWaveform(LinSpaceNode): + """This is just a wrapper to pipe arbitrary waveforms through the system.""" waveform: Waveform channels: Tuple[ChannelID, ...] @dataclass class LinSpaceRepeat(LinSpaceNode): + """Repeat the body count times.""" body: Tuple[LinSpaceNode, ...] count: int @@ -71,7 +79,9 @@ def dependencies(self): @dataclass class LinSpaceIter(LinSpaceNode): - """Iteration in linear space are restricted to range 0 to length. Offsets and spacing are stored in the set node.""" + """Iteration in linear space are restricted to range 0 to length. + + Offsets and spacing are stored in the hold node.""" body: Tuple[LinSpaceNode, ...] length: int @@ -79,6 +89,7 @@ def dependencies(self): dependencies = {} for node in self.body: for idx, deps in node.dependencies().items(): + # remove the last elemt in index because this iteration sets it -> no external dependency shortened = {dep[:-1] for dep in deps} if shortened != {()}: dependencies.setdefault(idx, set()).update(shortened) @@ -247,11 +258,10 @@ class DepState: iterations: Tuple[int, ...] - - - @dataclass -class TranslationState: +class _TranslationState: + """This is the state of a translation of a LinSpace program to a command sequence.""" + label_num: int = dataclasses.field(default=0) commands: List[Command] = dataclasses.field(default_factory=list) iterations: List[int] = dataclasses.field(default_factory=list) @@ -280,86 +290,86 @@ def set_voltage(self, channel: int, value: float): self.active_dep[channel] = key self.plain_voltage[channel] = value - -def to_atomic_commands(node: Union[LinSpaceNode, Sequence[LinSpaceNode]], state: TranslationState): - """This step replaces iterations with """ - if isinstance(node, Sequence): - for lin_node in node: - to_atomic_commands(lin_node, state) - - elif isinstance(node, LinSpaceRepeat): - pre_dep_state = state.get_dependency_state(node.dependencies()) - label, jmp = state.new_loop(node.count) - initial_position = len(state.commands) - state.commands.append(label) - to_atomic_commands(node.body, state) - post_dep_state = state.get_dependency_state(node.dependencies()) - if pre_dep_state != post_dep_state: - # hackedy - state.commands.pop(initial_position) - state.commands.append(label) - label.count -= 1 - to_atomic_commands(node.body, state) - state.commands.append(jmp) - - elif isinstance(node, LinSpaceIter): - state.iterations.append(0) - to_atomic_commands(node.body, state) - - if node.length > 1: - state.iterations[-1] = node.length - label, jmp = state.new_loop(node.length - 1) - state.commands.append(label) - to_atomic_commands(node.body, state) - state.commands.append(jmp) - state.iterations.pop() - - elif isinstance(node, LinSpaceHold): - if node.duration_factors: - raise NotImplementedError("TODO") - - for ch, (base, factors) in enumerate(zip(node.bases, node.factors)): - if factors is None: - state.set_voltage(ch, base) - continue - - dep_key = DepKey.from_voltages(voltages=factors, resolution=state.resolution) - new_dep_state = DepState( - base, - iterations=tuple(state.iterations) - ) - - current_dep_state = state.dep_states.setdefault(ch, {}).get(dep_key, None) - if current_dep_state is None: - assert all(it == 0 for it in state.iterations) - state.commands.append(Set(ch, base, dep_key)) - state.active_dep[ch] = dep_key - - else: - inc = current_dep_state.base - new_dep_state.base - for i, j, factor in zip(current_dep_state.iterations, new_dep_state.iterations, factors): - if i == j: - continue - if i < j: - assert i == 0 - # regular iteration - inc += factor - else: - assert j == 0 - inc -= factor * i - # we insert all inc here (also inc == 0) because it signals to activate this amplitude register - if inc or state.active_dep.get(ch, None) != dep_key: - state.commands.append(Increment(ch, inc, dep_key)) - state.active_dep[ch] = dep_key - state.dep_states[ch][dep_key] = new_dep_state - state.commands.append(Wait(node.duration_base)) - elif isinstance(node, LinSpaceArbitraryWaveform): - state.commands.append(Play(node.waveform, node.channels)) - else: - raise TypeError("The node type is not handled", type(node), node) + def add_node(self, node: Union[LinSpaceNode, Sequence[LinSpaceNode]]): + """Translate a (sequence of) linspace node(s) to commands and add it to the internal command list.""" + if isinstance(node, Sequence): + for lin_node in node: + self.add_node(lin_node) + + elif isinstance(node, LinSpaceRepeat): + pre_dep_state = self.get_dependency_state(node.dependencies()) + label, jmp = self.new_loop(node.count) + initial_position = len(self.commands) + self.commands.append(label) + self.add_node(node.body) + post_dep_state = self.get_dependency_state(node.dependencies()) + if pre_dep_state != post_dep_state: + # hackedy + self.commands.pop(initial_position) + self.commands.append(label) + label.count -= 1 + self.add_node(node.body) + self.commands.append(jmp) + + elif isinstance(node, LinSpaceIter): + self.iterations.append(0) + self.add_node(node.body) + + if node.length > 1: + self.iterations[-1] = node.length + label, jmp = self.new_loop(node.length - 1) + self.commands.append(label) + self.add_node(node.body) + self.commands.append(jmp) + self.iterations.pop() + + elif isinstance(node, LinSpaceHold): + if node.duration_factors: + raise NotImplementedError("TODO") + + for ch, (base, factors) in enumerate(zip(node.bases, node.factors)): + if factors is None: + self.set_voltage(ch, base) + continue + + dep_key = DepKey.from_voltages(voltages=factors, resolution=self.resolution) + new_dep_state = DepState( + base, + iterations=tuple(self.iterations) + ) + + current_dep_state = self.dep_states.setdefault(ch, {}).get(dep_key, None) + if current_dep_state is None: + assert all(it == 0 for it in self.iterations) + self.commands.append(Set(ch, base, dep_key)) + self.active_dep[ch] = dep_key + + else: + inc = current_dep_state.base - new_dep_state.base + for i, j, factor in zip(current_dep_state.iterations, new_dep_state.iterations, factors): + if i == j: + continue + if i < j: + assert i == 0 + # regular iteration + inc += factor + else: + assert j == 0 + inc -= factor * i + # we insert all inc here (also inc == 0) because it signals to activate this amplitude register + if inc or self.active_dep.get(ch, None) != dep_key: + self.commands.append(Increment(ch, inc, dep_key)) + self.active_dep[ch] = dep_key + self.dep_states[ch][dep_key] = new_dep_state + self.commands.append(Wait(node.duration_base)) + elif isinstance(node, LinSpaceArbitraryWaveform): + self.commands.append(Play(node.waveform, node.channels)) + else: + raise TypeError("The node type is not handled", type(node), node) def to_increment_commands(linspace_nodes: Sequence[LinSpaceNode]) -> List[Command]: - state = TranslationState(0, [], [], active_dep={}, dep_states={}, plain_voltage={}) - to_atomic_commands(linspace_nodes, state) + """translate the given linspace node tree to a minimal sequence of set and increment commands as well as loops.""" + state = _TranslationState() + state.add_node(linspace_nodes) return state.commands From 6670bbf6d90bb829eeaaede7bf9c3ee905437153 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 19 Oct 2023 17:41:23 +0200 Subject: [PATCH 153/441] Use python 3.8 compatible type union syntax --- qupulse/program/linspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qupulse/program/linspace.py b/qupulse/program/linspace.py index 2ac6c51a9..6e54392e8 100644 --- a/qupulse/program/linspace.py +++ b/qupulse/program/linspace.py @@ -249,7 +249,7 @@ class Play: channels: Tuple[ChannelID] -Command = Increment | Set | LoopLabel | LoopJmp | Wait | Play +Command = Union[Increment, Set, LoopLabel, LoopJmp, Wait, Play] @dataclass(frozen=True) From 917708101145d00e28a0a9c58736d10277d0a808 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Wed, 8 Nov 2023 11:08:26 +0100 Subject: [PATCH 154/441] Push version --- qupulse/__init__.py | 2 +- setup.cfg | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/qupulse/__init__.py b/qupulse/__init__.py index d7b836bbc..7a51cb38d 100644 --- a/qupulse/__init__.py +++ b/qupulse/__init__.py @@ -2,7 +2,7 @@ import lazy_loader as lazy -__version__ = '0.8' +__version__ = '0.9' __getattr__, __dir__, __all__ = lazy.attach_stub(__name__, __file__) diff --git a/setup.cfg b/setup.cfg index 8b33e9619..a53d823b9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -82,8 +82,8 @@ filterwarnings = [build_sphinx] project = 'qupulse' -version = 0.8 -release = 0.8 +version = 0.9 +release = 0.9 source-dir = ./doc/source build-dir = ./doc/build fresh-env = 1 From 5f9a8c7660aef769546888fdd7e226a44d3f99a4 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Wed, 8 Nov 2023 11:10:00 +0100 Subject: [PATCH 155/441] Compile release notes --- RELEASE_NOTES.rst | 22 ++++++++++++++++++++++ changes.d/692.feature | 1 - changes.d/750.feature | 5 ----- changes.d/771.misc | 1 - changes.d/779.feature | 1 - changes.d/801.feature | 2 -- 6 files changed, 22 insertions(+), 10 deletions(-) delete mode 100644 changes.d/692.feature delete mode 100644 changes.d/750.feature delete mode 100644 changes.d/771.misc delete mode 100644 changes.d/779.feature delete mode 100644 changes.d/801.feature diff --git a/RELEASE_NOTES.rst b/RELEASE_NOTES.rst index 51c5c0b07..aea933a6b 100644 --- a/RELEASE_NOTES.rst +++ b/RELEASE_NOTES.rst @@ -2,6 +2,28 @@ .. towncrier release notes start +qupulse 0.9 (2023-11-08) +======================== + +Features +-------- + +- Add `__pow__` as a repetition shortcut. This means you can do `my_pulse_template ** 5` or `my_pulse_template ** 'my_repetition_count'`. (`#692 `_) +- Promote ``qupulse.expression`` to a subpackage and create ``qupulse.expression.protocol`` with protocol classes that define the expression interface that is supposed to be used by qupulse. + The ```sympy`` based implementation is moved to ``qupulse.expressions.sympy`` and imported in ``qupulse.expressions``. + + The intended use is to be able to use less powerful but faster implementations of the ``Expression`` protocol where appropriate. + In this first iteration, qupulse still relies on internals of the ``sympy`` based implementation in many places which is to be removed in the future. (`#750 `_) +- Promote parts of the private subpackage `qupulse._program` to the public subpackage `qupulse.program`, i.e. `loop`, `volatile`, `transformation` and `waveforms`. This allows external packages/drivers to rely on stability of the `Loop` class. (`#779 `_) +- Add ``PulseTemplate.pad_to`` method to help padding to minimal lengths or multiples of given durations. (`#801 `_) + + +Misc +---- + +- `#771 `_ + + qupulse 0.8 (2023-03-28) ======================== diff --git a/changes.d/692.feature b/changes.d/692.feature deleted file mode 100644 index 7cbdb46cb..000000000 --- a/changes.d/692.feature +++ /dev/null @@ -1 +0,0 @@ -Add `__pow__` as a repetition shortcut. This means you can do `my_pulse_template ** 5` or `my_pulse_template ** 'my_repetition_count'`. \ No newline at end of file diff --git a/changes.d/750.feature b/changes.d/750.feature deleted file mode 100644 index 9e745d690..000000000 --- a/changes.d/750.feature +++ /dev/null @@ -1,5 +0,0 @@ -Promote ``qupulse.expression`` to a subpackage and create ``qupulse.expression.protocol`` with protocol classes that define the expression interface that is supposed to be used by qupulse. -The ```sympy`` based implementation is moved to ``qupulse.expressions.sympy`` and imported in ``qupulse.expressions``. - -The intended use is to be able to use less powerful but faster implementations of the ``Expression`` protocol where appropriate. -In this first iteration, qupulse still relies on internals of the ``sympy`` based implementation in many places which is to be removed in the future. diff --git a/changes.d/771.misc b/changes.d/771.misc deleted file mode 100644 index 168460257..000000000 --- a/changes.d/771.misc +++ /dev/null @@ -1 +0,0 @@ -The `repr` implementation of `Loop` will now return an evaluable python expression. The old behaviour moved to the `str` implementation. diff --git a/changes.d/779.feature b/changes.d/779.feature deleted file mode 100644 index edae2b25c..000000000 --- a/changes.d/779.feature +++ /dev/null @@ -1 +0,0 @@ -Promote parts of the private subpackage `qupulse._program` to the public subpackage `qupulse.program`, i.e. `loop`, `volatile`, `transformation` and `waveforms`. This allows external packages/drivers to rely on stability of the `Loop` class. diff --git a/changes.d/801.feature b/changes.d/801.feature deleted file mode 100644 index fa703198f..000000000 --- a/changes.d/801.feature +++ /dev/null @@ -1,2 +0,0 @@ -Add ``PulseTemplate.pad_to`` method to help padding to minimal lengths or multiples of given durations. - \ No newline at end of file From 86893e7464fd65a33bee3968bb0a4492e49e1af7 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 16 Nov 2023 13:03:22 +0100 Subject: [PATCH 156/441] Re factor code for better readability --- qupulse/program/linspace.py | 157 ++++++++++++++++++++++-------------- 1 file changed, 95 insertions(+), 62 deletions(-) diff --git a/qupulse/program/linspace.py b/qupulse/program/linspace.py index 6e54392e8..89cc96872 100644 --- a/qupulse/program/linspace.py +++ b/qupulse/program/linspace.py @@ -10,7 +10,6 @@ SimpleExpression) from qupulse.program.waveforms import MultiChannelWaveform - # this resolution is used to unify increments # the increments themselves remain floats DEFAULT_INCREMENT_RESOLUTION: float = 1e-9 @@ -36,6 +35,7 @@ def from_voltages(cls, voltages: Sequence[float], resolution: float): @dataclass class LinSpaceNode: """AST node for a program that supports linear spacing of set points as well as nested sequencing and repetitions""" + def dependencies(self) -> Mapping[int, set]: raise NotImplementedError @@ -104,6 +104,7 @@ class LinSpaceBuilder(ProgramBuilder): Arbitrary waveforms are not implemented yet """ + def __init__(self, channels: Tuple[ChannelID, ...]): super().__init__() self._name_to_idx = {name: idx for idx, name in enumerate(channels)} @@ -257,6 +258,31 @@ class DepState: base: float iterations: Tuple[int, ...] + def required_increment_from(self, previous: 'DepState', factors: Sequence[float]) -> float: + assert len(self.iterations) == len(previous.iterations) + assert len(self.iterations) == len(factors) + + increment = self.base - previous.base + for old, new, factor in zip(previous.iterations, self.iterations, factors): + # By convention there are only two possible values for each integer here: 0 or the last index + # The three possible increments are none, regular and jump to next line + + if old == new: + # we are still in the same iteration of this sweep + pass + + elif old < new: + assert old == 0 + # regular iteration, although the new value will probably be > 1, the resulting increment will be + # applied multiple times so only one factor is needed. + increment += factor + + else: + assert new == 0 + # we need to jump back. The old value gives us the number of increments to reverse + increment -= factor * old + return increment + @dataclass class _TranslationState: @@ -290,6 +316,69 @@ def set_voltage(self, channel: int, value: float): self.active_dep[channel] = key self.plain_voltage[channel] = value + def _add_repetition_node(self, node: LinSpaceRepeat): + pre_dep_state = self.get_dependency_state(node.dependencies()) + label, jmp = self.new_loop(node.count) + initial_position = len(self.commands) + self.commands.append(label) + self.add_node(node.body) + post_dep_state = self.get_dependency_state(node.dependencies()) + if pre_dep_state != post_dep_state: + # hackedy + self.commands.pop(initial_position) + self.commands.append(label) + label.count -= 1 + self.add_node(node.body) + self.commands.append(jmp) + + def _add_iteration_node(self, node: LinSpaceIter): + self.iterations.append(0) + self.add_node(node.body) + + if node.length > 1: + self.iterations[-1] = node.length + label, jmp = self.new_loop(node.length - 1) + self.commands.append(label) + self.add_node(node.body) + self.commands.append(jmp) + self.iterations.pop() + + def _set_indexed_voltage(self, channel: int, base: float, factors: Sequence[float]): + dep_key = DepKey.from_voltages(voltages=factors, resolution=self.resolution) + new_dep_state = DepState( + base, + iterations=tuple(self.iterations) + ) + + current_dep_state = self.dep_states.setdefault(channel, {}).get(dep_key, None) + if current_dep_state is None: + assert all(it == 0 for it in self.iterations) + self.commands.append(Set(channel, base, dep_key)) + self.active_dep[channel] = dep_key + + else: + inc = new_dep_state.required_increment_from(previous=current_dep_state, factors=factors) + + # we insert all inc here (also inc == 0) because it signals to activate this amplitude register + if inc or self.active_dep.get(channel, None) != dep_key: + self.commands.append(Increment(channel, inc, dep_key)) + self.active_dep[channel] = dep_key + self.dep_states[channel][dep_key] = new_dep_state + + def _add_hold_node(self, node: LinSpaceHold): + if node.duration_factors: + raise NotImplementedError("TODO") + + for ch, (base, factors) in enumerate(zip(node.bases, node.factors)): + if factors is None: + self.set_voltage(ch, base) + continue + + else: + self._set_indexed_voltage(ch, base, factors) + + self.commands.append(Wait(node.duration_base)) + def add_node(self, node: Union[LinSpaceNode, Sequence[LinSpaceNode]]): """Translate a (sequence of) linspace node(s) to commands and add it to the internal command list.""" if isinstance(node, Sequence): @@ -297,73 +386,17 @@ def add_node(self, node: Union[LinSpaceNode, Sequence[LinSpaceNode]]): self.add_node(lin_node) elif isinstance(node, LinSpaceRepeat): - pre_dep_state = self.get_dependency_state(node.dependencies()) - label, jmp = self.new_loop(node.count) - initial_position = len(self.commands) - self.commands.append(label) - self.add_node(node.body) - post_dep_state = self.get_dependency_state(node.dependencies()) - if pre_dep_state != post_dep_state: - # hackedy - self.commands.pop(initial_position) - self.commands.append(label) - label.count -= 1 - self.add_node(node.body) - self.commands.append(jmp) + self._add_repetition_node(node) elif isinstance(node, LinSpaceIter): - self.iterations.append(0) - self.add_node(node.body) - - if node.length > 1: - self.iterations[-1] = node.length - label, jmp = self.new_loop(node.length - 1) - self.commands.append(label) - self.add_node(node.body) - self.commands.append(jmp) - self.iterations.pop() + self._add_iteration_node(node) elif isinstance(node, LinSpaceHold): - if node.duration_factors: - raise NotImplementedError("TODO") - - for ch, (base, factors) in enumerate(zip(node.bases, node.factors)): - if factors is None: - self.set_voltage(ch, base) - continue - - dep_key = DepKey.from_voltages(voltages=factors, resolution=self.resolution) - new_dep_state = DepState( - base, - iterations=tuple(self.iterations) - ) - - current_dep_state = self.dep_states.setdefault(ch, {}).get(dep_key, None) - if current_dep_state is None: - assert all(it == 0 for it in self.iterations) - self.commands.append(Set(ch, base, dep_key)) - self.active_dep[ch] = dep_key - - else: - inc = current_dep_state.base - new_dep_state.base - for i, j, factor in zip(current_dep_state.iterations, new_dep_state.iterations, factors): - if i == j: - continue - if i < j: - assert i == 0 - # regular iteration - inc += factor - else: - assert j == 0 - inc -= factor * i - # we insert all inc here (also inc == 0) because it signals to activate this amplitude register - if inc or self.active_dep.get(ch, None) != dep_key: - self.commands.append(Increment(ch, inc, dep_key)) - self.active_dep[ch] = dep_key - self.dep_states[ch][dep_key] = new_dep_state - self.commands.append(Wait(node.duration_base)) + self._add_hold_node(node) + elif isinstance(node, LinSpaceArbitraryWaveform): self.commands.append(Play(node.waveform, node.channels)) + else: raise TypeError("The node type is not handled", type(node), node) From 6b23c89e97145eca57e4afefe5f521dbee1f63ce Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 16 Nov 2023 13:08:18 +0100 Subject: [PATCH 157/441] De stringify type annotation --- qupulse/program/linspace.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qupulse/program/linspace.py b/qupulse/program/linspace.py index 89cc96872..7dc3ddc60 100644 --- a/qupulse/program/linspace.py +++ b/qupulse/program/linspace.py @@ -224,14 +224,14 @@ class LoopLabel: class Increment: channel: int value: float - dependency_key: 'DepKey' + dependency_key: DepKey @dataclass class Set: channel: int value: float - key: 'DepKey' = dataclasses.field(default_factory=lambda: DepKey(())) + key: DepKey = dataclasses.field(default_factory=lambda: DepKey(())) @dataclass From 5cc52b420198e03a721a501bb77be109568a5d8e Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Sat, 9 Mar 2024 12:17:24 +0100 Subject: [PATCH 158/441] Move hdawg driver to legacy --- qupulse/_program/seqc.py | 1498 ------------------------------ qupulse/hardware/awgs/zihdawg.py | 1221 +----------------------- qupulse/hardware/util.py | 2 +- setup.cfg | 3 +- 4 files changed, 18 insertions(+), 2706 deletions(-) delete mode 100644 qupulse/_program/seqc.py diff --git a/qupulse/_program/seqc.py b/qupulse/_program/seqc.py deleted file mode 100644 index 989ffdc52..000000000 --- a/qupulse/_program/seqc.py +++ /dev/null @@ -1,1498 +0,0 @@ -"""This module contains the ZI HDAWG compatible description of programs. There is no code in here that interacts with -hardware directly. - -The public interface to all functionality is given by `HDAWGProgramManager`. This class can create seqc source code -which contains multiple programs and allows switching between these with the user registers of a device, - -Furthermore: -- `SEQCNode`: AST of a subset of sequencing C -- `loop_to_seqc`: conversion of `Loop` objects to this subset in a clever way -- `BinaryWaveform`: Bundles functionality of handling segments in a native way. -- `WaveformMemory`: Functionality to sync waveforms to the device (via the LabOne user folder) -- `ProgramWaveformManager` and `HDAWGProgramEntry`: Program wise handling of waveforms and seqc-code -classes that convert `Loop` objects""" -import warnings -from typing import Optional, Union, Sequence, Dict, Iterator, Tuple, Callable, NamedTuple, MutableMapping, Mapping,\ - Iterable, Any, List, Deque -from types import MappingProxyType -import abc -import itertools -import inspect -import logging -import hashlib -from weakref import WeakValueDictionary -from collections import OrderedDict -import re -import collections -import numbers -import string -import functools - -import numpy as np -from pathlib import Path - -from qupulse.utils.types import ChannelID, TimeType -from qupulse.utils import replace_multiple, grouper -from qupulse.program.waveforms import Waveform -from qupulse.program.loop import Loop -from qupulse.program.volatile import VolatileRepetitionCount, VolatileProperty -from qupulse.hardware.awgs.base import ProgramEntry -from qupulse.hardware.util import zhinst_voltage_to_uint16 - -try: - # zhinst fires a DeprecationWarning from its own code in some versions... - with warnings.catch_warnings(): - warnings.simplefilter('ignore', DeprecationWarning) - import zhinst.utils -except ImportError: - zhinst = None - - -__all__ = ["HDAWGProgramManager"] - - -def make_valid_identifier(name: str) -> str: - # replace all invalid characters and conactenate with hash of original name - name_hash = hashlib.sha256(name.encode('utf-8')).hexdigest() - valid_chars = string.ascii_letters + string.digits + '_' - namestub = ''.join(c for c in name if c in valid_chars) - return f'renamed_{namestub}_{name_hash}' - - -class BinaryWaveform: - """This class represents a sampled waveform in the native HDAWG format as returned - by zhinst.utils.convert_awg_waveform. - - BinaryWaveform.data can be uploaded directly to {device]/awgs/{awg}/waveform/waves/{wf} - - `to_csv_compatible_table` can be used to create a compatible compact csv file (with marker data included) - """ - __slots__ = ('data',) - - PLAYBACK_QUANTUM = 16 - PLAYBACK_MIN_QUANTA = 2 - - def __init__(self, data: np.ndarray): - """ TODO: always use both channels? - - Args: - data: data as returned from zhinst.utils.convert_awg_waveform - """ - n_quantum, remainder = divmod(data.size, 3 * self.PLAYBACK_QUANTUM) - assert n_quantum > 1, "Waveform too short (min len is 32)" - assert remainder == 0, "Waveform has not a valid length" - assert data.dtype is np.dtype('uint16') - assert np.all(data[2::3] < 16), "invalid marker data" - assert data.ndim == 1, "Data not one dimensional" - - self.data = data - self.data.flags.writeable = False - - @property - def ch1(self): - return self.data[::3] - - @property - def ch2(self): - return self.data[1::3] - - @property - def marker_data(self): - return self.data[2::3] - - @property - def markers_ch1(self): - return np.bitwise_and(self.marker_data, 0b0011) - - @property - def markers_ch2(self): - return np.right_shift(np.bitwise_and(self.marker_data, 0b1100), 2) - - @classmethod - def from_sampled(cls, ch1: Optional[np.ndarray], ch2: Optional[np.ndarray], - markers: Tuple[Optional[np.ndarray], Optional[np.ndarray], - Optional[np.ndarray], Optional[np.ndarray]]) -> 'BinaryWaveform': - """Combines the sampled and scaled waveform data into a single binary compatible waveform - - Args: - ch1: sampled waveform scaled to full range (-1., 1.) - ch2: sampled waveform scaled to full range (-1., 1.) - markers: (ch1_front_marker, ch1_dio_marker, ch2_front_marker, ch2_dio_marker) - - Returns: - - """ - return cls(zhinst_voltage_to_uint16(ch1, ch2, markers)) - - @classmethod - def zeroed(cls, size): - return cls(zhinst.utils.convert_awg_waveform(np.zeros(size), np.zeros(size), np.zeros(size, dtype=np.uint16))) - - def __len__(self): - return self.data.size // 3 - - def __eq__(self, other): - return np.array_equal(self.data, other.data) - - def __hash__(self): - return hash(bytes(self.data)) - - def fingerprint(self) -> str: - """This fingerprint is runtime independent""" - return hashlib.sha256(self.data).hexdigest() - - def to_csv_compatible_table(self) -> np.ndarray: - """The integer values in that file should be 18-bit unsigned integers with the two least significant bits - being the markers. The values are mapped to 0 => -FS, 262143 => +FS, with FS equal to the full scale. - - >>> np.savetxt(waveform_dir, binary_waveform.to_csv_compatible_table(), fmt='%u') - """ - assert self.data.size % self.PLAYBACK_QUANTUM == 0, "conversion to csv requires a valid length" - - table = np.zeros((len(self), 2), dtype=np.uint32) - table[:, 0] = self.ch1 - table[:, 1] = self.ch2 - np.left_shift(table, 2, out=table) - table[:, 0] += self.markers_ch1 - table[:, 1] += self.markers_ch2 - - return table - - def playback_possible(self) -> bool: - """Returns if the waveform can be played without padding""" - return self.data.size % self.PLAYBACK_QUANTUM == 0 - - def dynamic_rate(self, max_rate: int = 12) -> int: - min_pre_division_quanta = 2 * self.PLAYBACK_QUANTUM - - reduced = self.data.reshape(-1, 3) - for n in range(max_rate): - n_quantum, remainder = divmod(reduced.shape[0], min_pre_division_quanta) - if remainder != 0 or n_quantum < self.PLAYBACK_MIN_QUANTA or np.any(reduced[::2, :] != reduced[1::2, :]): - return n - reduced = reduced[::2, :] - return max_rate - - -class ConcatenatedWaveform: - def __init__(self): - """Handle the concatenation of multiple binary waveforms to create a big indexable waveform.""" - self._concatenated: Optional[List[Tuple[BinaryWaveform, ...]]] = [] - self._as_binary: Optional[Tuple[BinaryWaveform, ...]] = None - - def __bool__(self): - return bool(self._concatenated) - - def is_finalized(self): - return self._as_binary is not None or self._concatenated is None - - def as_binary(self) -> Optional[Tuple[BinaryWaveform, ...]]: - assert self.is_finalized() - return self._as_binary - - def append(self, binary_waveform: Tuple[BinaryWaveform, ...]): - assert not self.is_finalized() - assert not self._concatenated or len(self._concatenated[-1]) == len(binary_waveform) - self._concatenated.append(binary_waveform) - - def finalize(self): - assert not self.is_finalized() - if self._concatenated: - n_groups = len(self._concatenated[0]) - as_binary = [[] for _ in range(n_groups)] - for wf_tuple in self._concatenated: - for grp, wf in enumerate(wf_tuple): - as_binary[grp].append(wf.data) - self._as_binary = tuple(BinaryWaveform(np.concatenate(as_bin)) for as_bin in as_binary) - else: - self._concatenated = None - - def clear(self): - if self._concatenated is None: - self._concatenated = [] - else: - self._concatenated.clear() - self._as_binary = None - - -class WaveformFileSystem: - logger = logging.getLogger('qupulse.hdawg.waveforms') - _by_path = WeakValueDictionary() - - def __init__(self, path: Path): - """This class coordinates multiple AWGs (channel pairs) using the same file system to store the waveforms. - - Args: - path: Waveforms are stored here - """ - self._required = {} - self._path = path - - @classmethod - def get_waveform_file_system(cls, path: Path) -> 'WaveformFileSystem': - """Get the instance for the given path. Multiple instances that access the same path lead to inconsistencies.""" - return cls._by_path.setdefault(path, cls(path)) - - def sync(self, client: 'WaveformMemory', waveforms: Mapping[str, BinaryWaveform], **kwargs): - """Write the required waveforms to the filesystem.""" - self._required[id(client)] = waveforms - self._sync(**kwargs) - - def _sync(self, delete=True, write_all=False): - to_save = {self._path.joinpath(file_name): binary - for d in self._required.values() - for file_name, binary in d.items()} - - for existing_file in self._path.iterdir(): - if not existing_file.is_file(): - pass - elif existing_file in to_save: - if not write_all: - self.logger.debug('Skipping %r', existing_file.name) - to_save.pop(existing_file) - elif delete: - try: - self.logger.debug('Deleting %r', existing_file.name) - existing_file.unlink() - except OSError: - self.logger.exception("Error deleting: %r", existing_file.name) - - for file_name, binary_waveform in to_save.items(): - table = binary_waveform.to_csv_compatible_table() - np.savetxt(file_name, table, '%u') - self.logger.debug('Wrote %r', file_name) - - -class WaveformMemory: - """Global waveform "memory" representation (currently the file system)""" - CONCATENATED_WAVEFORM_TEMPLATE = '{program_name}_concatenated_waveform_{group_index}' - SHARED_WAVEFORM_TEMPLATE = '{program_name}_shared_waveform_{hash}' - WF_PLACEHOLDER_TEMPLATE = '*{id}*' - FILE_NAME_TEMPLATE = '{hash}.csv' - - _WaveInfo = NamedTuple('_WaveInfo', [('wave_name', str), - ('file_name', str), - ('binary_waveform', BinaryWaveform)]) - - def __init__(self): - self.shared_waveforms = OrderedDict() # type: MutableMapping[BinaryWaveform, set] - self.concatenated_waveforms = OrderedDict() # type: MutableMapping[str, ConcatenatedWaveform] - - def clear(self): - self.shared_waveforms.clear() - self.concatenated_waveforms.clear() - - def _shared_waveforms_iter(self) -> Iterator[Tuple[str, _WaveInfo]]: - for wf, program_set in self.shared_waveforms.items(): - if program_set: - wave_hash = wf.fingerprint() - wave_name = self.SHARED_WAVEFORM_TEMPLATE.format(program_name='_'.join(program_set), - hash=wave_hash) - wave_placeholder = self.WF_PLACEHOLDER_TEMPLATE.format(id=id(program_set)) - file_name = self.FILE_NAME_TEMPLATE.format(hash=wave_hash) - yield wave_placeholder, self._WaveInfo(wave_name, file_name, wf) - - def _concatenated_waveforms_iter(self) -> Iterator[Tuple[str, Tuple[_WaveInfo, ...]]]: - for program_name, concatenated_waveform in self.concatenated_waveforms.items(): - # we assume that if the first entry is not empty the rest also isn't - if concatenated_waveform: - infos = [] - for group_index, binary in enumerate(concatenated_waveform.as_binary()): - wave_hash = binary.fingerprint() - wave_name = self.CONCATENATED_WAVEFORM_TEMPLATE.format(program_name=program_name, - group_index=group_index) - file_name = self.FILE_NAME_TEMPLATE.format(hash=wave_hash) - infos.append(self._WaveInfo(wave_name, file_name, binary)) - - wave_placeholder = self.WF_PLACEHOLDER_TEMPLATE.format(id=id(concatenated_waveform)) - yield wave_placeholder, tuple(infos) - - def _all_info_iter(self) -> Iterator[_WaveInfo]: - for _, infos in self._concatenated_waveforms_iter(): - yield from infos - for _, info in self._shared_waveforms_iter(): - yield info - - def waveform_name_replacements(self) -> Dict[str, str]: - """replace place holders of complete seqc program with - - >>> waveform_name_translation = waveform_memory.waveform_name_replacements() - >>> seqc_program = qupulse.utils.replace_multiple(seqc_program, waveform_name_translation) - """ - translation = {} - for wave_placeholder, wave_info in self._shared_waveforms_iter(): - translation[wave_placeholder] = wave_info.wave_name - - for wave_placeholder, wave_infos in self._concatenated_waveforms_iter(): - translation[wave_placeholder] = ','.join(info.wave_name for info in wave_infos) - return translation - - def waveform_declaration(self) -> str: - """Produces a string that declares all needed waveforms. - It is needed to know the waveform index in case we want to update a waveform during playback.""" - declarations = [] - for wave_info in self._all_info_iter(): - declarations.append( - 'wave {wave_name} = "{file_name}";'.format(wave_name=wave_info.wave_name, - file_name=wave_info.file_name.replace('.csv', '')) - ) - return '\n'.join(declarations) - - def sync_to_file_system(self, file_system: WaveformFileSystem): - to_save = {wave_info.file_name: wave_info.binary_waveform - for wave_info in self._all_info_iter()} - file_system.sync(self, to_save) - - -class ProgramWaveformManager: - """Manages waveforms of a program""" - def __init__(self, name: str, memory: WaveformMemory): - if not name.isidentifier(): - waveform_name = make_valid_identifier(name) - else: - waveform_name = name - - self._waveform_name = waveform_name - self._program_name = name - self._memory = memory - - assert self._program_name not in self._memory.concatenated_waveforms - assert all(self._program_name not in programs for programs in self._memory.shared_waveforms.values()) - self._memory.concatenated_waveforms[waveform_name] = ConcatenatedWaveform() - - @property - def program_name(self) -> str: - return self._program_name - - @property - def main_waveform_name(self) -> str: - self._waveform_name - - def clear_requested(self): - for programs in self._memory.shared_waveforms.values(): - programs.discard(self._program_name) - self._memory.concatenated_waveforms[self._waveform_name].clear() - - def request_shared(self, binary_waveform: Tuple[BinaryWaveform, ...]) -> str: - """Register waveform if not already registered and return a unique identifier placeholder. - - The unique identifier currently is computed from the id of the set which stores all programs using this - waveform. - """ - placeholders = [] - for wf in binary_waveform: - program_set = self._memory.shared_waveforms.setdefault(wf, set()) - program_set.add(self._program_name) - placeholders.append(self._memory.WF_PLACEHOLDER_TEMPLATE.format(id=id(program_set))) - return ",".join(placeholders) - - def request_concatenated(self, binary_waveform: Tuple[BinaryWaveform, ...]) -> str: - """Append the waveform to the concatenated waveform""" - bin_wf_list = self._memory.concatenated_waveforms[self._waveform_name] - bin_wf_list.append(binary_waveform) - return self._memory.WF_PLACEHOLDER_TEMPLATE.format(id=id(bin_wf_list)) - - def finalize(self): - self._memory.concatenated_waveforms[self._waveform_name].finalize() - - def prepare_delete(self): - """Delete all references in waveform memory to this program. Cannot be used afterwards.""" - self.clear_requested() - del self._memory.concatenated_waveforms[self._waveform_name] - - -class UserRegister: - """This class is a helper class to avoid errors due to 0 and 1 based register indexing""" - __slots__ = ('_zero_based_value',) - - def __init__(self, *, zero_based_value: int = None, one_based_value: int = None): - assert None in (zero_based_value, one_based_value) - assert isinstance(zero_based_value, int) or isinstance(one_based_value, int) - - if one_based_value is not None: - assert one_based_value > 0, "A one based value needs to be larger zero" - self._zero_based_value = one_based_value - 1 - else: - self._zero_based_value = zero_based_value - - @classmethod - def from_seqc(cls, value: int) -> 'UserRegister': - return cls(zero_based_value=value) - - def to_seqc(self) -> int: - return self._zero_based_value - - @classmethod - def from_labone(cls, value: int) -> 'UserRegister': - return cls(zero_based_value=value) - - def to_labone(self) -> int: - return self._zero_based_value - - @classmethod - def from_web_interface(cls, value: int) -> 'UserRegister': - return cls(one_based_value=value) - - def to_web_interface(self) -> int: - return self._zero_based_value + 1 - - def __hash__(self): - return hash(self._zero_based_value) - - def __eq__(self, other): - return self._zero_based_value == getattr(other, '_zero_based_value', None) - - def __repr__(self): - return 'UserRegister(zero_based_value={zero_based_value})'.format(zero_based_value=self._zero_based_value) - - def __format__(self, format_spec: str) -> str: - if format_spec in ('zero_based', 'seqc', 'labone', 'lab_one'): - return str(self.to_seqc()) - elif format_spec in ('one_based', 'web', 'web_interface'): - return str(self.to_web_interface()) - elif format_spec in ('repr', 'r'): - return repr(self) - else: - raise ValueError('Invalid format spec for UserRegister: ', format_spec) - - -class UserRegisterManager: - """This class keeps track of the user registered that are used in a certain context""" - def __init__(self, available: Iterable[UserRegister], name_template: str): - assert 'register' in (x[1] for x in string.Formatter().parse(name_template)) - - self._available = set(available) - self._name_template = name_template - self._used = {} - - def request(self, obj) -> str: - """Request a user register name to store object. If an object that evaluates equal to obj was requested before - the name name is returned. - - Args: - obj: Object to store - - Returns: - Name of the variable with the user register - - Raises: - Value error if no register is available - """ - for register, registered_obj in self._used.items(): - if obj == registered_obj: - return self._name_template.format(register=register) - if self._available: - register = self._available.pop() - self._used[register] = obj - return self._name_template.format(register=register) - else: - raise ValueError("No register available for %r" % obj) - - def iter_used_register_names(self) -> Iterator[Tuple[UserRegister, str]]: - """ - - Returns: - An iterator over (register index, register name) pairs - """ - return ((register, self._name_template.format(register=register)) for register in self._used.keys()) - - def iter_used_register_values(self) -> Iterable[Tuple[UserRegister, Any]]: - return self._used.items() - - -class HDAWGProgramEntry(ProgramEntry): - USER_REG_NAME_TEMPLATE = 'user_reg_{register:seqc}' - - def __init__(self, loop: Loop, selection_index: int, waveform_memory: WaveformMemory, program_name: str, - channels: Tuple[Optional[ChannelID], ...], - markers: Tuple[Optional[ChannelID], ...], - amplitudes: Tuple[float, ...], - offsets: Tuple[float, ...], - voltage_transformations: Tuple[Optional[Callable], ...], - sample_rate: TimeType): - super().__init__(loop, channels=channels, markers=markers, - amplitudes=amplitudes, - offsets=offsets, - voltage_transformations=voltage_transformations, - sample_rate=sample_rate) - for waveform, (all_sampled_channels, all_sampled_markers) in self._waveforms.items(): - size = int(waveform.duration * sample_rate) - - # group in channel pairs for binary waveform - binary_waveforms = [] - for (sampled_channels, sampled_markers) in zip(grouper(all_sampled_channels, 2), - grouper(all_sampled_markers, 4)): - if all(x is None for x in (*sampled_channels, *sampled_markers)): - # empty channel pairs - binary_waveforms.append(BinaryWaveform.zeroed(size)) - else: - binary_waveforms.append(BinaryWaveform.from_sampled(*sampled_channels, sampled_markers)) - self._waveforms[waveform] = tuple(binary_waveforms) - - self._waveform_manager = ProgramWaveformManager(program_name, waveform_memory) - self.selection_index = selection_index - self._trigger_wait_code = None - self._seqc_node = None - self._seqc_source = None - self._var_declarations = None - self._user_registers = None - self._user_register_source = None - - def compile(self, - min_repetitions_for_for_loop: int, - min_repetitions_for_shared_wf: int, - indentation: str, - trigger_wait_code: str, - available_registers: Iterable[UserRegister]): - """Compile the loop representation to an internal sequencing c one using `loop_to_seqc` - - Args: - min_repetitions_for_for_loop: See `loop_to_seqc` - min_repetitions_for_shared_wf: See `loop_to_seqc` - indentation: Each line is prefixed with this - trigger_wait_code: The code is put before the playback start - available_registers - Returns: - - """ - pos_var_name = 'pos' - - if self._seqc_node: - self._waveform_manager.clear_requested() - - user_registers = UserRegisterManager(available_registers, self.USER_REG_NAME_TEMPLATE) - - self._seqc_node = loop_to_seqc(self._loop, - min_repetitions_for_for_loop=min_repetitions_for_for_loop, - min_repetitions_for_shared_wf=min_repetitions_for_shared_wf, - waveform_to_bin=self.get_binary_waveform, - user_registers=user_registers) - - self._user_register_source = '\n'.join( - '{indentation}var {user_reg_name} = getUserReg({register});'.format(indentation=indentation, - user_reg_name=user_reg_name, - register=register.to_seqc()) - for register, user_reg_name in user_registers.iter_used_register_names() - ) - self._user_registers = user_registers - - self._var_declarations = '{indentation}var {pos_var_name} = 0;'.format(pos_var_name=pos_var_name, - indentation=indentation) - self._trigger_wait_code = indentation + trigger_wait_code - self._seqc_source = '\n'.join(self._seqc_node.to_source_code(self._waveform_manager, - map(str, itertools.count(1)), - line_prefix=indentation, - pos_var_name=pos_var_name)) - self._waveform_manager.finalize() - - @property - def seqc_node(self) -> 'SEQCNode': - assert self._seqc_node is not None, "compile not called" - return self._seqc_node - - @property - def seqc_source(self) -> str: - assert self._seqc_source is not None, "compile not called" - return '\n'.join([self._var_declarations, - self._user_register_source, - self._trigger_wait_code, - self._seqc_source]) - - def volatile_repetition_counts(self) -> Iterable[Tuple[UserRegister, VolatileRepetitionCount]]: - """ - Returns: - An iterator over the register and parameter - """ - assert self._user_registers is not None, "compile not called" - return self._user_registers.iter_used_register_values() - - @property - def name(self) -> str: - return self._waveform_manager.program_name - - def parse_to_seqc(self, waveform_memory): - raise NotImplementedError() - - def get_binary_waveform(self, waveform: Waveform) -> Tuple[BinaryWaveform, ...]: - return self._waveforms[waveform] - - def prepare_delete(self): - """Delete all references to this program. Cannot be used afterwards""" - self._waveform_manager.prepare_delete() - self._seqc_node = None - self._seqc_source = None - - -class HDAWGProgramManager: - """This class contains everything that is needed to create the final seqc program and provides an interface to write - the required waveforms to the file system. It does not talk to the device.""" - - class Constants: - PROG_SEL_REGISTER = UserRegister(zero_based_value=0) - TRIGGER_REGISTER = UserRegister(zero_based_value=1) - TRIGGER_RESET_MASK = bin(1 << 31) - PROG_SEL_NONE = 0 - # if not set the register is set to PROG_SEL_NONE - NO_RESET_MASK = bin(1 << 31) - # set to one if playback finished - PLAYBACK_FINISHED_MASK = bin(1 << 30) - PROG_SEL_MASK = bin((1 << 30) - 1) - INVERTED_PROG_SEL_MASK = bin(((1 << 32) - 1) ^ int(PROG_SEL_MASK, 2)) - IDLE_WAIT_CYCLES = 300 - - @classmethod - def as_dict(cls) -> Dict[str, Any]: - return {name: value - for name, value in vars(cls).items() - if name[0] in string.ascii_uppercase} - - class GlobalVariables: - """Global variables of the program together with their (multiline) doc string. - The python names are uppercase.""" - - PROG_SEL = (['Selected program index (0 -> None)'], 0) - NEW_PROG_SEL = (('Value that gets written back to program selection register.', - 'Used to signal that at least one program was played completely.'), 0) - PLAYBACK_FINISHED = (('Is OR\'ed to new_prog_sel.', - 'Set to PLAYBACK_FINISHED_MASK if a program was played completely.',), 0) - - @classmethod - def as_dict(cls) -> Dict[str, Tuple[Sequence[str], int]]: - return {name: value - for name, value in vars(cls).items() - if name[0] in string.ascii_uppercase} - - @classmethod - def get_init_block(cls) -> str: - lines = ['// Declare and initialize global variables'] - for var_name, (comment, initial_value) in cls.as_dict().items(): - lines.extend(f'// {comment_line}' for comment_line in comment) - lines.append(f'var {var_name.lower()} = {initial_value};') - lines.append('') - return '\n'.join(lines) - - _PROGRAM_FUNCTION_NAME_TEMPLATE = '{program_name}_function' - WAIT_FOR_SOFTWARE_TRIGGER = "waitForSoftwareTrigger();" - SOFTWARE_WAIT_FOR_TRIGGER_FUNCTION_DEFINITION = ( - 'void waitForSoftwareTrigger() {\n' - ' while (true) {\n' - ' var trigger_register = getUserReg(TRIGGER_REGISTER);\n' - ' if (trigger_register & TRIGGER_RESET_MASK) setUserReg(TRIGGER_REGISTER, 0);\n' - ' if (trigger_register) return;\n' - ' }\n' - '}\n' - ) - DEFAULT_COMPILER_SETTINGS = { - 'trigger_wait_code': WAIT_FOR_SOFTWARE_TRIGGER, - 'min_repetitions_for_for_loop': 20, - 'min_repetitions_for_shared_wf': 1000, - 'indentation': ' ' - } - - @classmethod - def get_program_function_name(cls, program_name: str): - if not program_name.isidentifier(): - program_name = make_valid_identifier(program_name) - return cls._PROGRAM_FUNCTION_NAME_TEMPLATE.format(program_name=program_name) - - def __init__(self): - self._waveform_memory = WaveformMemory() - self._programs = OrderedDict() # type: MutableMapping[str, HDAWGProgramEntry] - self._compiler_settings = [ - # default settings: None -> take cls value - (re.compile('.*'), {'trigger_wait_code': None, - 'min_repetitions_for_for_loop': None, - 'min_repetitions_for_shared_wf': None, - 'indentation': None})] - - def _get_compiler_settings(self, program_name: str) -> dict: - arg_spec = inspect.getfullargspec(HDAWGProgramEntry.compile) - required_compiler_args = (set(arg_spec.args) | set(arg_spec.kwonlyargs)) - {'self', 'available_registers'} - - settings = {} - for regex, settings_dict in self._compiler_settings: - if regex.match(program_name): - settings.update(settings_dict) - if required_compiler_args - set(settings): - raise ValueError('Not all compiler arguments for program have been defined.' - ' (the default catch all has been removed)' - f'Missing: {required_compiler_args - set(settings)}') - for k, v in settings.items(): - if v is None: - settings[k] = self.DEFAULT_COMPILER_SETTINGS[k] - return settings - - @property - def waveform_memory(self): - return self._waveform_memory - - def _get_low_unused_index(self): - existing = {entry.selection_index for entry in self._programs.values()} - for idx in itertools.count(): - if idx not in existing and idx != self.Constants.PROG_SEL_NONE: - return idx - - def add_program(self, name: str, loop: Loop, - channels: Tuple[Optional[ChannelID], ...], - markers: Tuple[Optional[ChannelID], ...], - amplitudes: Tuple[float, ...], - offsets: Tuple[float, ...], - voltage_transformations: Tuple[Optional[Callable], ...], - sample_rate: TimeType): - """Register the given program and translate it to seqc. - - TODO: Add an interface to change the trigger mode - - Args: - name: Human readable name of the program (used f.i. for the function name) - loop: The program to upload - channels: see AWG.upload - markers: see AWG.upload - amplitudes: Used to sample the waveforms - offsets: Used to sample the waveforms - voltage_transformations: see AWG.upload - sample_rate: Used to sample the waveforms - """ - assert name not in self._programs - - selection_index = self._get_low_unused_index() - - # TODO: verify total number of registers - available_registers = [UserRegister.from_seqc(idx) for idx in range(2, 16)] - - program_entry = HDAWGProgramEntry(loop, selection_index, self._waveform_memory, name, - channels, markers, amplitudes, offsets, voltage_transformations, sample_rate) - - compiler_settings = self._get_compiler_settings(program_name=name) - - # TODO: put compilation in seperate function - program_entry.compile(**compiler_settings, - available_registers=available_registers) - - self._programs[name] = program_entry - - def get_register_values(self, name: str) -> Mapping[UserRegister, int]: - return {register: int(parameter) - for register, parameter in self._programs[name].volatile_repetition_counts()} - - def get_register_values_to_update_volatile_parameters(self, name: str, - parameters: Mapping[str, - numbers.Number]) -> Mapping[UserRegister, - int]: - """ - - Args: - name: Program name - parameters: new values for volatile parameters - - Returns: - A dict user_register->value that reflects the new parameter values - """ - program_entry = self._programs[name] - result = {} - for register, volatile_repetition in program_entry.volatile_repetition_counts(): - new_value = volatile_repetition.update_volatile_dependencies(parameters) - result[register] = new_value - return result - - @property - def programs(self) -> Mapping[str, HDAWGProgramEntry]: - return MappingProxyType(self._programs) - - def remove(self, name: str) -> None: - self._programs.pop(name).prepare_delete() - - def clear(self) -> None: - self._waveform_memory.clear() - self._programs.clear() - - def name_to_index(self, name: str) -> int: - assert self._programs[name].name == name - return self._programs[name].selection_index - - def _get_sub_program_source_code(self, program_name: str) -> str: - program = self.programs[program_name] - program_function_name = self.get_program_function_name(program_name) - return "\n".join( - [ - f"void {program_function_name}() {{", - program.seqc_source, - "}\n" - ] - ) - - def _get_program_selection_code(self) -> str: - return _make_program_selection_block((program.selection_index, self.get_program_function_name(program_name)) - for program_name, program in self.programs.items()) - - def to_seqc_program(self, single_program: Optional[str] = None) -> str: - """Generate sequencing c source code that is either capable of playing pack all uploaded programs where the - program is selected at runtime without re-compile or always will play the same program if `single_program` - is specified. - - The program selection is based on a user register in the first case. - - Args: - single_program: The seqc source only contains this program if not None - - Returns: - SEQC source code. - """ - lines = [] - for const_name, const_val in self.Constants.as_dict().items(): - if isinstance(const_val, (int, str)): - const_repr = str(const_val) - else: - const_repr = const_val.to_seqc() - lines.append('const {const_name} = {const_repr};'.format(const_name=const_name, const_repr=const_repr)) - - lines.append(self._waveform_memory.waveform_declaration()) - - lines.append('\n// function used by manually triggered programs') - lines.append(self.SOFTWARE_WAIT_FOR_TRIGGER_FUNCTION_DEFINITION) - - replacements = self._waveform_memory.waveform_name_replacements() - - lines.append('\n// program definitions') - if single_program: - lines.append( - replace_multiple(self._get_sub_program_source_code(single_program), replacements) - ) - - else: - for program_name, program in self.programs.items(): - lines.append(replace_multiple(self._get_sub_program_source_code(program_name), replacements)) - - lines.append(self.GlobalVariables.get_init_block()) - - lines.append('\n// runtime block') - if single_program: - lines.append(f"{self.get_program_function_name(single_program)}();") - else: - lines.append(self._get_program_selection_code()) - - return '\n'.join(lines) - - -def find_sharable_waveforms(node_cluster: Sequence['SEQCNode']) -> Optional[Sequence[bool]]: - """Expects nodes to have a compatible stepping - - TODO: encode in type system? - """ - waveform_playbacks = list(node_cluster[0].iter_waveform_playbacks()) - - candidates = [True] * len(waveform_playbacks) - - for node in itertools.islice(node_cluster, 1, None): - candidates_left = False - for idx, (wf, node_wf) in enumerate(zip(waveform_playbacks, node.iter_waveform_playbacks())): - if candidates[idx]: - candidates[idx] = wf == node_wf - candidates_left = candidates_left or candidates[idx] - - if not candidates_left: - return None - - return candidates - - -def mark_sharable_waveforms(node_cluster: Sequence['SEQCNode'], sharable_waveforms: Sequence[bool]): - for node in node_cluster: - for sharable, wf_playback in zip(sharable_waveforms, node.iter_waveform_playbacks()): - if sharable: - wf_playback.shared = True - - -def _find_repetition(nodes: Deque['SEQCNode'], - hashes: Deque[int], - cluster_dump: List[List['SEQCNode']]) -> Tuple[ - Tuple['SEQCNode', ...], - Tuple[int, ...], - List['SEQCNode'] -]: - """Finds repetitions of stepping patterns in nodes. Assumes hashes contains the stepping_hash of each node. If a - pattern is """ - assert len(nodes) == len(hashes) - - max_cluster_size = len(nodes) // 2 - for cluster_size in range(max_cluster_size, 0, -1): - n_repetitions = len(nodes) // cluster_size - for c_idx in range(cluster_size): - idx_a = -1 - c_idx - - for n in range(1, n_repetitions): - idx_b = idx_a - n * cluster_size - if hashes[idx_a] != hashes[idx_b] or not nodes[idx_a].same_stepping(nodes[idx_b]): - n_repetitions = n - break - - if n_repetitions < 2: - break - - else: - assert n_repetitions > 1 - # found a stepping pattern repetition of length cluster_size! - to_dump = len(nodes) - (n_repetitions * cluster_size) - for _ in range(to_dump): - cluster_dump.append([nodes.popleft()]) - hashes.popleft() - - assert len(nodes) == n_repetitions * cluster_size - - if cluster_size == 1: - current_cluster = list(nodes) - - cluster_template_hashes = (hashes.popleft(),) - cluster_template: Tuple[SEQCNode] = (nodes.popleft(),) - - nodes.clear() - hashes.clear() - - else: - cluster_template_hashes = tuple(hashes.popleft() for _ in range(cluster_size)) - cluster_template = tuple( - nodes.popleft() for _ in range(cluster_size) - ) - - current_cluster: List[SEQCNode] = [Scope(list(cluster_template))] - - for n in range(1, n_repetitions): - current_cluster.append(Scope([ - nodes.popleft() for _ in range(cluster_size) - ])) - assert not nodes - hashes.clear() - - return cluster_template, cluster_template_hashes, current_cluster - return (), (), [] - - -def to_node_clusters(loop: Union[Sequence[Loop], Loop], loop_to_seqc_kwargs: dict) -> Sequence[Sequence['SEQCNode']]: - """transform to seqc recursively noes and cluster them if they have compatible stepping""" - assert len(loop) > 1 - - # complexity: O( len(loop) * MAX_SUB_CLUSTER * loop.depth() ) - # I hope... - MAX_SUB_CLUSTER = 4 - - node_clusters: List[List[SEQCNode]] = [] - - # this is the period that we currently are collecting - current_period: List[SEQCNode] = [] - - # list of already collected periods. Each period is transformed into a SEQCNode - current_cluster: List[SEQCNode] = [] - - # this is a template for what we are currently collecting - current_template: Tuple[SEQCNode, ...] = () - current_template_hashes: Tuple[int, ...] = () - - # only populated if we are looking for a node template - last_node = loop_to_seqc(loop[0], **loop_to_seqc_kwargs) - last_hashes = collections.deque([last_node.stepping_hash()], maxlen=MAX_SUB_CLUSTER*2) - last_nodes = collections.deque([last_node], maxlen=MAX_SUB_CLUSTER*2) - - # compress all nodes in clusters of the same stepping - for child in itertools.islice(loop, 1, None): - current_node = loop_to_seqc(child, **loop_to_seqc_kwargs) - current_hash = current_node.stepping_hash() - - if current_template: - # we are currently collecting something - idx = len(current_period) - if current_template_hashes[idx] == current_hash and current_node.same_stepping(current_template[idx]): - current_period.append(current_node) - - if len(current_period) == len(current_template): - if idx == 0: - node = current_period.pop() - else: - node = Scope(current_period) - current_period = [] - current_cluster.append(node) - - else: - # current template became invalid - assert len(current_cluster) > 1 - node_clusters.append(current_cluster) - - assert not last_nodes - assert not last_hashes - last_nodes.extend(current_period) - last_hashes.extend(current_template_hashes[:len(current_period)]) - - current_period.clear() - - last_nodes.append(current_node) - last_hashes.append(current_hash) - - (current_template, - current_template_hashes, - current_cluster) = _find_repetition(last_nodes, last_hashes, - node_clusters) - else: - assert not current_period - if len(last_nodes) == last_nodes.maxlen: - # lookup deque is full - node_clusters.append([last_nodes.popleft()]) - last_hashes.popleft() - - last_nodes.append(current_node) - last_hashes.append(current_hash) - - (current_template, - current_template_hashes, - current_cluster) = _find_repetition(last_nodes, last_hashes, - node_clusters) - - assert not (current_cluster and last_nodes) - if current_cluster: - node_clusters.append(current_cluster) - node_clusters.extend([node] for node in current_period) - node_clusters.extend([node] for node in last_nodes) - - return node_clusters - - -def loop_to_seqc(loop: Loop, - min_repetitions_for_for_loop: int, - min_repetitions_for_shared_wf: int, - waveform_to_bin: Callable[[Waveform], Tuple[BinaryWaveform, ...]], - user_registers: UserRegisterManager) -> 'SEQCNode': - assert min_repetitions_for_for_loop <= min_repetitions_for_shared_wf - # At which point do we switch from indexed to shared - - if loop.is_leaf(): - node = WaveformPlayback(waveform_to_bin(loop.waveform)) - - elif len(loop) == 1: - node = loop_to_seqc(loop[0], - min_repetitions_for_for_loop=min_repetitions_for_for_loop, - min_repetitions_for_shared_wf=min_repetitions_for_shared_wf, - waveform_to_bin=waveform_to_bin, user_registers=user_registers) - - else: - node_clusters = to_node_clusters(loop, dict(min_repetitions_for_for_loop=min_repetitions_for_for_loop, - min_repetitions_for_shared_wf=min_repetitions_for_shared_wf, - waveform_to_bin=waveform_to_bin, - user_registers=user_registers)) - - seqc_nodes = [] - - # identify shared waveforms in node clusters - for node_cluster in node_clusters: - if len(node_cluster) < min_repetitions_for_for_loop: - seqc_nodes.extend(node_cluster) - - else: - if len(node_cluster) >= min_repetitions_for_shared_wf: - sharable_waveforms = find_sharable_waveforms(node_cluster) - if sharable_waveforms: - mark_sharable_waveforms(node_cluster, sharable_waveforms) - - seqc_nodes.append(SteppingRepeat(node_cluster)) - - node = Scope(seqc_nodes) - - if loop.volatile_repetition: - register_var = user_registers.request(loop.repetition_definition) - return Repeat(scope=node, repetition_count=register_var) - - elif loop.repetition_count != 1: - return Repeat(scope=node, repetition_count=loop.repetition_count) - else: - return node - - -class SEQCNode(metaclass=abc.ABCMeta): - __slots__ = () - - INDENTATION = ' ' - - @abc.abstractmethod - def samples(self) -> int: - pass - - @abc.abstractmethod - def stepping_hash(self) -> int: - """hash of the stepping properties of this node""" - - @abc.abstractmethod - def same_stepping(self, other: 'SEQCNode'): - pass - - @abc.abstractmethod - def iter_waveform_playbacks(self) -> Iterator['WaveformPlayback']: - pass - - def _get_single_indexed_playback(self) -> Optional['WaveformPlayback']: - """Returns None if there is no or if there are more than one indexed playbacks""" - # detect if there is only a single indexed playback - single_indexed_playback = None - for playback in self.iter_waveform_playbacks(): - if not playback.shared: - if single_indexed_playback is None: - single_indexed_playback = playback - else: - break - else: - return single_indexed_playback - return None - - @abc.abstractmethod - def _visit_nodes(self, waveform_manager: ProgramWaveformManager): - """push all concatenated waveforms in the waveform manager""" - - @abc.abstractmethod - def to_source_code(self, waveform_manager: ProgramWaveformManager, node_name_generator: Iterator[str], line_prefix: str, pos_var_name: str, - advance_pos_var: bool = True): - """besides creating the source code, this function registers all needed waveforms to the program manager - 1. shared waveforms - 2. concatenated waveforms in the correct order - - Args: - waveform_manager: - node_name_generator: generates unique names of nodes - line_prefix: - pos_var_name: - advance_pos_var: Indexed playback will not advance the position if set to False. This is used internally - to optimize repeat statements with a single indexed playback. - Returns: - - """ - - def __eq__(self, other): - """Compare objects based on __slots__""" - assert getattr(self, '__dict__', None) is None - return type(self) == type(other) and all(getattr(self, attr) == getattr(other, attr) - for base_class in inspect.getmro(type(self)) - for attr in getattr(base_class, '__slots__', ())) - - -class Scope(SEQCNode): - """Sequence of nodes""" - - __slots__ = ('nodes',) - - def __init__(self, nodes: Sequence[SEQCNode] = ()): - self.nodes = list(nodes) - - def samples(self): - return sum(node.samples() for node in self.nodes) - - def iter_waveform_playbacks(self) -> Iterator['WaveformPlayback']: - for node in self.nodes: - yield from node.iter_waveform_playbacks() - - def stepping_hash(self) -> int: - return functools.reduce(int.__xor__, (node.stepping_hash() for node in self.nodes), hash(type(self))) - - def same_stepping(self, other: 'Scope'): - return (type(other) is Scope and - len(self.nodes) == len(other.nodes) and - all(n1.same_stepping(n2) for n1, n2 in zip(self.nodes, other.nodes))) - - def _visit_nodes(self, waveform_manager: ProgramWaveformManager): - for node in self.nodes: - node._visit_nodes(waveform_manager) - - def to_source_code(self, waveform_manager: ProgramWaveformManager, node_name_generator: Iterator[str], - line_prefix: str, pos_var_name: str, - advance_pos_var: bool = True): - for node in self.nodes: - yield from node.to_source_code(waveform_manager, - line_prefix=line_prefix, - pos_var_name=pos_var_name, - node_name_generator=node_name_generator, - advance_pos_var=advance_pos_var) - - def __eq__(self, other): - if type(other) is type(self): - return self.nodes == other.nodes - else: - return NotImplemented - - def __repr__(self): - return f"Scope(nodes={self.nodes!r})" - - -class Repeat(SEQCNode): - """""" - __slots__ = ('repetition_count', 'scope') - INITIAL_POSITION_NAME_TEMPLATE = 'init_pos_{node_name}' - FOR_LOOP_NAME_TEMPLATE = 'idx_{node_name}' - - class _AdvanceStrategy: - """describes what happens how this node interacts with the position variable""" - INITIAL_RESET = 'initial_reset' - POST_ADVANCE = 'post_advance' - IGNORE = 'ignore' - - def __init__(self, repetition_count: Union[int, str], scope: SEQCNode): - """ - Args: - repetition_count: A const integer value or a string that is expected to be a "var" - scope: The repeated scope - """ - if isinstance(repetition_count, int): - assert repetition_count > 1 - else: - assert isinstance(repetition_count, str) and repetition_count.isidentifier() - - self.repetition_count = repetition_count - self.scope = scope - - def samples(self): - return self.scope.samples() - - def same_stepping(self, other: 'Repeat'): - return (type(self) == type(other) and - self.repetition_count == other.repetition_count and - self.scope.same_stepping(other.scope)) - - def stepping_hash(self) -> int: - return hash((type(self), self.repetition_count, self.scope.stepping_hash())) - - def iter_waveform_playbacks(self) -> Iterator['WaveformPlayback']: - return self.scope.iter_waveform_playbacks() - - def _visit_nodes(self, waveform_manager: ProgramWaveformManager): - self.scope._visit_nodes(waveform_manager) - - def _get_position_advance_strategy(self): - """Deduct the optimal position advance strategy: - - There is more than one indexed playback -> position needs to be advanced during each iteration and set back to - initial value at the begin of each new iteration - There is exactly one indexed playback -> The position is not advanced in the body but needs to be advanced after - all repetitions are done - There is no indexed playback -> We do not care about the position at all - """ - self_samples = self.samples() - if self_samples > 0: - single_playback = self.scope._get_single_indexed_playback() - if single_playback is None or single_playback.samples() != self_samples: - # TODO: I am not sure whether the 'single_playback.samples() != self_samples' is necessary - # there is more than one indexed playback - return self._AdvanceStrategy.INITIAL_RESET - else: - # there is only a single indexed playback - return self._AdvanceStrategy.POST_ADVANCE - else: - # there is no indexed playback - return self._AdvanceStrategy.IGNORE - - def to_source_code(self, waveform_manager: ProgramWaveformManager, node_name_generator: Iterator[str], - line_prefix: str, pos_var_name: str, advance_pos_var: bool = True): - body_prefix = line_prefix + self.INDENTATION - - advance_strategy = self._get_position_advance_strategy() if advance_pos_var else self._AdvanceStrategy.IGNORE - inner_advance_pos_var = advance_strategy == self._AdvanceStrategy.INITIAL_RESET - - def get_node_name(): - """Helper to assert node name only generated when needed and only generated once""" - if getattr(get_node_name, 'node_name', None) is None: - get_node_name.node_name = next(node_name_generator) - return get_node_name.node_name - - if advance_strategy == self._AdvanceStrategy.INITIAL_RESET: - initial_position_name = self.INITIAL_POSITION_NAME_TEMPLATE.format(node_name=get_node_name()) - - # store initial position - yield '{line_prefix}var {init_pos_name} = {pos_var_name};'.format(line_prefix=line_prefix, - init_pos_name=initial_position_name, - pos_var_name=pos_var_name) - - if isinstance(self.repetition_count, int): - yield '{line_prefix}repeat({repetition_count}) {{'.format(line_prefix=line_prefix, - repetition_count=self.repetition_count) - else: - # repeat requires a const-expression so we need to use a for loop for user reg vars - assert isinstance(self.repetition_count, str) - loop_var = self.FOR_LOOP_NAME_TEMPLATE.format(node_name=get_node_name()) - yield '{line_prefix}var {loop_var};'.format(line_prefix=line_prefix, loop_var=loop_var) - yield ('{line_prefix}for({loop_var} = 0; ' - '{loop_var} < {repetition_count}; ' - '{loop_var} = {loop_var} + 1) {{').format(line_prefix=line_prefix, - loop_var=loop_var, - repetition_count=self.repetition_count) - - if advance_strategy == self._AdvanceStrategy.INITIAL_RESET: - yield ('{body_prefix}{pos_var_name} = {init_pos_name};' - '').format(body_prefix=body_prefix, - pos_var_name=pos_var_name, - init_pos_name=initial_position_name) - yield from self.scope.to_source_code(waveform_manager, - line_prefix=body_prefix, pos_var_name=pos_var_name, - node_name_generator=node_name_generator, - advance_pos_var=inner_advance_pos_var) - yield '{line_prefix}}}'.format(line_prefix=line_prefix) - - if advance_strategy == self._AdvanceStrategy.POST_ADVANCE: - yield '{line_prefix}{pos_var_name} = {pos_var_name} + {samples};'.format(line_prefix=line_prefix, - pos_var_name=pos_var_name, - samples=self.samples()) - - -class SteppingRepeat(SEQCNode): - STEPPING_REPEAT_COMMENT = ' // stepping repeat' - __slots__ = ('node_cluster',) - - def __init__(self, node_cluster: Sequence[SEQCNode]): - self.node_cluster = node_cluster - - def samples(self) -> int: - return self.repetition_count * self.node_cluster[0].samples() - - @property - def repetition_count(self): - return len(self.node_cluster) - - def iter_waveform_playbacks(self) -> Iterator['WaveformPlayback']: - for node in self.node_cluster: - yield from node.iter_waveform_playbacks() - - def stepping_hash(self) -> int: - return hash((type(self), self.node_cluster[0].stepping_hash())) - - def same_stepping(self, other: 'SteppingRepeat'): - return (type(other) is SteppingRepeat and - len(self.node_cluster) == len(other.node_cluster) and - self.node_cluster[0].same_stepping(other.node_cluster[0])) - - def _visit_nodes(self, waveform_manager: ProgramWaveformManager): - for node in self.node_cluster: - node._visit_nodes(waveform_manager) - - def to_source_code(self, waveform_manager: ProgramWaveformManager, node_name_generator: Iterator[str], - line_prefix: str, pos_var_name: str, - advance_pos_var: bool = True): - body_prefix = line_prefix + self.INDENTATION - repeat_open = '{line_prefix}repeat({repetition_count}) {{' + self.STEPPING_REPEAT_COMMENT - yield repeat_open.format(line_prefix=line_prefix, - repetition_count=self.repetition_count) - yield from self.node_cluster[0].to_source_code(waveform_manager, - line_prefix=body_prefix, pos_var_name=pos_var_name, - node_name_generator=node_name_generator, - advance_pos_var=advance_pos_var) - - # register remaining concatenated waveforms - for node in itertools.islice(self.node_cluster, 1, None): - node._visit_nodes(waveform_manager) - - yield '{line_prefix}}}'.format(line_prefix=line_prefix) - - -class WaveformPlayback(SEQCNode): - ADVANCE_DISABLED_COMMENT = ' // advance disabled do to parent repetition' - ENABLE_DYNAMIC_RATE_REDUCTION = False - - __slots__ = ('waveform', 'shared', 'rate') - - def __init__(self, waveform: Tuple[BinaryWaveform, ...], shared: bool = False, rate: int = None): - assert isinstance(waveform, tuple) - if self.ENABLE_DYNAMIC_RATE_REDUCTION and rate is None: - for wf in waveform: - rate = wf.dynamic_rate(12 if rate is None else rate) - self.waveform = waveform - self.shared = shared - self.rate = rate - - def __repr__(self): - return f"WaveformPlayback(<{id(self)}>)" - - def samples(self) -> int: - """Samples consumed in the big concatenated waveform""" - if self.shared: - return 0 - else: - wf_lens = set(map(len, self.waveform)) - assert len(wf_lens) == 1 - wf_len, = wf_lens - if self.rate is not None: - wf_len //= (1 << self.rate) - return wf_len - - def rate_reduced_waveform(self) -> Tuple[BinaryWaveform]: - if self.rate is None: - return self.waveform - else: - return tuple(BinaryWaveform(wf.data.reshape((-1, 3))[::(1 << self.rate), :].ravel()) - for wf in self.waveform) - - def stepping_hash(self) -> int: - if self.shared: - return hash((type(self), self.waveform)) - else: - return hash((type(self), self.samples())) - - def same_stepping(self, other: 'WaveformPlayback') -> bool: - same_type = type(self) is type(other) and self.shared == other.shared - if self.shared: - return same_type and self.rate == other.rate and self.waveform == other.waveform - else: - return same_type and self.samples() == other.samples() - - def iter_waveform_playbacks(self) -> Iterator['WaveformPlayback']: - yield self - - def _visit_nodes(self, waveform_manager: ProgramWaveformManager): - if not self.shared: - waveform_manager.request_concatenated(self.rate_reduced_waveform()) - - def to_source_code(self, waveform_manager: ProgramWaveformManager, - node_name_generator: Iterator[str], line_prefix: str, pos_var_name: str, - advance_pos_var: bool = True): - rate_adjustment = "" if self.rate is None else f", {self.rate}" - if self.shared: - yield f'{line_prefix}playWave(' \ - f'{waveform_manager.request_shared(self.rate_reduced_waveform())}' \ - f'{rate_adjustment});' - else: - wf_name = waveform_manager.request_concatenated(self.rate_reduced_waveform()) - wf_len = self.samples() - play_cmd = f'{line_prefix}playWaveIndexed({wf_name}, {pos_var_name}, {wf_len}{rate_adjustment});' - - if advance_pos_var: - advance_cmd = f' {pos_var_name} = {pos_var_name} + {wf_len};' - else: - advance_cmd = self.ADVANCE_DISABLED_COMMENT - yield play_cmd + advance_cmd - - -_PROGRAM_SELECTION_BLOCK = """\ -while (true) {{ - // read program selection value - prog_sel = getUserReg(PROG_SEL_REGISTER); - - // calculate value to write back to PROG_SEL_REGISTER - new_prog_sel = prog_sel | playback_finished; - if (!(prog_sel & NO_RESET_MASK)) new_prog_sel &= INVERTED_PROG_SEL_MASK; - setUserReg(PROG_SEL_REGISTER, new_prog_sel); - - // reset playback flag - playback_finished = 0; - - // only use part of prog sel that does not mean other things to select the program. - prog_sel &= PROG_SEL_MASK; - - switch (prog_sel) {{ -{program_cases} - default: - wait(IDLE_WAIT_CYCLES); - }} -}}""" - -_PROGRAM_SELECTION_CASE = """\ - case {selection_index}: - {program_function_name}(); - waitWave(); - playback_finished = PLAYBACK_FINISHED_MASK;""" - - -def _make_program_selection_block(programs: Iterable[Tuple[int, str]]): - program_cases = [] - for selection_index, program_function_name in programs: - program_cases.append(_PROGRAM_SELECTION_CASE.format(selection_index=selection_index, - program_function_name=program_function_name)) - return _PROGRAM_SELECTION_BLOCK.format(program_cases="\n".join(program_cases)) diff --git a/qupulse/hardware/awgs/zihdawg.py b/qupulse/hardware/awgs/zihdawg.py index 68643db7d..519051c38 100644 --- a/qupulse/hardware/awgs/zihdawg.py +++ b/qupulse/hardware/awgs/zihdawg.py @@ -1,1211 +1,21 @@ -import numbers -from pathlib import Path -import functools -from typing import Tuple, Set, Callable, Optional, Mapping, Generator, Union, Sequence, Dict -from enum import Enum -import weakref -import logging -import warnings -import pathlib -import hashlib +import sys import argparse -import re -from abc import abstractmethod, ABC - -try: - # zhinst fires a DeprecationWarning from its own code in some versions... - with warnings.catch_warnings(): - warnings.simplefilter('ignore', DeprecationWarning) - import zhinst.utils -except ImportError: - warnings.warn('Zurich Instruments LabOne python API is distributed via the Python Package Index. Install with pip.') - raise - -try: - from zhinst import core as zhinst_core -except ImportError: - # backward compability - from zhinst import ziPython as zhinst_core - -import time - -from qupulse.utils.types import ChannelID, TimeType, time_from_float -from qupulse.program.loop import Loop, make_compatible -from qupulse._program.seqc import HDAWGProgramManager, UserRegister, WaveformFileSystem -from qupulse.hardware.awgs.base import AWG, ChannelNotFoundException, AWGAmplitudeOffsetHandling -from qupulse.hardware.util import traced - - -logger = logging.getLogger('qupulse.hdawg') - - -def valid_channel(function_object): - """Check if channel is a valid AWG channels. Expects channel to be 2nd argument after self.""" - @functools.wraps(function_object) - def valid_fn(*args, **kwargs): - if len(args) < 2: - raise HDAWGTypeError('Channel is an required argument.') - channel = args[1] # Expect channel to be second positional argument after self. - if channel not in range(1, 9): - raise ChannelNotFoundException(channel) - value = function_object(*args, **kwargs) - return value - return valid_fn - - -def _amplitude_scales(api_session, serial: str): - return tuple( - api_session.getDouble(f'/{serial}/awgs/{ch // 2:d}/outputs/{ch % 2:d}/amplitude') - for ch in range(8) - ) - -def _sigout_double(api_session, prop: str, serial: str, channel: int, value: float = None) -> float: - """Query channel offset voltage and optionally set it.""" - node_path = f'/{serial}/sigouts/{channel-1:d}/{prop}' - if value is not None: - api_session.setDouble(node_path, value) - api_session.sync() # Global sync: Ensure settings have taken effect on the device. - return api_session.getDouble(node_path) - -def _sigout_range(api_session, serial: str, channel: int, voltage: float = None) -> float: - return _sigout_double(api_session, 'range', serial, channel, voltage) - -def _sigout_offset(api_session, serial: str, channel: int, voltage: float = None) -> float: - return _sigout_double(api_session, 'offset', serial, channel, voltage) - -def _sigout_on(api_session, serial: str, channel: int, value: bool = None) -> bool: - """Query channel signal output status (enabled/disabled) and optionally set it. Corresponds to front LED.""" - node_path = f'/{serial}/sigouts/{channel-1:d}/on' - if value is not None: - api_session.setInt(node_path, value) - api_session.sync() # Global sync: Ensure settings have taken effect on the device. - return bool(api_session.getInt(node_path)) - - -@traced -class HDAWGRepresentation: - """HDAWGRepresentation represents an HDAWG8 instruments and manages a LabOne data server api session. A data server - must be running and the device be discoverable. Channels are per default grouped into pairs.""" - - def __init__(self, device_serial: str = None, - device_interface: str = '1GbE', - data_server_addr: str = 'localhost', - data_server_port: int = 8004, - api_level_number: int = 6, - reset: bool = False, - timeout: float = 20, - grouping: 'HDAWGChannelGrouping' = None) -> None: - """ - :param device_serial: Device serial that uniquely identifies this device to the LabOne data server - :param device_interface: Either '1GbE' for ethernet or 'USB' - :param data_server_addr: Data server address. Must be already running. Default: localhost - :param data_server_port: Data server port. Default: 8004 for HDAWG, MF and UHF devices - :param api_level_number: Version of API to use for the session, higher number, newer. Default: 6 most recent - :param reset: Reset device before initialization - :param timeout: Timeout in seconds for uploading - """ - self._api_session = zhinst_core.ziDAQServer(data_server_addr, data_server_port, api_level_number) - assert zhinst.utils.api_server_version_check(self.api_session) # Check equal data server and api version. - self.api_session.connectDevice(device_serial, device_interface) - self.default_timeout = timeout - self._dev_ser = device_serial - - if reset: - # Create a base configuration: Disable all available outputs, awgs, demods, scopes,... - zhinst.utils.disable_everything(self.api_session, self.serial) - - self._initialize() - - waveform_path = pathlib.Path(self.api_session.awgModule().getString('directory'), 'awg', 'waves') - self._waveform_file_system = WaveformFileSystem.get_waveform_file_system(waveform_path) - self._channel_groups: Dict[HDAWGChannelGrouping, Tuple[HDAWGChannelGroup, ...]] = {} - - # TODO: lookup method to find channel count - n_channels = 8 - - for grouping in HDAWGChannelGrouping: - group_size = grouping.group_size() - if group_size is None: - # MDS - groups = [ - MDSChannelGroup(self.group_name(0, None), self.default_timeout) - ] - else: - groups = [] - for group_idx in range(n_channels // group_size): - groups.append(SingleDeviceChannelGroup(group_idx, group_size, - identifier=self.group_name(group_idx, group_size), - timeout=self.default_timeout)) - self._channel_groups[grouping] = tuple(groups) - - if grouping is None: - grouping = self.channel_grouping - # activates channel groups - self.channel_grouping = grouping - - @property - def waveform_file_system(self) -> WaveformFileSystem: - return self._waveform_file_system - - @property - def channel_tuples(self) -> Tuple['HDAWGChannelGroup', ...]: - return self._get_groups(self.channel_grouping) - - @property - def channel_pair_AB(self) -> 'HDAWGChannelGroup': - return self._channel_groups[HDAWGChannelGrouping.CHAN_GROUP_4x2][0] - - @property - def channel_pair_CD(self) -> 'HDAWGChannelGroup': - return self._channel_groups[HDAWGChannelGrouping.CHAN_GROUP_4x2][1] - - @property - def channel_pair_EF(self) -> 'HDAWGChannelGroup': - return self._channel_groups[HDAWGChannelGrouping.CHAN_GROUP_4x2][2] - - @property - def channel_pair_GH(self) -> 'HDAWGChannelGroup': - return self._channel_groups[HDAWGChannelGrouping.CHAN_GROUP_4x2][3] - - @property - def api_session(self) -> zhinst_core.ziDAQServer: - return self._api_session - - @property - def serial(self) -> str: - return self._dev_ser - - def _initialize(self) -> None: - settings = [(f'/{self.serial}/awgs/*/userregs/*', 0), # Reset all user registers to 0. - (f'/{self.serial}/*/single', 1)] # Single execution mode of sequence. - for ch in range(0, 8): # Route marker 1 signal for each channel to marker output. - if ch % 2 == 0: - output = HDAWGTriggerOutSource.OUT_1_MARK_1.value - else: - output = HDAWGTriggerOutSource.OUT_1_MARK_2.value - settings.append(['/{}/triggers/out/{}/source'.format(self.serial, ch), output]) - - self.api_session.set(settings) - self.api_session.sync() # Global sync: Ensure settings have taken effect on the device. - - def reset(self) -> None: - zhinst.utils.disable_everything(self.api_session, self.serial) - self._initialize() - for tuple in self.channel_tuples: - tuple.clear() - self.api_session.set([ - (f'/{self.serial}/awgs/*/time', 0), - (f'/{self.serial}/sigouts/*/range', HDAWGVoltageRange.RNG_1V.value), - (f'/{self.serial}/awgs/*/outputs/*/amplitude', 1.0), - (f'/{self.serial}/outputs/*/modulation/mode', HDAWGModulationMode.OFF.value), - ]) - - # marker outputs - marker_settings = [] - for ch in range(0, 8): # Route marker 1 signal for each channel to marker output. - if ch % 2 == 0: - output = HDAWGTriggerOutSource.OUT_1_MARK_1.value - else: - output = HDAWGTriggerOutSource.OUT_1_MARK_2.value - marker_settings.append([f'/{self.serial}/triggers/out/{ch}/source', output]) - self.api_session.set(marker_settings) - self.api_session.sync() - - def group_name(self, group_idx, group_size) -> str: - if group_size is None: - return f'{self.serial}_MDS' - return str(self.serial) + '_' + 'ABCDEFGH'[group_idx*group_size:][:group_size] - - def _get_groups(self, grouping: 'HDAWGChannelGrouping') -> Tuple['HDAWGChannelGroup', ...]: - try: - return self._channel_groups[grouping] - except KeyError: - # python reload... - for grouping_key, group in self._channel_groups.items(): - if grouping_key.value == grouping.value: - return group - else: - raise - - @property - def channel_grouping(self) -> 'HDAWGChannelGrouping': - grouping = self.api_session.getInt(f'/{self.serial}/SYSTEM/AWG/CHANNELGROUPING') - return HDAWGChannelGrouping(grouping) - - @channel_grouping.setter - def channel_grouping(self, channel_grouping: 'HDAWGChannelGrouping'): - # ipython reload ... - if not type(channel_grouping).__name__ == 'HDAWGChannelGrouping': - raise HDAWGTypeError('Channel grouping must be an enum of type "HDAWGChannelGrouping" to avoid confusions ' - 'between enum value and group size.') - old_channel_grouping = self.channel_grouping - if old_channel_grouping != channel_grouping: - self.api_session.setInt(f'/{self.serial}/AWGS/*/ENABLE', 0) - self.api_session.setInt(f'/{self.serial}/SYSTEM/AWG/CHANNELGROUPING', channel_grouping.value) - # disable old groups - for group in self._get_groups(old_channel_grouping): - group.disconnect_group() - - if channel_grouping.value == HDAWGChannelGrouping.MDS.value and not self._is_mds_master(): - # do not connect channel group - return - - for group in self._get_groups(channel_grouping): - if not group.is_connected(): - group.connect_group(self) - - @valid_channel - def offset(self, channel: int, voltage: float = None) -> float: - """Query channel offset voltage and optionally set it.""" - return _sigout_offset(self.api_session, self.serial, channel, voltage) - - @valid_channel - def range(self, channel: int, voltage: float = None) -> float: - """Query channel voltage range and optionally set it. The instruments selects the next higher available range. - This is the one-sided range Vp. Total range: -Vp...Vp""" - return _sigout_range(self.api_session, self.serial, channel, voltage) - - @valid_channel - def output(self, channel: int, status: bool = None) -> bool: - """Query channel signal output status (enabled/disabled) and optionally set it. Corresponds to front LED.""" - return _sigout_on(self.api_session, self.serial, channel, status) - - def get_status_table(self): - """Return node tree of instrument with all important settings, as well as each channel group as tuple.""" - return (self.api_session.get('/{}/*'.format(self.serial)), - self.channel_pair_AB.awg_module.get('awgModule/*'), - self.channel_pair_CD.awg_module.get('awgModule/*'), - self.channel_pair_EF.awg_module.get('awgModule/*'), - self.channel_pair_GH.awg_module.get('awgModule/*')) - - def _get_mds_group_idx(self) -> Optional[int]: - idx = 0 - while True: - try: - if self.serial in self.api_session.getString(f'/ZI/MDS/GROUPS/{idx}/DEVICES'): - return idx - except RuntimeError: - break - idx += 1 - - def _is_mds_master(self) -> Optional[bool]: - idx = 0 - while True: - try: - devices = self.api_session.getString(f'/ZI/MDS/GROUPS/{idx}/DEVICES').split(',') - except RuntimeError: - break - - if self.serial in devices: - return devices[0] == self.serial - idx += 1 - - def __repr__(self): - return f"{type(self).__name__}({self.serial}, ... {self.api_session})" - - -class HDAWGTriggerOutSource(Enum): - """Assign a signal to a marker output. This is per AWG Core.""" - AWG_TRIG_1 = 0 # Trigger output assigned to AWG trigger 1, controlled by AWG sequencer commands. - AWG_TRIG_2 = 1 # Trigger output assigned to AWG trigger 2, controlled by AWG sequencer commands. - AWG_TRIG_3 = 2 # Trigger output assigned to AWG trigger 3, controlled by AWG sequencer commands. - AWG_TRIG_4 = 3 # Trigger output assigned to AWG trigger 4, controlled by AWG sequencer commands. - OUT_1_MARK_1 = 4 # Trigger output assigned to output 1 marker 1. - OUT_1_MARK_2 = 5 # Trigger output assigned to output 1 marker 2. - OUT_2_MARK_1 = 6 # Trigger output assigned to output 2 marker 1. - OUT_2_MARK_2 = 7 # Trigger output assigned to output 2 marker 2. - TRIG_IN_1 = 8 # Trigger output assigned to trigger inout 1. - TRIG_IN_2 = 9 # Trigger output assigned to trigger inout 2. - TRIG_IN_3 = 10 # Trigger output assigned to trigger inout 3. - TRIG_IN_4 = 11 # Trigger output assigned to trigger inout 4. - TRIG_IN_5 = 12 # Trigger output assigned to trigger inout 5. - TRIG_IN_6 = 13 # Trigger output assigned to trigger inout 6. - TRIG_IN_7 = 14 # Trigger output assigned to trigger inout 7. - TRIG_IN_8 = 15 # Trigger output assigned to trigger inout 8. - HIGH = 17 # Trigger output is set to high. - LOW = 18 # Trigger output is set to low. - - -class HDAWGChannelGrouping(Enum): - """How many independent sequencers should run on the AWG and how the outputs should be grouped by sequencer.""" - MDS = -1 # All channels that are in the current multi device synchronized group - CHAN_GROUP_4x2 = 0 # 4x2 with HDAWG8; 2x2 with HDAWG4. /dev.../awgs/0..3/ - CHAN_GROUP_2x4 = 1 # 2x4 with HDAWG8; 1x4 with HDAWG4. /dev.../awgs/0 & 2/ - CHAN_GROUP_1x8 = 2 # 1x8 with HDAWG8. /dev.../awgs/0/ - - def group_size(self) -> int: - return { - HDAWGChannelGrouping.CHAN_GROUP_4x2: 2, - HDAWGChannelGrouping.CHAN_GROUP_2x4: 4, - HDAWGChannelGrouping.CHAN_GROUP_1x8: 8, - HDAWGChannelGrouping.MDS: None - }[self] - - -class HDAWGVoltageRange(Enum): - """All available voltage ranges for the HDAWG wave outputs. Define maximum output voltage.""" - RNG_5V = 5 - RNG_4V = 4 - RNG_3V = 3 - RNG_2V = 2 - RNG_1V = 1 - RNG_800mV = 0.8 - RNG_600mV = 0.6 - RNG_400mV = 0.4 - RNG_200mV = 0.2 - - -class HDAWGModulationMode(Enum): - """Modulation mode of waveform generator.""" - OFF = 0 # AWG output goes directly to signal output. - SINE_1 = 1 # AWG output multiplied with sine generator signal 0. - SINE_2 = 2 # AWG output multiplied with sine generator signal 1. - FG_1 = 3 # AWG output multiplied with function generator signal 0. Requires FG option. - FG_2 = 4 # AWG output multiplied with function generator signal 1. Requires FG option. - ADVANCED = 5 # AWG output modulates corresponding sines from modulation carriers. - - -@traced -class HDAWGChannelGroup(AWG): - MIN_WAVEFORM_LEN = 192 - WAVEFORM_LEN_QUANTUM = 16 - - def __init__(self, - identifier: str, - timeout: float) -> None: - super().__init__(identifier) - self.timeout = timeout - - self._awg_module = None - self._program_manager = HDAWGProgramManager() - self._elf_manager = None - self._required_seqc_source = self._program_manager.to_seqc_program() - self._uploaded_seqc_source = None - self._current_program = None # Currently armed program. - self._upload_generator = () - - self._master_device = None - - def _initialize_awg_module(self): - """Only run once""" - if self._awg_module: - self._awg_module.clear() - self._awg_module = self.master_device.api_session.awgModule() - self._awg_module.set('awgModule/device', self.master_device.serial) - self._awg_module.set('awgModule/index', self.awg_group_index) - self._awg_module.execute() - self._elf_manager = ELFManager.DEFAULT_CLS(self._awg_module) - self._upload_generator = () - - @property - def master_device(self) -> HDAWGRepresentation: - """Reference to HDAWG representation.""" - if self._master_device is None: - raise HDAWGValueError('Channel group is currently not connected') - return self._master_device - - @property - def awg_module(self) -> zhinst_core.AwgModule: - """Each AWG channel group has its own awg module to manage program compilation and upload.""" - if self._awg_module is None: - raise HDAWGValueError('Channel group is not connected and was never initialized') - return self._awg_module - - @property - @abstractmethod - def awg_group_index(self) -> int: - raise NotImplementedError() - - @property - def num_markers(self) -> int: - """Number of marker channels""" - return 2 * self.num_channels - - def upload(self, name: str, - program: Loop, - channels: Tuple[Optional[ChannelID], ...], - markers: Tuple[Optional[ChannelID], ...], - voltage_transformation: Tuple[Callable, ...], - force: bool = False) -> None: - """Upload a program to the AWG. - - Physically uploads all waveforms required by the program - excluding those already present - - to the device and sets up playback sequences accordingly. - This method should be cheap for program already on the device and can therefore be used - for syncing. Programs that are uploaded should be fast(~1 sec) to arm. - - Args: - name: A name for the program on the AWG. - program: The program (a sequence of instructions) to upload. - channels: Tuple of length num_channels that ChannelIDs of in the program to use. Position in the list - corresponds to the AWG channel - markers: List of channels in the program to use. Position in the List in the list corresponds to - the AWG channel - voltage_transformation: transformations applied to the waveforms extracted rom the program. Position - in the list corresponds to the AWG channel - force: If a different sequence is already present with the same name, it is - overwritten if force is set to True. (default = False) - - Known programs are handled in host memory most of the time. Only when uploading the - device memory is touched at all. - - Returning from setting user register in seqc can take from 50ms to 60 ms. Fluctuates heavily. Not a good way to - have deterministic behaviour "setUserReg(PROG_SEL, PROG_IDLE);". - """ - if len(channels) != self.num_channels: - raise HDAWGValueError('Channel ID not specified') - if len(markers) != self.num_markers: - raise HDAWGValueError('Markers not specified') - if len(voltage_transformation) != self.num_channels: - raise HDAWGValueError('Wrong number of voltage transformations') - - if name in self.programs and not force: - raise HDAWGValueError('{} is already known on {}'.format(name, self.identifier)) - - # Go to qupulse nanoseconds time base. - q_sample_rate = self.sample_rate / 10**9 - - # Adjust program to fit criteria. - make_compatible(program, - minimal_waveform_length=self.MIN_WAVEFORM_LEN, - waveform_quantum=self.WAVEFORM_LEN_QUANTUM, - sample_rate=q_sample_rate) - - if self._amplitude_offset_handling == AWGAmplitudeOffsetHandling.IGNORE_OFFSET: - voltage_offsets = (0.,) * self.num_channels - elif self._amplitude_offset_handling == AWGAmplitudeOffsetHandling.CONSIDER_OFFSET: - voltage_offsets = self.offsets() - else: - raise ValueError('{} is invalid as AWGAmplitudeOffsetHandling'.format(self._amplitude_offset_handling)) - - amplitudes = self.amplitudes() - - if name in self._program_manager.programs: - self._program_manager.remove(name) - - self._program_manager.add_program(name, - program, - channels=channels, - markers=markers, - voltage_transformations=voltage_transformation, - sample_rate=q_sample_rate, - amplitudes=amplitudes, - offsets=voltage_offsets) - - self._required_seqc_source = self._program_manager.to_seqc_program() - self._program_manager.waveform_memory.sync_to_file_system(self.master_device.waveform_file_system) - - # start compiling the source (non-blocking) - self._start_compile_and_upload() - - def _start_compile_and_upload(self): - self._uploaded_seqc_source = None - self._upload_generator = self._elf_manager.compile_and_upload(self._required_seqc_source) - logger.debug(f"_start_compile_and_upload: %r", next(self._upload_generator, "Finished")) - - def _wait_for_compile_and_upload(self): - for state in self._upload_generator: - logger.debug("wait_for_compile_and_upload: %r", state) - time.sleep(.1) - self._uploaded_seqc_source = self._required_seqc_source - logger.debug("AWG %d: wait_for_compile_and_upload has finished", self.awg_group_index) - - def was_current_program_finished(self) -> bool: - """Return true if the current program has finished at least once""" - playback_finished_mask = int(HDAWGProgramManager.Constants.PLAYBACK_FINISHED_MASK, 2) - return bool(self.user_register(HDAWGProgramManager.Constants.PROG_SEL_REGISTER) & playback_finished_mask) - - def set_volatile_parameters(self, program_name: str, parameters: Mapping[str, numbers.Real]): - """Set the values of parameters which were marked as volatile on program creation.""" - new_register_values = self._program_manager.get_register_values_to_update_volatile_parameters(program_name, - parameters) - if self._current_program == program_name: - for register, value in new_register_values.items(): - self.user_register(register, value) - - def remove(self, name: str) -> None: - """Remove a program from the AWG. - - Also discards all waveforms referenced only by the program identified by name. - - Args: - name: The name of the program to remove. - """ - self._program_manager.remove(name) - self._required_seqc_source = self._program_manager.to_seqc_program() - - def clear(self) -> None: - """Removes all programs and waveforms from the AWG. - - Caution: This affects all programs and waveforms on the AWG, not only those uploaded using qupulse! - """ - self._program_manager.clear() - self._current_program = None - self._required_seqc_source = self._program_manager.to_seqc_program() - self._start_compile_and_upload() - self.arm(None) - - def arm(self, name: Optional[str]) -> None: - """Load the program 'name' and arm the device for running it. If name is None the awg will "dearm" its current - program. - - Currently hardware triggering is not implemented. The HDAWGProgramManager needs to emit code that calls - `waitDigTrigger` to do that. - """ - if self.num_channels > 8: - if name is None: - self._required_seqc_source = "" - else: - self._required_seqc_source = self._program_manager.to_seqc_program(name) - self._start_compile_and_upload() - - if self._required_seqc_source != self._uploaded_seqc_source: - self._wait_for_compile_and_upload() - assert self._required_seqc_source == self._uploaded_seqc_source, "_wait_for_compile_and_upload did not work " \ - "as expected." - - self.user_register(self._program_manager.Constants.TRIGGER_REGISTER, 0) - - if name is None: - self.user_register(self._program_manager.Constants.PROG_SEL_REGISTER, - self._program_manager.Constants.PROG_SEL_NONE) - self._current_program = None - else: - if name not in self.programs: - raise HDAWGValueError('{} is unknown on {}'.format(name, self.identifier)) - self._current_program = name - - # set the registers of initial repetition counts - for register, value in self._program_manager.get_register_values(name).items(): - assert register not in (self._program_manager.Constants.PROG_SEL_REGISTER, - self._program_manager.Constants.TRIGGER_REGISTER) - self.user_register(register, value) - - self.user_register(self._program_manager.Constants.PROG_SEL_REGISTER, - self._program_manager.name_to_index(name) | int(self._program_manager.Constants.NO_RESET_MASK, 2)) - - if name is not None: - self.enable(True) - - def run_current_program(self) -> None: - """Run armed program.""" - if self._current_program is not None: - if self._current_program not in self.programs: - raise HDAWGValueError('{} is unknown on {}'.format(self._current_program, self.identifier)) - if not self.enable(): - self.enable(True) - self.user_register(self._program_manager.Constants.TRIGGER_REGISTER, - int(self._program_manager.Constants.TRIGGER_RESET_MASK, 2)) - else: - raise HDAWGRuntimeError('No program active') - - @property - def programs(self) -> Set[str]: - """The set of program names that can currently be executed on the hardware AWG.""" - return set(self._program_manager.programs.keys()) - - @property - def sample_rate(self) -> TimeType: - """The default sample rate of the AWG channel group.""" - node_path = '/{}/awgs/{}/time'.format(self.master_device.serial, self.awg_group_index) - sample_rate_num = self.master_device.api_session.getInt(node_path) - node_path = '/{}/system/clocks/sampleclock/freq'.format(self.master_device.serial) - sample_clock = self.master_device.api_session.getDouble(node_path) - - """Calculate exact rational number based on (sample_clock Sa/s) / 2^sample_rate_num. Otherwise numerical - imprecision will give rise to errors for very long pulses. fractions.Fraction does not accept floating point - numerator, which sample_clock could potentially be.""" - return time_from_float(sample_clock) / 2 ** sample_rate_num - - def connect_group(self, hdawg_device: HDAWGRepresentation): - self.disconnect_group() - self._master_device = weakref.proxy(hdawg_device) - self._initialize_awg_module() - # Seems creating AWG module sets SINGLE (single execution mode of sequence) to 0 per default. - self.master_device.api_session.setInt(f'/{self.master_device.serial}/awgs/0/single', 1) - - def disconnect_group(self): - """Disconnect this group from device so groups of another size can be used""" - if self._awg_module: - self.awg_module.clear() - self._master_device = None - self._elf_manager = None - self._upload_generator = () - - def is_connected(self) -> bool: - return self._master_device is not None - - def user_register(self, reg: UserRegister, value: int = None) -> int: - """Query user registers (1-16) and optionally set it. - - Args: - reg: User register. If it is an int, a warning is raised and it is interpreted as a one based index - value: Value to set - - Returns: - User Register value after setting it - """ - if isinstance(reg, int): - warnings.warn("User register is not a UserRegister instance. It is interpreted as one based index.") - reg = UserRegister(one_based_value=reg) - - if reg.to_web_interface() not in range(1, 17): - raise HDAWGValueError(f'{reg:!r} not a valid (1-16) register.') - - node_path = '/{}/awgs/{:d}/userregs/{:labone}'.format(self.master_device.serial, self.awg_group_index, reg) - if value is not None: - self.master_device.api_session.setInt(node_path, value) - # hackedy - for mds_serial in getattr(self, '_mds_devices', [])[1:]: - self.master_device.api_session.setInt(node_path.replace(self.master_device.serial, mds_serial), value) - self.master_device.api_session.sync() # Global sync: Ensure settings have taken effect on the device. - return self.master_device.api_session.getInt(node_path) - - -@traced -class MDSChannelGroup(HDAWGChannelGroup): - def __init__(self, - identifier: str, - timeout: float) -> None: - super().__init__(identifier, timeout) - - self._master_device = None - self._mds_devices = None - - @property - def num_channels(self) -> int: - """Number of channels""" - return len(self._mds_devices) * 8 - - @property - def awg_group_index(self): - return 0 - - def disconnect_group(self): - super().disconnect_group() - self._mds_devices = None - - def connect_group(self, hdawg_device: HDAWGRepresentation): - mds_group = hdawg_device._get_mds_group_idx() - if mds_group is None: - raise HDAWGException("AWG not in any MDS group", hdawg_device) - mds_devices = hdawg_device.api_session.getString(f'/ZI/MDS/GROUPS/{mds_group}/DEVICES').split(',') - if hdawg_device.serial != mds_devices[0]: - raise HDAWGException("Only the master device can connect to the HDAWG MDS channel group.") - super().connect_group(hdawg_device) - self._mds_devices = mds_devices - - def enable(self, status: bool = None) -> bool: - """Start the AWG sequencer.""" - # There is also 'awgModule/awg/enable', which seems to have the same functionality. - node_path = '/{}/awgs/{:d}/enable'.format(self.master_device.serial, 0) - if status is not None: - self.awg_module.set('awg/enable', int(status)) - else: - status = self.awg_module.get('awg/module') - - #return bool(status) - """ - if status is not None: - self.master_device.api_session.setInt(node_path, int(status)) - for mds_device in self._mds_devices[1:]: - self.master_device.api_session.setInt(node_path.replace(self._mds_devices[0], mds_device), int(status)) - self.master_device.api_session.sync() # Global sync: Ensure settings have taken effect on the device. - """ - return bool(self.master_device.api_session.getInt(node_path)) - - def amplitudes(self) -> Tuple[float, ...]: - """Query AWG channel amplitude value (not peak to peak). - - From manual: - The final signal amplitude is given by the product of the full scale - output range of 1 V[in this example], the dimensionless amplitude - scaling factor 1.0, and the actual dimensionless signal amplitude - stored in the waveform memory.""" - amplitudes = [] - - api_session = self.master_device.api_session - for mds_device in self._mds_devices: - amplitude_scales = _amplitude_scales(api_session, mds_device) - ranges = [_sigout_range(api_session, mds_device, ch) for ch in range(1, 9)] - amplitudes.extend(zi_amplitude * zi_range / 2 for zi_amplitude, zi_range in zip(amplitude_scales, ranges)) - return tuple(amplitudes) - - def offsets(self) -> Tuple[float, ...]: - offsets = [] - api_session = self.master_device.api_session - for mds_device in self._mds_devices: - offsets.extend(_sigout_offset(api_session, mds_device, ch) for ch in range(1, 9)) - return tuple(offsets) - - -class SingleDeviceChannelGroup(HDAWGChannelGroup): - def __init__(self, - group_idx: int, - group_size: int, - identifier: str, - timeout: float) -> None: - super().__init__(identifier, timeout) - self._device = None - - assert group_idx in range(4) - assert group_size in (2, 4, 8) - - self._group_idx = group_idx - self._group_size = group_size - - @property - def num_channels(self) -> int: - """Number of channels""" - return self._group_size - - def _channels(self, index_start=1) -> Tuple[int, ...]: - """1 indexed channel""" - offset = index_start + self._group_size * self._group_idx - return tuple(ch + offset for ch in range(self.num_channels)) - - @property - def awg_group_index(self) -> int: - """AWG node group index assuming 4x2 channel grouping. Then 0...3 will give appropriate index of group.""" - return self._group_idx - - @property - def user_directory(self) -> str: - """LabOne user directory with subdirectories: "awg/src" (seqc sourcefiles), "awg/elf" (compiled AWG binaries), - "awag/waves" (user defined csv waveforms).""" - return self.awg_module.getString('awgModule/directory') - - def enable(self, status: bool = None) -> bool: - """Start the AWG sequencer.""" - # There is also 'awgModule/awg/enable', which seems to have the same functionality. - node_path = '/{}/awgs/{:d}/enable'.format(self.master_device.serial, self.awg_group_index) - if status is not None: - self.master_device.api_session.setInt(node_path, int(status)) - self.master_device.api_session.sync() # Global sync: Ensure settings have taken effect on the device. - return bool(self.master_device.api_session.getInt(node_path)) - - def amplitudes(self) -> Tuple[float, ...]: - """Query AWG channel amplitude value (not peak to peak). - - From manual: - The final signal amplitude is given by the product of the full scale - output range of 1 V[in this example], the dimensionless amplitude - scaling factor 1.0, and the actual dimensionless signal amplitude - stored in the waveform memory.""" - amplitudes = [] - - for ch, zi_amplitude in zip(self._channels(), _amplitude_scales(self.master_device.api_session, self.master_device.serial)): - zi_range = self.master_device.range(ch) - amplitudes.append(zi_amplitude * zi_range / 2) - return tuple(amplitudes) - - def offsets(self) -> Tuple[float, ...]: - return tuple(map(self.master_device.offset, self._channels())) - - -class ELFManager(ABC): - DEFAULT_CLS = None - - class AWGModule: - def __init__(self, awg_module: zhinst_core.AwgModule): - """Provide an easily mockable interface to the zhinst AwgModule object""" - self._module = awg_module - - @property - def src_dir(self) -> pathlib.Path: - return pathlib.Path(self._module.getString('directory'), 'awg', 'src') - - @property - def elf_dir(self) -> pathlib.Path: - return pathlib.Path(self._module.getString('directory'), 'awg', 'elf') - - @property - def compiler_start(self) -> bool: - """True if the compiler is running""" - return self._module.getInt('compiler/start') == 1 - - @compiler_start.setter - def compiler_start(self, value: bool): - """Set true to start the compiler""" - self._module.set('compiler/start', value) - - @property - def compiler_status(self) -> Tuple[int, str]: - return self._module.getInt('compiler/status'), self._module.getString('compiler/statusstring') - - @property - def compiler_source_file(self) -> str: - return self._module.getString('compiler/sourcefile') - - @compiler_source_file.setter - def compiler_source_file(self, source_file: str): - self._module.set('compiler/sourcefile', source_file) - - @property - def compiler_source_string(self) -> str: - return self._module.getString('compiler/sourcestring') - - @compiler_source_string.setter - def compiler_source_string(self, source_string: str): - self._module.set('compiler/sourcestring', source_string) - - @property - def compiler_upload(self) -> bool: - """auto upload after compiling""" - return self._module.getInt('compiler/upload') == 1 - - @compiler_upload.setter - def compiler_upload(self, value: bool): - self._module.set('compiler/upload', value) - - @property - def elf_file(self) -> str: - return self._module.getString('elf/file') - - @elf_file.setter - def elf_file(self, elf_file: str): - self._module.set('elf/file', elf_file) - - @property - def elf_upload(self) -> bool: - return bool(self._module.getInt('elf/upload')) - - @elf_upload.setter - def elf_upload(self, value: bool): - self._module.set('elf/upload', value) - - @property - def elf_status(self) -> Tuple[int, float]: - return self._module.getInt('elf/status'), self._module.getDouble('progress') - - @property - def index(self) -> int: - return self._module.getInt('index') - - def __init__(self, awg_module: zhinst_core.AwgModule): - """This class organizes compiling and uploading of compiled programs. The source code file is named based on the - code hash to cache compilation results. This requires that the waveform names are unique. - - The compilation and upload itself are done asynchronously by zhinst.core. To avoid spawning a useless - thread for updating the status the method :py:meth:`~ELFManager.compile_and_upload` returns a generator which - talks to the undelying library when needed.""" - self.awg_module = self.AWGModule(awg_module) - - # automatically upload after successful compilation - self.awg_module.compiler_upload = True - - self._compile_job = None # type: Optional[Union[str, Tuple[str, int, str]]] - self._upload_job = None # type: Optional[Union[Tuple[str, float], Tuple[str, int]]] - - def clear(self): - """Deletes all files with a SHA512 hash name""" - src_regex = re.compile(r'[a-z0-9]{128}\.seqc') - elf_regex = re.compile(r'[a-z0-9]{128}\.elf') - - for p in self.awg_module.src_dir.iterdir(): - if src_regex.match(p.name): - p.unlink() - - for p in self.awg_module.elf_dir.iterdir(): - if elf_regex.match(p.name): - p.unlink() - - @staticmethod - def _source_hash(source_string: str) -> str: - """Calulate the SHA512 hash of the given source. - - Args: - source_string: seqc source code - - Returns: - hex representation of SHA512 `source_string` hash - """ - # use utf-16 because str is UTF16 on most relevant machines (Windows) - return hashlib.sha512(bytes(source_string, 'utf-16')).hexdigest() - - @abstractmethod - def compile_and_upload(self, source_string: str) -> Generator[str, str, None]: - """The function returns a generator that yields the current state of the progress. The generator is empty iff - the upload is complete. An exception is raised if there is an error. - - To abort send 'abort' to the generator. (not implemented :P) - - Example: - >>> my_source = 'playWave("my_wave");' - >>> for state in elf_manager.compile_and_upload(my_source): - ... print('Current state:', state) - ... time.sleep(1) - - Args: - source_string: Source code to compile - - Returns: - Generator object that needs to be consumed - """ - - -class SimpleELFManager(ELFManager): - def __init__(self, awg_module: zhinst.ziPython.AwgModule): - """This implementation does not attempt to do something clever like caching.""" - super().__init__(awg_module) - - def compile_and_upload(self, source_string: str) -> Generator[str, str, None]: - self.awg_module.compiler_upload = True - self.awg_module.compiler_source_string = source_string - - while True: - status, msg = self.awg_module.compiler_status - if status == - 1: - yield 'compiling' - elif status == 0: - break - elif status == 1: - raise HDAWGCompilationException(msg) - elif status == 2: - logger.warning("Compiler warings: %s", msg) - break - else: - raise RuntimeError("Unexpected status", status, msg) - - while True: - status_int, progress = self.awg_module.elf_status - if progress == 1.0: - break - elif status_int == 1: - HDAWGUploadException(self.awg_module.compiler_status) - else: - yield 'uploading @ %d%%' % (100*progress) - - -ELFManager.DEFAULT_CLS = SimpleELFManager - - -class CachingELFManager(ELFManager): - def __init__(self, awg_module: zhinst.ziPython.AwgModule): - """FAILS TO UPLOAD THE CORRECT ELF FOR SOME REASON""" - super().__init__(awg_module) - - # automatically upload after successful compilation - self.awg_module.compiler_upload = True - - self._compile_job = None # type: Optional[Union[str, Tuple[str, int, str]]] - self._upload_job = None # type: Optional[Union[Tuple[str, float], Tuple[str, int]]] - - def _update_compile_job_status(self): - """Store current compile status in self._compile_job.""" - compiler_start = self.awg_module.compiler_start - if self._compile_job is None: - assert compiler_start == 0 - - elif isinstance(self._compile_job, str): - if compiler_start: - logger.debug("Compiler is running.") - - else: - compiler_status, status_string = self.awg_module.compiler_status - assert compiler_status in (-1, 0, 1, 2) - if compiler_status == -1: - raise RuntimeError('Compile job is set but no compilation is running', status_string) - elif compiler_status == 2: - logger.warning("AWG %d: Compilation finished with warning: %s", self.awg_module.index, status_string) - self._compile_job = (self._compile_job, compiler_status, status_string) - - def _start_compile_job(self, source_file): - logger.debug("Starting compilation of %r", source_file) - self._update_compile_job_status() - assert not isinstance(self._compile_job, str) - self.awg_module.compiler_source_file = source_file - self.awg_module.compiler_start = True - self._compile_job = source_file - logger.debug("AWG %d: Compilation of %r started", self.awg_module.index, source_file) - - def _compile(self, source_file) -> Generator[str, str, None]: - self._start_compile_job(source_file) - - while True: - self._update_compile_job_status() - if not isinstance(self._compile_job, str): - # finished compiling - logger.debug("AWG %d: Compilation of %r finished", self.awg_module.index, source_file) - break - cmd = yield 'compiling' - if cmd is None: - logger.debug('No command received during compiling') - elif cmd == 'abort': - raise NotImplementedError('clean abort not implemented') - else: - raise HDAWGValueError('Unknown command', cmd) - - _, status_int, status_str = self._compile_job - if status_int == 1: - raise HDAWGRuntimeError('Compilation failed', status_str) - logger.info("AWG %d: Compilation of %r successful", self.awg_module.index, source_file) - - def _start_elf_upload(self, elf_file): - logger.debug("Uploading %r", elf_file) - current_elf = self.awg_module.elf_file - if current_elf != elf_file: - logger.info("AWG %d: Overwriting elf file", self.awg_module.index) - self.awg_module.elf_file = elf_file - self.awg_module.elf_upload = True - self._upload_job = (elf_file, None) - time.sleep(.001) - - def _update_upload_job_status(self): - elf_upload = self.awg_module.elf_upload - if self._upload_job is None: - assert not elf_upload - return - - elf_file, old_status = self._upload_job - assert self.awg_module.elf_file == elf_file - - if isinstance(old_status, float) or old_status is None: - status_int, progress = self.awg_module.elf_status - if status_int == 2: - # in progress - assert elf_upload == 1 - self._upload_job = elf_file, progress - else: - # fetch new value here - self._upload_job = elf_file, status_int - - else: - logger.debug('AWG %d: _update_upload_job_status called on finished upload', self.awg_module.index) - assert elf_upload == 0 - - def _upload(self, elf_file) -> Generator[str, str, None]: - if self.awg_module.compiler_upload: - pass - else: - self._start_elf_upload(elf_file) - - while True: - self._update_upload_job_status() - _, status = self._upload_job - if isinstance(status, int): - assert status in (-1, 0, 1) - if status == 1: - raise RuntimeError('ELF upload failed') - else: - break - else: - progress = status - logger.debug('AWG %d: Upload progress is %d%%', self.awg_module.index, progress*100) - - cmd = yield 'uploading @ %d%%' % (100*progress) - if cmd is None: - logger.debug("No command received during upload") - if cmd == 'abort': - # TODO: check if this stops the upload - self.awg_module.elf_upload = False - raise NotImplementedError('Abort upload not cleanly implemented') - else: - raise HDAWGValueError('Unknown command', cmd) - - # enable auto upload on compilation again - # TODO: research whether this is necessary - # self.awg_module.elf_file = '' - - def compile_and_upload(self, source_string: str) -> Generator[str, str, None]: - """The source code is saved to a file determined by the source hash, compiled and uploaded to the instrument. - The function returns a generator that yields the current state of the progress. The generator is empty iff the - upload is complete. An exception is raised if there is an error. - - To abort send 'abort' to the generator. - - Example: - >>> my_source = 'playWave("my_wave");' - >>> for state in elf_manager.compile_and_upload(my_source): - ... print('Current state:', state) - ... time.sleep(1) - - Args: - source_string: Source code to compile - - Returns: - Generator object that needs to be consumed - """ - self._update_compile_job_status() - if isinstance(self._compile_job, str): - raise NotImplementedError('cannot upload: compilation in progress') - - source_hash = self._source_hash(source_string) - - seqc_file_name = '%s.seqc' % source_hash - elf_file_name = '%s.elf' % source_hash - - full_source_name = self.awg_module.src_dir.joinpath(seqc_file_name) - full_elf_name = self.awg_module.elf_dir.joinpath(elf_file_name) - - if not full_source_name.exists(): - full_source_name.write_text(source_string, 'utf-8') - - # we assume same source == same program here - if not full_elf_name.exists(): - yield from self._compile(seqc_file_name) - else: - # set this so the web interface shows the correct source - # self.awg_module.compiler_source_file = seqc_file_name - logger.info('Already compiled. ELF: %r', elf_file_name) - - yield from self._upload(elf_file_name) - - -class HDAWGException(Exception): - """Base exception class for HDAWG errors.""" - pass - - -class HDAWGValueError(HDAWGException, ValueError): - pass - - -class HDAWGTypeError(HDAWGException, TypeError): - pass - - -class HDAWGRuntimeError(HDAWGException, RuntimeError): - pass - - -class HDAWGIOError(HDAWGException, IOError): - pass - - -class HDAWGTimeoutError(HDAWGException, TimeoutError): - pass - - -class HDAWGCompilationException(HDAWGException): - def __init__(self, msg): - self.msg = msg - - def __str__(self) -> str: - return "Compilation failed: {}".format(self.msg) - - -class HDAWGUploadException(HDAWGException): - def __str__(self) -> str: - return "Upload to the instrument failed." - +import logging -def get_group_for_channels(hdawg: HDAWGRepresentation, channels: Set[int]) -> HDAWGChannelGroup: - channels = set(channels) - assert not channels - set(range(8)), "Channels must be in 0..=7" +from typing import Set - channel_range = range(min(channels) // 2 * 2, (max(channels) + 2) // 2 * 2) - if len(channel_range) > 4 or len(channel_range) == 4 and channel_range.start == 2: - c = (HDAWGChannelGrouping.CHAN_GROUP_1x8, 0) - elif len(channel_range) == 4: - assert channel_range.start in (0, 4) - c = (HDAWGChannelGrouping.CHAN_GROUP_2x4, channel_range.start // 4) - else: - assert len(channel_range) == 2 - c = (HDAWGChannelGrouping.CHAN_GROUP_4x2, channel_range.start // 2) - - hdawg.channel_grouping = c[0] - return hdawg.channel_tuples[c[1]] +if sys.version_info.minor > 8: + try: + from qupulse_hdawg.zihdawg import * + except ImportError: + print("Install the qupulse_hdawg package to use HDAWG with this python version.") + raise +else: + try: + from qupulse_hdawg_legacy.zihdawg import * + except ImportError: + print("Install the qupulse_hdawg_legacy package to use HDAWG with this python version.") + raise def example_upload(hdawg_kwargs: dict, channels: Set[int], markers: Set[Tuple[int, int]]): # pragma: no cover @@ -1305,5 +115,4 @@ def example_upload(hdawg_kwargs: dict, channels: Set[int], markers: Set[Tuple[in markers = [(m // 2, m % 2) for m in parsed.pop('markers')] logging.basicConfig(stream=sys.stdout) - logger.setLevel(logging.DEBUG) example_upload(hdawg_kwargs=parsed, channels=channels, markers=markers) diff --git a/qupulse/hardware/util.py b/qupulse/hardware/util.py index f7d173572..2d656d529 100644 --- a/qupulse/hardware/util.py +++ b/qupulse/hardware/util.py @@ -22,7 +22,7 @@ def traced(obj): njit = lambda x: x try: - import zhinst + import zhinst.utils except ImportError: # pragma: no cover zhinst = None diff --git a/setup.cfg b/setup.cfg index a53d823b9..a77b1e909 100644 --- a/setup.cfg +++ b/setup.cfg @@ -44,7 +44,8 @@ plotting = matplotlib tabor-instruments = tabor_control>=0.1.1 zurich-instruments = - zhinst<=20.7.2701;python_version<'3.9' + qupulse-hdawg-legacy;python_version<'3.9' + qupulse-hdawg;python_version>='3.9' Faster-fractions = gmpy2 tektronix = tek_awg>=0.2.1 autologging = autologging From a0256e3e199786d3f0d90c3b8ec59d4cb2554a69 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 14 Mar 2024 16:35:23 +0100 Subject: [PATCH 159/441] Add newspiece --- changes.d/779.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes.d/779.feature diff --git a/changes.d/779.feature b/changes.d/779.feature new file mode 100644 index 000000000..b455084f0 --- /dev/null +++ b/changes.d/779.feature @@ -0,0 +1 @@ +Move HDAWG driver to qupulse-hdawg-legacy to disentangle driver version from qupulse version. The new HDAWG driver will be published under qupulse-hdawg. From f0a957dd2adb6f408936f144c2b3ab56f70d10dc Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 14 Mar 2024 16:35:41 +0100 Subject: [PATCH 160/441] Remove seqc tests --- tests/_program/seqc_tests.py | 1092 ---------------------------------- 1 file changed, 1092 deletions(-) delete mode 100644 tests/_program/seqc_tests.py diff --git a/tests/_program/seqc_tests.py b/tests/_program/seqc_tests.py deleted file mode 100644 index 12522ac5c..000000000 --- a/tests/_program/seqc_tests.py +++ /dev/null @@ -1,1092 +0,0 @@ -import hashlib -import pathlib -import sys -import tempfile -import time -import unittest -from itertools import zip_longest, islice -from unittest import TestCase, mock - -import numpy as np - -from qupulse._program.seqc import BinaryWaveform, loop_to_seqc, WaveformPlayback, Repeat, SteppingRepeat, Scope, \ - to_node_clusters, find_sharable_waveforms, mark_sharable_waveforms, UserRegisterManager, HDAWGProgramManager, \ - UserRegister, WaveformFileSystem -from qupulse.expressions import ExpressionScalar -from qupulse.hardware.util import zhinst_voltage_to_uint16 -from qupulse.parameter_scope import DictScope -from qupulse.program.loop import Loop -from qupulse.program.volatile import VolatileRepetitionCount -from qupulse.program.waveforms import ConstantWaveform -from tests.pulses.sequencing_dummies import DummyWaveform - -try: - import zhinst -except ImportError: - zhinst = None - -# This block checks if zhinst_voltage_to_uint16 works. A failing implementation (due to missing dependencies) -# skips tests further down -try: - zhinst_voltage_to_uint16(np.zeros(16), np.zeros(16), - (np.zeros(16), np.zeros(16), np.zeros(16), np.zeros(16))) -except AttributeError: - # prerequisites not installed - zhinst_voltage_to_uint16 = None - - -def take(n, iterable): - "Return first n items of the iterable as a list" - return list(islice(iterable, n)) - - -def dummy_loop_to_seqc(loop, **kwargs): - return loop - - -class BinaryWaveformTest(unittest.TestCase): - MAX_RATE = 14 - - def test_dynamic_rate_reduction(self): - - ones = np.ones(2**(self.MAX_RATE + 2) * 3, np.uint16) - - for n in (2, 3, 5): - self.assertEqual(BinaryWaveform(ones[:n * 16 * 3]).dynamic_rate(), 0, f"Reducing {n}") - for n in (4, 6): - self.assertEqual(BinaryWaveform(ones[:16 * n * 3]).dynamic_rate(), 1) - - irreducibles = [ - np.array([0, 0, 1, 1, 0, 1] * 16, dtype=np.uint16), - np.array([0, 0, 0] * 16 + [0, 1, 0] + [0, 0, 0] * 15, dtype=np.uint16), - np.array([0, 0, 0] * 16 + [1, 0, 0] + [0, 0, 0] * 15, dtype=np.uint16), - ] - for max_rate in range(self.MAX_RATE): - for n in range(self.MAX_RATE): - for irreducible in irreducibles: - data = np.tile(np.tile(irreducible.reshape(-1, 1, 3), (1, 2**n, 1)).ravel(), (16,)) - - dyn_n = BinaryWaveform(data).dynamic_rate(max_rate=max_rate) - - self.assertEqual(min(max_rate, n), dyn_n) - - @unittest.skipIf(zhinst_voltage_to_uint16 is None, "BinaryWaveform.from_sampled backend missing") - def test_marker_data(self): - channel_1_data = np.linspace(-0.3, 0.4, num=192) - channel_2_data = np.linspace(-0.1, 0.1, num=192) - - bit_gen = np.random.PCG64(49174928843) - rng = np.random.Generator(bit_gen) - - m1, m2, m3, m4 = rng.integers(2, size=(4, 192), dtype=np.uint16) - - bwf = BinaryWaveform.from_sampled(channel_1_data, channel_2_data, (m1, m2, m3, m4)) - - ch1_markers = m1 | m2 << 1 - ch2_markers = m3 | m4 << 1 - - np.testing.assert_equal(ch1_markers, bwf.markers_ch1) - np.testing.assert_equal(ch2_markers, bwf.markers_ch2) - - -def make_binary_waveform(waveform): - if zhinst is None: - # TODO: mock used function - raise unittest.SkipTest("zhinst not present") - - if waveform.duration == 0: - data = np.asarray(3 * [1, 2, 3, 4, 5], dtype=np.uint16) - return (BinaryWaveform(data),) - else: - chs = sorted(waveform.defined_channels) - t = np.arange(0., float(waveform.duration), 1.) - - sampled = [None if ch is None else waveform.get_sampled(ch, t) - for _, ch in zip_longest(range(6), take(6, chs), fillvalue=None)] - ch1, ch2, *markers = sampled - return (BinaryWaveform.from_sampled(ch1, ch2, markers),) - - -def _key_to_int(n: int, duration: int, defined_channels: frozenset): - key_bytes = str((n, duration, sorted(defined_channels))).encode('ascii') - key_int64 = int(hashlib.sha256(key_bytes).hexdigest()[:2*8], base=16) // 2 - return key_int64 - - -def get_unique_wfs(n=10000, duration=32, defined_channels=frozenset(['A'])): - if not hasattr(get_unique_wfs, 'cache'): - get_unique_wfs.cache = {} - - key = (n, duration, defined_channels) - - if key not in get_unique_wfs.cache: - # positive deterministic int64 - h = _key_to_int(n, duration, defined_channels) - base = np.bitwise_xor(np.linspace(-h, h, num=duration + n, dtype=np.int64), h) - base = base / np.max(np.abs(base)) - - get_unique_wfs.cache[key] = [ - DummyWaveform(duration=duration, sample_output=base[idx:idx+duration], - defined_channels=defined_channels) - for idx in range(n) - ] - return get_unique_wfs.cache[key] - - -def get_constant_unique_wfs(n=10000, duration=192, defined_channels=frozenset(['A'])): - if not hasattr(get_unique_wfs, 'cache'): - get_unique_wfs.cache = {} - - key = (n, duration, defined_channels) - - if key not in get_unique_wfs.cache: - bit_gen = np.random.PCG64(_key_to_int(n, duration, defined_channels)) - rng = np.random.Generator(bit_gen) - - random_values = rng.random(size=(n, len(defined_channels))) - - sorted_channels = sorted(defined_channels) - get_unique_wfs.cache[key] = [ - ConstantWaveform.from_mapping(duration, {ch: ch_value - for ch, ch_value in zip(sorted_channels, wf_values)}) - for wf_values in random_values - ] - return get_unique_wfs.cache[key] - - -def complex_program_as_loop(unique_wfs, wf_same): - root = Loop(repetition_count=12) - - for wf_unique in unique_wfs: - root.append_child(children=[Loop(repetition_count=42, waveform=wf_unique), - Loop(repetition_count=98, waveform=wf_same)], - repetition_count=10) - - root.append_child(waveform=unique_wfs[0], repetition_count=21) - root.append_child(waveform=wf_same, repetition_count=23) - - volatile_repetition = VolatileRepetitionCount(ExpressionScalar('n + 4'), - DictScope.from_kwargs(n=3, volatile={'n'})) - root.append_child(waveform=wf_same, repetition_count=volatile_repetition) - - return root - - -def complex_program_as_seqc(unique_wfs, wf_same): - return Repeat(12, - Scope([ - SteppingRepeat([ - Repeat(repetition_count=10, scope=Scope([ - Repeat(42, WaveformPlayback(make_binary_waveform(unique_wf))), - Repeat(98, WaveformPlayback(make_binary_waveform(wf_same), shared=True)), - ])) - for unique_wf in unique_wfs - ]), - Repeat(21, WaveformPlayback(make_binary_waveform(unique_wfs[0]))), - Repeat(23, WaveformPlayback(make_binary_waveform(wf_same))), - Repeat('test_14', WaveformPlayback(make_binary_waveform(wf_same))) - ]) - ) - - -class DummyWfManager: - def __init__(self): - self.shared = {} - self.concatenated = [] - - def request_shared(self, wf): - return self.shared.setdefault(wf, len(self.shared) + 1) - - def request_concatenated(self, wf): - self.concatenated.append(wf) - return 0 - - -class WaveformFileSystemTests(TestCase): - def setUp(self) -> None: - clients = [mock.Mock(), mock.Mock()] - bin_waveforms = [mock.Mock(), mock.Mock(), mock.Mock()] - table_data = [np.ones(1, dtype=np.uint16) * i for i, _ in enumerate(bin_waveforms)] - for bin_wf, tab in zip(bin_waveforms, table_data): - bin_wf.to_csv_compatible_table.return_value = tab - - self.temp_dir = tempfile.TemporaryDirectory() - self.table_data = table_data - self.clients = clients - self.waveforms = [ - {'0': bin_waveforms[0], '1': bin_waveforms[1]}, - {'1': bin_waveforms[1], '2': bin_waveforms[2]} - ] - self.fs = WaveformFileSystem(pathlib.Path(self.temp_dir.name)) - - def read_files(self) -> dict: - return { - p.name: p.read_text().strip() for p in self.fs._path.iterdir() - } - - def tearDown(self) -> None: - self.temp_dir.cleanup() - - def test_pub_sync(self): - with mock.patch.object(self.fs, '_sync') as mock_sync: - self.fs.sync(self.clients[0], self.waveforms[0], hallo=0) - mock_sync.assert_called_once_with(hallo=0) - - self.assertEqual({id(self.clients[0]): self.waveforms[0]}, self.fs._required) - - def test_sync(self): - self.fs.sync(self.clients[0], self.waveforms[0]) - self.assertEqual({'0': '0', '1': '1'}, self.read_files()) - - self.fs.sync(self.clients[0], self.waveforms[1]) - self.assertEqual({'2': '2', '1': '1'}, self.read_files()) - - self.fs.sync(self.clients[1], self.waveforms[0]) - self.assertEqual({'2': '2', '1': '1', '0': '0'}, self.read_files()) - - def test_sync_write_all(self): - self.fs.sync(self.clients[0], self.waveforms[0]) - self.assertEqual({'0': '0', '1': '1'}, self.read_files()) - - self.table_data[0][:] = 7 - self.fs.sync(self.clients[0], self.waveforms[0]) - self.assertEqual({'0': '0', '1': '1'}, self.read_files()) - - self.fs.sync(self.clients[0], self.waveforms[0], write_all=True) - self.assertEqual({'0': '7', '1': '1'}, self.read_files()) - - def test_sync_no_delete(self): - self.fs.sync(self.clients[0], self.waveforms[0]) - self.assertEqual({'0': '0', '1': '1'}, self.read_files()) - - self.fs.sync(self.clients[0], self.waveforms[1], delete=False) - self.assertEqual({'2': '2', '1': '1', '0': '0'}, self.read_files()) - - -class SEQCNodeTests(TestCase): - """Test everything besides source code generation""" - @unittest.skipIf(zhinst is None, "test requires zhinst") - def test_visit_nodes(self): - wf, wf_shared = map(WaveformPlayback, map(make_binary_waveform, get_unique_wfs(2))) - wf_shared.shared = True - - waveform_manager = mock.Mock(wraps=DummyWfManager()) - wf._visit_nodes(waveform_manager) - waveform_manager.request_concatenated.assert_called_once_with(wf.waveform) - - waveform_manager = mock.Mock(wraps=DummyWfManager()) - wf_shared._visit_nodes(waveform_manager) - waveform_manager.request_concatenated.assert_not_called() - - scope = Scope([mock.Mock(wraps=wf), mock.Mock(wraps=wf_shared)]) - scope._visit_nodes(waveform_manager) - scope.nodes[0]._visit_nodes.assert_called_once_with(waveform_manager) - scope.nodes[1]._visit_nodes.assert_called_once_with(waveform_manager) - waveform_manager.request_concatenated.assert_called_once_with(wf.waveform) - - waveform_manager = mock.Mock(wraps=DummyWfManager()) - repeat = Repeat(12, mock.Mock(wraps=wf)) - repeat._visit_nodes(waveform_manager) - repeat.scope._visit_nodes.assert_called_once_with(waveform_manager) - waveform_manager.request_concatenated.assert_called_once_with(wf.waveform) - - waveform_manager = mock.Mock(wraps=DummyWfManager()) - stepping_repeat = SteppingRepeat([mock.Mock(wraps=wf), mock.Mock(wraps=wf), mock.Mock(wraps=wf)]) - stepping_repeat._visit_nodes(waveform_manager) - for node in stepping_repeat.node_cluster: - node._visit_nodes.assert_called_once_with(waveform_manager) - - @unittest.skipIf(zhinst is None, "test requires zhinst") - def test_same_stepping(self): - wf1, wf2 = map(WaveformPlayback, map(make_binary_waveform, get_unique_wfs(2, 32))) - wf3, wf_shared = map(WaveformPlayback, map(make_binary_waveform, get_unique_wfs(2, 64))) - wf_shared.shared = True - - scope1 = Scope([wf1, wf1, wf2]) - scope2 = Scope([wf1, wf2, wf2]) - scope3 = Scope([wf1, wf2, wf3]) - scope4 = Scope([wf1, wf2, wf2, wf2]) - - repeat1 = Repeat(13, wf1) - repeat2 = Repeat(13, wf2) - repeat3 = Repeat(15, wf2) - repeat4 = Repeat(13, wf3) - - stepping_repeat1 = SteppingRepeat([wf1, wf1, wf2]) - stepping_repeat2 = SteppingRepeat([wf2, wf2, wf2]) - stepping_repeat3 = SteppingRepeat([wf3, wf3, wf3]) - stepping_repeat4 = SteppingRepeat([wf1, wf1, wf2, wf1]) - - self.assertTrue(wf1.same_stepping(wf1)) - self.assertTrue(wf1.same_stepping(wf2)) - self.assertFalse(wf1.same_stepping(wf3)) - self.assertFalse(wf3.same_stepping(wf_shared)) - self.assertFalse(wf_shared.same_stepping(wf3)) - - self.assertFalse(scope1.same_stepping(wf1)) - self.assertTrue(scope1.same_stepping(scope2)) - self.assertFalse(scope1.same_stepping(scope3)) - self.assertFalse(scope1.same_stepping(scope4)) - - self.assertFalse(repeat1.same_stepping(scope1)) - self.assertTrue(repeat1.same_stepping(repeat2)) - self.assertFalse(repeat1.same_stepping(repeat3)) - self.assertFalse(repeat1.same_stepping(repeat4)) - - self.assertFalse(stepping_repeat1.same_stepping(scope1)) - self.assertTrue(stepping_repeat1.same_stepping(stepping_repeat2)) - self.assertFalse(stepping_repeat1.same_stepping(stepping_repeat3)) - self.assertFalse(stepping_repeat1.same_stepping(stepping_repeat4)) - - @unittest.skipIf(zhinst is None, "test requires zhinst") - def test_iter_waveform_playback(self): - wf1, wf2 = map(WaveformPlayback, map(make_binary_waveform, get_unique_wfs(2, 32))) - wf3, wf_shared = map(WaveformPlayback, map(make_binary_waveform, get_unique_wfs(2, 64))) - - for wf in (wf1, wf2, wf3, wf_shared): - pb, = wf.iter_waveform_playbacks() - self.assertIs(pb, wf) - - repeat = Repeat(13, wf1) - self.assertEqual(list(repeat.iter_waveform_playbacks()), [wf1]) - - scope = Scope([wf1, repeat, wf2, wf3, wf_shared]) - self.assertEqual(list(scope.iter_waveform_playbacks()), [wf1, wf1, wf2, wf3, wf_shared]) - - stepping_repeat = SteppingRepeat([wf1, repeat, wf2, wf3, wf_shared]) - self.assertEqual(list(stepping_repeat.iter_waveform_playbacks()), [wf1, wf1, wf2, wf3, wf_shared]) - - @unittest.skipIf(zhinst is None, "test requires zhinst") - def test_get_single_indexed_playback(self): - wf1, wf_shared = map(WaveformPlayback, map(make_binary_waveform, get_unique_wfs(2, 32))) - wf_shared.shared = True - self.assertIs(wf1._get_single_indexed_playback(), wf1) - self.assertIsNone(wf_shared._get_single_indexed_playback()) - - self.assertIs(Scope([wf1, wf_shared])._get_single_indexed_playback(), wf1) - self.assertIsNone(Scope([wf1, wf_shared, wf1])._get_single_indexed_playback(), wf1) - - def test_get_position_advance_strategy(self): - node = mock.Mock() - node.samples.return_value = 0 - node._get_single_indexed_playback.return_value.samples.return_value = 128 - repeat = Repeat(10, node) - - # no samples at all - self.assertIs(repeat._get_position_advance_strategy(), repeat._AdvanceStrategy.IGNORE) - node.samples.assert_called_once_with() - node._get_single_indexed_playback.assert_not_called() - - node.reset_mock() - node.samples.return_value = 64 - - # samples do differ - self.assertIs(repeat._get_position_advance_strategy(), repeat._AdvanceStrategy.INITIAL_RESET) - node.samples.assert_called_once_with() - node._get_single_indexed_playback.assert_called_once_with() - node._get_single_indexed_playback.return_value.samples.assert_called_once_with() - - node.reset_mock() - node.samples.return_value = 128 - - # samples are the same - self.assertIs(repeat._get_position_advance_strategy(), repeat._AdvanceStrategy.POST_ADVANCE) - node.samples.assert_called_once_with() - node._get_single_indexed_playback.assert_called_once_with() - node._get_single_indexed_playback.return_value.samples.assert_called_once_with() - - node.reset_mock() - node._get_single_indexed_playback.return_value = None - # multiple indexed playbacks - self.assertIs(repeat._get_position_advance_strategy(), repeat._AdvanceStrategy.INITIAL_RESET) - node.samples.assert_called_once_with() - node._get_single_indexed_playback.assert_called_once_with() - - -@unittest.skipIf(zhinst is None, "test requires zhinst") -class LoopToSEQCTranslationTests(TestCase): - def test_loop_to_seqc_leaf(self): - """Test the translation of leaves""" - # we use None because it is not used in this test - user_registers = None - - wf = DummyWaveform(duration=32, sample_output=lambda x: np.sin(x)) - loop = Loop(waveform=wf) - - # with wrapping repetition - loop.repetition_count = 15 - waveform_to_bin = mock.Mock(wraps=make_binary_waveform) - expected = Repeat(loop.repetition_count, WaveformPlayback(waveform=make_binary_waveform(wf))) - result = loop_to_seqc(loop, 1, 1, waveform_to_bin, user_registers=user_registers) - waveform_to_bin.assert_called_once_with(wf) - self.assertEqual(expected, result) - - # without wrapping repetition - loop.repetition_count = 1 - waveform_to_bin = mock.Mock(wraps=make_binary_waveform) - expected = WaveformPlayback(waveform=make_binary_waveform(wf)) - result = loop_to_seqc(loop, 1, 1, waveform_to_bin, user_registers=user_registers) - waveform_to_bin.assert_called_once_with(wf) - self.assertEqual(expected, result) - - def test_loop_to_seqc_len_1(self): - """Test the translation of loops with len(loop) == 1""" - # we use None because it is not used in this test - user_registers = None - - loop = Loop(children=[Loop()]) - waveform_to_bin = mock.Mock(wraps=make_binary_waveform) - loop_to_seqc_kwargs = dict(min_repetitions_for_for_loop=2, - min_repetitions_for_shared_wf=3, - waveform_to_bin=waveform_to_bin, - user_registers=user_registers) - - expected = 'asdf' - with mock.patch('qupulse._program.seqc.loop_to_seqc', return_value=expected) as mocked_loop_to_seqc: - result = loop_to_seqc(loop, **loop_to_seqc_kwargs) - self.assertEqual(result, expected) - mocked_loop_to_seqc.assert_called_once_with(loop[0], **loop_to_seqc_kwargs) - - loop.repetition_count = 14 - expected = Repeat(14, 'asdfg') - with mock.patch('qupulse._program.seqc.loop_to_seqc', return_value=expected.scope) as mocked_loop_to_seqc: - result = loop_to_seqc(loop, **loop_to_seqc_kwargs) - self.assertEqual(result, expected) - mocked_loop_to_seqc.assert_called_once_with(loop[0], **loop_to_seqc_kwargs) - - waveform_to_bin.assert_not_called() - - def test_to_node_clusters(self): - """Test cluster generation""" - wf1, wf2 = map(WaveformPlayback, map(make_binary_waveform, get_unique_wfs(2, 32))) - wf3, wf_shared = map(WaveformPlayback, map(make_binary_waveform, get_unique_wfs(2, 64))) - - loop_to_seqc_kwargs = {'my': 'kwargs'} - - loops = [wf1, wf2, wf1, wf1, wf3, wf1, wf1, wf1, wf3, wf1, wf3, wf1, wf3] - expected_calls = [mock.call(loop, **loop_to_seqc_kwargs) for loop in loops] - expected_result = [[wf1, wf2, wf1, wf1], [wf3], [wf1, wf1, wf1], [Scope([wf3, wf1]), Scope([wf3, wf1])], [wf3]] - - with mock.patch('qupulse._program.seqc.loop_to_seqc', wraps=dummy_loop_to_seqc) as mock_loop_to_seqc: - result = to_node_clusters(loops, loop_to_seqc_kwargs) - self.assertEqual(mock_loop_to_seqc.mock_calls, expected_calls) - self.assertEqual(expected_result, result) - - def test_to_node_clusters_crash(self): - wf1 = WaveformPlayback(make_binary_waveform(*get_unique_wfs(1, 32))) - wf2 = WaveformPlayback(make_binary_waveform(*get_unique_wfs(1, 64))) - wf3 = WaveformPlayback(make_binary_waveform(*get_unique_wfs(1, 128))) - wf4 = WaveformPlayback(make_binary_waveform(*get_unique_wfs(1, 256))) - - loop_to_seqc_kwargs = {'my': 'kwargs'} - - loops = [wf1, wf2, wf3] * 3 + [wf1] + [wf2, wf4] * 3 + [wf1] - with mock.patch('qupulse._program.seqc.loop_to_seqc', wraps=dummy_loop_to_seqc) as mock_loop_to_seqc: - result = to_node_clusters(loops, loop_to_seqc_kwargs) - expected_result = [[Scope([wf1, wf2, wf3])]*3, [wf1], [Scope([wf2, wf4])]*3, [wf1]] - self.assertEqual(expected_result, result) - - def test_find_sharable_waveforms(self): - wf1, wf2 = map(WaveformPlayback, map(make_binary_waveform, get_unique_wfs(2, 32))) - wf3, wf_shared = map(WaveformPlayback, map(make_binary_waveform, get_unique_wfs(2, 64))) - - scope1 = Scope([wf1, wf1, wf_shared, wf1]) - scope2 = Scope([wf1, wf2, wf_shared, wf2]) - scope3 = Scope([wf2, wf2, wf_shared, wf3]) - scope4 = Scope([wf2, wf2, wf3, wf3]) - - self.assertIsNone(find_sharable_waveforms([scope1, scope2, scope3, scope4])) - - shareable = find_sharable_waveforms([scope1, scope2, scope3]) - self.assertEqual([False, False, True, False], shareable) - - def test_mark_sharable_waveforms(self): - shareable = [False, False, True, False] - - pb_gen = map(WaveformPlayback, map(make_binary_waveform, get_unique_wfs(12, 32))) - - nodes = [Scope([mock.Mock(wraps=next(pb_gen)), mock.Mock(wraps=next(pb_gen)), mock.Mock(wraps=next(pb_gen)), mock.Mock(wraps=next(pb_gen))]), - Scope([mock.Mock(wraps=next(pb_gen)), mock.Mock(wraps=next(pb_gen)), mock.Mock(wraps=next(pb_gen)), mock.Mock(wraps=next(pb_gen))]), - Scope([mock.Mock(wraps=next(pb_gen)), mock.Mock(wraps=next(pb_gen)), mock.Mock(wraps=next(pb_gen)), mock.Mock(wraps=next(pb_gen))])] - - mocks = [mock.Mock(wraps=scope) for scope in nodes] - - mark_sharable_waveforms(mocks, shareable) - - for mock_scope, scope in zip(mocks, nodes): - mock_scope.iter_waveform_playbacks.assert_called_once_with() - m1, m2, m3, m4 = scope.nodes - self.assertIsInstance(m1.shared, mock.Mock) - m1.iter_waveform_playbacks.assert_called_once_with() - self.assertIsInstance(m2.shared, mock.Mock) - m2.iter_waveform_playbacks.assert_called_once_with() - self.assertTrue(m3.shared) - m3.iter_waveform_playbacks.assert_called_once_with() - self.assertIsInstance(m4.shared, mock.Mock) - m4.iter_waveform_playbacks.assert_called_once_with() - - def test_loop_to_seqc_cluster_handling(self): - """Test handling of clusters""" - - # we use None because it is not used in this test - user_registers = None - - with self.assertRaises(AssertionError): - loop_to_seqc(Loop(repetition_count=12, children=[Loop()]), - min_repetitions_for_for_loop=3, min_repetitions_for_shared_wf=2, - waveform_to_bin=make_binary_waveform, user_registers=user_registers) - - loop_to_seqc_kwargs = dict(min_repetitions_for_for_loop=3, - min_repetitions_for_shared_wf=4, - waveform_to_bin=make_binary_waveform, user_registers=user_registers) - - wf_same = map(WaveformPlayback, map(make_binary_waveform, get_unique_wfs(100000, 32))) - wf_sep, = map(WaveformPlayback, map(make_binary_waveform, get_unique_wfs(1, 64))) - - node_clusters = [take(2, wf_same), [wf_sep], - take(3, wf_same), [wf_sep], - take(4, wf_same), take(4, wf_same)] - root = Loop(repetition_count=12, children=[Loop() for _ in range(2 + 1 + 3 + 1 + 4 + 1 + 4)]) - - expected = Repeat(12, Scope([ - *node_clusters[0], - wf_sep, - SteppingRepeat(node_clusters[2]), - wf_sep, - SteppingRepeat(node_clusters[4]), - SteppingRepeat(node_clusters[5]) - ])) - - def dummy_find_sharable_waveforms(cluster): - if cluster is node_clusters[4]: - return [True] - else: - return None - - p1 = mock.patch('qupulse._program.seqc.to_node_clusters', return_value=node_clusters) - p2 = mock.patch('qupulse._program.seqc.find_sharable_waveforms', wraps=dummy_find_sharable_waveforms) - p3 = mock.patch('qupulse._program.seqc.mark_sharable_waveforms') - - with p1 as to_node_clusters_mock, p2 as find_share_mock, p3 as mark_share_mock: - result = loop_to_seqc(root, **loop_to_seqc_kwargs) - self.assertEqual(expected, result) - - to_node_clusters_mock.assert_called_once_with(root, loop_to_seqc_kwargs) - self.assertEqual(find_share_mock.mock_calls, - [mock.call(node_clusters[4]), mock.call(node_clusters[5])]) - mark_share_mock.assert_called_once_with(node_clusters[4], [True]) - - def test_program_translation(self): - """Integration test""" - user_registers = UserRegisterManager(range(14, 15), 'test_{register}') - - unique_wfs = get_unique_wfs() - same_wf = DummyWaveform(duration=32, sample_output=np.ones(32)) - root = complex_program_as_loop(unique_wfs, wf_same=same_wf) - - t0 = time.perf_counter() - - seqc = loop_to_seqc(root, 50, 100, make_binary_waveform, user_registers=user_registers) - - t1 = time.perf_counter() - print('took', t1 - t0, 's') - - expected = complex_program_as_seqc(unique_wfs, wf_same=same_wf) - self.assertEqual(expected, seqc) - - -@unittest.skipIf(zhinst is None, "test requires zhinst") -class SEQCToCodeTranslationTests(TestCase): - def setUp(self) -> None: - self.line_prefix = ' ' - self.node_name_generator = map(str, range(10000000000000000000)) - self.pos_var_name = 'foo' - self.waveform_manager = DummyWfManager() - - def test_shared_playback(self): - wf, = map(WaveformPlayback, map(make_binary_waveform, get_unique_wfs(1, 32))) - wf.shared = True - - expected = [' playWave(1);'] - result = list(wf.to_source_code(self.waveform_manager, self.node_name_generator, self.line_prefix, self.pos_var_name, True)) - self.assertEqual(expected, result) - - def test_indexed_playback(self): - wf, = map(WaveformPlayback, map(make_binary_waveform, get_unique_wfs(1, 32))) - - expected = [' playWaveIndexed(0, foo, 32); foo = foo + 32;'] - result = list( - wf.to_source_code(self.waveform_manager, self.node_name_generator, self.line_prefix, self.pos_var_name, - True)) - self.assertEqual(expected, result) - - expected = [' playWaveIndexed(0, foo, 32);' + wf.ADVANCE_DISABLED_COMMENT] - result = list( - wf.to_source_code(self.waveform_manager, self.node_name_generator, self.line_prefix, self.pos_var_name, - False)) - self.assertEqual(expected, result) - - def test_scope(self): - nodes = [mock.Mock(), mock.Mock(), mock.Mock()] - for idx, node in enumerate(nodes): - node.to_source_code = mock.Mock(return_value=map(str, [idx + 100, idx + 200])) - - scope = Scope(nodes) - expected = ['100', '200', '101', '201', '102', '202'] - result = list(scope.to_source_code(self.waveform_manager, self.node_name_generator, - self.line_prefix, self.pos_var_name, False)) - self.assertEqual(expected, result) - for node in nodes: - node.to_source_code.assert_called_once_with(self.waveform_manager, - line_prefix=self.line_prefix, - pos_var_name=self.pos_var_name, - node_name_generator=self.node_name_generator, - advance_pos_var=False) - - def test_stepped_repeat(self): - nodes = [mock.Mock(), mock.Mock(), mock.Mock()] - for idx, node in enumerate(nodes): - node.to_source_code = mock.Mock(return_value=map(str, [idx + 100, idx + 200])) - - stepping_repeat = SteppingRepeat(nodes) - - body_prefix = self.line_prefix + stepping_repeat.INDENTATION - expected = [ - ' repeat(3) {' + stepping_repeat.STEPPING_REPEAT_COMMENT, - '100', - '200', - ' }' - ] - result = list(stepping_repeat.to_source_code(self.waveform_manager, self.node_name_generator, - self.line_prefix, self.pos_var_name, False)) - self.assertEqual(expected, result) - nodes[0].to_source_code.assert_called_once_with(self.waveform_manager, - line_prefix=body_prefix, - pos_var_name=self.pos_var_name, - node_name_generator=self.node_name_generator, - advance_pos_var=False) - nodes[1].to_source_code.assert_not_called() - nodes[2].to_source_code.assert_not_called() - nodes[0]._visit_nodes.assert_not_called() - nodes[1]._visit_nodes.assert_called_once_with(self.waveform_manager) - nodes[2]._visit_nodes.assert_called_once_with(self.waveform_manager) - - def test_repeat(self): - node = mock.Mock() - node.to_source_code = mock.Mock(return_value=['asd', 'jkl']) - node._get_single_indexed_playback = mock.Mock(return_value=None) - node.samples = mock.Mock(return_value=64) - - repeat = Repeat(12, node) - - body_prefix = self.line_prefix + repeat.INDENTATION - expected = [' var init_pos_0 = foo;', - ' repeat(12) {', - ' foo = init_pos_0;', - 'asd', 'jkl', ' }'] - - result = list(repeat.to_source_code(self.waveform_manager, - node_name_generator=self.node_name_generator, - line_prefix=self.line_prefix, pos_var_name=self.pos_var_name, - advance_pos_var=True)) - self.assertEqual(expected, result) - node.to_source_code.assert_called_once_with(self.waveform_manager, node_name_generator=self.node_name_generator, - line_prefix=body_prefix, - pos_var_name=self.pos_var_name, - advance_pos_var=True) - node._get_single_indexed_playback.assert_called_once_with() - node.samples.assert_called_once_with() - - def test_repeat_detect_no_advance(self): - node = mock.Mock() - node.to_source_code = mock.Mock(return_value=['asd', 'jkl']) - node._get_single_indexed_playback = mock.Mock(return_value=None) - node.samples = mock.Mock(return_value=0) - - repeat = Repeat(12, node) - body_prefix = self.line_prefix + repeat.INDENTATION - - expected = [' repeat(12) {', - 'asd', 'jkl', ' }'] - result_no_advance = list(repeat.to_source_code(self.waveform_manager, - node_name_generator=self.node_name_generator, - line_prefix=self.line_prefix, pos_var_name=self.pos_var_name, - advance_pos_var=True)) - self.assertEqual(expected, result_no_advance) - node.to_source_code.assert_called_once_with(self.waveform_manager, node_name_generator=self.node_name_generator, - line_prefix=body_prefix, - pos_var_name=self.pos_var_name, - advance_pos_var=False) - node._get_single_indexed_playback.assert_not_called() - node.samples.assert_called_once_with() - - def test_repeat_extern_no_advance(self): - node = mock.Mock() - node.to_source_code = mock.Mock(return_value=['asd', 'jkl']) - node._get_single_indexed_playback = mock.Mock(return_value=None) - node.samples = mock.Mock(return_value=64) - - repeat = Repeat(12, node) - - body_prefix = self.line_prefix + repeat.INDENTATION - - expected = [' repeat(12) {', - 'asd', 'jkl', ' }'] - result_no_advance = list(repeat.to_source_code(self.waveform_manager, - node_name_generator=self.node_name_generator, - line_prefix=self.line_prefix, pos_var_name=self.pos_var_name, - advance_pos_var=False)) - self.assertEqual(expected, result_no_advance) - node.to_source_code.assert_called_once_with(self.waveform_manager, node_name_generator=self.node_name_generator, - line_prefix=body_prefix, - pos_var_name=self.pos_var_name, - advance_pos_var=False) - node._get_single_indexed_playback.assert_not_called() - node.samples.assert_not_called() - - def test_program_to_code_translation(self): - """Integration test""" - unique_wfs = get_unique_wfs() - same_wf = DummyWaveform(duration=48, sample_output=np.ones(48)) - seqc_nodes = complex_program_as_seqc(unique_wfs, wf_same=same_wf) - - wf_manager = DummyWfManager() - def node_name_gen(): - for i in range(100): - yield str(i) - - seqc_code = '\n'.join(seqc_nodes.to_source_code(wf_manager, - line_prefix='', - pos_var_name='pos', - node_name_generator=node_name_gen())) - # this is just copied from the result... - expected = """var init_pos_0 = pos; -repeat(12) { - pos = init_pos_0; - repeat(10000) { // stepping repeat - repeat(10) { - repeat(42) { - playWaveIndexed(0, pos, 32); // advance disabled do to parent repetition - } - repeat(98) { - playWave(1); - } - } - pos = pos + 32; - } - repeat(21) { - playWaveIndexed(0, pos, 32); // advance disabled do to parent repetition - } - pos = pos + 32; - repeat(23) { - playWaveIndexed(0, pos, 48); // advance disabled do to parent repetition - } - pos = pos + 48; - var idx_1; - for(idx_1 = 0; idx_1 < test_14; idx_1 = idx_1 + 1) { - playWaveIndexed(0, pos, 48); // advance disabled do to parent repetition - } - pos = pos + 48; -}""" - self.assertEqual(expected, seqc_code) - - -class UserRegisterTest(unittest.TestCase): - def test_conversions(self): - reg = UserRegister(zero_based_value=3) - self.assertEqual(3, reg.to_seqc()) - self.assertEqual(3, reg.to_labone()) - self.assertEqual(4, reg.to_web_interface()) - - reg = UserRegister(one_based_value=4) - self.assertEqual(3, reg.to_seqc()) - self.assertEqual(3, reg.to_labone()) - self.assertEqual(4, reg.to_web_interface()) - - self.assertEqual(reg, UserRegister.from_seqc(3)) - self.assertEqual(reg, UserRegister.from_labone(3)) - self.assertEqual(reg, UserRegister.from_web_interface(4)) - - def test_formatting(self): - reg = UserRegister.from_seqc(3) - - with self.assertRaises(ValueError): - '{}'.format(reg) - - self.assertEqual('3', '{:seqc}'.format(reg)) - self.assertEqual('4', '{:web}'.format(reg)) - self.assertEqual('UserRegister(zero_based_value=3)', repr(reg)) - self.assertEqual(repr(reg), '{:r}'.format(reg)) - - -class UserRegisterManagerTest(unittest.TestCase): - def test_require(self): - manager = UserRegisterManager([7, 8, 9], 'test{register}') - - required = [manager.request(0), manager.request(1), manager.request(2)] - - self.assertEqual({'test7', 'test8', 'test9'}, set(required)) - self.assertEqual(required[1], manager.request(1)) - - with self.assertRaisesRegex(ValueError, "No register"): - manager.request(3) - - -class HDAWGProgramManagerTest(unittest.TestCase): - @unittest.skipIf(sys.version_info.minor < 6, "This test requires dict to be ordered.") - def test_full_run(self): - defined_channels = frozenset(['A', 'B', 'C']) - - unique_n = 1000 - unique_duration = 32 - - unique_wfs = get_unique_wfs(n=unique_n, duration=unique_duration, defined_channels=defined_channels) - same_wf = DummyWaveform(duration=48, sample_output=np.ones(48), defined_channels=defined_channels) - - channels = ('A', 'B') - markers = ('C', None, 'A', None) - amplitudes = (1., 1.) - offsets = (0., 0.) - volatage_transformations = (lambda x: x, lambda x: x) - sample_rate = 1 - - root = complex_program_as_loop(unique_wfs, wf_same=same_wf) - seqc_nodes = complex_program_as_seqc(unique_wfs, wf_same=same_wf) - - manager = HDAWGProgramManager() - - manager.add_program('test', root, channels, markers, amplitudes, offsets, volatage_transformations, sample_rate) - - # 0: Program selection - # 1: Trigger - self.assertEqual({UserRegister(zero_based_value=2): 7}, manager.get_register_values('test')) - seqc_program = manager.to_seqc_program() - expected_program = """const PROG_SEL_REGISTER = 0; -const TRIGGER_REGISTER = 1; -const TRIGGER_RESET_MASK = 0b10000000000000000000000000000000; -const PROG_SEL_NONE = 0; -const NO_RESET_MASK = 0b10000000000000000000000000000000; -const PLAYBACK_FINISHED_MASK = 0b1000000000000000000000000000000; -const PROG_SEL_MASK = 0b111111111111111111111111111111; -const INVERTED_PROG_SEL_MASK = 0b11000000000000000000000000000000; -const IDLE_WAIT_CYCLES = 300; -wave test_concatenated_waveform_0 = "c45d955d9dc472d46bf74f7eb9ae2ed4d159adea7d6fe9ce3f48c95423535333"; -wave test_shared_waveform_121f5c6e8822793b3836fb3098fa4591b91d4c205cc2d8afd01ee1bf6956e518 = "121f5c6e8822793b3836fb3098fa4591b91d4c205cc2d8afd01ee1bf6956e518"; - -// function used by manually triggered programs -void waitForSoftwareTrigger() { - while (true) { - var trigger_register = getUserReg(TRIGGER_REGISTER); - if (trigger_register & TRIGGER_RESET_MASK) setUserReg(TRIGGER_REGISTER, 0); - if (trigger_register) return; - } -} - - -// program definitions -void test_function() { - var pos = 0; - var user_reg_2 = getUserReg(2); - waitForSoftwareTrigger(); - var init_pos_1 = pos; - repeat(12) { - pos = init_pos_1; - repeat(1000) { // stepping repeat - repeat(10) { - repeat(42) { - playWaveIndexed(test_concatenated_waveform_0, pos, 32); // advance disabled do to parent repetition - } - repeat(98) { - playWave(test_shared_waveform_121f5c6e8822793b3836fb3098fa4591b91d4c205cc2d8afd01ee1bf6956e518); - } - } - pos = pos + 32; - } - repeat(21) { - playWaveIndexed(test_concatenated_waveform_0, pos, 32); // advance disabled do to parent repetition - } - pos = pos + 32; - repeat(23) { - playWaveIndexed(test_concatenated_waveform_0, pos, 48); // advance disabled do to parent repetition - } - pos = pos + 48; - var idx_2; - for(idx_2 = 0; idx_2 < user_reg_2; idx_2 = idx_2 + 1) { - playWaveIndexed(test_concatenated_waveform_0, pos, 48); // advance disabled do to parent repetition - } - pos = pos + 48; - } -} - -// Declare and initialize global variables -// Selected program index (0 -> None) -var prog_sel = 0; - -// Value that gets written back to program selection register. -// Used to signal that at least one program was played completely. -var new_prog_sel = 0; - -// Is OR'ed to new_prog_sel. -// Set to PLAYBACK_FINISHED_MASK if a program was played completely. -var playback_finished = 0; - - -// runtime block -while (true) { - // read program selection value - prog_sel = getUserReg(PROG_SEL_REGISTER); - - // calculate value to write back to PROG_SEL_REGISTER - new_prog_sel = prog_sel | playback_finished; - if (!(prog_sel & NO_RESET_MASK)) new_prog_sel &= INVERTED_PROG_SEL_MASK; - setUserReg(PROG_SEL_REGISTER, new_prog_sel); - - // reset playback flag - playback_finished = 0; - - // only use part of prog sel that does not mean other things to select the program. - prog_sel &= PROG_SEL_MASK; - - switch (prog_sel) { - case 1: - test_function(); - waitWave(); - playback_finished = PLAYBACK_FINISHED_MASK; - default: - wait(IDLE_WAIT_CYCLES); - } -}""" - self.assertEqual(expected_program, seqc_program) - - @unittest.skipIf(sys.version_info.minor < 6, "This test requires dict to be ordered.") - def test_full_run_with_dynamic_rate_reduction(self): - defined_channels = frozenset(['A', 'B', 'C']) - - unique_n = 1000 - unique_duration = 192 - - unique_wfs = get_constant_unique_wfs(n=unique_n, duration=unique_duration, - defined_channels=defined_channels) - same_wf = DummyWaveform(duration=48, sample_output=np.ones(48), defined_channels=defined_channels) - - channels = ('A', 'B') - markers = ('C', None, 'A', None) - amplitudes = (1., 1.) - offsets = (0., 0.) - volatage_transformations = (lambda x: x, lambda x: x) - sample_rate = 1 - - old_value, WaveformPlayback.ENABLE_DYNAMIC_RATE_REDUCTION = WaveformPlayback.ENABLE_DYNAMIC_RATE_REDUCTION, True - try: - root = complex_program_as_loop(unique_wfs, wf_same=same_wf) - seqc_nodes = complex_program_as_seqc(unique_wfs, wf_same=same_wf) - - manager = HDAWGProgramManager() - - manager.add_program('test', root, channels, markers, amplitudes, offsets, volatage_transformations, - sample_rate) - finally: - WaveformPlayback.ENABLE_DYNAMIC_RATE_REDUCTION = old_value - - - - # 0: Program selection - # 1: Trigger - self.assertEqual({UserRegister(zero_based_value=2): 7}, manager.get_register_values('test')) - seqc_program = manager.to_seqc_program() - expected_program = """const PROG_SEL_REGISTER = 0; -const TRIGGER_REGISTER = 1; -const TRIGGER_RESET_MASK = 0b10000000000000000000000000000000; -const PROG_SEL_NONE = 0; -const NO_RESET_MASK = 0b10000000000000000000000000000000; -const PLAYBACK_FINISHED_MASK = 0b1000000000000000000000000000000; -const PROG_SEL_MASK = 0b111111111111111111111111111111; -const INVERTED_PROG_SEL_MASK = 0b11000000000000000000000000000000; -const IDLE_WAIT_CYCLES = 300; -wave test_concatenated_waveform_0 = "7fd412eb866ad371f717857ea33b309ec458c6c3469c7e51dcffcdce9a8c2679"; -wave test_shared_waveform_121f5c6e8822793b3836fb3098fa4591b91d4c205cc2d8afd01ee1bf6956e518 = "121f5c6e8822793b3836fb3098fa4591b91d4c205cc2d8afd01ee1bf6956e518"; - -// function used by manually triggered programs -void waitForSoftwareTrigger() { - while (true) { - var trigger_register = getUserReg(TRIGGER_REGISTER); - if (trigger_register & TRIGGER_RESET_MASK) setUserReg(TRIGGER_REGISTER, 0); - if (trigger_register) return; - } -} - - -// program definitions -void test_function() { - var pos = 0; - var user_reg_2 = getUserReg(2); - waitForSoftwareTrigger(); - var init_pos_1 = pos; - repeat(12) { - pos = init_pos_1; - repeat(1000) { // stepping repeat - repeat(10) { - repeat(42) { - playWaveIndexed(test_concatenated_waveform_0, pos, 48, 2); // advance disabled do to parent repetition - } - repeat(98) { - playWave(test_shared_waveform_121f5c6e8822793b3836fb3098fa4591b91d4c205cc2d8afd01ee1bf6956e518, 0); - } - } - pos = pos + 48; - } - repeat(21) { - playWaveIndexed(test_concatenated_waveform_0, pos, 48, 2); // advance disabled do to parent repetition - } - pos = pos + 48; - repeat(23) { - playWaveIndexed(test_concatenated_waveform_0, pos, 48, 0); // advance disabled do to parent repetition - } - pos = pos + 48; - var idx_2; - for(idx_2 = 0; idx_2 < user_reg_2; idx_2 = idx_2 + 1) { - playWaveIndexed(test_concatenated_waveform_0, pos, 48, 0); // advance disabled do to parent repetition - } - pos = pos + 48; - } -} - -// Declare and initialize global variables -// Selected program index (0 -> None) -var prog_sel = 0; - -// Value that gets written back to program selection register. -// Used to signal that at least one program was played completely. -var new_prog_sel = 0; - -// Is OR'ed to new_prog_sel. -// Set to PLAYBACK_FINISHED_MASK if a program was played completely. -var playback_finished = 0; - - -// runtime block -while (true) { - // read program selection value - prog_sel = getUserReg(PROG_SEL_REGISTER); - - // calculate value to write back to PROG_SEL_REGISTER - new_prog_sel = prog_sel | playback_finished; - if (!(prog_sel & NO_RESET_MASK)) new_prog_sel &= INVERTED_PROG_SEL_MASK; - setUserReg(PROG_SEL_REGISTER, new_prog_sel); - - // reset playback flag - playback_finished = 0; - - // only use part of prog sel that does not mean other things to select the program. - prog_sel &= PROG_SEL_MASK; - - switch (prog_sel) { - case 1: - test_function(); - waitWave(); - playback_finished = PLAYBACK_FINISHED_MASK; - default: - wait(IDLE_WAIT_CYCLES); - } -}""" - self.assertEqual(expected_program, seqc_program) \ No newline at end of file From 570ae79588ecc791ae332605396c21de92702848 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 14 Mar 2024 16:36:46 +0100 Subject: [PATCH 161/441] Add average_windows function for use in new DAQ drivers --- qupulse/utils/performance.py | 90 ++++++++++++++++++++++++++++++++ tests/utils/performance_tests.py | 29 +++++++++- 2 files changed, 118 insertions(+), 1 deletion(-) diff --git a/qupulse/utils/performance.py b/qupulse/utils/performance.py index 4076b664c..12abfa809 100644 --- a/qupulse/utils/performance.py +++ b/qupulse/utils/performance.py @@ -81,4 +81,94 @@ def time_windows_to_samples(begins: np.ndarray, lengths: np.ndarray, is_monotonic = _is_monotonic_numba +@njit +def _average_windows_numba(time: np.ndarray, values: np.ndarray, + begins: np.ndarray, ends: np.ndarray) -> np.ndarray: + n_samples, = time.shape + n_windows, = begins.shape + + assert len(begins) == len(ends) + assert values.shape[0] == n_samples + + result = np.zeros(begins.shape + values.shape[1:], dtype=float) + count = np.zeros(n_windows, dtype=np.uint64) + + start = 0 + for i in range(n_samples): + t = time[i] + v = values[i, ...] + + while start < n_windows and ends[start] <= t: + n = count[start] + if n == 0: + result[start] = np.nan + else: + result[start] /= n + start += 1 + + idx = start + while idx < n_windows and begins[idx] <= t: + result[idx] += v + count[idx] += 1 + idx += 1 + + for idx in range(start, n_windows): + n = count[idx] + if n == 0: + result[idx] = np.nan + else: + result[idx] /= count[idx] + + return result + + +def _average_windows_numpy(time: np.ndarray, values: np.ndarray, + begins: np.ndarray, ends: np.ndarray) -> np.ndarray: + start = np.searchsorted(time, begins) + end = np.searchsorted(time, ends) + + val_shape = values.shape[1:] + + count = end - start + val_mask = result_mask = start < end + + result = np.zeros(begins.shape + val_shape, dtype=float) + while np.any(val_mask): + result[val_mask, ...] += values[start[val_mask], ...] + start[val_mask] += 1 + val_mask = start < end + + result[~result_mask, ...] = np.nan + if result.ndim == 1: + result[result_mask, ...] /= count[result_mask] + else: + result[result_mask, ...] /= count[result_mask, None] + + return result + + +def average_windows(time: np.ndarray, values: np.ndarray, begins: np.ndarray, ends: np.ndarray): + """This function calculates the average over all windows that are defined by begins and ends. + The function assumes that the given time array is monotonically increasing and might produce + nonsensical results if not. + Args: + time: Time associated with the values of shape (n_samples,) + values: Values to average of shape (n_samples,) or (n_samples, n_channels) + begins: Beginning time stamps of the windows of shape (n_windows,) + ends: Ending time stamps of the windows of shape (n_windows,) + + Returns: + Averaged values for each window of shape (n_windows,) or (n_windows, n_channels). + Windows without samples are NaN. + """ + n_samples, = time.shape + n_windows, = begins.shape + + assert n_windows == len(ends) + assert values.shape[0] == n_samples + + if numba is None: + return _average_windows_numpy(time, values, begins, ends) + else: + return _average_windows_numba(time, values, begins, ends) diff --git a/tests/utils/performance_tests.py b/tests/utils/performance_tests.py index d158dce5c..b90237511 100644 --- a/tests/utils/performance_tests.py +++ b/tests/utils/performance_tests.py @@ -2,7 +2,8 @@ import numpy as np -from qupulse.utils.performance import _time_windows_to_samples_numba, _time_windows_to_samples_numpy +from qupulse.utils.performance import (_time_windows_to_samples_numba, _time_windows_to_samples_numpy, + _average_windows_numba, _average_windows_numpy, average_windows) class TimeWindowsToSamplesTest(unittest.TestCase): @@ -28,3 +29,29 @@ def test_unsorted(self): self.assert_implementations_equal(begins, lengths, sr) +class WindowAverageTest(unittest.TestCase): + @staticmethod + def assert_implementations_equal(time, values, begins, ends): + numpy_result = _average_windows_numpy(time, values, begins, ends) + numba_result = _average_windows_numba(time, values, begins, ends) + np.testing.assert_allclose(numpy_result, numba_result) + + def setUp(self): + self.begins = np.array([1., 2., 3.] + [4.] + [6., 7., 8., 9., 10.]) + self.ends = self.begins + np.array([1., 1., 1.] + [3.] + [2., 2., 2., 2., 2.]) + self.time = np.arange(10).astype(float) + self.values = np.asarray([ + np.sin(self.time), + np.cos(self.time), + ]).T + + def test_dispatch(self): + _ = average_windows(self.time, self.values, self.begins, self.ends) + _ = average_windows(self.time, self.values[..., 0], self.begins, self.ends) + + def test_single_channel(self): + self.assert_implementations_equal(self.time, self.values[..., 0], self.begins, self.ends) + self.assert_implementations_equal(self.time, self.values[..., :1], self.begins, self.ends) + + def test_dual_channel(self): + self.assert_implementations_equal(self.time, self.values, self.begins, self.ends) From d311b69d1abeab2811b2ff0652bf0d7c47bc05d4 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 14 Mar 2024 17:11:59 +0100 Subject: [PATCH 162/441] Do not skip tests via exception during test collection --- .../tabor_backward_compatibility_tests.py | 9 ++++----- tests/hardware/tabor_simulator_based_tests.py | 14 +++++++++----- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/tests/backward_compatibility/tabor_backward_compatibility_tests.py b/tests/backward_compatibility/tabor_backward_compatibility_tests.py index 6e44ab796..7b635283e 100644 --- a/tests/backward_compatibility/tabor_backward_compatibility_tests.py +++ b/tests/backward_compatibility/tabor_backward_compatibility_tests.py @@ -7,11 +7,9 @@ import warnings try: - import tabor_control -except ImportError as err: - raise unittest.SkipTest("tabor_control not present") from err - -from tests.hardware.tabor_simulator_based_tests import TaborSimulatorManager + from tests.hardware.tabor_simulator_based_tests import TaborSimulatorManager +except ImportError: + TaborSimulatorManager = None from tests.hardware.dummy_devices import DummyDAC from tests.backward_compatibility.hardware_test_helper import LoadingAndSequencingHelper @@ -102,6 +100,7 @@ def read_program(self): return self.program_AB, self.program_CD +@unittest.skipIf(tabor_control is None, "tabor_control not available") class CompleteIntegrationTestHelper(unittest.TestCase): data_folder = None pulse_name = None diff --git a/tests/hardware/tabor_simulator_based_tests.py b/tests/hardware/tabor_simulator_based_tests.py index 97a715424..ace3b2fbc 100644 --- a/tests/hardware/tabor_simulator_based_tests.py +++ b/tests/hardware/tabor_simulator_based_tests.py @@ -7,12 +7,16 @@ try: import pyvisa.resources import tabor_control -except ImportError as err: - raise unittest.SkipTest("pyvisa and/or tabor_control not present") from err +except ImportError: + tabor_control = None + pyvisa = None import numpy as np -from qupulse.hardware.awgs.tabor import TaborAWGRepresentation, TaborChannelPair +try: + from qupulse.hardware.awgs.tabor import TaborAWGRepresentation, TaborChannelPair +except ImportError: + pass from qupulse._program.tabor import TaborSegment, PlottableProgram, TaborException, TableDescription, TableEntry from typing import List, Tuple, Optional, Any @@ -48,7 +52,7 @@ def kill_running_simulators(self): def simulator_full_path(self): return os.path.join(self.simulator_path, self.simulator_executable) - def start_simulator(self, try_connecting_to_existing_simulator=True, max_wait_time=30) -> pyvisa.resources.MessageBasedResource: + def start_simulator(self, try_connecting_to_existing_simulator=True, max_wait_time=30) -> 'pyvisa.resources.MessageBasedResource': try: pyvisa.ResourceManager() except ValueError: @@ -95,7 +99,7 @@ def __del__(self): self.simulator_process.kill() -@unittest.skipIf(platform.system() != 'Windows', "Simulator currently only available on Windows :(") +@unittest.skipIf(tabor_control is None or platform.system() != 'Windows', "Simulator currently only available on Windows :(") class TaborSimulatorBasedTest(unittest.TestCase): simulator_manager = None From e9bdcf590ff24ab662e44a8ee218df3b0731f202 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 14 Mar 2024 17:17:08 +0100 Subject: [PATCH 163/441] Catch deprecation warnings --- tests/backward_compatibility/hardware_test_helper.py | 7 +++++-- ...nst_charge_scan_test.py => zhinst_charge_scan_tests.py} | 0 2 files changed, 5 insertions(+), 2 deletions(-) rename tests/backward_compatibility/{zhinst_charge_scan_test.py => zhinst_charge_scan_tests.py} (100%) diff --git a/tests/backward_compatibility/hardware_test_helper.py b/tests/backward_compatibility/hardware_test_helper.py index 1314283c2..7a1b4da88 100644 --- a/tests/backward_compatibility/hardware_test_helper.py +++ b/tests/backward_compatibility/hardware_test_helper.py @@ -4,6 +4,7 @@ import typing import importlib.util import sys +import warnings from qupulse.serialization import Serializer, FilesystemBackend, PulseStorage from qupulse.pulses.pulse_template import PulseTemplate @@ -54,8 +55,10 @@ def load_function_from_file(self, file_name, function_name): return getattr(module, function_name, None) def deserialize_pulse(self): - serializer = Serializer(FilesystemBackend(os.path.join(self.data_folder, 'pulse_storage'))) - self.pulse = typing.cast(PulseTemplate, serializer.deserialize(self.pulse_name)) + with warnings.catch_warnings(): + warnings.simplefilter('ignore', category=DeprecationWarning) + serializer = Serializer(FilesystemBackend(os.path.join(self.data_folder, 'pulse_storage'))) + self.pulse = typing.cast(PulseTemplate, serializer.deserialize(self.pulse_name)) def deserialize_pulse_2018(self) -> None: pulse_storage = PulseStorage(FilesystemBackend(os.path.join(self.data_folder, 'pulse_storage_converted_2018'))) diff --git a/tests/backward_compatibility/zhinst_charge_scan_test.py b/tests/backward_compatibility/zhinst_charge_scan_tests.py similarity index 100% rename from tests/backward_compatibility/zhinst_charge_scan_test.py rename to tests/backward_compatibility/zhinst_charge_scan_tests.py From 46b487016939bd4cb71656c07b515503c3e47242 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 14 Mar 2024 17:19:49 +0100 Subject: [PATCH 164/441] Remove zihdawg tests --- tests/hardware/zihdawg_tests.py | 217 -------------------------------- 1 file changed, 217 deletions(-) delete mode 100644 tests/hardware/zihdawg_tests.py diff --git a/tests/hardware/zihdawg_tests.py b/tests/hardware/zihdawg_tests.py deleted file mode 100644 index a9b619cc5..000000000 --- a/tests/hardware/zihdawg_tests.py +++ /dev/null @@ -1,217 +0,0 @@ -import unittest -from unittest import mock -from collections import OrderedDict - -import numpy as np - -try: - import pytest -except ImportError: - pytest = None - -if pytest: - zhinst = pytest.importorskip("zhinst") - - try: - import zhinst.core as zhinst_core - except ImportError: - import zhinst.ziPython as zhinst_core -else: - try: - try: - import zhinst.core as zhinst_core - except ImportError: - import zhinst.ziPython as zhinst_core - except ImportError as err: - raise unittest.SkipTest("zhinst not present") from err - -from qupulse.utils.types import TimeType -from qupulse.program.loop import Loop -from tests.pulses.sequencing_dummies import DummyWaveform -from qupulse.hardware.awgs.zihdawg import HDAWGChannelGroup, HDAWGRepresentation, HDAWGValueError, UserRegister,\ - ELFManager, HDAWGChannelGrouping, SingleDeviceChannelGroup - - -class HDAWGRepresentationTests(unittest.TestCase): - def test_init(self): - """We do not test anything lab one related""" - device_serial = 'dev6ä6ä6' - device_interface = 'telepathy' - data_server_addr = 'asd' - data_server_port = 42 - api_level_number = 23 - channel_grouping = HDAWGChannelGrouping.CHAN_GROUP_1x8 - - with \ - mock.patch('zhinst.utils.api_server_version_check') as mock_version_check,\ - mock.patch.object(zhinst_core, 'ziDAQServer') as mock_daq_server, \ - mock.patch('qupulse.hardware.awgs.zihdawg.HDAWGRepresentation._initialize') as mock_init, \ - mock.patch('qupulse.hardware.awgs.zihdawg.HDAWGRepresentation.channel_grouping', new_callable=mock.PropertyMock) as mock_grouping, \ - mock.patch('qupulse.hardware.awgs.zihdawg.SingleDeviceChannelGroup') as mock_channel_pair,\ - mock.patch('zhinst.utils.disable_everything') as mock_reset,\ - mock.patch('pathlib.Path') as mock_path: - - representation = HDAWGRepresentation(device_serial, - device_interface, - data_server_addr, data_server_port, api_level_number, - False, 1.3, grouping=channel_grouping) - - mock_daq_server.return_value.awgModule.return_value.getString.assert_called_once_with('directory') - module_dir = mock_daq_server.return_value.awgModule.return_value.getString.return_value - mock_path.assert_called_once_with(module_dir, 'awg', 'waves') - - self.assertIs(representation.api_session, mock_daq_server.return_value) - mock_daq_server.assert_called_once_with(data_server_addr, data_server_port, api_level_number) - - mock_version_check.assert_called_once_with(representation.api_session) - representation.api_session.connectDevice.assert_called_once_with(device_serial, device_interface) - self.assertEqual(device_serial, representation.serial) - - mock_grouping.assert_called_once_with(channel_grouping) - - mock_reset.assert_not_called() - mock_init.assert_called_once_with() - - group_calls = [mock.call(0, 2, identifier=str(device_serial) + '_AB', timeout=1.3), - mock.call(1, 2, identifier=str(device_serial) + '_CD', timeout=1.3), - mock.call(2, 2, identifier=str(device_serial) + '_EF', timeout=1.3), - mock.call(3, 2, identifier=str(device_serial) + '_GH', timeout=1.3), - mock.call(0, 4, identifier=str(device_serial) + '_ABCD', timeout=1.3), - mock.call(1, 4, identifier=str(device_serial) + '_EFGH', timeout=1.3), - mock.call(0, 8, identifier=str(device_serial) + '_ABCDEFGH', timeout=1.3)] - for c1, c2 in zip(group_calls, mock_channel_pair.call_args_list): - self.assertEqual(c1, c2) - - self.assertIs(representation.channel_pair_AB, mock_channel_pair.return_value) - self.assertIs(representation.channel_pair_CD, mock_channel_pair.return_value) - self.assertIs(representation.channel_pair_EF, mock_channel_pair.return_value) - self.assertIs(representation.channel_pair_GH, mock_channel_pair.return_value) - - mock_version_check.reset_mock() - mock_daq_server.reset_mock() - mock_init.reset_mock() - mock_channel_pair.reset_mock() - mock_reset.reset_mock() - - representation = HDAWGRepresentation(device_serial, - device_interface, - data_server_addr, data_server_port, api_level_number, True) - - self.assertIs(representation.api_session, mock_daq_server.return_value) - mock_daq_server.assert_called_once_with(data_server_addr, data_server_port, api_level_number) - - mock_version_check.assert_called_once_with(representation.api_session) - representation.api_session.connectDevice.assert_called_once_with(device_serial, device_interface) - self.assertEqual(device_serial, representation.serial) - - mock_reset.assert_called_once_with(representation.api_session, representation.serial) - mock_init.assert_called_once_with() - - group_calls = [mock.call(0, 2, identifier=str(device_serial) + '_AB', timeout=20), - mock.call(1, 2, identifier=str(device_serial) + '_CD', timeout=20), - mock.call(2, 2, identifier=str(device_serial) + '_EF', timeout=20), - mock.call(3, 2, identifier=str(device_serial) + '_GH', timeout=20), - mock.call(0, 4, identifier=str(device_serial) + '_ABCD', timeout=20), - mock.call(1, 4, identifier=str(device_serial) + '_EFGH', timeout=20), - mock.call(0, 8, identifier=str(device_serial) + '_ABCDEFGH', timeout=20)] - self.assertEqual(group_calls, mock_channel_pair.call_args_list) - - self.assertIs(representation.channel_pair_AB, mock_channel_pair.return_value) - self.assertIs(representation.channel_pair_CD, mock_channel_pair.return_value) - self.assertIs(representation.channel_pair_EF, mock_channel_pair.return_value) - self.assertIs(representation.channel_pair_GH, mock_channel_pair.return_value) - - -class HDAWGChannelGroupTests(unittest.TestCase): - def test_init(self): - with mock.patch('weakref.proxy') as proxy_mock: - mock_device = mock.Mock() - - channels = (3, 4) - awg_group_idx = 1 - - channel_pair = SingleDeviceChannelGroup(awg_group_idx, 2, 'foo', 3.4) - - self.assertEqual(channel_pair.timeout, 3.4) - self.assertEqual(channel_pair._channels(), channels) - self.assertEqual(channel_pair.awg_group_index, awg_group_idx) - self.assertEqual(channel_pair.num_channels, 2) - self.assertEqual(channel_pair.num_markers, 4) - - self.assertFalse(channel_pair.is_connected()) - - proxy_mock.return_value.channel_grouping = HDAWGChannelGrouping.CHAN_GROUP_4x2 - - channel_pair.connect_group(mock_device) - self.assertTrue(channel_pair.is_connected()) - proxy_mock.assert_called_once_with(mock_device) - self.assertIs(channel_pair.master_device, proxy_mock.return_value) - self.assertIs(channel_pair.awg_module, channel_pair.master_device.api_session.awgModule.return_value) - - def test_set_volatile_parameters(self): - mock_device = mock.Mock() - - parameters = {'a': 9} - requested_changes = OrderedDict([(UserRegister.from_seqc(4), 2), (UserRegister.from_seqc(3), 6)]) - - expected_user_reg_calls = [mock.call(*args) for args in requested_changes.items()] - - channel_pair = SingleDeviceChannelGroup(1, 2, 'foo', 3.4) - - channel_pair._current_program = 'active_program' - with mock.patch.object(channel_pair._program_manager, 'get_register_values_to_update_volatile_parameters', - return_value=requested_changes) as get_reg_val: - with mock.patch.object(channel_pair, 'user_register') as user_register: - channel_pair.set_volatile_parameters('other_program', parameters) - - user_register.assert_not_called() - get_reg_val.assert_called_once_with('other_program', parameters) - - with mock.patch.object(channel_pair._program_manager, 'get_register_values_to_update_volatile_parameters', - return_value=requested_changes) as get_reg_val: - with mock.patch.object(channel_pair, 'user_register') as user_register: - channel_pair.set_volatile_parameters('active_program', parameters) - - self.assertEqual(expected_user_reg_calls, user_register.call_args_list) - get_reg_val.assert_called_once_with('active_program', parameters) - - def test_upload(self): - mock_loop = mock.MagicMock(wraps=Loop(repetition_count=2, - waveform=DummyWaveform(duration=192, - sample_output=np.arange(192) / 192))) - - voltage_trafos = (lambda x: x, lambda x: x) - - with mock.patch('weakref.proxy'),\ - mock.patch('qupulse.hardware.awgs.zihdawg.make_compatible') as mock_make_compatible: - channel_pair = SingleDeviceChannelGroup(1, 2, 'foo', 3.4) - - with self.assertRaisesRegex(HDAWGValueError, 'Channel ID'): - channel_pair.upload('bar', mock_loop, ('A'), (None, 'A', None, None), voltage_trafos) - with self.assertRaisesRegex(HDAWGValueError, 'Markers'): - channel_pair.upload('bar', mock_loop, ('A', None), (None, 'A', None), voltage_trafos) - with self.assertRaisesRegex(HDAWGValueError, 'transformations'): - channel_pair.upload('bar', mock_loop, ('A', None), (None, 'A', None, None), voltage_trafos[:1]) - - # TODO: draw the rest of the owl - - -@mock.patch('qupulse.hardware.awgs.zihdawg.ELFManager.AWGModule.compiler_upload', new_callable=mock.PropertyMock) -class ELFManagerTests(unittest.TestCase): - def test_init(self, compiler_upload): - manager = ELFManager.DEFAULT_CLS(None) - compiler_upload.assert_called_once_with(True) - self.assertIsNone(manager._compile_job) - self.assertIsNone(manager._upload_job) - - @unittest.skip("Write test after more hardware tests") - def test_upload(self, compiler_upload): - raise NotImplementedError() - - @unittest.skip("Write test after more hardware tests") - def test_update_compile_job_status(self, compiler_upload): - raise NotImplementedError() - - @unittest.skip("Write test after more hardware tests") - def test_compile(self, compiler_upload): - raise NotImplementedError() From 62d92cf893473e2c99ba05aa20877f26dcd5188b Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 14 Mar 2024 17:52:30 +0100 Subject: [PATCH 165/441] Move deprecation warning catch --- tests/backward_compatibility/hardware_test_helper.py | 6 ++---- tests/backward_compatibility/zhinst_charge_scan_tests.py | 3 ++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/backward_compatibility/hardware_test_helper.py b/tests/backward_compatibility/hardware_test_helper.py index 7a1b4da88..2f2cef447 100644 --- a/tests/backward_compatibility/hardware_test_helper.py +++ b/tests/backward_compatibility/hardware_test_helper.py @@ -55,10 +55,8 @@ def load_function_from_file(self, file_name, function_name): return getattr(module, function_name, None) def deserialize_pulse(self): - with warnings.catch_warnings(): - warnings.simplefilter('ignore', category=DeprecationWarning) - serializer = Serializer(FilesystemBackend(os.path.join(self.data_folder, 'pulse_storage'))) - self.pulse = typing.cast(PulseTemplate, serializer.deserialize(self.pulse_name)) + serializer = Serializer(FilesystemBackend(os.path.join(self.data_folder, 'pulse_storage'))) + self.pulse = typing.cast(PulseTemplate, serializer.deserialize(self.pulse_name)) def deserialize_pulse_2018(self) -> None: pulse_storage = PulseStorage(FilesystemBackend(os.path.join(self.data_folder, 'pulse_storage_converted_2018'))) diff --git a/tests/backward_compatibility/zhinst_charge_scan_tests.py b/tests/backward_compatibility/zhinst_charge_scan_tests.py index 93c0c8038..de932f8b1 100644 --- a/tests/backward_compatibility/zhinst_charge_scan_tests.py +++ b/tests/backward_compatibility/zhinst_charge_scan_tests.py @@ -100,7 +100,8 @@ def setUpClass(cls): cls.test_state = HDAWGLoadingAndSequencingHelper(cls.data_folder, cls.pulse_name) def test_1_1_deserialization(self): - self.test_state.deserialize_pulse() + with self.assertWarns(DeprecationWarning): + self.test_state.deserialize_pulse() def test_1_2_deserialization_2018(self) -> None: self.test_state.deserialize_pulse_2018() From 7d2749501bedb8e7a595366b0cc3ddcdbeca26bc Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 15 Mar 2024 16:02:38 +0100 Subject: [PATCH 166/441] Make SimpleExpression more expression conformant --- qupulse/program/__init__.py | 44 +++++++++++++++++++++++++++---------- qupulse/program/linspace.py | 2 +- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/qupulse/program/__init__.py b/qupulse/program/__init__.py index 117c7301c..8ab5137b3 100644 --- a/qupulse/program/__init__.py +++ b/qupulse/program/__init__.py @@ -1,16 +1,17 @@ import contextlib from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import Optional, Union, Sequence, ContextManager, Mapping, Tuple, Generic, TypeVar, Iterable -from numbers import Real +from typing import Optional, Union, Sequence, ContextManager, Mapping, Tuple, Generic, TypeVar, Iterable, Dict +from numbers import Real, Number import numpy as np from qupulse._program.waveforms import Waveform -from qupulse.utils.types import MeasurementWindow, TimeType +from qupulse.utils.types import MeasurementWindow, TimeType, FrozenMapping from qupulse._program.volatile import VolatileRepetitionCount from qupulse.parameter_scope import Scope -from qupulse.expressions import sympy as sym_expr +from qupulse.expressions import sympy as sym_expr, Expression +from qupulse.utils.sympy import _lambdify_modules from typing import Protocol, runtime_checkable @@ -30,7 +31,7 @@ class SimpleExpression(Generic[NumVal]): """ base: NumVal - offsets: Sequence[Tuple[str, NumVal]] + offsets: Dict[str, NumVal] def value(self, scope: Mapping[str, NumVal]) -> NumVal: value = self.base @@ -43,7 +44,10 @@ def __add__(self, other): return SimpleExpression(self.base + other, self.offsets) if type(other) == type(self): - return SimpleExpression(self.base + other.base, self.offsets + other.offsets) + offsets = self.offsets.copy() + for name, value in other.offsets.items(): + offsets[name] = value + offsets.get(name, 0) + return SimpleExpression(self.base + other.base, offsets) return NotImplemented @@ -57,22 +61,40 @@ def __rsub__(self, other): (-self).__add__(other) def __neg__(self): - return SimpleExpression(-self.base, tuple((name, -value) for name, value in self.offsets)) + return SimpleExpression(-self.base, {name: -value for name, value in self.offsets.items()}) def __mul__(self, other: NumVal): - if isinstance(other, SimpleExpression): - return NotImplemented - return SimpleExpression(self.base * other, tuple((name, value * other) for name, value in self.offsets)) + if isinstance(other, (float, int, TimeType)): + return SimpleExpression(self.base * other, {name: other * value for name, value in self.offsets.items()}) + + return NotImplemented def __rmul__(self, other): return self.__mul__(other) - def evaluate_in_scope(self, *args, **kwargs): + def __truediv__(self, other): + inv = 1 / other + return self.__mul__(inv) + + @property + def free_symbols(self): + return () + + def _sympy_(self): + return self + + def replace(self, r, s): + return self + + def evaluate_in_scope_(self, *args, **kwargs): # TODO: remove. It is currently required to avoid nesting this class in an expression for the MappedScope # We can maybe replace is with a HardwareScope or something along those lines return self +_lambdify_modules.append({'SimpleExpression': SimpleExpression}) + + RepetitionCount = Union[int, VolatileRepetitionCount, SimpleExpression[int]] HardwareTime = Union[TimeType, SimpleExpression[TimeType]] HardwareVoltage = Union[float, SimpleExpression[float]] diff --git a/qupulse/program/linspace.py b/qupulse/program/linspace.py index 6e54392e8..27b2b264d 100644 --- a/qupulse/program/linspace.py +++ b/qupulse/program/linspace.py @@ -123,7 +123,7 @@ def inner_scope(self, scope: Scope) -> Scope: process.""" if self._ranges: name, _ = self._ranges[-1] - return MappedScope(scope, FrozenDict({name: SimpleExpression(base=0, offsets=[(name, 1)])})) + return scope.overwrite({name: SimpleExpression(base=0, offsets=[(name, 1)])}) else: return scope From 7d110edb28b85c279a28a417a7bcdd9a4ac20fd1 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 15 Mar 2024 16:11:22 +0100 Subject: [PATCH 167/441] Move DummyAWG and DummyDAC into main package --- qupulse/hardware/awgs/dummy.py | 74 ++++++++++++++++++ qupulse/hardware/dacs/dummy.py | 44 +++++++++++ tests/hardware/dummy_devices.py | 128 +------------------------------- 3 files changed, 120 insertions(+), 126 deletions(-) create mode 100644 qupulse/hardware/awgs/dummy.py create mode 100644 qupulse/hardware/dacs/dummy.py diff --git a/qupulse/hardware/awgs/dummy.py b/qupulse/hardware/awgs/dummy.py new file mode 100644 index 000000000..a132f958f --- /dev/null +++ b/qupulse/hardware/awgs/dummy.py @@ -0,0 +1,74 @@ +from .base import AWG + +class DummyAWG(AWG): + """Dummy AWG for debugging purposes.""" + + def __init__(self, + sample_rate: float=10, + output_range: Tuple[float, float]=(-5, 5), + num_channels: int=1, + num_markers: int=1) -> None: + """Create a new DummyAWG instance. + + Args: + memory (int): Available memory slots for waveforms. (default = 100) + sample_rate (float): The sample rate of the dummy. (default = 10) + output_range (float, float): A (min,max)-tuple of possible output values. + (default = (-5,5)). + """ + super().__init__(identifier="DummyAWG{0}".format(id(self))) + + self._programs = {} # contains program names and programs + self._sample_rate = sample_rate + self._output_range = output_range + self._num_channels = num_channels + self._num_markers = num_markers + self._channels = ('default',) + self._armed = None + + def set_volatile_parameters(self, program_name: str, parameters): + raise NotImplementedError() + + def upload(self, name, program, channels, markers, voltage_transformation, force=False) -> None: + if name in self.programs: + if not force: + raise ProgramOverwriteException(name) + else: + self.remove(name) + self.upload(name, program) + else: + self._programs[name] = (program, channels, markers, voltage_transformation) + + def remove(self, name) -> None: + if name in self.programs: + self._programs.pop(name) + + def clear(self) -> None: + self._programs = {} + + def arm(self, name: str) -> None: + self._armed = name + + @property + def programs(self) -> Set[str]: + return set(self._programs.keys()) + + @property + def output_range(self) -> Tuple[float, float]: + return self._output_range + + @property + def identifier(self) -> str: + return "DummyAWG{0}".format(id(self)) + + @property + def sample_rate(self) -> float: + return self._sample_rate + + @property + def num_channels(self): + return self._num_channels + + @property + def num_markers(self): + return self._num_markers diff --git a/qupulse/hardware/dacs/dummy.py b/qupulse/hardware/dacs/dummy.py new file mode 100644 index 000000000..7fba3342d --- /dev/null +++ b/qupulse/hardware/dacs/dummy.py @@ -0,0 +1,44 @@ +from typing import Tuple, Set, Dict +from collections import deque + +from qupulse.hardware.dacs.dac_base import DAC + +class DummyDAC(DAC): + def __init__(self): + self._measurement_windows = dict() + self._operations = dict() + self.measured_data = deque([]) + self._meas_masks = {} + self._armed_program = None + + @property + def armed_program(self): + return self._armed_program + + def register_measurement_windows(self, program_name: str, windows: Dict[str, Tuple['numpy.ndarray', + 'numpy.ndarray']]): + self._measurement_windows[program_name] = windows + + def register_operations(self, program_name: str, operations): + self._operations[program_name] = operations + + def arm_program(self, program_name: str): + self._armed_program = program_name + + def delete_program(self, program_name): + if program_name in self._operations: + self._operations.pop(program_name) + if program_name in self._measurement_windows: + self._measurement_windows.pop(program_name) + + def clear(self) -> None: + self._measurement_windows = dict() + self._operations = dict() + self._armed_program = None + + def measure_program(self, channels): + return self.measured_data.pop() + + def set_measurement_mask(self, program_name, mask_name, begins, lengths) -> Tuple['numpy.ndarray', 'numpy.ndarray']: + self._meas_masks.setdefault(program_name, {})[mask_name] = (begins, lengths) + return begins, lengths diff --git a/tests/hardware/dummy_devices.py b/tests/hardware/dummy_devices.py index a92ce1282..60dee6f9e 100644 --- a/tests/hardware/dummy_devices.py +++ b/tests/hardware/dummy_devices.py @@ -1,126 +1,2 @@ -from typing import Tuple, Set, Dict -from collections import deque - - -from qupulse.hardware.awgs.base import AWG, ProgramOverwriteException -from qupulse.hardware.dacs import DAC - -class DummyDAC(DAC): - def __init__(self): - self._measurement_windows = dict() - self._operations = dict() - self.measured_data = deque([]) - self._meas_masks = {} - self._armed_program = None - - @property - def armed_program(self): - return self._armed_program - - def register_measurement_windows(self, program_name: str, windows: Dict[str, Tuple['numpy.ndarray', - 'numpy.ndarray']]): - self._measurement_windows[program_name] = windows - - def register_operations(self, program_name: str, operations): - self._operations[program_name] = operations - - def arm_program(self, program_name: str): - self._armed_program = program_name - - def delete_program(self, program_name): - if program_name in self._operations: - self._operations.pop(program_name) - if program_name in self._measurement_windows: - self._measurement_windows.pop(program_name) - - def clear(self) -> None: - self._measurement_windows = dict() - self._operations = dict() - self._armed_program = None - - def measure_program(self, channels): - return self.measured_data.pop() - - def set_measurement_mask(self, program_name, mask_name, begins, lengths) -> Tuple['numpy.ndarray', 'numpy.ndarray']: - self._meas_masks.setdefault(program_name, {})[mask_name] = (begins, lengths) - return begins, lengths - - -class DummyAWG(AWG): - """Dummy AWG for debugging purposes.""" - - def __init__(self, - memory: int=100, - sample_rate: float=10, - output_range: Tuple[float, float]=(-5, 5), - num_channels: int=1, - num_markers: int=1) -> None: - """Create a new DummyAWG instance. - - Args: - memory (int): Available memory slots for waveforms. (default = 100) - sample_rate (float): The sample rate of the dummy. (default = 10) - output_range (float, float): A (min,max)-tuple of possible output values. - (default = (-5,5)). - """ - super().__init__(identifier="DummyAWG{0}".format(id(self))) - - self._programs = {} # contains program names and programs - self._sample_rate = sample_rate - self._output_range = output_range - self._num_channels = num_channels - self._num_markers = num_markers - self._channels = ('default',) - self._armed = None - - # todo [2018-06-14]: The following attributes (and thus the memory argument) are never used. Remove? - self._waveform_memory = [None for i in range(memory)] - self._waveform_indices = {} # dict that maps from waveform hash to memory index - self._program_wfs = {} # contains program names and necessary waveforms indices - - def set_volatile_parameters(self, program_name: str, parameters): - raise NotImplementedError() - - def upload(self, name, program, channels, markers, voltage_transformation, force=False) -> None: - if name in self.programs: - if not force: - raise ProgramOverwriteException(name) - else: - self.remove(name) - self.upload(name, program) - else: - self._programs[name] = (program, channels, markers, voltage_transformation) - - def remove(self, name) -> None: - if name in self.programs: - self._programs.pop(name) - - def clear(self) -> None: - self._programs = {} - - def arm(self, name: str) -> None: - self._armed = name - - @property - def programs(self) -> Set[str]: - return set(self._programs.keys()) - - @property - def output_range(self) -> Tuple[float, float]: - return self._output_range - - @property - def identifier(self) -> str: - return "DummyAWG{0}".format(id(self)) - - @property - def sample_rate(self) -> float: - return self._sample_rate - - @property - def num_channels(self): - return self._num_channels - - @property - def num_markers(self): - return self._num_markers \ No newline at end of file +from qupulse.hardware.dacs.dummy import DummyDAC +from qupulse.hardware.awgs.dummy import DummyAWG From ed1f3d34103863515f89c6855339c175450ab10f Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 15 Mar 2024 16:13:35 +0100 Subject: [PATCH 168/441] Document intent --- qupulse/hardware/awgs/dummy.py | 5 +---- qupulse/hardware/dacs/dummy.py | 2 ++ 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/qupulse/hardware/awgs/dummy.py b/qupulse/hardware/awgs/dummy.py index a132f958f..f3199d4de 100644 --- a/qupulse/hardware/awgs/dummy.py +++ b/qupulse/hardware/awgs/dummy.py @@ -1,17 +1,14 @@ from .base import AWG class DummyAWG(AWG): - """Dummy AWG for debugging purposes.""" - def __init__(self, sample_rate: float=10, output_range: Tuple[float, float]=(-5, 5), num_channels: int=1, num_markers: int=1) -> None: - """Create a new DummyAWG instance. + """Dummy AWG for automated testing, debugging and usage in examples. Args: - memory (int): Available memory slots for waveforms. (default = 100) sample_rate (float): The sample rate of the dummy. (default = 10) output_range (float, float): A (min,max)-tuple of possible output values. (default = (-5,5)). diff --git a/qupulse/hardware/dacs/dummy.py b/qupulse/hardware/dacs/dummy.py index 7fba3342d..ba0cf6f51 100644 --- a/qupulse/hardware/dacs/dummy.py +++ b/qupulse/hardware/dacs/dummy.py @@ -4,6 +4,8 @@ from qupulse.hardware.dacs.dac_base import DAC class DummyDAC(DAC): + """Dummy DAC for automated testing, debugging and usage in examples. """ + def __init__(self): self._measurement_windows = dict() self._operations = dict() From 9258399091cca1e67c318ab8efbab68892737090 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 15 Mar 2024 16:16:14 +0100 Subject: [PATCH 169/441] Missing imports --- qupulse/hardware/awgs/dummy.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qupulse/hardware/awgs/dummy.py b/qupulse/hardware/awgs/dummy.py index f3199d4de..93ef38343 100644 --- a/qupulse/hardware/awgs/dummy.py +++ b/qupulse/hardware/awgs/dummy.py @@ -1,3 +1,5 @@ +from typing import Tuple, Set + from .base import AWG class DummyAWG(AWG): From 1e0c43700a1184946cf76c957d48fdf67d7d3027 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 15 Mar 2024 17:00:17 +0100 Subject: [PATCH 170/441] Fix some runtime errors --- qupulse/hardware/awgs/dummy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qupulse/hardware/awgs/dummy.py b/qupulse/hardware/awgs/dummy.py index 93ef38343..249497483 100644 --- a/qupulse/hardware/awgs/dummy.py +++ b/qupulse/hardware/awgs/dummy.py @@ -1,6 +1,6 @@ from typing import Tuple, Set -from .base import AWG +from .base import AWG, ProgramOverwriteException class DummyAWG(AWG): def __init__(self, @@ -34,7 +34,7 @@ def upload(self, name, program, channels, markers, voltage_transformation, force raise ProgramOverwriteException(name) else: self.remove(name) - self.upload(name, program) + self.upload(name, program, channels, markers, voltage_transformation) else: self._programs[name] = (program, channels, markers, voltage_transformation) From ea48f8e87d13c786f8444342431de4b87e8555d8 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 15 Mar 2024 17:31:04 +0100 Subject: [PATCH 171/441] Add minimal documentation --- changes.d/781.feature | 1 + doc/source/concepts/program.rst | 10 ++++++++-- qupulse/program/__init__.py | 6 ++---- qupulse/pulses/pulse_template.py | 6 ++++-- 4 files changed, 15 insertions(+), 8 deletions(-) create mode 100644 changes.d/781.feature diff --git a/changes.d/781.feature b/changes.d/781.feature new file mode 100644 index 000000000..697299436 --- /dev/null +++ b/changes.d/781.feature @@ -0,0 +1 @@ +Add the `ProgramBuilder` interface pattern to make the generated program of `PulseTemplate.create_program` easily customizable. \ No newline at end of file diff --git a/doc/source/concepts/program.rst b/doc/source/concepts/program.rst index 3d4172bc5..82208ac15 100644 --- a/doc/source/concepts/program.rst +++ b/doc/source/concepts/program.rst @@ -6,8 +6,11 @@ Instantiated Pulse: Program In qupulse an instantiated pulse template is called a program as it is something that an arbitrary waveform generator (AWG) can execute/playback. It is created by the ``create_program`` method of the pulse template which returns a hardware -independent representation which is of type ``Loop``. -This ``Loop`` object is the root node of a tree ``Loop``s of arbitrary depth. +independent representation which is of type ``Loop`` by default. The method takes a ``program_builder`` keyword argument +which is passed through the pulse template tree and thereby implements the visitor pattern. If the argument is not +passed ``default_program_builder()`` is used instead which is ``LoopBuilder`` by default. + +The ``Loop`` default program is the root node of a tree ``Loop``s of arbitrary depth. Each node consists of a repetition count and either a waveform or a sequence of nodes which are repeated that many times. Iterations like the ```ForLoopPT`` cannot be represented natively but are unrolled into a sequence of items. The repetition count is currently the only property of a program that can be defined as volatile. This means that the AWG driver tries to upload the program in a way, where the repetition count can quickly be changed. This is implemented via the ```VolatileRepetitionCount`` class. @@ -20,3 +23,6 @@ The dimensions are named by the channel names. The ``Loop`` class and its constituents ``Waveform`` and ``VolatileRepetitionCount`` are defined in the ``qupulse.program`` subpackage and it's submodules. The private subpackage ``qupulse._program`` contains AWG driver internals that can change with any release, for example a transpiler to Zurich Instruments sequencing C in ``qupulse._program.seqc``. + +Another program format that is currently under development is ``LinSpaceProgram`` which efficiently encodes linearly +spaced sweeps in voltage space. However, the status of this is preliminary and not yet documented here. diff --git a/qupulse/program/__init__.py b/qupulse/program/__init__.py index 117c7301c..b5040676a 100644 --- a/qupulse/program/__init__.py +++ b/qupulse/program/__init__.py @@ -88,13 +88,11 @@ def duration(self) -> TimeType: class ProgramBuilder(Protocol): - """This protocol is used by PulseTemplate to build the program. + """This protocol is used by PulseTemplate to build the program via the visitor pattern. There is a default implementation which is the loop class. - Other hardware backends can use this protocol to implement easy translation of pulse templates. - - """ + Other hardware backends can use this protocol to implement easy translation of pulse templates.""" def inner_scope(self, scope: Scope) -> Scope: """This function is necessary to inject program builder specific parameter implementations into the build diff --git a/qupulse/pulses/pulse_template.py b/qupulse/pulses/pulse_template.py index 171ca17fc..97dd5cda4 100644 --- a/qupulse/pulses/pulse_template.py +++ b/qupulse/pulses/pulse_template.py @@ -27,7 +27,7 @@ from qupulse.pulses.measurement import MeasurementDefiner, MeasurementDeclaration from qupulse.parameter_scope import Scope, DictScope -from qupulse.program import ProgramBuilder, default_program_builder +from qupulse.program import ProgramBuilder, default_program_builder, Program __all__ = ["PulseTemplate", "AtomicPulseTemplate", "DoubleParameterNameException", "MappingTuple", "UnknownVolatileParameter"] @@ -127,7 +127,7 @@ def create_program(self, *, global_transformation: Optional[Transformation]=None, to_single_waveform: Set[Union[str, 'PulseTemplate']]=None, volatile: Union[Set[str], str] = None, - program_builder: ProgramBuilder = None) -> Optional['Loop']: + program_builder: ProgramBuilder = None) -> Optional[Program]: """Translates this PulseTemplate into a program Loop. The returned Loop represents the PulseTemplate with all parameter values instantiated provided as dictated by @@ -142,6 +142,8 @@ def create_program(self, *, to_single_waveform: A set of pulse templates (or identifiers) which are directly translated to a waveform. This might change how transformations are applied. TODO: clarify volatile: Everything in the final program that depends on these parameters is marked as volatile + program_builder: This program builder is used to build the return value. If `None` `default_program_builder` + is used. Returns: A Loop object corresponding to this PulseTemplate. """ From dbc0db4bf609e4c955d791d464cf3a230b3836b7 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Thu, 28 Mar 2024 11:24:20 +0100 Subject: [PATCH 172/441] Avoid warning on interactive plotting in Jupyter --- qupulse/plotting.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qupulse/plotting.py b/qupulse/plotting.py index 9810fb6ae..600087ed9 100644 --- a/qupulse/plotting.py +++ b/qupulse/plotting.py @@ -257,6 +257,7 @@ def plot(pulse: PulseTemplate, with warnings.catch_warnings(): # do not show warnings in jupyter notebook with matplotlib inline backend warnings.filterwarnings(action="ignore",message=".*which is a non-GUI backend, so cannot show the figure.*") + warnings.filterwarnings(action="ignore",message=".*is non-interactive, and thus cannot be shown.*") axes.get_figure().show() return axes.get_figure() From 108c28d021d3726fe06650748f648d967f96702b Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 4 Apr 2024 11:07:58 +0200 Subject: [PATCH 173/441] Make overlapping windows a default error upon import and extend tests --- qupulse/utils/performance.py | 12 +++++-- tests/utils/performance_tests.py | 55 ++++++++++++++++++++++++-------- 2 files changed, 51 insertions(+), 16 deletions(-) diff --git a/qupulse/utils/performance.py b/qupulse/utils/performance.py index 6159b21a5..f68737a06 100644 --- a/qupulse/utils/performance.py +++ b/qupulse/utils/performance.py @@ -59,7 +59,15 @@ def _shrink_overlapping_windows_numba(begins, lengths) -> bool: class WindowOverlapWarning(RuntimeWarning): - pass + COMMENT = (" This warning is an error by default. " + "Call 'warnings.simplefilter(WindowOverlapWarning, \"always\")' " + "to demote it to a regular warning.") + + def __str__(self): + return super().__str__() + self.COMMENT + + +warnings.simplefilter(category=WindowOverlapWarning, action='error') def shrink_overlapping_windows(begins, lengths, use_numba: bool = numba is not None) -> Tuple[np.array, np.array]: @@ -78,7 +86,7 @@ def shrink_overlapping_windows(begins, lengths, use_numba: bool = numba is not N begins = begins.copy() lengths = lengths.copy() if backend(begins, lengths): - warnings.warn("Found overlapping measurement windows which are automatically shrunken if possible.", + warnings.warn("Found overlapping measurement windows which can be automatically shrunken if possible.", category=WindowOverlapWarning) return begins, lengths diff --git a/tests/utils/performance_tests.py b/tests/utils/performance_tests.py index 9a75485f7..736bb3227 100644 --- a/tests/utils/performance_tests.py +++ b/tests/utils/performance_tests.py @@ -1,11 +1,12 @@ import unittest +import warnings import numpy as np -from qupulse.utils.performance import (_time_windows_to_samples_numba, _time_windows_to_samples_numpy, - shrink_overlapping_windows) -from qupulse.utils.performance import (_time_windows_to_samples_numba, _time_windows_to_samples_numpy, - _average_windows_numba, _average_windows_numpy, average_windows) +from qupulse.utils.performance import ( + _time_windows_to_samples_numba, _time_windows_to_samples_numpy, + _average_windows_numba, _average_windows_numpy, average_windows, + shrink_overlapping_windows, WindowOverlapWarning) class TimeWindowsToSamplesTest(unittest.TestCase): @@ -57,17 +58,43 @@ def test_single_channel(self): def test_dual_channel(self): self.assert_implementations_equal(self.time, self.values, self.begins, self.ends) + + class TestOverlappingWindowReduction(unittest.TestCase): + def setUp(self): + self.shrank = np.array([1, 4, 8]), np.array([3, 4, 4]) + self.to_shrink = np.array([1, 4, 7]), np.array([3, 4, 5]) + + def assert_noop(self, shrink_fn): + begins = np.array([1, 3, 5]) + lengths = np.array([2, 1, 6]) + result = shrink_fn(begins, lengths) + np.testing.assert_equal((begins, lengths), result) + + def assert_shrinks(self, shrink_fn): + with warnings.catch_warnings(): + warnings.simplefilter("always", WindowOverlapWarning) + with self.assertWarns(WindowOverlapWarning): + shrank = shrink_fn(*self.to_shrink) + np.testing.assert_equal(self.shrank, shrank) + + def assert_empty_window_error(self, shrink_fn): + invalid = np.array([1, 2]), np.array([5, 1]) + with self.assertRaisesRegex(ValueError, "Overlap is bigger than measurement window"): + shrink_fn(*invalid) + def test_shrink_overlapping_windows_numba(self): - np.testing.assert_equal( - (np.array([1, 4, 8]), np.array([3, 4, 4])), - shrink_overlapping_windows(np.array([1, 4, 7]), - np.array([3, 4, 5]), use_numba=True) - ) + def shrink_fn(begins, lengths): + return shrink_overlapping_windows(begins, lengths, use_numba=True) + + self.assert_noop(shrink_fn) + self.assert_shrinks(shrink_fn) + self.assert_empty_window_error(shrink_fn) def test_shrink_overlapping_windows_numpy(self): - np.testing.assert_equal( - (np.array([1, 4, 8]), np.array([3, 4, 4])), - shrink_overlapping_windows(np.array([1, 4, 7]), - np.array([3, 4, 5]), use_numba=False) - ) \ No newline at end of file + def shrink_fn(begins, lengths): + return shrink_overlapping_windows(begins, lengths, use_numba=False) + + self.assert_noop(shrink_fn) + self.assert_shrinks(shrink_fn) + self.assert_empty_window_error(shrink_fn) From a5092dad629db95a029d9e099d38dafd6cbf5ee1 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 4 Apr 2024 11:53:11 +0200 Subject: [PATCH 174/441] Fix most tests --- qupulse/utils/performance.py | 12 ++++++++---- tests/utils/performance_tests.py | 20 +++++++++++++++----- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/qupulse/utils/performance.py b/qupulse/utils/performance.py index f68737a06..ebdb3b0a8 100644 --- a/qupulse/utils/performance.py +++ b/qupulse/utils/performance.py @@ -26,16 +26,20 @@ def _is_monotonic_numpy(arr: np.ndarray) -> bool: def _shrink_overlapping_windows_numpy(begins, lengths) -> bool: + supported_dtypes = ('int64', 'uint64') + if begins.dtype.name not in supported_dtypes or lengths.dtype.name not in supported_dtypes: + raise NotImplementedError("This function only supports 64 bit integer types yet.") + ends = begins + lengths - overlaps = np.zeros_like(ends) - np.maximum(ends[:-1] - begins[1:], 0, out=overlaps[1:]) + overlaps = np.zeros_like(ends, dtype=np.int64) + np.maximum(ends[:-1].view(np.int64) - begins[1:].view(np.int64), 0, out=overlaps[1:]) if np.any(overlaps >= lengths): raise ValueError("Overlap is bigger than measurement window") if np.any(overlaps > 0): - begins += overlaps - lengths -= overlaps + begins += overlaps.view(begins.dtype) + lengths -= overlaps.view(lengths.dtype) return True return False diff --git a/tests/utils/performance_tests.py b/tests/utils/performance_tests.py index 736bb3227..bc31960ae 100644 --- a/tests/utils/performance_tests.py +++ b/tests/utils/performance_tests.py @@ -62,12 +62,22 @@ def test_dual_channel(self): class TestOverlappingWindowReduction(unittest.TestCase): def setUp(self): - self.shrank = np.array([1, 4, 8]), np.array([3, 4, 4]) - self.to_shrink = np.array([1, 4, 7]), np.array([3, 4, 5]) + self.shrank = np.array([1, 4, 8], dtype=np.uint64), np.array([3, 4, 4], dtype=np.uint64) + self.to_shrink = np.array([1, 4, 7], dtype=np.uint64), np.array([3, 4, 5], dtype=np.uint64) def assert_noop(self, shrink_fn): - begins = np.array([1, 3, 5]) - lengths = np.array([2, 1, 6]) + begins = np.array([1, 3, 5], dtype=np.uint64) + lengths = np.array([2, 1, 6], dtype=np.uint64) + result = shrink_fn(begins, lengths) + np.testing.assert_equal((begins, lengths), result) + + begins = (np.arange(100) * 176.5).astype(dtype=np.uint64) + lengths = (np.ones(100) * 10 * np.pi).astype(dtype=np.uint64) + result = shrink_fn(begins, lengths) + np.testing.assert_equal((begins, lengths), result) + + begins = np.arange(15, dtype=np.uint64)*16 + lengths = 1+np.arange(15, dtype=np.uint64) result = shrink_fn(begins, lengths) np.testing.assert_equal((begins, lengths), result) @@ -79,7 +89,7 @@ def assert_shrinks(self, shrink_fn): np.testing.assert_equal(self.shrank, shrank) def assert_empty_window_error(self, shrink_fn): - invalid = np.array([1, 2]), np.array([5, 1]) + invalid = np.array([1, 2], dtype=np.uint64), np.array([5, 1], dtype=np.uint64) with self.assertRaisesRegex(ValueError, "Overlap is bigger than measurement window"): shrink_fn(*invalid) From 346e980a857d107958824fa7610a8c507ec85453 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 4 Apr 2024 12:01:40 +0200 Subject: [PATCH 175/441] Make offset type consistent and add runtime check --- qupulse/program/__init__.py | 5 ++++- qupulse/program/linspace.py | 10 +++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/qupulse/program/__init__.py b/qupulse/program/__init__.py index a1b52b7c6..611a96fcd 100644 --- a/qupulse/program/__init__.py +++ b/qupulse/program/__init__.py @@ -31,7 +31,10 @@ class SimpleExpression(Generic[NumVal]): """ base: NumVal - offsets: Dict[str, NumVal] + offsets: Mapping[str, NumVal] + + def __post_init__(self): + assert isinstance(self.offsets, Mapping) def value(self, scope: Mapping[str, NumVal]) -> NumVal: value = self.base diff --git a/qupulse/program/linspace.py b/qupulse/program/linspace.py index d224648b7..4fe387856 100644 --- a/qupulse/program/linspace.py +++ b/qupulse/program/linspace.py @@ -124,7 +124,7 @@ def inner_scope(self, scope: Scope) -> Scope: process.""" if self._ranges: name, _ = self._ranges[-1] - return scope.overwrite({name: SimpleExpression(base=0, offsets=[(name, 1)])}) + return scope.overwrite({name: SimpleExpression(base=0, offsets={name: 1})}) else: return scope @@ -149,10 +149,10 @@ def hold_voltage(self, duration: HardwareTime, voltages: Mapping[ChannelID, Hard for rng_name, rng in ranges.items(): start = 0. step = 0. - for off_name, offset in offsets: - if off_name == rng_name: - start += rng.start * offset - step += rng.step * offset + offset = offsets.get(rng_name, None) + if offset: + start += rng.start * offset + step += rng.step * offset base += start incs.append(step) factors.append(tuple(incs)) From 9c4507e7b39e268d3284d85b220c12a47b6389fe Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 4 Apr 2024 12:11:24 +0200 Subject: [PATCH 176/441] Add newspiece --- changes.d/791.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes.d/791.feature diff --git a/changes.d/791.feature b/changes.d/791.feature new file mode 100644 index 000000000..7c64960fd --- /dev/null +++ b/changes.d/791.feature @@ -0,0 +1 @@ +Measurement windows can now automatically shrank in case of overlap to counteract small numeric errors. \ No newline at end of file From 87f39f52deebeaada945a0395ccedfb1b4863ed3 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 4 Apr 2024 19:59:33 +0200 Subject: [PATCH 177/441] Rename newspieces --- changes.d/{696.fix => 696.bugfix} | 0 changes.d/{707.fix => 707.bugfix} | 0 changes.d/{775.fix => 775.bugfix} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename changes.d/{696.fix => 696.bugfix} (100%) rename changes.d/{707.fix => 707.bugfix} (100%) rename changes.d/{775.fix => 775.bugfix} (100%) diff --git a/changes.d/696.fix b/changes.d/696.bugfix similarity index 100% rename from changes.d/696.fix rename to changes.d/696.bugfix diff --git a/changes.d/707.fix b/changes.d/707.bugfix similarity index 100% rename from changes.d/707.fix rename to changes.d/707.bugfix diff --git a/changes.d/775.fix b/changes.d/775.bugfix similarity index 100% rename from changes.d/775.fix rename to changes.d/775.bugfix From 9fc2f1e50a61887b0e682f8c0655feed15232626 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 4 Apr 2024 20:01:30 +0200 Subject: [PATCH 178/441] Push version --- doc/source/conf.py | 2 +- qupulse/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 5c25f321c..1a90f416d 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -81,7 +81,7 @@ # General information about the project. project = 'qupulse' -copyright = '2015-2022, Quantum Technology Group, RWTH Aachen University' +copyright = '2015-2024, Quantum Technology Group, RWTH Aachen University' author = 'Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University' # The version info for the project you're documenting, acts as replacement for diff --git a/qupulse/__init__.py b/qupulse/__init__.py index 7a51cb38d..5bf9d2ffe 100644 --- a/qupulse/__init__.py +++ b/qupulse/__init__.py @@ -2,7 +2,7 @@ import lazy_loader as lazy -__version__ = '0.9' +__version__ = '0.10' __getattr__, __dir__, __all__ = lazy.attach_stub(__name__, __file__) From 8ba0412069ee5897c306d38f1c8b075a9979cfef Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 4 Apr 2024 20:02:17 +0200 Subject: [PATCH 179/441] Update release notes --- RELEASE_NOTES.rst | 19 +++++++++++++++++++ changes.d/696.bugfix | 1 - changes.d/707.bugfix | 1 - changes.d/775.bugfix | 1 - changes.d/779.feature | 1 - changes.d/781.feature | 1 - changes.d/791.feature | 1 - 7 files changed, 19 insertions(+), 6 deletions(-) delete mode 100644 changes.d/696.bugfix delete mode 100644 changes.d/707.bugfix delete mode 100644 changes.d/775.bugfix delete mode 100644 changes.d/779.feature delete mode 100644 changes.d/781.feature delete mode 100644 changes.d/791.feature diff --git a/RELEASE_NOTES.rst b/RELEASE_NOTES.rst index aea933a6b..c76c7e9c9 100644 --- a/RELEASE_NOTES.rst +++ b/RELEASE_NOTES.rst @@ -2,6 +2,25 @@ .. towncrier release notes start +qupulse 0.10 (2024-04-04) +========================= + +Features +-------- + +- Move HDAWG driver to qupulse-hdawg-legacy to disentangle driver version from qupulse version. The new HDAWG driver will be published under qupulse-hdawg. (`#779 `_) +- Add the `ProgramBuilder` interface pattern to make the generated program of `PulseTemplate.create_program` easily customizable. (`#781 `_) +- Measurement windows can now automatically shrank in case of overlap to counteract small numeric errors. (`#791 `_) + + +Bugfixes +-------- + +- ``ConstantPulseTemplate``s from all versions can now be deserialized. (`#696 `_) +- Fixed that single segment tables where always interpreted to be constant. (`#707 `_) +- Add missing pulse registry support to `ArithmeticPT`. (`#775 `_) + + qupulse 0.9 (2023-11-08) ======================== diff --git a/changes.d/696.bugfix b/changes.d/696.bugfix deleted file mode 100644 index b93008fd5..000000000 --- a/changes.d/696.bugfix +++ /dev/null @@ -1 +0,0 @@ -``ConstantPulseTemplate``s from all versions can now be deserialized. \ No newline at end of file diff --git a/changes.d/707.bugfix b/changes.d/707.bugfix deleted file mode 100644 index 33e9e9f8e..000000000 --- a/changes.d/707.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fixed that single segment tables where always interpreted to be constant. diff --git a/changes.d/775.bugfix b/changes.d/775.bugfix deleted file mode 100644 index acbe9a605..000000000 --- a/changes.d/775.bugfix +++ /dev/null @@ -1 +0,0 @@ -Add missing pulse registry support to `ArithmeticPT`. diff --git a/changes.d/779.feature b/changes.d/779.feature deleted file mode 100644 index b455084f0..000000000 --- a/changes.d/779.feature +++ /dev/null @@ -1 +0,0 @@ -Move HDAWG driver to qupulse-hdawg-legacy to disentangle driver version from qupulse version. The new HDAWG driver will be published under qupulse-hdawg. diff --git a/changes.d/781.feature b/changes.d/781.feature deleted file mode 100644 index 697299436..000000000 --- a/changes.d/781.feature +++ /dev/null @@ -1 +0,0 @@ -Add the `ProgramBuilder` interface pattern to make the generated program of `PulseTemplate.create_program` easily customizable. \ No newline at end of file diff --git a/changes.d/791.feature b/changes.d/791.feature deleted file mode 100644 index 7c64960fd..000000000 --- a/changes.d/791.feature +++ /dev/null @@ -1 +0,0 @@ -Measurement windows can now automatically shrank in case of overlap to counteract small numeric errors. \ No newline at end of file From b6dda795370ac7562e3635f93d6bdf22a6fffce4 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 5 Apr 2024 16:30:54 +0200 Subject: [PATCH 180/441] First stub of zi hardware setup --- .../examples/04ZurichInstrumentsSetup.ipynb | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 doc/source/examples/04ZurichInstrumentsSetup.ipynb diff --git a/doc/source/examples/04ZurichInstrumentsSetup.ipynb b/doc/source/examples/04ZurichInstrumentsSetup.ipynb new file mode 100644 index 000000000..49b25d027 --- /dev/null +++ b/doc/source/examples/04ZurichInstrumentsSetup.ipynb @@ -0,0 +1,129 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "source": [ + "# Zurich Instruments Hardware Setup\n", + "\n", + "This notebook shows an exemplary use of qupulse with a ZI HDAWG and MFLI. The drivers for these instruments are kept in external packages to facilitate easy driver customization. Depending on your python version and hardware version you either need `qupulse-hdawg-legacy` or `qupulse-hdawg` for the HDAWG and `qupulse-mfli` for the MFLI.\n", + "\n", + "## Hardware Setup\n", + "\n", + "The hardware setup class provides a layer to map output channels to an arbitrary number of physical channels.\n", + "It also provides a mapping of measurement windows to specific dac instruments" + ], + "metadata": { + "collapsed": false + }, + "id": "7e31fbc9f47a77ce" + }, + { + "cell_type": "code", + "outputs": [], + "source": [ + "from qupulse.hardware.setup import HardwareSetup\n", + "\n", + "hw_setup = HardwareSetup()" + ], + "metadata": { + "collapsed": false + }, + "id": "6432f1ccf75c7d58", + "execution_count": null + }, + { + "cell_type": "code", + "outputs": [], + "source": [ + "# This abstracts over possibly installed hdawg drivers\n", + "from qupulse.hardware.awgs.zihdawg import HDAWGRepresentation\n", + "\n", + "awg_serial = 'DEVXXXX'\n", + "assert awg_serial != 'DEVXXXX', \"Please enter the serial of a connected HDAWG\"\n", + "\n", + "hdawg = HDAWGRepresentation(awg_serial)" + ], + "metadata": { + "collapsed": true + }, + "id": "initial_id", + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "### Channel groupings\n", + "\n", + "The `AWG` class abstracts over a set of dependently programmable channels. The HDAWG supports multiple channel groupings which decouples individual channel groups. The most robust setting for qupulse is to use the `1x8` channel grouping which executes the same sequencing program on all channels and only differs in the waveform data that is sequenced. This results in a single channel tuple/`AWG` object which represents all eight channels.\n", + "\n" + ], + "metadata": { + "collapsed": false + }, + "id": "4f15ba19d0961dbb" + }, + { + "cell_type": "code", + "outputs": [], + "source": [ + "from qupulse.hardware.awgs.zihdawg import HDAWGChannelGrouping\n", + "from qupulse.hardware.setup import PlaybackChannel, MarkerChannel\n", + "\n", + "hdawg.channel_grouping = HDAWGChannelGrouping.CHAN_GROUP_1x8\n", + "awg, = hdawg.channel_tuples\n", + "\n", + "# here we assume plunger one and two are connected to the two first channels of the AWG\n", + "hw_setup.set_channel('P1', PlaybackChannel(awg, 0))\n", + "hw_setup.set_channel('P2', PlaybackChannel(awg, 1))\n", + "\n", + "# We connect the trigger to the marker output of the first channel\n", + "hw_setup.set_channel('Trig', MarkerChannel(awg, 0))\n", + "\n", + "\n", + "for " + ], + "metadata": { + "collapsed": false + }, + "id": "eb9a838c161c244d", + "execution_count": null + }, + { + "cell_type": "code", + "outputs": [], + "source": [ + "from qupulse_mfli.mfli import MFLIDAQ\n", + "\n", + "mfli_serial = 'DEVXXXX'\n", + "assert mfli_serial != 'DEVXXXX', \"Please enter the serial of a connected HDAWG\"\n", + "\n", + "mfli = MFLIDAQ.connect_to(mfli_serial)" + ], + "metadata": { + "collapsed": false + }, + "id": "99e3edfcdf4ff697" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 82ac0cd8b9fcc1d5a56f6b00aca9a7443e6417bc Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 5 Apr 2024 16:47:08 +0200 Subject: [PATCH 181/441] Add MeasurementMask to public hardware setup interface --- qupulse/hardware/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qupulse/hardware/setup.py b/qupulse/hardware/setup.py index d034e3b26..69c15461f 100644 --- a/qupulse/hardware/setup.py +++ b/qupulse/hardware/setup.py @@ -12,7 +12,7 @@ import numpy as np -__all__ = ['PlaybackChannel', 'MarkerChannel', 'HardwareSetup'] +__all__ = ['PlaybackChannel', 'MarkerChannel', 'MeasurementMask', 'HardwareSetup'] class MeasurementMask: From 8034eebe0b65c99f584353c093aa717fe21feff0 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 5 Apr 2024 17:24:12 +0200 Subject: [PATCH 182/441] Add more examlple code --- .../examples/04ZurichInstrumentsSetup.ipynb | 161 +++++++++++++++++- 1 file changed, 159 insertions(+), 2 deletions(-) diff --git a/doc/source/examples/04ZurichInstrumentsSetup.ipynb b/doc/source/examples/04ZurichInstrumentsSetup.ipynb index 49b25d027..48e9baa15 100644 --- a/doc/source/examples/04ZurichInstrumentsSetup.ipynb +++ b/doc/source/examples/04ZurichInstrumentsSetup.ipynb @@ -7,6 +7,10 @@ "\n", "This notebook shows an exemplary use of qupulse with a ZI HDAWG and MFLI. The drivers for these instruments are kept in external packages to facilitate easy driver customization. Depending on your python version and hardware version you either need `qupulse-hdawg-legacy` or `qupulse-hdawg` for the HDAWG and `qupulse-mfli` for the MFLI.\n", "\n", + "## Connections and wiring\n", + "\n", + "The example here assumes a very nonsensical wiring that does not require anything else besides an HDAWG, and MFLI and three cables/adapters to connect SMB to BNC ports.\n", + "\n", "## Hardware Setup\n", "\n", "The hardware setup class provides a layer to map output channels to an arbitrary number of physical channels.\n", @@ -73,14 +77,20 @@ "awg, = hdawg.channel_tuples\n", "\n", "# here we assume plunger one and two are connected to the two first channels of the AWG\n", + "# It is considered best practice to use such names that relate to the connected sample gates\n", "hw_setup.set_channel('P1', PlaybackChannel(awg, 0))\n", "hw_setup.set_channel('P2', PlaybackChannel(awg, 1))\n", "\n", "# We connect the trigger to the marker output of the first channel\n", "hw_setup.set_channel('Trig', MarkerChannel(awg, 0))\n", "\n", + "# We can assign the same channel to multiple identifiers. Here we just assign all channels to a hardware name\n", + "for channel_idx, channel_letter in enumerate('ABCDEFGH'):\n", + " channel_name = f\"{hdawg.serial}_{channel_letter}\"\n", + " hw_setup.set_channel(channel_name, PlaybackChannel(awg, channel_idx), allow_multiple_registration=True)\n", "\n", - "for " + "# We can also assign multiple channels to the same identifier\n", + "hw_setup.set_channel(f\"{hdawg.serial}_ALL\", [PlaybackChannel(awg, idx) for idx in range(8)])" ], "metadata": { "collapsed": false @@ -88,6 +98,18 @@ "id": "eb9a838c161c244d", "execution_count": null }, + { + "cell_type": "markdown", + "source": [ + "## MFLI\n", + "\n", + "Next we will connect the MFLI." + ], + "metadata": { + "collapsed": false + }, + "id": "10ada657dd098fc7" + }, { "cell_type": "code", "outputs": [], @@ -95,7 +117,7 @@ "from qupulse_mfli.mfli import MFLIDAQ\n", "\n", "mfli_serial = 'DEVXXXX'\n", - "assert mfli_serial != 'DEVXXXX', \"Please enter the serial of a connected HDAWG\"\n", + "assert mfli_serial != 'DEVXXXX', \"Please enter the serial of a connected MFLI\"\n", "\n", "mfli = MFLIDAQ.connect_to(mfli_serial)" ], @@ -103,6 +125,141 @@ "collapsed": false }, "id": "99e3edfcdf4ff697" + }, + { + "cell_type": "markdown", + "source": [], + "metadata": { + "collapsed": false + }, + "id": "30b5fb5fec55af39" + }, + { + "cell_type": "markdown", + "source": [ + "So" + ], + "metadata": { + "collapsed": false + }, + "id": "bf2a9ed85290c479" + }, + { + "cell_type": "code", + "outputs": [], + "source": [ + "from qupulse.hardware.setup import MeasurementMask\n", + "\n", + "hw_setup.set_measurement('SET1', MeasurementMask(mfli, 'AverageR'))\n", + "hw_setup.set_measurement('SET2', MeasurementMask(mfli, 'AverageAux1'))\n", + "\n", + "raise NotImplementedError(\"TODO: Implement the rest of the MFLI operations and so on\")" + ], + "metadata": { + "collapsed": false + }, + "id": "6afb0d40c704a02" + }, + { + "cell_type": "markdown", + "source": [ + "from\n", + "\n", + "We define the pulse template in terms of the potentials of quantum dot one and two `Q1` and `Q2` and provide a linear transformation that maps them to the output voltages `P1` and `P2`." + ], + "metadata": { + "collapsed": false + }, + "id": "a7c5b77d781b5ab2" + }, + { + "cell_type": "code", + "outputs": [], + "source": [ + "from qupulse.pulses import *\n", + "import numpy as np\n", + "from qupulse.program.transformation import LinearTransformation\n", + "\n", + "pt = (ConstantPT(2**20, {\n", + " 'Q1': '-0.1 + x_i * 0.02',\n", + " 'Q2': '-0.2 + y_i * 0.05'})\n", + " .with_iteration('x_i', 'N_x')\n", + " .with_iteration('y_i', 'N_y'))\n", + "\n", + "trafo = LinearTransformation(np.array([[1., -.1], [-.09, 1.]]),\n", + " ('Q1', 'Q2'),\n", + " ('P1', 'P2'))\n", + "\n", + "program = pt.create_program(parameters={'N_x': 50, 'N_y': 30}, global_transformation=trafo)" + ], + "metadata": { + "collapsed": false + }, + "id": "27610ca4eb6cda25", + "execution_count": null + }, + { + "cell_type": "code", + "outputs": [], + "source": [ + "hw_setup.register_program('csd', program, awg.run_current_program)" + ], + "metadata": { + "collapsed": false + }, + "id": "346dabd84ea976fd", + "execution_count": null + }, + { + "cell_type": "code", + "outputs": [], + "source": [ + "hw_setup.arm_program('csd')" + ], + "metadata": { + "collapsed": false + }, + "id": "832c501c9082dc5d" + }, + { + "cell_type": "code", + "outputs": [], + "source": [ + "hw_setup.run_program('csd')" + ], + "metadata": { + "collapsed": false + }, + "id": "954f4e09664d073f" + }, + { + "cell_type": "markdown", + "source": [ + "The data extration is not standardized at the time of writing this example because it heavily depends on your data processing pipeline how the data is handled and where it shall go. qupulse has no functionality to associate a measured value with the value of some parameter that might have been varied during the measurement." + ], + "metadata": { + "collapsed": false + }, + "id": "e517992b6a35fc07" + }, + { + "cell_type": "code", + "outputs": [], + "source": [], + "metadata": { + "collapsed": false + }, + "id": "636ea90347e59fe5", + "execution_count": null + }, + { + "cell_type": "code", + "outputs": [], + "source": [], + "metadata": { + "collapsed": false + }, + "id": "90137bb3a5dd2f22" } ], "metadata": { From c8ff1025c26b60f1025fda6e4e0710c2885adb3a Mon Sep 17 00:00:00 2001 From: Paul Surrey Date: Sun, 7 Apr 2024 12:47:45 +0200 Subject: [PATCH 183/441] added an MFLI example tot he zi example notebook --- .../examples/04ZurichInstrumentsSetup.ipynb | 266 ++++++++++++------ 1 file changed, 175 insertions(+), 91 deletions(-) diff --git a/doc/source/examples/04ZurichInstrumentsSetup.ipynb b/doc/source/examples/04ZurichInstrumentsSetup.ipynb index 48e9baa15..b44bf373f 100644 --- a/doc/source/examples/04ZurichInstrumentsSetup.ipynb +++ b/doc/source/examples/04ZurichInstrumentsSetup.ipynb @@ -2,6 +2,10 @@ "cells": [ { "cell_type": "markdown", + "id": "7e31fbc9f47a77ce", + "metadata": { + "collapsed": false + }, "source": [ "# Zurich Instruments Hardware Setup\n", "\n", @@ -15,28 +19,29 @@ "\n", "The hardware setup class provides a layer to map output channels to an arbitrary number of physical channels.\n", "It also provides a mapping of measurement windows to specific dac instruments" - ], - "metadata": { - "collapsed": false - }, - "id": "7e31fbc9f47a77ce" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "6432f1ccf75c7d58", + "metadata": { + "collapsed": false + }, "outputs": [], "source": [ "from qupulse.hardware.setup import HardwareSetup\n", "\n", "hw_setup = HardwareSetup()" - ], - "metadata": { - "collapsed": false - }, - "id": "6432f1ccf75c7d58", - "execution_count": null + ] }, { "cell_type": "code", + "execution_count": null, + "id": "initial_id", + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "# This abstracts over possibly installed hdawg drivers\n", @@ -46,28 +51,28 @@ "assert awg_serial != 'DEVXXXX', \"Please enter the serial of a connected HDAWG\"\n", "\n", "hdawg = HDAWGRepresentation(awg_serial)" - ], - "metadata": { - "collapsed": true - }, - "id": "initial_id", - "execution_count": null + ] }, { "cell_type": "markdown", + "id": "4f15ba19d0961dbb", + "metadata": { + "collapsed": false + }, "source": [ "### Channel groupings\n", "\n", "The `AWG` class abstracts over a set of dependently programmable channels. The HDAWG supports multiple channel groupings which decouples individual channel groups. The most robust setting for qupulse is to use the `1x8` channel grouping which executes the same sequencing program on all channels and only differs in the waveform data that is sequenced. This results in a single channel tuple/`AWG` object which represents all eight channels.\n", "\n" - ], - "metadata": { - "collapsed": false - }, - "id": "4f15ba19d0961dbb" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "eb9a838c161c244d", + "metadata": { + "collapsed": false + }, "outputs": [], "source": [ "from qupulse.hardware.awgs.zihdawg import HDAWGChannelGrouping\n", @@ -91,89 +96,160 @@ "\n", "# We can also assign multiple channels to the same identifier\n", "hw_setup.set_channel(f\"{hdawg.serial}_ALL\", [PlaybackChannel(awg, idx) for idx in range(8)])" - ], - "metadata": { - "collapsed": false - }, - "id": "eb9a838c161c244d", - "execution_count": null + ] }, { "cell_type": "markdown", + "id": "10ada657dd098fc7", + "metadata": { + "collapsed": false + }, "source": [ "## MFLI\n", "\n", "Next we will connect the MFLI." - ], - "metadata": { - "collapsed": false - }, - "id": "10ada657dd098fc7" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "99e3edfcdf4ff697", + "metadata": { + "collapsed": false + }, "outputs": [], "source": [ - "from qupulse_mfli.mfli import MFLIDAQ\n", + "from qupulse_mfli.mfli import MFLIDAQ, postprocessing_average_within_windows\n", "\n", "mfli_serial = 'DEVXXXX'\n", "assert mfli_serial != 'DEVXXXX', \"Please enter the serial of a connected MFLI\"\n", "\n", "mfli = MFLIDAQ.connect_to(mfli_serial)" - ], - "metadata": { - "collapsed": false - }, - "id": "99e3edfcdf4ff697" + ] }, { "cell_type": "markdown", - "source": [], + "id": "30b5fb5fec55af39", "metadata": { "collapsed": false }, - "id": "30b5fb5fec55af39" + "source": [] }, { "cell_type": "markdown", - "source": [ - "So" - ], + "id": "bf2a9ed85290c479", "metadata": { "collapsed": false }, - "id": "bf2a9ed85290c479" + "source": [ + "So" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "6afb0d40c704a02", + "metadata": { + "collapsed": false + }, "outputs": [], "source": [ "from qupulse.hardware.setup import MeasurementMask\n", "\n", "hw_setup.set_measurement('SET1', MeasurementMask(mfli, 'AverageR'))\n", - "hw_setup.set_measurement('SET2', MeasurementMask(mfli, 'AverageAux1'))\n", + "hw_setup.set_measurement('SET2', MeasurementMask(mfli, 'AverageAux1'))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c943fb4d", + "metadata": {}, + "outputs": [], + "source": [ + "# linking the measurement mask names to physical input channels\n", + "mfli.register_measurement_channel(program_name=None, channel_path=\"demods/0/sample.R\", window_name=\"AverageR\")\n", + "mfli.register_measurement_channel(program_name=None, channel_path=\"auxins/0/sample.AuxIn0.avg\", window_name=\"AverageAux1\")\n", "\n", - "raise NotImplementedError(\"TODO: Implement the rest of the MFLI operations and so on\")" - ], - "metadata": { - "collapsed": false - }, - "id": "6afb0d40c704a02" + "'''\n", + "\n", + "The other inputs can be addressed via strings as the following:\n", + "{\n", + " \"R\": [\"demods/0/sample.R\"],\n", + " \"X\": [\"demods/0/sample.X\"],\n", + " \"Y\": [\"demods/0/sample.Y\"],\n", + " \"A\": [\"auxins/0/sample.AuxIn0.avg\"],\n", + " \"many\": [\"demods/0/sample.R\", \"auxins/0/sample.AuxIn0.avg\", \"demods/0/sample.X\", \"demods/0/sample.Y\"]\n", + "}\n", + "\n", + "where the keys of the dict are the values for the window_name, and the values of the dict are the channel_path inputs. Note that these can also be lists to record multiple channels under one name. I.e. for IQ demodulation.\n", + "\n", + "'''" + ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, + "id": "51401e51", + "metadata": {}, + "outputs": [], "source": [ - "from\n", + "# configuring the driver to average all datapoint for each window.\n", + "mfli.register_operations(None, postprocessing_average_within_windows)\n", "\n", - "We define the pulse template in terms of the potentials of quantum dot one and two `Q1` and `Q2` and provide a linear transformation that maps them to the output voltages `P1` and `P2`." - ], + "# one can also register the ```qupulse_mfli.mfli.postprocessing_crop_windows``` post processing function to return the data that was recorded for within window without averaging.\n", + "# Or one could register ```None``` to return the raw data recorded without considering the windows." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "417c4976", + "metadata": {}, + "outputs": [], + "source": [ + "# registering the trigger for the program named \"run_for_ever\". Which armes the lockin once, and recordes for each observed trigger on AuxIn1.\n", + "mfli.register_trigger_settings(program_name=\"run_for_ever\",\n", + " trigger_input=f\"demods/0/sample.AuxIn1\", # here AuxInN referese to the printer label N+1\n", + " edge=\"rising\",\n", + " trigger_count=3, # this defines the number of triggers to capture in one measurement (i.e. rows). E.g. one measurement contains 3 Trigger events, which might be somehting one could do when crafting the programs carefully.\n", + " level=.5, # this sets the trigger level\n", + " measurement_count=np.inf, # this defined the number of rounds that are to be measured (e.g. how often the \"single\" button should be pressed). E.g. after one arm call, i would like to perform np.inf measurements\n", + " other_settings={\"holdoff/time\": 1e-3} # this sets the duration for which new triggers are ignored\n", + " )\n", + "# the aquesition can be ended via mfli.stop_acquisition()\n", + "\n", + "# registering trigger settings for a standard configuration\n", + "# The measurement is perfomed once after one trigger on TrigIn1 is observed.\n", + "mfli.register_trigger_settings(program_name=None,\n", + " trigger_input=f\"demods/0/sample.TrigIn1\", # here TrigInN referrers to the printer label N\n", + " edge=\"rising\",\n", + " trigger_count=1,\n", + " level=.5,\n", + " measurement_count=1,\n", + " other_settings={\"holdoff/time\": 1e-3}\n", + " ) " + ] + }, + { + "cell_type": "markdown", + "id": "a7c5b77d781b5ab2", "metadata": { "collapsed": false }, - "id": "a7c5b77d781b5ab2" + "source": [ + "from\n", + "\n", + "We define the pulse template in terms of the potentials of quantum dot one and two `Q1` and `Q2` and provide a linear transformation that maps them to the output voltages `P1` and `P2`." + ] }, { "cell_type": "code", + "execution_count": null, + "id": "27610ca4eb6cda25", + "metadata": { + "collapsed": false + }, "outputs": [], "source": [ "from qupulse.pulses import *\n", @@ -191,75 +267,83 @@ " ('P1', 'P2'))\n", "\n", "program = pt.create_program(parameters={'N_x': 50, 'N_y': 30}, global_transformation=trafo)" - ], - "metadata": { - "collapsed": false - }, - "id": "27610ca4eb6cda25", - "execution_count": null + ] }, { "cell_type": "code", - "outputs": [], - "source": [ - "hw_setup.register_program('csd', program, awg.run_current_program)" - ], + "execution_count": null, + "id": "346dabd84ea976fd", "metadata": { "collapsed": false }, - "id": "346dabd84ea976fd", - "execution_count": null + "outputs": [], + "source": [ + "hw_setup.register_program('csd', program, awg.run_current_program)" + ] }, { "cell_type": "code", - "outputs": [], - "source": [ - "hw_setup.arm_program('csd')" - ], + "execution_count": null, + "id": "832c501c9082dc5d", "metadata": { "collapsed": false }, - "id": "832c501c9082dc5d" + "outputs": [], + "source": [ + "hw_setup.arm_program('csd')" + ] }, { "cell_type": "code", - "outputs": [], - "source": [ - "hw_setup.run_program('csd')" - ], + "execution_count": null, + "id": "954f4e09664d073f", "metadata": { "collapsed": false }, - "id": "954f4e09664d073f" + "outputs": [], + "source": [ + "hw_setup.run_program('csd')" + ] }, { "cell_type": "markdown", - "source": [ - "The data extration is not standardized at the time of writing this example because it heavily depends on your data processing pipeline how the data is handled and where it shall go. qupulse has no functionality to associate a measured value with the value of some parameter that might have been varied during the measurement." - ], + "id": "e517992b6a35fc07", "metadata": { "collapsed": false }, - "id": "e517992b6a35fc07" + "source": [ + "The data extration is not standardized at the time of writing this example because it heavily depends on your data processing pipeline how the data is handled and where it shall go. qupulse has no functionality to associate a measured value with the value of some parameter that might have been varied during the measurement." + ] }, { "cell_type": "code", - "outputs": [], - "source": [], + "execution_count": null, + "id": "636ea90347e59fe5", "metadata": { "collapsed": false }, - "id": "636ea90347e59fe5", - "execution_count": null + "outputs": [], + "source": [ + "# receaving the recorded data from the MFLI\n", + "\n", + "data = mfli.measure_program(wait=False) # wait=True would wait until the aquesition is finished.\n" + ] }, { - "cell_type": "code", - "outputs": [], - "source": [], + "cell_type": "markdown", + "id": "90137bb3a5dd2f22", "metadata": { "collapsed": false }, - "id": "90137bb3a5dd2f22" + "source": [ + "The recorded data is sliced to the measurement windows in the default configuration. Thus ```my_lockin.measure_program``` returns a list (number of measurements) of dicts (the qupulse channels), of dicts (the lockin channels), of lists (the observed trigger), of lists of xarray DataArrays (each DataArray containing the data sliced for one window) or numpy arrays (containing the data resulting from averaging over the windows). I.e. ```returned_data[][][][]``` leads to ether the list of DataArrays or to a numpy array." + ] + }, + { + "cell_type": "markdown", + "id": "d88b11db", + "metadata": {}, + "source": [] } ], "metadata": { From 706ba71f893f9ce2c42264be30bf4c29a0644b2f Mon Sep 17 00:00:00 2001 From: Nomos11 <82180697+Nomos11@users.noreply.github.com> Date: Wed, 10 Apr 2024 14:02:12 +0200 Subject: [PATCH 184/441] some hacks to make HDAWG linspace builder easier --- qupulse/hardware/awgs/base.py | 84 ++++++++++++++++++++++++++++++----- qupulse/program/linspace.py | 24 +++++++--- 2 files changed, 92 insertions(+), 16 deletions(-) diff --git a/qupulse/hardware/awgs/base.py b/qupulse/hardware/awgs/base.py index 108230179..af2707dd7 100644 --- a/qupulse/hardware/awgs/base.py +++ b/qupulse/hardware/awgs/base.py @@ -9,11 +9,15 @@ from abc import abstractmethod from numbers import Real -from typing import Set, Tuple, Callable, Optional, Mapping, Sequence, List +from typing import Set, Tuple, Callable, Optional, Mapping, Sequence, List, Union from collections import OrderedDict +from enum import Enum +# from itertools import chain from qupulse.hardware.util import get_sample_times, not_none_indices from qupulse.utils.types import ChannelID +from qupulse.program.linspace import LinSpaceNode, LinSpaceArbitraryWaveform, to_increment_commands, Command, \ + Increment, Set as LSPSet, LoopLabel, LoopJmp, Wait, Play from qupulse.program.loop import Loop from qupulse.program.waveforms import Waveform from qupulse.comparable import Comparable @@ -162,17 +166,26 @@ def __str__(self) -> str: " Use force to overwrite.".format(self.name) +AllowedProgramTypes = Union[Loop,Sequence[LinSpaceNode],] + +class _ProgramType(Enum): + FSP = -1 + Loop = 0 + Linspace = 1 + + class ProgramEntry: """This is a helper class for implementing awgs drivers. A driver can subclass it to help organizing sampled waveforms""" - def __init__(self, loop: Loop, + def __init__(self, program: AllowedProgramTypes, channels: Tuple[Optional[ChannelID], ...], markers: Tuple[Optional[ChannelID], ...], amplitudes: Tuple[float, ...], offsets: Tuple[float, ...], voltage_transformations: Tuple[Optional[Callable], ...], sample_rate: TimeType, - waveforms: Sequence[Waveform] = None): + waveforms: Sequence[Waveform] = None, + program_type: _ProgramType = _ProgramType.Loop): """ Args: @@ -195,17 +208,44 @@ def __init__(self, loop: Loop, self._voltage_transformations = tuple(voltage_transformations) self._sample_rate = sample_rate - - self._loop = loop - + + self._program_type = program_type + self._program = program #non-normalized + self.__loop = program + + print(type(program_type)) + print(type(_ProgramType.Linspace)) + print(program_type is _ProgramType.Linspace) + if program_type == _ProgramType.Linspace: + self._transformed_commands = self._transform_linspace_commands(to_increment_commands(program)) + if waveforms is None: - waveforms = OrderedDict((node.waveform, None) - for node in loop.get_depth_first_iterator() if node.is_leaf()).keys() + if program_type is _ProgramType.Loop: + waveforms = OrderedDict((node.waveform, None) + for node in program.get_depth_first_iterator() if node.is_leaf()).keys() + elif program_type is _ProgramType.Linspace: + #not so clean + #TODO: also marker handling not optimal + waveforms = OrderedDict((command.waveform, None) + for command in self._transformed_commands if isinstance(command,Play)).keys() + else: + raise NotImplementedError() + if waveforms: self._waveforms = OrderedDict(zip(waveforms, self._sample_waveforms(waveforms))) else: self._waveforms = OrderedDict() - + + @property + def _loop(self,) -> _ProgramType: + if self._program_type is not _ProgramType.Loop and self._program_type is not _ProgramType.FSP: + raise DeprecationWarning() + return self.__loop + + @_loop.setter + def _loop(self,program:_ProgramType): + self.__loop = program + def _sample_empty_channel(self, time: numpy.ndarray) -> Optional[numpy.ndarray]: """Override this in derived class to change how empty channels are handled""" return None @@ -213,7 +253,31 @@ def _sample_empty_channel(self, time: numpy.ndarray) -> Optional[numpy.ndarray]: def _sample_empty_marker(self, time: numpy.ndarray) -> Optional[numpy.ndarray]: """Override this in derived class to change how empty channels are handled""" return None - + + def _transform_linspace_commands(self, command_list: List[Command]) -> List[Command]: + + # all commands = Union[Increment, Set, LoopLabel, LoopJmp, Wait, Play] + if any(self._voltage_transformations): + raise NotImplementedError('how to handle this?') + + _channelname_to_idx = {name: idx for idx, name in enumerate(self._channels)} + trafos_by_channel_idx = {ch: (v,a,o) for ch,v,a,o in zip(_channelname_to_idx.values(),self._voltage_transformations,self._amplitudes,self._offsets)} + #the channels are now in idx in the commands. + + print(trafos_by_channel_idx) + + for command in command_list: + if isinstance(command,Union[LoopLabel, LoopJmp, Play, Wait]): + continue + elif isinstance(command,Increment): + command.value = command.value / trafos_by_channel_idx[command.channel][1] + elif isinstance(command,LSPSet): + command.value = (command.value - trafos_by_channel_idx[command.channel][2]) / trafos_by_channel_idx[command.channel][1] + else: + raise NotImplementedError() + + return command_list + def _sample_waveforms(self, waveforms: Sequence[Waveform]) -> List[Tuple[Tuple[numpy.ndarray, ...], Tuple[numpy.ndarray, ...]]]: sampled_waveforms = [] diff --git a/qupulse/program/linspace.py b/qupulse/program/linspace.py index 4fe387856..ee7a704b9 100644 --- a/qupulse/program/linspace.py +++ b/qupulse/program/linspace.py @@ -1,8 +1,9 @@ import abc import contextlib import dataclasses +import numpy as np from dataclasses import dataclass -from typing import Mapping, Optional, Sequence, ContextManager, Iterable, Tuple, Union, Dict, List +from typing import Mapping, Optional, Sequence, ContextManager, Iterable, Tuple, Union, Dict, List, Iterator from qupulse import ChannelID, MeasurementWindow from qupulse.parameter_scope import Scope, MappedScope, FrozenDict @@ -124,7 +125,7 @@ def inner_scope(self, scope: Scope) -> Scope: process.""" if self._ranges: name, _ = self._ranges[-1] - return scope.overwrite({name: SimpleExpression(base=0, offsets={name: 1})}) + return MappedScope(scope, FrozenDict({name: SimpleExpression(base=0, offsets=[(name, 1)])})) else: return scope @@ -143,16 +144,26 @@ def hold_voltage(self, duration: HardwareTime, voltages: Mapping[ChannelID, Hard bases.append(value) factors.append(None) continue + #there mightbe a bug in some waveform.constant_value, where an array of float instead of float is returned + #(goes against the typehints) + if isinstance(value, np.ndarray): + try: + value = float(value) + bases.append(value) + factors.append(None) + except: + raise RuntimeError('hack doesnt work') + continue offsets = value.offsets base = value.base incs = [] for rng_name, rng in ranges.items(): start = 0. step = 0. - offset = offsets.get(rng_name, None) - if offset: - start += rng.start * offset - step += rng.step * offset + for off_name, offset in offsets: + if off_name == rng_name: + start += rng.start * offset + step += rng.step * offset base += start incs.append(step) factors.append(tuple(incs)) @@ -406,3 +417,4 @@ def to_increment_commands(linspace_nodes: Sequence[LinSpaceNode]) -> List[Comman state = _TranslationState() state.add_node(linspace_nodes) return state.commands + From b333e25af43cb04f79eff1ab4c2ba0218d41af61 Mon Sep 17 00:00:00 2001 From: Nomos11 <82180697+Nomos11@users.noreply.github.com> Date: Wed, 10 Apr 2024 14:08:44 +0200 Subject: [PATCH 185/441] some basic tests --- tests/program/linspace_tests2.py | 289 +++++++++++++++++++++++++++++++ 1 file changed, 289 insertions(+) create mode 100644 tests/program/linspace_tests2.py diff --git a/tests/program/linspace_tests2.py b/tests/program/linspace_tests2.py new file mode 100644 index 000000000..32494d18d --- /dev/null +++ b/tests/program/linspace_tests2.py @@ -0,0 +1,289 @@ +import copy +import unittest +from unittest import TestCase + +from qupulse.pulses import * +from qupulse.program.linspace import * +from qupulse.program.transformation import * + +class SingleRampTest(TestCase): + def setUp(self): + hold = ConstantPT(64*10 ** 5, {'a': '-1. + idx * 0.01'}) + self.pulse_template = hold.with_iteration('idx', 200) + + self.program = LinSpaceIter( + length=200, + body=(LinSpaceHold( + bases=(-1.,), + factors=((0.01,),), + duration_base=TimeType(10**6), + duration_factors=None + ),) + ) + + key = DepKey.from_voltages((0.01,), DEFAULT_INCREMENT_RESOLUTION) + + self.commands = [ + Set(0, -1.0, key), + Wait(TimeType(10 ** 6)), + LoopLabel(0, 199), + Increment(0, 0.01, key), + Wait(TimeType(10 ** 6)), + LoopJmp(0) + ] + + def test_program(self): + program_builder = LinSpaceBuilder(('a',)) + program = self.pulse_template.create_program(program_builder=program_builder) + self.assertEqual([self.program], program) + + def test_commands(self): + commands = to_increment_commands([self.program]) + self.assertEqual(self.commands, commands) + + +class PlainCSDTest(TestCase): + def setUp(self,time_factor=1e3,rep_factor=2): + hold = ConstantPT(64*time_factor, {'a': '-1. + idx_a * 0.01', 'b': '-.5 + idx_b * 0.02'}) + scan_a = hold.with_iteration('idx_a', rep_factor*10) + self.pulse_template = scan_a.with_iteration('idx_b', rep_factor*10) + + self.program = LinSpaceIter(length=100, body=(LinSpaceIter( + length=200, + body=(LinSpaceHold( + bases=(-1., -0.5), + factors=((0.0, 0.01), + (0.02, 0.0)), + duration_base=TimeType(10**6), + duration_factors=None + ),) + ),)) + + key_0 = DepKey.from_voltages((0, 0.01,), DEFAULT_INCREMENT_RESOLUTION) + key_1 = DepKey.from_voltages((0.02,), DEFAULT_INCREMENT_RESOLUTION) + + self.commands = [ + Set(0, -1.0, key_0), + Set(1, -0.5, key_1), + Wait(TimeType(10 ** 6)), + + LoopLabel(0, 199), + Increment(0, 0.01, key_0), + Wait(TimeType(10 ** 6)), + LoopJmp(0), + + LoopLabel(1, 99), + + Increment(0, -2.0, key_0), + Increment(1, 0.02, key_1), + Wait(TimeType(10 ** 6)), + + LoopLabel(2, 199), + Increment(0, 0.01, key_0), + Wait(TimeType(10 ** 6)), + LoopJmp(2), + + LoopJmp(1), + ] + + def test_program(self): + program_builder = LinSpaceBuilder(('a', 'b')) + program = self.pulse_template.create_program(program_builder=program_builder) + self.assertEqual([self.program], program) + + def test_increment_commands(self): + commands = to_increment_commands([self.program]) + self.assertEqual(self.commands, commands) + + +class TiltedCSDTest(TestCase): + def setUp(self,time_factor=1e3,rep_factor=2): + hold = ConstantPT(64*time_factor, {'a': '-1. + idx_a * 0.01 + idx_b * 1e-3', 'b': '-.5 + idx_b * 0.02 - 3e-3 * idx_a'}) + scan_a = hold.with_iteration('idx_a', rep_factor*10) + self.pulse_template = scan_a.with_iteration('idx_b', rep_factor*10) + self.repeated_pt = self.pulse_template.with_repetition(42) + + self.program = LinSpaceIter(length=100, body=(LinSpaceIter( + length=200, + body=(LinSpaceHold( + bases=(-1., -0.5), + factors=((1e-3, 0.01), + (0.02, -3e-3)), + duration_base=TimeType(10**6), + duration_factors=None + ),) + ),)) + self.repeated_program = LinSpaceRepeat(body=(self.program,), count=42) + + key_0 = DepKey.from_voltages((1e-3, 0.01,), DEFAULT_INCREMENT_RESOLUTION) + key_1 = DepKey.from_voltages((0.02, -3e-3), DEFAULT_INCREMENT_RESOLUTION) + + self.commands = [ + Set(0, -1.0, key_0), + Set(1, -0.5, key_1), + Wait(TimeType(10 ** 6)), + + LoopLabel(0, 199), + Increment(0, 0.01, key_0), + Increment(1, -3e-3, key_1), + Wait(TimeType(10 ** 6)), + LoopJmp(0), + + LoopLabel(1, 99), + + Increment(0, 1e-3 + -200 * 1e-2, key_0), + Increment(1, 0.02 + -200 * -3e-3, key_1), + Wait(TimeType(10 ** 6)), + + LoopLabel(2, 199), + Increment(0, 0.01, key_0), + Increment(1, -3e-3, key_1), + Wait(TimeType(10 ** 6)), + LoopJmp(2), + + LoopJmp(1), + ] + inner_commands = copy.deepcopy(self.commands) + for cmd in inner_commands: + if hasattr(cmd, 'idx'): + cmd.idx += 1 + self.repeated_commands = [LoopLabel(0, 42)] + inner_commands + [LoopJmp(0)] + + def test_program(self): + program_builder = LinSpaceBuilder(('a', 'b')) + program = self.pulse_template.create_program(program_builder=program_builder) + self.assertEqual([self.program], program) + + def test_repeated_program(self): + program_builder = LinSpaceBuilder(('a', 'b')) + program = self.repeated_pt.create_program(program_builder=program_builder) + self.assertEqual([self.repeated_program], program) + + def test_increment_commands(self): + commands = to_increment_commands([self.program]) + self.assertEqual(self.commands, commands) + + def test_repeated_increment_commands(self): + commands = to_increment_commands([self.repeated_program]) + self.assertEqual(self.repeated_commands, commands) + + +class SingletLoadProcessing(): + def setUp(self,time_factor=1e2,rep_factor=2): + wait = ConstantPT(64*time_factor*1e1, {'a': '-1. + idx_a * 0.01', 'b': '-.5 + idx_b * 0.02'}) + load_random = ConstantPT(10 ** 5, {'a': -.4, 'b': -.3}) + meas = ConstantPT(64*time_factor, {'a': 0.05, 'b': 0.06}) + + singlet_scan = (load_random @ wait @ meas).with_iteration('idx_a', rep_factor*10*2).with_iteration('idx_b', rep_factor*10) + self.pulse_template = singlet_scan + + self.program = LinSpaceIter(length=100, body=(LinSpaceIter( + length=200, + body=( + LinSpaceHold(bases=(-0.4, -0.3), factors=(None, None), duration_base=TimeType(10 ** 5), + duration_factors=None), + LinSpaceHold(bases=(-1., -0.5), + factors=((0.0, 0.01), + (0.02, 0.0)), + duration_base=TimeType(10 ** 6), + duration_factors=None), + LinSpaceHold(bases=(0.05, 0.06), factors=(None, None), duration_base=TimeType(10 ** 5), + duration_factors=None), + ) + ),)) + + key_0 = DepKey.from_voltages((0, 0.01,), DEFAULT_INCREMENT_RESOLUTION) + key_1 = DepKey.from_voltages((0.02,), DEFAULT_INCREMENT_RESOLUTION) + + self.commands = [ + Set(0, -0.4), + Set(1, -0.3), + Wait(TimeType(10 ** 5)), + Set(0, -1.0, key_0), + Set(1, -0.5, key_1), + Wait(TimeType(10 ** 6)), + Set(0, 0.05), + Set(1, 0.06), + Wait(TimeType(10 ** 5)), + + LoopLabel(0, 199), + Set(0, -0.4), + Set(1, -0.3), + Wait(TimeType(10 ** 5)), + Increment(0, 0.01, key_0), + Increment(1, 0.00, key_1), + Wait(TimeType(10 ** 6)), + Set(0, 0.05), + Set(1, 0.06), + Wait(TimeType(10 ** 5)), + LoopJmp(0), + + LoopLabel(1, 99), + + Set(0, -0.4), + Set(1, -0.3), + Wait(TimeType(10 ** 5)), + Increment(0, -2.0, key_0), + Increment(1, 0.02, key_1), + Wait(TimeType(10 ** 6)), + Set(0, 0.05), + Set(1, 0.06), + Wait(TimeType(10 ** 5)), + + LoopLabel(2, 199), + + Set(0, -0.4), + Set(1, -0.3), + Wait(TimeType(10 ** 5)), + Increment(0, 0.01, key_0), + Increment(1, 0.00, key_1), + Wait(TimeType(10 ** 6)), + Set(0, 0.05), + Set(1, 0.06), + Wait(TimeType(10 ** 5)), + + LoopJmp(2), + + LoopJmp(1), + ] + + def test_singlet_scan_program(self): + program_builder = LinSpaceBuilder(('a', 'b')) + program = self.pulse_template.create_program(program_builder=program_builder) + self.assertEqual([self.program], program) + + def test_singlet_scan_commands(self): + commands = to_increment_commands([self.program]) + self.assertEqual(self.commands, commands) + + +class TransformedRampTest(TestCase): + def setUp(self): + hold = ConstantPT(10 ** 6, {'a': '-1. + idx * 0.01'}) + self.pulse_template = hold.with_iteration('idx', 200) + self.transformation = ScalingTransformation({'a': 2.0}) + + self.program = LinSpaceIter( + length=200, + body=(LinSpaceHold( + bases=(-2.,), + factors=((0.02,),), + duration_base=TimeType(10 ** 6), + duration_factors=None + ),) + ) + + def test_global_trafo_program(self): + program_builder = LinSpaceBuilder(('a',)) + program = self.pulse_template.create_program(program_builder=program_builder, + global_transformation=self.transformation) + self.assertEqual([self.program], program) + + def test_local_trafo_program(self): + program_builder = LinSpaceBuilder(('a',)) + with self.assertRaises(NotImplementedError): + # not implemented yet. This test should work as soon as its implemented + program = self.pulse_template.create_program(program_builder=program_builder, + global_transformation=self.transformation, + to_single_waveform={self.pulse_template}) + self.assertEqual([self.program], program) From 3f4eaee186b0a22cdb1a5d4c5507aebee9f5d9a7 Mon Sep 17 00:00:00 2001 From: Nomos11 <82180697+Nomos11@users.noreply.github.com> Date: Wed, 10 Apr 2024 14:49:34 +0200 Subject: [PATCH 186/441] revert unintended change, delete prints --- qupulse/hardware/awgs/base.py | 3 --- qupulse/program/linspace.py | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/qupulse/hardware/awgs/base.py b/qupulse/hardware/awgs/base.py index af2707dd7..48a06f1ae 100644 --- a/qupulse/hardware/awgs/base.py +++ b/qupulse/hardware/awgs/base.py @@ -213,9 +213,6 @@ def __init__(self, program: AllowedProgramTypes, self._program = program #non-normalized self.__loop = program - print(type(program_type)) - print(type(_ProgramType.Linspace)) - print(program_type is _ProgramType.Linspace) if program_type == _ProgramType.Linspace: self._transformed_commands = self._transform_linspace_commands(to_increment_commands(program)) diff --git a/qupulse/program/linspace.py b/qupulse/program/linspace.py index ee7a704b9..0a514323a 100644 --- a/qupulse/program/linspace.py +++ b/qupulse/program/linspace.py @@ -125,7 +125,7 @@ def inner_scope(self, scope: Scope) -> Scope: process.""" if self._ranges: name, _ = self._ranges[-1] - return MappedScope(scope, FrozenDict({name: SimpleExpression(base=0, offsets=[(name, 1)])})) + return scope.overwrite({name: SimpleExpression(base=0, offsets={name: 1})}) else: return scope From 098cee639ff9a92a7a00093934838398b3cb3058 Mon Sep 17 00:00:00 2001 From: Nomos11 <82180697+Nomos11@users.noreply.github.com> Date: Wed, 10 Apr 2024 15:05:57 +0200 Subject: [PATCH 187/441] revert unintended changes pt 2 --- qupulse/program/linspace.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qupulse/program/linspace.py b/qupulse/program/linspace.py index 0a514323a..ee6aa10ca 100644 --- a/qupulse/program/linspace.py +++ b/qupulse/program/linspace.py @@ -160,10 +160,10 @@ def hold_voltage(self, duration: HardwareTime, voltages: Mapping[ChannelID, Hard for rng_name, rng in ranges.items(): start = 0. step = 0. - for off_name, offset in offsets: - if off_name == rng_name: - start += rng.start * offset - step += rng.step * offset + offset = offsets.get(rng_name, None) + if offset: + start += rng.start * offset + step += rng.step * offset base += start incs.append(step) factors.append(tuple(incs)) From 8e0a7c1344af1a69d3a887f0abedad44e9dd3c9d Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 11 Apr 2024 09:58:52 +0200 Subject: [PATCH 188/441] Extend example --- .../examples/04ZurichInstrumentsSetup.ipynb | 152 +++++++++++------- 1 file changed, 91 insertions(+), 61 deletions(-) diff --git a/doc/source/examples/04ZurichInstrumentsSetup.ipynb b/doc/source/examples/04ZurichInstrumentsSetup.ipynb index b44bf373f..efe974376 100644 --- a/doc/source/examples/04ZurichInstrumentsSetup.ipynb +++ b/doc/source/examples/04ZurichInstrumentsSetup.ipynb @@ -23,26 +23,24 @@ }, { "cell_type": "code", - "execution_count": null, "id": "6432f1ccf75c7d58", "metadata": { "collapsed": false }, - "outputs": [], "source": [ "from qupulse.hardware.setup import HardwareSetup\n", "\n", "hw_setup = HardwareSetup()" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "id": "initial_id", "metadata": { "collapsed": true }, - "outputs": [], "source": [ "# This abstracts over possibly installed hdawg drivers\n", "from qupulse.hardware.awgs.zihdawg import HDAWGRepresentation\n", @@ -50,8 +48,10 @@ "awg_serial = 'DEVXXXX'\n", "assert awg_serial != 'DEVXXXX', \"Please enter the serial of a connected HDAWG\"\n", "\n", - "hdawg = HDAWGRepresentation(awg_serial)" - ] + "hdawg = HDAWGRepresentation(awg_serial, 'USB' )" + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -68,12 +68,10 @@ }, { "cell_type": "code", - "execution_count": null, "id": "eb9a838c161c244d", "metadata": { "collapsed": false }, - "outputs": [], "source": [ "from qupulse.hardware.awgs.zihdawg import HDAWGChannelGrouping\n", "from qupulse.hardware.setup import PlaybackChannel, MarkerChannel\n", @@ -95,8 +93,10 @@ " hw_setup.set_channel(channel_name, PlaybackChannel(awg, channel_idx), allow_multiple_registration=True)\n", "\n", "# We can also assign multiple channels to the same identifier\n", - "hw_setup.set_channel(f\"{hdawg.serial}_ALL\", [PlaybackChannel(awg, idx) for idx in range(8)])" - ] + "hw_setup.set_channel(f\"{hdawg.serial}_ALL\", [PlaybackChannel(awg, idx) for idx in range(8)], allow_multiple_registration=True)" + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -112,12 +112,10 @@ }, { "cell_type": "code", - "execution_count": null, "id": "99e3edfcdf4ff697", "metadata": { "collapsed": false }, - "outputs": [], "source": [ "from qupulse_mfli.mfli import MFLIDAQ, postprocessing_average_within_windows\n", "\n", @@ -125,15 +123,9 @@ "assert mfli_serial != 'DEVXXXX', \"Please enter the serial of a connected MFLI\"\n", "\n", "mfli = MFLIDAQ.connect_to(mfli_serial)" - ] - }, - { - "cell_type": "markdown", - "id": "30b5fb5fec55af39", - "metadata": { - "collapsed": false - }, - "source": [] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -147,25 +139,24 @@ }, { "cell_type": "code", - "execution_count": null, "id": "6afb0d40c704a02", "metadata": { "collapsed": false }, - "outputs": [], "source": [ "from qupulse.hardware.setup import MeasurementMask\n", "\n", "hw_setup.set_measurement('SET1', MeasurementMask(mfli, 'AverageR'))\n", - "hw_setup.set_measurement('SET2', MeasurementMask(mfli, 'AverageAux1'))" - ] + "hw_setup.set_measurement('SET2', MeasurementMask(mfli, 'AverageAux1'))\n", + "hw_setup.set_measurement('SET_ALL', [MeasurementMask(mfli, 'AverageR'), MeasurementMask(mfli, 'AverageAux1')], allow_multiple_registration=True)\n" + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "id": "c943fb4d", "metadata": {}, - "outputs": [], "source": [ "# linking the measurement mask names to physical input channels\n", "mfli.register_measurement_channel(program_name=None, channel_path=\"demods/0/sample.R\", window_name=\"AverageR\")\n", @@ -185,29 +176,31 @@ "where the keys of the dict are the values for the window_name, and the values of the dict are the channel_path inputs. Note that these can also be lists to record multiple channels under one name. I.e. for IQ demodulation.\n", "\n", "'''" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "id": "51401e51", "metadata": {}, - "outputs": [], "source": [ "# configuring the driver to average all datapoint for each window.\n", "mfli.register_operations(None, postprocessing_average_within_windows)\n", "\n", "# one can also register the ```qupulse_mfli.mfli.postprocessing_crop_windows``` post processing function to return the data that was recorded for within window without averaging.\n", "# Or one could register ```None``` to return the raw data recorded without considering the windows." - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "id": "417c4976", "metadata": {}, - "outputs": [], "source": [ + "\n", + "import numpy as np\n", "# registering the trigger for the program named \"run_for_ever\". Which armes the lockin once, and recordes for each observed trigger on AuxIn1.\n", "mfli.register_trigger_settings(program_name=\"run_for_ever\",\n", " trigger_input=f\"demods/0/sample.AuxIn1\", # here AuxInN referese to the printer label N+1\n", @@ -229,7 +222,9 @@ " measurement_count=1,\n", " other_settings={\"holdoff/time\": 1e-3}\n", " ) " - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -237,73 +232,106 @@ "metadata": { "collapsed": false }, - "source": [ - "from\n", - "\n", - "We define the pulse template in terms of the potentials of quantum dot one and two `Q1` and `Q2` and provide a linear transformation that maps them to the output voltages `P1` and `P2`." - ] + "source": "We define the pulse template in terms of the potentials of quantum dot one and two `Q1` and `Q2` and provide a linear transformation that maps them to the output voltages `P1` and `P2`." }, { "cell_type": "code", - "execution_count": null, "id": "27610ca4eb6cda25", "metadata": { "collapsed": false }, - "outputs": [], "source": [ "from qupulse.pulses import *\n", "import numpy as np\n", "from qupulse.program.transformation import LinearTransformation\n", + "from qupulse.program.loop import Loop, LoopBuilder, roll_constant_waveforms\n", + "\n", + "sample_rate = awg.sample_rate\n", "\n", "pt = (ConstantPT(2**20, {\n", " 'Q1': '-0.1 + x_i * 0.02',\n", - " 'Q2': '-0.2 + y_i * 0.05'})\n", + " 'Q2': '-0.2 + y_i * 0.05'}, measurements=[('meas', 0, 2**20)])\n", " .with_iteration('x_i', 'N_x')\n", " .with_iteration('y_i', 'N_y'))\n", "\n", - "trafo = LinearTransformation(np.array([[1., -.1], [-.09, 1.]]),\n", + "trafo = LinearTransformation(np.array([[1., -.1], [-.09, 1.]])*0.5,\n", " ('Q1', 'Q2'),\n", " ('P1', 'P2'))\n", "\n", - "program = pt.create_program(parameters={'N_x': 50, 'N_y': 30}, global_transformation=trafo)" - ] + "measurement_mapping = {'meas': 'SET_ALL'}\n", + "\n", + "# we chose the default LoopBuilder program builder here as it is the only supported as the time of writing this example \n", + "program: Loop = pt.create_program(parameters={'N_x': 20, 'N_y': 30}, global_transformation=trafo, program_builder=LoopBuilder(), measurement_mapping=measurement_mapping)" + ], + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## HDAWG: Waveform compression and sample rate reduction\n", + "\n", + "The HDAWG has the capability to dynamically reduce the sample rate by a power of two during playback. The driver does this automatically if it detects a compatible waveform that is (piecewise) constant.\n", + "\n", + "However, the current implementation samples all waveforms in the computer memory. We have a lot (N_x * N_y) of very long waveforms which each take 4 MB in computer memory when sampled with 1GHz. For a sufficiently high resolution this will eat up our RAM with constant waveforms. qupulse provides `roll_constant_waveforms` to detect long constant waveforms and roll them into loops **inplace** if possible with the given parameters. This will remove the measurements from the `Loop` program because they cannot be preserved by the logic. Therefore, we extract them beforehand." + ], + "id": "cb014734179113dd" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# extract measurement positions\n", + "measurements = program.get_measurement_windows(drop=True)\n", + "\n", + "print(f'Single point before rolling: {program[0]!r}')\n", + "\n", + "# Compress program\n", + "roll_constant_waveforms(program, sample_rate=sample_rate / 10**9, waveform_quantum=256, minimal_waveform_quanta=16)\n", + "\n", + "print(f'Single point after rolling: {program[0]!r}')" + ], + "id": "435420307d15bfd2", + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "id": "346dabd84ea976fd", "metadata": { "collapsed": false }, - "outputs": [], "source": [ + "hw_setup.clear_programs()\n", "hw_setup.register_program('csd', program, awg.run_current_program)" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "id": "832c501c9082dc5d", "metadata": { "collapsed": false }, - "outputs": [], "source": [ "hw_setup.arm_program('csd')" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "id": "954f4e09664d073f", "metadata": { "collapsed": false }, - "outputs": [], "source": [ "hw_setup.run_program('csd')" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -317,17 +345,17 @@ }, { "cell_type": "code", - "execution_count": null, "id": "636ea90347e59fe5", "metadata": { "collapsed": false }, - "outputs": [], "source": [ "# receaving the recorded data from the MFLI\n", "\n", "data = mfli.measure_program(wait=False) # wait=True would wait until the aquesition is finished.\n" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -340,10 +368,12 @@ ] }, { - "cell_type": "markdown", - "id": "d88b11db", "metadata": {}, - "source": [] + "cell_type": "code", + "source": "", + "id": "d3aa849c80214139", + "outputs": [], + "execution_count": null } ], "metadata": { From f9624ff8661a338f19f140d838c984ada4c798ba Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 11 Apr 2024 10:01:08 +0200 Subject: [PATCH 189/441] Make linear transformations return native scalars if the input is scalar --- qupulse/program/transformation.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qupulse/program/transformation.py b/qupulse/program/transformation.py index 784e8e193..dff8682ff 100644 --- a/qupulse/program/transformation.py +++ b/qupulse/program/transformation.py @@ -175,8 +175,9 @@ def __call__(self, time: Union[np.ndarray, float], transformed_data = self._matrix @ data_in - for idx, out_channel in enumerate(self._output_channels): - data_out[out_channel] = transformed_data[idx, ...] + assert transformed_data.shape[0] == len(self._output_channels) + for out_channel, transformed_channel_data in zip(self._output_channels, transformed_data): + data_out[out_channel] = transformed_channel_data return data_out From 0efb98f11d0e99fb854ef8bf8f376cfe0093d32b Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 12 Apr 2024 10:46:50 +0200 Subject: [PATCH 190/441] Add optional measurement arguemnt to HardwareSetup --- qupulse/hardware/setup.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/qupulse/hardware/setup.py b/qupulse/hardware/setup.py index 69c15461f..e976a6743 100644 --- a/qupulse/hardware/setup.py +++ b/qupulse/hardware/setup.py @@ -92,7 +92,20 @@ def __init__(self): def register_program(self, name: str, program: Loop, - run_callback=lambda: None, update=False) -> None: + run_callback=lambda: None, + update: bool = False, + measurements: Mapping[str, Tuple[np.ndarray, np.ndarray]] = None) -> None: + """Register a program under a given name at the hardware setup. The program will be uploaded to the + participating AWGs and DACs. The run callback is used for triggering the program after arming. + + Args: + name: Name of the program. + program: Output of :py:meth:`~PulseTemplate.create_program` + run_callback: Used to trigger the program after arming + update: Must be set if the program is already known. + measurements: Will be used as measurements if provided. Otherwise, the measurements are extracted from the program. + + """ if not callable(run_callback): raise TypeError('The provided run_callback is not callable') @@ -101,8 +114,11 @@ def register_program(self, name: str, raise KeyError('The following channels are unknown to the HardwareSetup: {}'.format( channels - set(self._channel_map.keys()))) + if measurements is None: + measurements = program.get_measurement_windows(drop=True) + temp_measurement_windows = defaultdict(list) - for mw_name, begins_lengths in program.get_measurement_windows(drop=True).items(): + for mw_name, begins_lengths in measurements.items(): temp_measurement_windows[mw_name].append(begins_lengths) if set(temp_measurement_windows.keys()) - set(self._measurement_map.keys()): From e776d80b19e0a71bdb9b72a485d8a687684673ba Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 12 Apr 2024 10:47:59 +0200 Subject: [PATCH 191/441] Convert scalar numpy arrays to builtins for hashability in ConstantWaveform --- qupulse/program/waveforms.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qupulse/program/waveforms.py b/qupulse/program/waveforms.py index 5080a92e8..94c395532 100644 --- a/qupulse/program/waveforms.py +++ b/qupulse/program/waveforms.py @@ -398,6 +398,9 @@ class ConstantWaveform(Waveform): def __init__(self, duration: Real, amplitude: Any, channel: ChannelID): """ Create a qupulse waveform corresponding to a ConstantPulseTemplate """ super().__init__(duration=_to_time_type(duration)) + if hasattr(amplitude, 'shape'): + amplitude = amplitude[()] + hash(amplitude) self._amplitude = amplitude self._channel = channel From 336b8a12a7fd37c7e40e6f8d412c4ced76aee204 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 12 Apr 2024 10:48:31 +0200 Subject: [PATCH 192/441] Nearly working HDAWG example --- .../examples/04ZurichInstrumentsSetup.ipynb | 76 ++++++++++++++++--- 1 file changed, 67 insertions(+), 9 deletions(-) diff --git a/doc/source/examples/04ZurichInstrumentsSetup.ipynb b/doc/source/examples/04ZurichInstrumentsSetup.ipynb index efe974376..a1642b87a 100644 --- a/doc/source/examples/04ZurichInstrumentsSetup.ipynb +++ b/doc/source/examples/04ZurichInstrumentsSetup.ipynb @@ -203,7 +203,8 @@ "import numpy as np\n", "# registering the trigger for the program named \"run_for_ever\". Which armes the lockin once, and recordes for each observed trigger on AuxIn1.\n", "mfli.register_trigger_settings(program_name=\"run_for_ever\",\n", - " trigger_input=f\"demods/0/sample.AuxIn1\", # here AuxInN referese to the printer label N+1\n", + " #trigger_input=f\"demods/0/sample.AuxIn1\", # here AuxInN referese to the printer label N+1\n", + " trigger_input=\"scopes/0/trigchannel\",\n", " edge=\"rising\",\n", " trigger_count=3, # this defines the number of triggers to capture in one measurement (i.e. rows). E.g. one measurement contains 3 Trigger events, which might be somehting one could do when crafting the programs carefully.\n", " level=.5, # this sets the trigger level\n", @@ -232,7 +233,12 @@ "metadata": { "collapsed": false }, - "source": "We define the pulse template in terms of the potentials of quantum dot one and two `Q1` and `Q2` and provide a linear transformation that maps them to the output voltages `P1` and `P2`." + "source": [ + "## Pulse definition\n", + "\n", + "Next we define a pulse that we want to use. We settle for a two-dimensional scan of a voltage space but we define the scan in terms of virtual gates, i.e. the potentials that the quantum dots `Q1` and `Q2` see.\n", + "Then we provide a linear transformation that maps them to the output voltages `P1` and `P2`." + ] }, { "cell_type": "code", @@ -246,13 +252,15 @@ "from qupulse.program.transformation import LinearTransformation\n", "from qupulse.program.loop import Loop, LoopBuilder, roll_constant_waveforms\n", "\n", - "sample_rate = awg.sample_rate\n", + "awg_sample_rate = 10**9\n", + "hdawg.set_sample_clock(awg_sample_rate)\n", "\n", "pt = (ConstantPT(2**20, {\n", " 'Q1': '-0.1 + x_i * 0.02',\n", " 'Q2': '-0.2 + y_i * 0.05'}, measurements=[('meas', 0, 2**20)])\n", " .with_iteration('x_i', 'N_x')\n", - " .with_iteration('y_i', 'N_y'))\n", + " .with_iteration('y_i', 'N_y')\n", + " .with_parallel_channels({'Marker': 1}))\n", "\n", "trafo = LinearTransformation(np.array([[1., -.1], [-.09, 1.]])*0.5,\n", " ('Q1', 'Q2'),\n", @@ -261,7 +269,11 @@ "measurement_mapping = {'meas': 'SET_ALL'}\n", "\n", "# we chose the default LoopBuilder program builder here as it is the only supported as the time of writing this example \n", - "program: Loop = pt.create_program(parameters={'N_x': 20, 'N_y': 30}, global_transformation=trafo, program_builder=LoopBuilder(), measurement_mapping=measurement_mapping)" + "program: Loop = pt.create_program(parameters={'N_x': 20, 'N_y': 30},\n", + " global_transformation=trafo,\n", + " program_builder=LoopBuilder(),\n", + " measurement_mapping=measurement_mapping,\n", + " channel_mapping={'Marker': 'Trig'})" ], "outputs": [], "execution_count": null @@ -288,7 +300,7 @@ "print(f'Single point before rolling: {program[0]!r}')\n", "\n", "# Compress program\n", - "roll_constant_waveforms(program, sample_rate=sample_rate / 10**9, waveform_quantum=256, minimal_waveform_quanta=16)\n", + "roll_constant_waveforms(program, sample_rate=awg_sample_rate / 10**9, waveform_quantum=256, minimal_waveform_quanta=16)\n", "\n", "print(f'Single point after rolling: {program[0]!r}')" ], @@ -304,8 +316,19 @@ }, "source": [ "hw_setup.clear_programs()\n", - "hw_setup.register_program('csd', program, awg.run_current_program)" + "hw_setup.register_program('csd', program, awg.run_current_program, measurements=measurements)" + ], + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "hdawg.output(1, True)\n", + "hdawg.output(2, True)" ], + "id": "5d7d63d18bd6fc25", "outputs": [], "execution_count": null }, @@ -333,6 +356,17 @@ "outputs": [], "execution_count": null }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "hdawg.output(1, False)\n", + "hdawg.output(2, False)" + ], + "id": "f4537c8c741597b0", + "outputs": [], + "execution_count": null + }, { "cell_type": "markdown", "id": "e517992b6a35fc07", @@ -352,7 +386,7 @@ "source": [ "# receaving the recorded data from the MFLI\n", "\n", - "data = mfli.measure_program(wait=False) # wait=True would wait until the aquesition is finished.\n" + "data = mfli.measure_program(wait=False) # wait=True would wait until the aquisition is finished.\n" ], "outputs": [], "execution_count": null @@ -370,10 +404,34 @@ { "metadata": {}, "cell_type": "code", - "source": "", + "source": [ + "data_0 = data[0]\n", + "(average_r,), = data_0['AverageR'].values()\n", + "(average_aux,), = data_0['AverageAux1'].values()" + ], "id": "d3aa849c80214139", "outputs": [], "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "plt.plot(average_r)" + ], + "id": "bc7de11e789884f2", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": "plt.plot(average_aux)", + "id": "14c5cd608d2238a3", + "outputs": [], + "execution_count": null } ], "metadata": { From 6960b5bacbfde5a50c7d1a6e37926b9c170b7e30 Mon Sep 17 00:00:00 2001 From: Nomos11 <82180697+Nomos11@users.noreply.github.com> Date: Mon, 15 Apr 2024 13:53:01 +0200 Subject: [PATCH 193/441] ProgramEntry(loop=...)->ProgramEntry(program=...) only affects tabor --- qupulse/_program/tabor.py | 2 +- tests/hardware/base_tests.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/qupulse/_program/tabor.py b/qupulse/_program/tabor.py index 235cc945c..78f2c4f93 100644 --- a/qupulse/_program/tabor.py +++ b/qupulse/_program/tabor.py @@ -408,7 +408,7 @@ def __init__(self, else: mode = TaborSequencing.SINGLE - super().__init__(loop=program, + super().__init__(program=program, channels=channels, markers=markers, amplitudes=amplitudes, diff --git a/tests/hardware/base_tests.py b/tests/hardware/base_tests.py index db6585737..137fd071c 100644 --- a/tests/hardware/base_tests.py +++ b/tests/hardware/base_tests.py @@ -43,7 +43,7 @@ def test_init(self): expected_waveforms = OrderedDict(zip(self.waveforms, sampled)) with mock.patch.object(ProgramEntry, '_sample_waveforms', return_value=sampled) as sample_waveforms: - entry = ProgramEntry(loop=self.loop, + entry = ProgramEntry(program=self.loop, channels=self.channels, markers=self.marker, amplitudes=self.amplitudes, @@ -56,7 +56,7 @@ def test_init(self): sample_waveforms.assert_not_called() with mock.patch.object(ProgramEntry, '_sample_waveforms', return_value=sampled) as sample_waveforms: - entry = ProgramEntry(loop=self.loop, + entry = ProgramEntry(program=self.loop, channels=self.channels, markers=self.marker, amplitudes=self.amplitudes, @@ -68,7 +68,7 @@ def test_init(self): sample_waveforms.assert_called_once_with(expected_default) with mock.patch.object(ProgramEntry, '_sample_waveforms', return_value=sampled[:1]) as sample_waveforms: - entry = ProgramEntry(loop=self.loop, + entry = ProgramEntry(program=self.loop, channels=self.channels, markers=self.marker, amplitudes=self.amplitudes, @@ -89,7 +89,7 @@ def test_sample_waveforms(self): ((self.sampled[1]['A'], empty_ch, 2.*(self.sampled[1]['C'] - 0.1)), (empty_m, self.sampled[1]['M'] != 0)) ] - entry = ProgramEntry(loop=self.loop, + entry = ProgramEntry(program=self.loop, channels=self.channels, markers=self.marker, amplitudes=self.amplitudes, From a1dc886aad000c6d55f434cf24d10ebd601df480 Mon Sep 17 00:00:00 2001 From: Nomos11 <82180697+Nomos11@users.noreply.github.com> Date: Mon, 15 Apr 2024 13:53:11 +0200 Subject: [PATCH 194/441] remove duplicate tests --- tests/program/linspace_tests2.py | 289 ------------------------------- 1 file changed, 289 deletions(-) delete mode 100644 tests/program/linspace_tests2.py diff --git a/tests/program/linspace_tests2.py b/tests/program/linspace_tests2.py deleted file mode 100644 index 32494d18d..000000000 --- a/tests/program/linspace_tests2.py +++ /dev/null @@ -1,289 +0,0 @@ -import copy -import unittest -from unittest import TestCase - -from qupulse.pulses import * -from qupulse.program.linspace import * -from qupulse.program.transformation import * - -class SingleRampTest(TestCase): - def setUp(self): - hold = ConstantPT(64*10 ** 5, {'a': '-1. + idx * 0.01'}) - self.pulse_template = hold.with_iteration('idx', 200) - - self.program = LinSpaceIter( - length=200, - body=(LinSpaceHold( - bases=(-1.,), - factors=((0.01,),), - duration_base=TimeType(10**6), - duration_factors=None - ),) - ) - - key = DepKey.from_voltages((0.01,), DEFAULT_INCREMENT_RESOLUTION) - - self.commands = [ - Set(0, -1.0, key), - Wait(TimeType(10 ** 6)), - LoopLabel(0, 199), - Increment(0, 0.01, key), - Wait(TimeType(10 ** 6)), - LoopJmp(0) - ] - - def test_program(self): - program_builder = LinSpaceBuilder(('a',)) - program = self.pulse_template.create_program(program_builder=program_builder) - self.assertEqual([self.program], program) - - def test_commands(self): - commands = to_increment_commands([self.program]) - self.assertEqual(self.commands, commands) - - -class PlainCSDTest(TestCase): - def setUp(self,time_factor=1e3,rep_factor=2): - hold = ConstantPT(64*time_factor, {'a': '-1. + idx_a * 0.01', 'b': '-.5 + idx_b * 0.02'}) - scan_a = hold.with_iteration('idx_a', rep_factor*10) - self.pulse_template = scan_a.with_iteration('idx_b', rep_factor*10) - - self.program = LinSpaceIter(length=100, body=(LinSpaceIter( - length=200, - body=(LinSpaceHold( - bases=(-1., -0.5), - factors=((0.0, 0.01), - (0.02, 0.0)), - duration_base=TimeType(10**6), - duration_factors=None - ),) - ),)) - - key_0 = DepKey.from_voltages((0, 0.01,), DEFAULT_INCREMENT_RESOLUTION) - key_1 = DepKey.from_voltages((0.02,), DEFAULT_INCREMENT_RESOLUTION) - - self.commands = [ - Set(0, -1.0, key_0), - Set(1, -0.5, key_1), - Wait(TimeType(10 ** 6)), - - LoopLabel(0, 199), - Increment(0, 0.01, key_0), - Wait(TimeType(10 ** 6)), - LoopJmp(0), - - LoopLabel(1, 99), - - Increment(0, -2.0, key_0), - Increment(1, 0.02, key_1), - Wait(TimeType(10 ** 6)), - - LoopLabel(2, 199), - Increment(0, 0.01, key_0), - Wait(TimeType(10 ** 6)), - LoopJmp(2), - - LoopJmp(1), - ] - - def test_program(self): - program_builder = LinSpaceBuilder(('a', 'b')) - program = self.pulse_template.create_program(program_builder=program_builder) - self.assertEqual([self.program], program) - - def test_increment_commands(self): - commands = to_increment_commands([self.program]) - self.assertEqual(self.commands, commands) - - -class TiltedCSDTest(TestCase): - def setUp(self,time_factor=1e3,rep_factor=2): - hold = ConstantPT(64*time_factor, {'a': '-1. + idx_a * 0.01 + idx_b * 1e-3', 'b': '-.5 + idx_b * 0.02 - 3e-3 * idx_a'}) - scan_a = hold.with_iteration('idx_a', rep_factor*10) - self.pulse_template = scan_a.with_iteration('idx_b', rep_factor*10) - self.repeated_pt = self.pulse_template.with_repetition(42) - - self.program = LinSpaceIter(length=100, body=(LinSpaceIter( - length=200, - body=(LinSpaceHold( - bases=(-1., -0.5), - factors=((1e-3, 0.01), - (0.02, -3e-3)), - duration_base=TimeType(10**6), - duration_factors=None - ),) - ),)) - self.repeated_program = LinSpaceRepeat(body=(self.program,), count=42) - - key_0 = DepKey.from_voltages((1e-3, 0.01,), DEFAULT_INCREMENT_RESOLUTION) - key_1 = DepKey.from_voltages((0.02, -3e-3), DEFAULT_INCREMENT_RESOLUTION) - - self.commands = [ - Set(0, -1.0, key_0), - Set(1, -0.5, key_1), - Wait(TimeType(10 ** 6)), - - LoopLabel(0, 199), - Increment(0, 0.01, key_0), - Increment(1, -3e-3, key_1), - Wait(TimeType(10 ** 6)), - LoopJmp(0), - - LoopLabel(1, 99), - - Increment(0, 1e-3 + -200 * 1e-2, key_0), - Increment(1, 0.02 + -200 * -3e-3, key_1), - Wait(TimeType(10 ** 6)), - - LoopLabel(2, 199), - Increment(0, 0.01, key_0), - Increment(1, -3e-3, key_1), - Wait(TimeType(10 ** 6)), - LoopJmp(2), - - LoopJmp(1), - ] - inner_commands = copy.deepcopy(self.commands) - for cmd in inner_commands: - if hasattr(cmd, 'idx'): - cmd.idx += 1 - self.repeated_commands = [LoopLabel(0, 42)] + inner_commands + [LoopJmp(0)] - - def test_program(self): - program_builder = LinSpaceBuilder(('a', 'b')) - program = self.pulse_template.create_program(program_builder=program_builder) - self.assertEqual([self.program], program) - - def test_repeated_program(self): - program_builder = LinSpaceBuilder(('a', 'b')) - program = self.repeated_pt.create_program(program_builder=program_builder) - self.assertEqual([self.repeated_program], program) - - def test_increment_commands(self): - commands = to_increment_commands([self.program]) - self.assertEqual(self.commands, commands) - - def test_repeated_increment_commands(self): - commands = to_increment_commands([self.repeated_program]) - self.assertEqual(self.repeated_commands, commands) - - -class SingletLoadProcessing(): - def setUp(self,time_factor=1e2,rep_factor=2): - wait = ConstantPT(64*time_factor*1e1, {'a': '-1. + idx_a * 0.01', 'b': '-.5 + idx_b * 0.02'}) - load_random = ConstantPT(10 ** 5, {'a': -.4, 'b': -.3}) - meas = ConstantPT(64*time_factor, {'a': 0.05, 'b': 0.06}) - - singlet_scan = (load_random @ wait @ meas).with_iteration('idx_a', rep_factor*10*2).with_iteration('idx_b', rep_factor*10) - self.pulse_template = singlet_scan - - self.program = LinSpaceIter(length=100, body=(LinSpaceIter( - length=200, - body=( - LinSpaceHold(bases=(-0.4, -0.3), factors=(None, None), duration_base=TimeType(10 ** 5), - duration_factors=None), - LinSpaceHold(bases=(-1., -0.5), - factors=((0.0, 0.01), - (0.02, 0.0)), - duration_base=TimeType(10 ** 6), - duration_factors=None), - LinSpaceHold(bases=(0.05, 0.06), factors=(None, None), duration_base=TimeType(10 ** 5), - duration_factors=None), - ) - ),)) - - key_0 = DepKey.from_voltages((0, 0.01,), DEFAULT_INCREMENT_RESOLUTION) - key_1 = DepKey.from_voltages((0.02,), DEFAULT_INCREMENT_RESOLUTION) - - self.commands = [ - Set(0, -0.4), - Set(1, -0.3), - Wait(TimeType(10 ** 5)), - Set(0, -1.0, key_0), - Set(1, -0.5, key_1), - Wait(TimeType(10 ** 6)), - Set(0, 0.05), - Set(1, 0.06), - Wait(TimeType(10 ** 5)), - - LoopLabel(0, 199), - Set(0, -0.4), - Set(1, -0.3), - Wait(TimeType(10 ** 5)), - Increment(0, 0.01, key_0), - Increment(1, 0.00, key_1), - Wait(TimeType(10 ** 6)), - Set(0, 0.05), - Set(1, 0.06), - Wait(TimeType(10 ** 5)), - LoopJmp(0), - - LoopLabel(1, 99), - - Set(0, -0.4), - Set(1, -0.3), - Wait(TimeType(10 ** 5)), - Increment(0, -2.0, key_0), - Increment(1, 0.02, key_1), - Wait(TimeType(10 ** 6)), - Set(0, 0.05), - Set(1, 0.06), - Wait(TimeType(10 ** 5)), - - LoopLabel(2, 199), - - Set(0, -0.4), - Set(1, -0.3), - Wait(TimeType(10 ** 5)), - Increment(0, 0.01, key_0), - Increment(1, 0.00, key_1), - Wait(TimeType(10 ** 6)), - Set(0, 0.05), - Set(1, 0.06), - Wait(TimeType(10 ** 5)), - - LoopJmp(2), - - LoopJmp(1), - ] - - def test_singlet_scan_program(self): - program_builder = LinSpaceBuilder(('a', 'b')) - program = self.pulse_template.create_program(program_builder=program_builder) - self.assertEqual([self.program], program) - - def test_singlet_scan_commands(self): - commands = to_increment_commands([self.program]) - self.assertEqual(self.commands, commands) - - -class TransformedRampTest(TestCase): - def setUp(self): - hold = ConstantPT(10 ** 6, {'a': '-1. + idx * 0.01'}) - self.pulse_template = hold.with_iteration('idx', 200) - self.transformation = ScalingTransformation({'a': 2.0}) - - self.program = LinSpaceIter( - length=200, - body=(LinSpaceHold( - bases=(-2.,), - factors=((0.02,),), - duration_base=TimeType(10 ** 6), - duration_factors=None - ),) - ) - - def test_global_trafo_program(self): - program_builder = LinSpaceBuilder(('a',)) - program = self.pulse_template.create_program(program_builder=program_builder, - global_transformation=self.transformation) - self.assertEqual([self.program], program) - - def test_local_trafo_program(self): - program_builder = LinSpaceBuilder(('a',)) - with self.assertRaises(NotImplementedError): - # not implemented yet. This test should work as soon as its implemented - program = self.pulse_template.create_program(program_builder=program_builder, - global_transformation=self.transformation, - to_single_waveform={self.pulse_template}) - self.assertEqual([self.program], program) From 1c74cbded175d6c27ea07b1bdf9fbc00950cfcf1 Mon Sep 17 00:00:00 2001 From: Nomos11 <82180697+Nomos11@users.noreply.github.com> Date: Mon, 15 Apr 2024 13:55:17 +0200 Subject: [PATCH 195/441] remove outdated workaround --- qupulse/program/linspace.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/qupulse/program/linspace.py b/qupulse/program/linspace.py index ee6aa10ca..0d454c090 100644 --- a/qupulse/program/linspace.py +++ b/qupulse/program/linspace.py @@ -144,16 +144,6 @@ def hold_voltage(self, duration: HardwareTime, voltages: Mapping[ChannelID, Hard bases.append(value) factors.append(None) continue - #there mightbe a bug in some waveform.constant_value, where an array of float instead of float is returned - #(goes against the typehints) - if isinstance(value, np.ndarray): - try: - value = float(value) - bases.append(value) - factors.append(None) - except: - raise RuntimeError('hack doesnt work') - continue offsets = value.offsets base = value.base incs = [] From 32058e97bdea597c7fae6eef0b2c06525cca6785 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Wed, 24 Apr 2024 12:33:31 +0200 Subject: [PATCH 196/441] Make plot function accept a program --- qupulse/plotting.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/qupulse/plotting.py b/qupulse/plotting.py index 600087ed9..0a62451fa 100644 --- a/qupulse/plotting.py +++ b/qupulse/plotting.py @@ -122,9 +122,9 @@ def _render_loop(loop: Loop, return waveform, measurements -def plot(pulse: PulseTemplate, - parameters: Dict[str, Real]=None, - sample_rate: Optional[Real]=10, +def plot(pulse: Union[PulseTemplate, Loop], + parameters: Optional[Dict[str, Real]] = None, + sample_rate: Optional[Real] = 10, axes: Any=None, show: bool=True, plot_channels: Optional[Set[ChannelID]]=None, @@ -162,14 +162,14 @@ def plot(pulse: PulseTemplate, """ from matplotlib import pyplot as plt - channels = pulse.defined_channels - - if parameters is None: - parameters = dict() + try: + program = pulse.create_program(parameters=parameters) + except AttributeError: + program = pulse if sample_rate is None: if time_slice is None: - duration = pulse.duration + duration = program.duration else: duration = time_slice[1]-time_slice[0] if duration == 0: @@ -177,10 +177,6 @@ def plot(pulse: PulseTemplate, else: duration_per_sample = float(duration) / 1000 sample_rate = 1 / duration_per_sample - - program = pulse.create_program(parameters=parameters, - channel_mapping={ch: ch for ch in channels}, - measurement_mapping={w: w for w in pulse.measurement_names}) if program is not None: times, voltages, measurements = render(program, From a36a0b406a377bbfe35fa95e0a695fc52c957871 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Wed, 24 Apr 2024 12:34:14 +0200 Subject: [PATCH 197/441] Working example --- .../examples/04ZurichInstrumentsSetup.ipynb | 109 +++++++++++------- 1 file changed, 68 insertions(+), 41 deletions(-) diff --git a/doc/source/examples/04ZurichInstrumentsSetup.ipynb b/doc/source/examples/04ZurichInstrumentsSetup.ipynb index a1642b87a..851c806bc 100644 --- a/doc/source/examples/04ZurichInstrumentsSetup.ipynb +++ b/doc/source/examples/04ZurichInstrumentsSetup.ipynb @@ -13,12 +13,19 @@ "\n", "## Connections and wiring\n", "\n", - "The example here assumes a very nonsensical wiring that does not require anything else besides an HDAWG, and MFLI and three cables/adapters to connect SMB to BNC ports.\n", + "The example here assumes a very nonsensical wiring that does not require anything else besides an HDAWG, and MFLI and three cables/adapters to connect SMB to BNC ports. We assume the following connections:\n", + "\n", + "```\n", + "HDAWG_1_WAVE -> MFLI_AUX_IN_1\n", + "HDAWG_2_WAVE -> MFLI_AUX_IN_2\n", + "HDAWG_1_MARK_FRONT -> MFLI_TRIG_IN_1\n", + "```\n", + "`MFLI_TRIG_IN_1` is located on the back of the device.\n", "\n", "## Hardware Setup\n", "\n", "The hardware setup class provides a layer to map output channels to an arbitrary number of physical channels.\n", - "It also provides a mapping of measurement windows to specific dac instruments" + "It also provides a mapping of measurement windows to specific dac instruments." ] }, { @@ -105,7 +112,7 @@ "collapsed": false }, "source": [ - "## MFLI\n", + "### MFLI\n", "\n", "Next we will connect the MFLI." ] @@ -134,7 +141,9 @@ "collapsed": false }, "source": [ - "So" + "### Measurement masks\n", + "\n", + "qupulse has multiple layers where measurements are mapped. The hardware setup can map measurement windows to potentially multiple measurement masks, which are a combination of an instrument and an instrument specific identifier." ] }, { @@ -146,25 +155,41 @@ "source": [ "from qupulse.hardware.setup import MeasurementMask\n", "\n", - "hw_setup.set_measurement('SET1', MeasurementMask(mfli, 'AverageR'))\n", - "hw_setup.set_measurement('SET2', MeasurementMask(mfli, 'AverageAux1'))\n", - "hw_setup.set_measurement('SET_ALL', [MeasurementMask(mfli, 'AverageR'), MeasurementMask(mfli, 'AverageAux1')], allow_multiple_registration=True)\n" + "hw_setup.set_measurement('SET1', MeasurementMask(mfli, 'AverageAux1'))\n", + "hw_setup.set_measurement('SET2', MeasurementMask(mfli, 'AverageAux2'))\n", + "hw_setup.set_measurement('SET_ALL', [MeasurementMask(mfli, 'AverageAux1'), MeasurementMask(mfli, 'AverageAux2')], allow_multiple_registration=True)\n" ], "outputs": [], "execution_count": null }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "Each instrument can do arbitrary things with the identifier from the mask which heavily depends on what the instrument can do and what you use it for.\n", + "\n", + "The MLFI maps the names to internal paths following your configuration. You can make the configuration global or program specific." + ], + "id": "e7353faf52ebd31d" + }, { "cell_type": "code", "id": "c943fb4d", "metadata": {}, "source": [ "# linking the measurement mask names to physical input channels\n", - "mfli.register_measurement_channel(program_name=None, channel_path=\"demods/0/sample.R\", window_name=\"AverageR\")\n", - "mfli.register_measurement_channel(program_name=None, channel_path=\"auxins/0/sample.AuxIn0.avg\", window_name=\"AverageAux1\")\n", - "\n", - "'''\n", - "\n", + "mfli.register_measurement_channel(program_name=None, channel_path=\"demods/0/sample.AuxIn0\", window_name=\"AverageAux2\")\n", + "mfli.register_measurement_channel(program_name=None, channel_path=\"auxins/0/sample.AuxIn1\", window_name=\"AverageAux1\")" + ], + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ "The other inputs can be addressed via strings as the following:\n", + "```\n", "{\n", " \"R\": [\"demods/0/sample.R\"],\n", " \"X\": [\"demods/0/sample.X\"],\n", @@ -172,13 +197,16 @@ " \"A\": [\"auxins/0/sample.AuxIn0.avg\"],\n", " \"many\": [\"demods/0/sample.R\", \"auxins/0/sample.AuxIn0.avg\", \"demods/0/sample.X\", \"demods/0/sample.Y\"]\n", "}\n", - "\n", + "```\n", "where the keys of the dict are the values for the window_name, and the values of the dict are the channel_path inputs. Note that these can also be lists to record multiple channels under one name. I.e. for IQ demodulation.\n", "\n", - "'''" + "### Operations\n", + "\n", + "Each driver can automatically perform certain operations on the recorded data. The MFLI expects a callable that processes the raw data returned by the instrument. This is suboptimal but the current solution. If you want to implement your own operation look at the shipped postprocessing functions for the signature.\n", + "\n", + "There are other functions you can use defined in the mfli package like `postprocessing_crop_windows`. Please file an issue if this documentation here is out of date." ], - "outputs": [], - "execution_count": null + "id": "9bb6bf76c80276e9" }, { "cell_type": "code", @@ -186,33 +214,25 @@ "metadata": {}, "source": [ "# configuring the driver to average all datapoint for each window.\n", - "mfli.register_operations(None, postprocessing_average_within_windows)\n", - "\n", - "# one can also register the ```qupulse_mfli.mfli.postprocessing_crop_windows``` post processing function to return the data that was recorded for within window without averaging.\n", - "# Or one could register ```None``` to return the raw data recorded without considering the windows." + "mfli.register_operations(\n", + " program_name=None,\n", + " operations=postprocessing_average_within_windows\n", + ")" ], "outputs": [], "execution_count": null }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "", + "id": "ee6314ed25574b8f" + }, { "cell_type": "code", "id": "417c4976", "metadata": {}, "source": [ - "\n", - "import numpy as np\n", - "# registering the trigger for the program named \"run_for_ever\". Which armes the lockin once, and recordes for each observed trigger on AuxIn1.\n", - "mfli.register_trigger_settings(program_name=\"run_for_ever\",\n", - " #trigger_input=f\"demods/0/sample.AuxIn1\", # here AuxInN referese to the printer label N+1\n", - " trigger_input=\"scopes/0/trigchannel\",\n", - " edge=\"rising\",\n", - " trigger_count=3, # this defines the number of triggers to capture in one measurement (i.e. rows). E.g. one measurement contains 3 Trigger events, which might be somehting one could do when crafting the programs carefully.\n", - " level=.5, # this sets the trigger level\n", - " measurement_count=np.inf, # this defined the number of rounds that are to be measured (e.g. how often the \"single\" button should be pressed). E.g. after one arm call, i would like to perform np.inf measurements\n", - " other_settings={\"holdoff/time\": 1e-3} # this sets the duration for which new triggers are ignored\n", - " )\n", - "# the aquesition can be ended via mfli.stop_acquisition()\n", - "\n", "# registering trigger settings for a standard configuration\n", "# The measurement is perfomed once after one trigger on TrigIn1 is observed.\n", "mfli.register_trigger_settings(program_name=None,\n", @@ -257,7 +277,7 @@ "\n", "pt = (ConstantPT(2**20, {\n", " 'Q1': '-0.1 + x_i * 0.02',\n", - " 'Q2': '-0.2 + y_i * 0.05'}, measurements=[('meas', 0, 2**20)])\n", + " 'Q2': '-0.2 + y_i * 0.01'}, measurements=[('meas', 0, 2**20)])\n", " .with_iteration('x_i', 'N_x')\n", " .with_iteration('y_i', 'N_y')\n", " .with_parallel_channels({'Marker': 1}))\n", @@ -351,7 +371,8 @@ "collapsed": false }, "source": [ - "hw_setup.run_program('csd')" + "hw_setup.run_program('csd')\n", + "import time; time.sleep(float(program.duration) / 1e9)" ], "outputs": [], "execution_count": null @@ -386,7 +407,7 @@ "source": [ "# receaving the recorded data from the MFLI\n", "\n", - "data = mfli.measure_program(wait=False) # wait=True would wait until the aquisition is finished.\n" + "data = mfli.measure_program(wait=True) # wait=True would wait until the aquisition is finished.\n" ], "outputs": [], "execution_count": null @@ -406,20 +427,26 @@ "cell_type": "code", "source": [ "data_0 = data[0]\n", - "(average_r,), = data_0['AverageR'].values()\n", - "(average_aux,), = data_0['AverageAux1'].values()" + "(average_1,), = data_0['AverageAux1'].values()\n", + "(average_2,), = data_0['AverageAux2'].values()" ], "id": "d3aa849c80214139", "outputs": [], "execution_count": null }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Warning: As the time of writing this example there are problems when no demodulator is used at all. One channel looks like it has a sliding window average. Contribution in fixing that is highly appreciated.", + "id": "fee1a294623c50e8" + }, { "metadata": {}, "cell_type": "code", "source": [ "import matplotlib.pyplot as plt\n", "\n", - "plt.plot(average_r)" + "plt.plot(average_1, '*')" ], "id": "bc7de11e789884f2", "outputs": [], @@ -428,7 +455,7 @@ { "metadata": {}, "cell_type": "code", - "source": "plt.plot(average_aux)", + "source": "plt.plot(average_2)", "id": "14c5cd608d2238a3", "outputs": [], "execution_count": null From 306d4dce90ce402fba3f8f7a354fd9a48ec50675 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 30 May 2022 10:11:38 +0200 Subject: [PATCH 198/441] Change license to GPL --- LICENSE | 695 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- setup.cfg | 4 +- 2 files changed, 676 insertions(+), 23 deletions(-) diff --git a/LICENSE b/LICENSE index a0b058a8e..f288702d2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,674 @@ -MIT License - -Copyright (c) 2018 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/setup.cfg b/setup.cfg index a77b1e909..c053e526e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,12 +5,12 @@ description = A Quantum compUting PULse parametrization and SEquencing framework long_description = file: README.md long_description_content_type = text/markdown author = Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University -license = MIT +license = GPLv3+ license_files = LICENSE keywords = quantum, physics, control pulse, qubit classifiers = Programming Language :: Python :: 3 - License :: OSI Approved :: MIT License + OSI Approved :: GNU General Public License v3 or later (GPLv3+) Operating System :: OS Independent Topic :: Scientific/Engineering Intended Audience :: Science/Research From 2282171dec31e43a3006158b5f53dc0a0b754610 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 27 May 2024 15:25:30 +0200 Subject: [PATCH 199/441] Add SPDX license headers and move license file --- LICENSE | 674 ------------------ LICENSES/GPL-3.0-or-later.txt | 232 ++++++ qctoolkit/__init__.py | 4 + qupulse/__init__.py | 4 + qupulse/__init__.pyi | 3 + qupulse/_program/__init__.py | 4 + qupulse/_program/_loop.py | 4 + qupulse/_program/tabor.py | 4 + qupulse/_program/transformation.py | 4 + qupulse/_program/volatile.py | 4 + qupulse/_program/waveforms.py | 4 + qupulse/comparable.py | 4 + qupulse/examples/VolatileParameters.py | 4 + qupulse/expressions/__init__.py | 4 + qupulse/expressions/protocol.py | 4 + qupulse/expressions/sympy.py | 4 + qupulse/expressions/wrapper.py | 4 + qupulse/hardware/__init__.py | 4 + qupulse/hardware/awgs/__init__.py | 4 + qupulse/hardware/awgs/base.py | 4 + qupulse/hardware/awgs/dummy.py | 4 + qupulse/hardware/awgs/tabor.py | 4 + qupulse/hardware/awgs/tektronix.py | 4 + qupulse/hardware/awgs/zihdawg.py | 4 + qupulse/hardware/dacs/__init__.py | 4 + qupulse/hardware/dacs/alazar.py | 4 + qupulse/hardware/dacs/alazar2.py | 4 + qupulse/hardware/dacs/dac_base.py | 4 + qupulse/hardware/dacs/dummy.py | 4 + qupulse/hardware/feature_awg/base.py | 4 + qupulse/hardware/feature_awg/base_features.py | 4 + .../feature_awg/channel_tuple_wrapper.py | 4 + qupulse/hardware/feature_awg/features.py | 4 + qupulse/hardware/feature_awg/tabor.py | 4 + qupulse/hardware/setup.py | 4 + qupulse/hardware/util.py | 4 + qupulse/parameter_scope.py | 4 + qupulse/plotting.py | 4 + qupulse/program/__init__.py | 4 + qupulse/program/linspace.py | 4 + qupulse/program/loop.py | 4 + qupulse/program/transformation.py | 4 + qupulse/program/volatile.py | 4 + qupulse/program/waveforms.py | 4 + qupulse/pulses/__init__.py | 4 + qupulse/pulses/abstract_pulse_template.py | 4 + qupulse/pulses/arithmetic_pulse_template.py | 3 + qupulse/pulses/constant_pulse_template.py | 4 + qupulse/pulses/function_pulse_template.py | 4 + qupulse/pulses/interpolation.py | 4 + qupulse/pulses/loop_pulse_template.py | 4 + qupulse/pulses/mapping_pulse_template.py | 4 + qupulse/pulses/measurement.py | 4 + .../pulses/multi_channel_pulse_template.py | 4 + qupulse/pulses/parameters.py | 4 + qupulse/pulses/plotting.py | 4 + qupulse/pulses/point_pulse_template.py | 4 + qupulse/pulses/pulse_template.py | 4 + .../pulse_template_parameter_mapping.py | 4 + qupulse/pulses/range.py | 4 + qupulse/pulses/repetition_pulse_template.py | 4 + qupulse/pulses/sequence_pulse_template.py | 4 + qupulse/pulses/table_pulse_template.py | 4 + .../pulses/time_reversal_pulse_template.py | 4 + qupulse/serialization.py | 4 + qupulse/utils/__init__.py | 4 + qupulse/utils/numeric.py | 4 + qupulse/utils/performance.py | 4 + qupulse/utils/sympy.py | 4 + qupulse/utils/tree.py | 4 + qupulse/utils/types.py | 4 + setup.cfg | 4 +- 72 files changed, 508 insertions(+), 676 deletions(-) delete mode 100644 LICENSE create mode 100644 LICENSES/GPL-3.0-or-later.txt diff --git a/LICENSE b/LICENSE deleted file mode 100644 index f288702d2..000000000 --- a/LICENSE +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/LICENSES/GPL-3.0-or-later.txt b/LICENSES/GPL-3.0-or-later.txt new file mode 100644 index 000000000..f6cdd22a6 --- /dev/null +++ b/LICENSES/GPL-3.0-or-later.txt @@ -0,0 +1,232 @@ +GNU GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright © 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +Preamble + +The GNU General Public License is a free, copyleft license for software and other kinds of works. + +The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. + +To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. + +For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. + +Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. + +Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. + +The precise terms and conditions for copying, distribution and modification follow. + +TERMS AND CONDITIONS + +0. Definitions. + +“This License†refers to version 3 of the GNU General Public License. + +“Copyright†also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. + +“The Program†refers to any copyrightable work licensed under this License. Each licensee is addressed as “youâ€. “Licensees†and “recipients†may be individuals or organizations. + +To “modify†a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a “modified version†of the earlier work or a work “based on†the earlier work. + +A “covered work†means either the unmodified Program or a work based on the Program. + +To “propagate†a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. + +To “convey†a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays “Appropriate Legal Notices†to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. + +1. Source Code. +The “source code†for a work means the preferred form of the work for making modifications to it. “Object code†means any non-source form of a work. + +A “Standard Interface†means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. + +The “System Libraries†of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A “Major Componentâ€, in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. + +The “Corresponding Source†for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. + +The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same work. + +2. Basic Permissions. +All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. +No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. + +4. Conveying Verbatim Copies. +You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. +You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to “keep intact all noticesâ€. + + c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. + +A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an “aggregate†if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. + +6. Conveying Non-Source Forms. +You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: + + a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. + + d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. + +A “User Product†is either (1) a “consumer productâ€, which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, “normally used†refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. + +“Installation Information†for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. + +If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). + +The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. + +7. Additional Terms. +“Additional permissions†are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or authors of the material; or + + e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. + +All other non-permissive additional terms are considered “further restrictions†within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. + +8. Termination. +You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). + +However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. + +9. Acceptance Not Required for Having Copies. +You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. +Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. + +An “entity transaction†is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. + +11. Patents. +A “contributor†is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's “contributor versionâ€. + +A contributor's “essential patent claims†are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, “control†includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. + +In the following three paragraphs, a “patent license†is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To “grant†such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. “Knowingly relying†means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. + +A patent license is “discriminatory†if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. + +12. No Surrender of Others' Freedom. +If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. + +13. Use with the GNU Affero General Public License. +Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. + +14. Revised Versions of this License. +The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License “or any later version†applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. + +Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. + +15. Disclaimer of Warranty. +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS†WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. +If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright†line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an “about boxâ€. + +You should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer†for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . + +The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . diff --git a/qctoolkit/__init__.py b/qctoolkit/__init__.py index b9a2ab843..208c2083f 100644 --- a/qctoolkit/__init__.py +++ b/qctoolkit/__init__.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + """This is a (hopefully temporary) alias package to not break existing code. If you know a better way please change""" import sys import re diff --git a/qupulse/__init__.py b/qupulse/__init__.py index 5bf9d2ffe..92a430692 100644 --- a/qupulse/__init__.py +++ b/qupulse/__init__.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + """A Quantum compUting PULse parametrization and SEquencing framework.""" import lazy_loader as lazy diff --git a/qupulse/__init__.pyi b/qupulse/__init__.pyi index 8c311a9e3..f0e1cf5ff 100644 --- a/qupulse/__init__.pyi +++ b/qupulse/__init__.pyi @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later from . import pulses from . import hardware diff --git a/qupulse/_program/__init__.py b/qupulse/_program/__init__.py index 93773ebb1..5f2427b92 100644 --- a/qupulse/_program/__init__.py +++ b/qupulse/_program/__init__.py @@ -1 +1,5 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + """This is a private package meaning there are no stability guarantees.""" diff --git a/qupulse/_program/_loop.py b/qupulse/_program/_loop.py index b5ad53deb..3eec640cc 100644 --- a/qupulse/_program/_loop.py +++ b/qupulse/_program/_loop.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + """Backwards compatibility link to qupulse.program.loop""" from qupulse.program.loop import * diff --git a/qupulse/_program/tabor.py b/qupulse/_program/tabor.py index 235cc945c..5b690b4d4 100644 --- a/qupulse/_program/tabor.py +++ b/qupulse/_program/tabor.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + import dataclasses import sys from typing import NamedTuple, Optional, List, Generator, Tuple, Sequence, Mapping, Union, Dict, FrozenSet, cast,\ diff --git a/qupulse/_program/transformation.py b/qupulse/_program/transformation.py index c6a0d0def..977b01c5f 100644 --- a/qupulse/_program/transformation.py +++ b/qupulse/_program/transformation.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + from qupulse.program.transformation import * import qupulse.program.transformation diff --git a/qupulse/_program/volatile.py b/qupulse/_program/volatile.py index ddfe2aa16..e50970865 100644 --- a/qupulse/_program/volatile.py +++ b/qupulse/_program/volatile.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + from qupulse.program.volatile import * import qupulse.program.volatile diff --git a/qupulse/_program/waveforms.py b/qupulse/_program/waveforms.py index 5038c3988..964236438 100644 --- a/qupulse/_program/waveforms.py +++ b/qupulse/_program/waveforms.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + """Backwards compatibility link to qupulse.program.waveforms""" from qupulse.program.waveforms import * diff --git a/qupulse/comparable.py b/qupulse/comparable.py index 2582faa8b..c6d929364 100644 --- a/qupulse/comparable.py +++ b/qupulse/comparable.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + """This module defines the abstract Comparable class.""" from abc import abstractmethod from typing import Hashable, Any diff --git a/qupulse/examples/VolatileParameters.py b/qupulse/examples/VolatileParameters.py index 53dcc3dab..ec8228a6c 100644 --- a/qupulse/examples/VolatileParameters.py +++ b/qupulse/examples/VolatileParameters.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + from qupulse.hardware.setup import HardwareSetup, PlaybackChannel, MarkerChannel from qupulse.pulses import PointPT, RepetitionPT, TablePT diff --git a/qupulse/expressions/__init__.py b/qupulse/expressions/__init__.py index 52aa5f325..398edb394 100644 --- a/qupulse/expressions/__init__.py +++ b/qupulse/expressions/__init__.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + """This subpackage contains qupulse's expression logic. The submodule :py:mod:`.expressions.protocol` defines the :py:class:`typing.Protocol` that expression functionality providers must implement. This allows to substitute the powerful and expressive but slow default implementation with a faster less expressive backend. diff --git a/qupulse/expressions/protocol.py b/qupulse/expressions/protocol.py index 667337c3d..917291e81 100644 --- a/qupulse/expressions/protocol.py +++ b/qupulse/expressions/protocol.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + """This module contains the interface / protocol descriptions of ``Expression``, ``ExpressionScalar`` and ``ExpressionVector``.""" diff --git a/qupulse/expressions/sympy.py b/qupulse/expressions/sympy.py index 150a1b6a5..30d230d5d 100644 --- a/qupulse/expressions/sympy.py +++ b/qupulse/expressions/sympy.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + """ This module defines the class Expression to represent mathematical expression as well as corresponding exception classes. diff --git a/qupulse/expressions/wrapper.py b/qupulse/expressions/wrapper.py index 1052c1174..984e6479a 100644 --- a/qupulse/expressions/wrapper.py +++ b/qupulse/expressions/wrapper.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + """This module contains the function :py:``make_wrappers`` to define wrapper classes for expression protocol implementations which only implements methods of the protocol. It is used for finding code that relies on expression implementation details.""" diff --git a/qupulse/hardware/__init__.py b/qupulse/hardware/__init__.py index a545e01d8..0d5cf722b 100644 --- a/qupulse/hardware/__init__.py +++ b/qupulse/hardware/__init__.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + """Contains drivers for AWG control and digitizer configuration as well as a unifying interface to all instruments: :class:`~qupulse.hardware.setup.HardwareSetup`""" diff --git a/qupulse/hardware/awgs/__init__.py b/qupulse/hardware/awgs/__init__.py index e76d4d9b8..ab11be2d9 100644 --- a/qupulse/hardware/awgs/__init__.py +++ b/qupulse/hardware/awgs/__init__.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + import lazy_loader as lazy diff --git a/qupulse/hardware/awgs/base.py b/qupulse/hardware/awgs/base.py index 108230179..9ed5e5756 100644 --- a/qupulse/hardware/awgs/base.py +++ b/qupulse/hardware/awgs/base.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + """This module defines the common interface for arbitrary waveform generators. Classes: diff --git a/qupulse/hardware/awgs/dummy.py b/qupulse/hardware/awgs/dummy.py index 249497483..fd010d0fc 100644 --- a/qupulse/hardware/awgs/dummy.py +++ b/qupulse/hardware/awgs/dummy.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + from typing import Tuple, Set from .base import AWG, ProgramOverwriteException diff --git a/qupulse/hardware/awgs/tabor.py b/qupulse/hardware/awgs/tabor.py index 90e03866e..b500f3645 100644 --- a/qupulse/hardware/awgs/tabor.py +++ b/qupulse/hardware/awgs/tabor.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + import fractions import functools import warnings diff --git a/qupulse/hardware/awgs/tektronix.py b/qupulse/hardware/awgs/tektronix.py index eaaa7a360..26cfb8d17 100644 --- a/qupulse/hardware/awgs/tektronix.py +++ b/qupulse/hardware/awgs/tektronix.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + from typing import Tuple, Callable, Optional, Sequence, Union, Dict, Mapping, Set from types import MappingProxyType import numpy as np diff --git a/qupulse/hardware/awgs/zihdawg.py b/qupulse/hardware/awgs/zihdawg.py index 519051c38..2b66c8c79 100644 --- a/qupulse/hardware/awgs/zihdawg.py +++ b/qupulse/hardware/awgs/zihdawg.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + import sys import argparse import logging diff --git a/qupulse/hardware/dacs/__init__.py b/qupulse/hardware/dacs/__init__.py index b2e3277cb..f40cf97d7 100644 --- a/qupulse/hardware/dacs/__init__.py +++ b/qupulse/hardware/dacs/__init__.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + import lazy_loader as lazy __getattr__, __dir__, __all__ = lazy.attach( diff --git a/qupulse/hardware/dacs/alazar.py b/qupulse/hardware/dacs/alazar.py index c694c4be6..0a4c08158 100644 --- a/qupulse/hardware/dacs/alazar.py +++ b/qupulse/hardware/dacs/alazar.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + import dataclasses from typing import Dict, Any, Optional, Tuple, List, Iterable, Callable, Sequence from collections import defaultdict diff --git a/qupulse/hardware/dacs/alazar2.py b/qupulse/hardware/dacs/alazar2.py index 15a881cf8..4cd7db2f7 100644 --- a/qupulse/hardware/dacs/alazar2.py +++ b/qupulse/hardware/dacs/alazar2.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + import dataclasses from typing import Union, Iterable, Dict, Tuple, Mapping, Optional from types import MappingProxyType diff --git a/qupulse/hardware/dacs/dac_base.py b/qupulse/hardware/dacs/dac_base.py index cab2e7b7f..14be68934 100644 --- a/qupulse/hardware/dacs/dac_base.py +++ b/qupulse/hardware/dacs/dac_base.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + from abc import ABCMeta, abstractmethod from typing import Dict, Tuple, Iterable, TYPE_CHECKING diff --git a/qupulse/hardware/dacs/dummy.py b/qupulse/hardware/dacs/dummy.py index ba0cf6f51..3a8fc3a7a 100644 --- a/qupulse/hardware/dacs/dummy.py +++ b/qupulse/hardware/dacs/dummy.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + from typing import Tuple, Set, Dict from collections import deque diff --git a/qupulse/hardware/feature_awg/base.py b/qupulse/hardware/feature_awg/base.py index 0ea254b96..868e84b2b 100644 --- a/qupulse/hardware/feature_awg/base.py +++ b/qupulse/hardware/feature_awg/base.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + from abc import ABC, abstractmethod from typing import Optional, Collection import weakref diff --git a/qupulse/hardware/feature_awg/base_features.py b/qupulse/hardware/feature_awg/base_features.py index 7461bf046..a95d140eb 100644 --- a/qupulse/hardware/feature_awg/base_features.py +++ b/qupulse/hardware/feature_awg/base_features.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + from types import MappingProxyType from typing import Callable, Generic, Mapping, Optional, Type, TypeVar from abc import ABC diff --git a/qupulse/hardware/feature_awg/channel_tuple_wrapper.py b/qupulse/hardware/feature_awg/channel_tuple_wrapper.py index a840ce9c9..40ce75e9c 100644 --- a/qupulse/hardware/feature_awg/channel_tuple_wrapper.py +++ b/qupulse/hardware/feature_awg/channel_tuple_wrapper.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + from typing import Tuple, Optional, Callable, Set from qupulse import ChannelID diff --git a/qupulse/hardware/feature_awg/features.py b/qupulse/hardware/feature_awg/features.py index 29439f70a..bdd070096 100644 --- a/qupulse/hardware/feature_awg/features.py +++ b/qupulse/hardware/feature_awg/features.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + from abc import ABC, abstractmethod from typing import Callable, Optional, Set, Tuple, Dict, Union, Any, Mapping from numbers import Real diff --git a/qupulse/hardware/feature_awg/tabor.py b/qupulse/hardware/feature_awg/tabor.py index 554939719..4cbd0bf18 100644 --- a/qupulse/hardware/feature_awg/tabor.py +++ b/qupulse/hardware/feature_awg/tabor.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + import functools import logging import numbers diff --git a/qupulse/hardware/setup.py b/qupulse/hardware/setup.py index d034e3b26..01e2e240b 100644 --- a/qupulse/hardware/setup.py +++ b/qupulse/hardware/setup.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + from typing import NamedTuple, Set, Callable, Dict, Tuple, Union, Iterable, Any, Mapping from collections import defaultdict import warnings diff --git a/qupulse/hardware/util.py b/qupulse/hardware/util.py index 2d656d529..5134b6043 100644 --- a/qupulse/hardware/util.py +++ b/qupulse/hardware/util.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + from typing import Collection, Sequence, Tuple, Union, Optional import itertools diff --git a/qupulse/parameter_scope.py b/qupulse/parameter_scope.py index a59f94a0b..ca2959ddf 100644 --- a/qupulse/parameter_scope.py +++ b/qupulse/parameter_scope.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + """Contains various implementations of the parameter lookup interface :class:`.Scope`""" from abc import abstractmethod diff --git a/qupulse/plotting.py b/qupulse/plotting.py index 600087ed9..5b7d42827 100644 --- a/qupulse/plotting.py +++ b/qupulse/plotting.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + """This module defines plotting functionality for instantiated PulseTemplates using matplotlib. Classes: diff --git a/qupulse/program/__init__.py b/qupulse/program/__init__.py index 611a96fcd..644cf9bd2 100644 --- a/qupulse/program/__init__.py +++ b/qupulse/program/__init__.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + import contextlib from abc import ABC, abstractmethod from dataclasses import dataclass diff --git a/qupulse/program/linspace.py b/qupulse/program/linspace.py index 4fe387856..81081ecde 100644 --- a/qupulse/program/linspace.py +++ b/qupulse/program/linspace.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + import abc import contextlib import dataclasses diff --git a/qupulse/program/loop.py b/qupulse/program/loop.py index 0f0356531..5f127fbef 100644 --- a/qupulse/program/loop.py +++ b/qupulse/program/loop.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + import reprlib import warnings from collections import defaultdict diff --git a/qupulse/program/transformation.py b/qupulse/program/transformation.py index 784e8e193..21a7eb382 100644 --- a/qupulse/program/transformation.py +++ b/qupulse/program/transformation.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + from typing import Any, Mapping, Set, Tuple, Sequence, AbstractSet, Union, TYPE_CHECKING, Hashable from abc import abstractmethod from numbers import Real diff --git a/qupulse/program/volatile.py b/qupulse/program/volatile.py index afd4692ff..ab0925ae1 100644 --- a/qupulse/program/volatile.py +++ b/qupulse/program/volatile.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + from typing import NamedTuple, Mapping import warnings import numbers diff --git a/qupulse/program/waveforms.py b/qupulse/program/waveforms.py index 5080a92e8..dc777a231 100644 --- a/qupulse/program/waveforms.py +++ b/qupulse/program/waveforms.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + """This module contains all waveform classes Classes: diff --git a/qupulse/pulses/__init__.py b/qupulse/pulses/__init__.py index 4a8e1016f..3ed818c57 100644 --- a/qupulse/pulses/__init__.py +++ b/qupulse/pulses/__init__.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + """This is the central package for defining pulses. All :class:`~qupulse.pulses.pulse_template.PulseTemplate` subclasses that are final and ready to be used are imported here with their recommended abbreviation as an alias. diff --git a/qupulse/pulses/abstract_pulse_template.py b/qupulse/pulses/abstract_pulse_template.py index 05e75307c..272534035 100644 --- a/qupulse/pulses/abstract_pulse_template.py +++ b/qupulse/pulses/abstract_pulse_template.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + from typing import Set, Optional, Dict, Any, cast from functools import partial, partialmethod import warnings diff --git a/qupulse/pulses/arithmetic_pulse_template.py b/qupulse/pulses/arithmetic_pulse_template.py index eedfce20b..19b9b45d2 100644 --- a/qupulse/pulses/arithmetic_pulse_template.py +++ b/qupulse/pulses/arithmetic_pulse_template.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later from typing import Any, Dict, List, Set, Optional, Union, Mapping, FrozenSet, cast, Callable from numbers import Real diff --git a/qupulse/pulses/constant_pulse_template.py b/qupulse/pulses/constant_pulse_template.py index 192ee82b1..839548f2a 100644 --- a/qupulse/pulses/constant_pulse_template.py +++ b/qupulse/pulses/constant_pulse_template.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + """This module defines the ConstantPulseTemplate, a pulse tempalte representating a pulse with constant values on all channels Classes: diff --git a/qupulse/pulses/function_pulse_template.py b/qupulse/pulses/function_pulse_template.py index 24d98fbe2..b943ef5a9 100644 --- a/qupulse/pulses/function_pulse_template.py +++ b/qupulse/pulses/function_pulse_template.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + """This module defines the FunctionPulseTemplate, one of the elementary pulse templates and its waveform representation. diff --git a/qupulse/pulses/interpolation.py b/qupulse/pulses/interpolation.py index 40eac69e9..e5c81f8c1 100644 --- a/qupulse/pulses/interpolation.py +++ b/qupulse/pulses/interpolation.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + """This module defines strategies for interpolation between points in a pulse table or similar. Classes: diff --git a/qupulse/pulses/loop_pulse_template.py b/qupulse/pulses/loop_pulse_template.py index 0f458c687..cd7aefaaa 100644 --- a/qupulse/pulses/loop_pulse_template.py +++ b/qupulse/pulses/loop_pulse_template.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + """This module defines LoopPulseTemplate, a higher-order hierarchical pulse template that loops another PulseTemplate based on a condition.""" import dataclasses diff --git a/qupulse/pulses/mapping_pulse_template.py b/qupulse/pulses/mapping_pulse_template.py index 07e7d1024..af5119b66 100644 --- a/qupulse/pulses/mapping_pulse_template.py +++ b/qupulse/pulses/mapping_pulse_template.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + from typing import Optional, Set, Dict, Union, List, Any, Tuple, Mapping import itertools import numbers diff --git a/qupulse/pulses/measurement.py b/qupulse/pulses/measurement.py index 2a12575f9..e44b7d0e9 100644 --- a/qupulse/pulses/measurement.py +++ b/qupulse/pulses/measurement.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + from typing import Optional, List, Tuple, Union, Dict, Set, Mapping, AbstractSet from numbers import Real import itertools diff --git a/qupulse/pulses/multi_channel_pulse_template.py b/qupulse/pulses/multi_channel_pulse_template.py index 6b76bb49f..a7e11914d 100644 --- a/qupulse/pulses/multi_channel_pulse_template.py +++ b/qupulse/pulses/multi_channel_pulse_template.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + """This module defines MultiChannelPulseTemplate, which allows the combination of several AtomicPulseTemplates into a single template spanning several channels. diff --git a/qupulse/pulses/parameters.py b/qupulse/pulses/parameters.py index da381b053..d78bf4632 100644 --- a/qupulse/pulses/parameters.py +++ b/qupulse/pulses/parameters.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + """This module defines parameter constriants. """ diff --git a/qupulse/pulses/plotting.py b/qupulse/pulses/plotting.py index 8d98f3112..907629fe4 100644 --- a/qupulse/pulses/plotting.py +++ b/qupulse/pulses/plotting.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + """Deprecated plotting location. Was moved to :py:`qupulse.plotting`. No deprecation warning because we will keep it around forever.""" diff --git a/qupulse/pulses/point_pulse_template.py b/qupulse/pulses/point_pulse_template.py index 0075a98dc..77ebada8f 100644 --- a/qupulse/pulses/point_pulse_template.py +++ b/qupulse/pulses/point_pulse_template.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + from typing import Optional, List, Union, Set, Dict, Sequence, Any, Tuple from numbers import Real import itertools diff --git a/qupulse/pulses/pulse_template.py b/qupulse/pulses/pulse_template.py index 97dd5cda4..68d4adac9 100644 --- a/qupulse/pulses/pulse_template.py +++ b/qupulse/pulses/pulse_template.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + """This module defines the abstract PulseTemplate class which is the basis of any pulse model in the qupulse. diff --git a/qupulse/pulses/pulse_template_parameter_mapping.py b/qupulse/pulses/pulse_template_parameter_mapping.py index 5daf2695a..3043670a3 100644 --- a/qupulse/pulses/pulse_template_parameter_mapping.py +++ b/qupulse/pulses/pulse_template_parameter_mapping.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + """..deprecated:: 0.1 """ diff --git a/qupulse/pulses/range.py b/qupulse/pulses/range.py index 34f7e8a8e..c39ad39e6 100644 --- a/qupulse/pulses/range.py +++ b/qupulse/pulses/range.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + from typing import Tuple, Any, AbstractSet, Mapping, Union, Iterator from numbers import Number from dataclasses import dataclass diff --git a/qupulse/pulses/repetition_pulse_template.py b/qupulse/pulses/repetition_pulse_template.py index ead19c6d9..a88485ed0 100644 --- a/qupulse/pulses/repetition_pulse_template.py +++ b/qupulse/pulses/repetition_pulse_template.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + """This module defines RepetitionPulseTemplate, a higher-order hierarchical pulse template that represents the n-times repetition of another PulseTemplate.""" diff --git a/qupulse/pulses/sequence_pulse_template.py b/qupulse/pulses/sequence_pulse_template.py index 5107bb104..19e80ed04 100644 --- a/qupulse/pulses/sequence_pulse_template.py +++ b/qupulse/pulses/sequence_pulse_template.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + """This module defines SequencePulseTemplate, a higher-order hierarchical pulse template that combines several other PulseTemplate objects for sequential execution.""" diff --git a/qupulse/pulses/table_pulse_template.py b/qupulse/pulses/table_pulse_template.py index f8b631add..cb9287b01 100644 --- a/qupulse/pulses/table_pulse_template.py +++ b/qupulse/pulses/table_pulse_template.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + """This module defines the TablePulseTemplate, one of the elementary pulse templates and its waveform representation. diff --git a/qupulse/pulses/time_reversal_pulse_template.py b/qupulse/pulses/time_reversal_pulse_template.py index 35a1884be..5dc9fcabd 100644 --- a/qupulse/pulses/time_reversal_pulse_template.py +++ b/qupulse/pulses/time_reversal_pulse_template.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + from typing import Optional, Set, Dict, Union from qupulse import ChannelID diff --git a/qupulse/serialization.py b/qupulse/serialization.py index 389748666..3cadee40f 100644 --- a/qupulse/serialization.py +++ b/qupulse/serialization.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + """ This module provides serialization and storage functionality. Classes: diff --git a/qupulse/utils/__init__.py b/qupulse/utils/__init__.py index 326072f4b..84a05d8be 100644 --- a/qupulse/utils/__init__.py +++ b/qupulse/utils/__init__.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + """This package contains utility functions and classes as well as custom sympy extensions(hacks).""" from typing import Union, Iterable, Any, Tuple, Mapping, Iterator, TypeVar, Sequence, AbstractSet, Optional, Callable diff --git a/qupulse/utils/numeric.py b/qupulse/utils/numeric.py index 5a263a39a..4fdeed43b 100644 --- a/qupulse/utils/numeric.py +++ b/qupulse/utils/numeric.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + from typing import Tuple, Type from numbers import Rational from math import gcd diff --git a/qupulse/utils/performance.py b/qupulse/utils/performance.py index ebdb3b0a8..8d0c6141e 100644 --- a/qupulse/utils/performance.py +++ b/qupulse/utils/performance.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + import warnings from typing import Tuple import numpy as np diff --git a/qupulse/utils/sympy.py b/qupulse/utils/sympy.py index 7a04d53f6..aa2056143 100644 --- a/qupulse/utils/sympy.py +++ b/qupulse/utils/sympy.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + from typing import Union, Dict, Tuple, Any, Sequence, Optional, Callable from numbers import Number from types import CodeType diff --git a/qupulse/utils/tree.py b/qupulse/utils/tree.py index 2585a5f57..f320f9534 100644 --- a/qupulse/utils/tree.py +++ b/qupulse/utils/tree.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + """This module contains a tree implementation.""" from typing import Iterable, Union, List, Generator, Tuple, TypeVar, Optional, Sequence diff --git a/qupulse/utils/types.py b/qupulse/utils/types.py index 7ec9d8c7c..ca51d1965 100644 --- a/qupulse/utils/types.py +++ b/qupulse/utils/types.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2024 Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University +# +# SPDX-License-Identifier: GPL-3.0-or-later + import typing import abc import inspect diff --git a/setup.cfg b/setup.cfg index c053e526e..2c4b13050 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,8 +5,8 @@ description = A Quantum compUting PULse parametrization and SEquencing framework long_description = file: README.md long_description_content_type = text/markdown author = Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University -license = GPLv3+ -license_files = LICENSE +license = GPL-3.0-or-later +license_files = LICENSE/GPL-3.0-or-later.txt keywords = quantum, physics, control pulse, qubit classifiers = Programming Language :: Python :: 3 From 4ebd314eaf8e9a1da658e5c5349c574239969149 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 27 May 2024 15:38:23 +0200 Subject: [PATCH 200/441] Add section about licensing in README --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index d47d1e09f..4dfe22973 100644 --- a/README.md +++ b/README.md @@ -63,3 +63,6 @@ The repository primarily consists of the folders `qupulse` (toolkit core code) a Contents of `tests` mirror the structure of `qupulse`. For every `` somewhere in `qupulse` there should exist a `Tests.py` in the corresponding subdirectory of `tests`. +## License + +The current version of qupulse is available under the `GPL-3.0-or-later` license. Versions up to and including 0.10 were licensed under the MIT license. If you require different licensing terms, please contact us to discuss your needs. From d7e2aacbbb4d3612ae0bd1ec4c642d0d0e4bdc17 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 27 May 2024 15:46:14 +0200 Subject: [PATCH 201/441] Add newsfragment --- changes.d/808.doc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes.d/808.doc diff --git a/changes.d/808.doc b/changes.d/808.doc new file mode 100644 index 000000000..a27837ceb --- /dev/null +++ b/changes.d/808.doc @@ -0,0 +1 @@ +Add an example with a Zurich Instruments HDAWG and MFLI. \ No newline at end of file From 54a0e30cb1a61f06bcd4eddc62280ecdb834f204 Mon Sep 17 00:00:00 2001 From: Nomos11 <82180697+Nomos11@users.noreply.github.com> Date: Tue, 28 May 2024 21:05:40 +0200 Subject: [PATCH 202/441] linspacebuilder tests on hdawg programentrys --- tests/hardware/hdawg_tests.py | 313 ++++++++++++++++++++++++++++++++ tests/program/linspace_tests.py | 21 ++- 2 files changed, 333 insertions(+), 1 deletion(-) create mode 100644 tests/hardware/hdawg_tests.py diff --git a/tests/hardware/hdawg_tests.py b/tests/hardware/hdawg_tests.py new file mode 100644 index 000000000..9d01b19fb --- /dev/null +++ b/tests/hardware/hdawg_tests.py @@ -0,0 +1,313 @@ +import unittest +import inspect +import numpy as np +import types + +try: + import qupulse_hdawg +except ImportError as err: + raise unittest.SkipTest("qupulse_hdawg not present") from err + +import tests.program.linspace_tests as linspace_tests +from qupulse_hdawg.seqc import HDAWGProgramManager +from qupulse.utils.types import TimeType + + +CT_SCHEMA_2405 = r"""{ + "$schema": "https://json-schema.org/draft-07/schema#", + "title": "AWG Command Table Schema", + "description": "Schema for ZI HDAWG AWG Command Table", + "version": "1.2.1", + "definitions": { + "header": { + "type": "object", + "properties": { + "version": { + "type": "string", + "pattern": "^1\\.[0-2](\\.[0-9]+)?$", + "description": "File format version (Major.Minor / Major.Minor.Patch). This version must match with the relevant schema version." + }, + "partial": { + "description": "Set to true for incremental table updates", + "type": "boolean", + "default": false + }, + "userString": { + "description": "User-definable label", + "type": "string", + "maxLength": 30 + } + }, + "required": [ + "version" + ] + }, + "table": { + "type": "array", + "items": { + "$ref": "#/definitions/entry" + }, + "minItems": 0, + "maxItems": 1024 + }, + "entry": { + "type": "object", + "properties": { + "index": { + "$ref": "#/definitions/tableindex" + }, + "waveform": { + "$ref": "#/definitions/waveform" + }, + "phase0": { + "$ref": "#/definitions/phase" + }, + "phase1": { + "$ref": "#/definitions/phase" + }, + "amplitude0": { + "$ref": "#/definitions/amplitude" + }, + "amplitude1": { + "$ref": "#/definitions/amplitude" + } + }, + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "index", + "waveform" + ] + }, + { + "required": [ + "index", + "phase0" + ] + }, + { + "required": [ + "index", + "phase1" + ] + }, + { + "required": [ + "index", + "amplitude0" + ] + }, + { + "required": [ + "index", + "amplitude1" + ] + } + ] + }, + "tableindex": { + "type": "integer", + "minimum": 0, + "maximum": 1023 + }, + "waveform": { + "type": "object", + "properties": { + "index": { + "$ref": "#/definitions/waveformindex" + }, + "length": { + "$ref": "#/definitions/waveformlength" + }, + "samplingRateDivider": { + "$ref": "#/definitions/samplingratedivider" + }, + "awgChannel0": { + "$ref": "#/definitions/awgchannel" + }, + "awgChannel1": { + "$ref": "#/definitions/awgchannel" + }, + "precompClear": { + "$ref": "#/definitions/precompclear" + }, + "playZero": { + "$ref": "#/definitions/playzero" + }, + "playHold": { + "$ref": "#/definitions/playhold" + } + }, + "additionalProperties": false, + "oneOf": [ + { + "required": [ + "index" + ] + }, + { + "required": [ + "playZero", + "length" + ] + }, + { + "required": [ + "playHold", + "length" + ] + } + ] + }, + "waveformindex": { + "description": "Index of the waveform to play as defined with the assignWaveIndex sequencer instruction", + "type": "integer", + "minimum": 0, + "maximum": 15999 + }, + "waveformlength": { + "description": "The length of the waveform in samples", + "type": "integer", + "multipleOf": 16, + "minimum": 32 + }, + "samplingratedivider": { + "descpription": "Integer exponent n of the sample rate divider: SampleRate / 2^n, n in range 0 ... 13", + "type": "integer", + "minimum": 0, + "maximum": 13 + }, + "awgchannel": { + "description": "Assign the given AWG channel to signal output 0 & 1", + "type": "array", + "minItems": 1, + "maxItems": 2, + "uniqueItems": true, + "items": [ + { + "type": "string", + "enum": [ + "sigout0", + "sigout1" + ] + } + ] + }, + "precompclear": { + "description": "Set to true to clear the precompensation filters", + "type": "boolean", + "default": false + }, + "playzero": { + "description": "Play a zero-valued waveform for specified length of waveform, equivalent to the playZero sequencer instruction", + "type": "boolean", + "default": false + }, + "playhold": { + "description": "Hold the last played value for the specified number of samples, equivalent to the playHold sequencer instruction", + "type": "boolean", + "default": false + }, + "phase": { + "type": "object", + "properties": { + "value": { + "description": "Phase value of the given sine generator in degree", + "type": "number" + }, + "increment": { + "description": "Set to true for incremental phase value, or to false for absolute", + "type": "boolean", + "default": false + } + }, + "additionalProperties": false, + "required": [ + "value" + ] + }, + "amplitude": { + "type": "object", + "properties": { + "value": { + "description": "Amplitude scaling factor of the given AWG channel", + "type": "number", + "minimum": -1.0, + "maximum": 1.0 + }, + "increment": { + "description": "Set to true for incremental amplitude value, or to false for absolute", + "type": "boolean", + "default": false + }, + "register": { + "description": "Index of amplitude register that is selected for scaling the pulse amplitude.", + "type": "integer", + "minimum": 0, + "maximum": 3 + } + }, + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "value" + ] + }, + { + "required": [ + "register" + ] + } + ] + } + }, + "type": "object", + "properties": { + "$schema": { + "type": "string" + }, + "header": { + "$ref": "#/definitions/header" + }, + "table": { + "$ref": "#/definitions/table" + } + }, + "additionalProperties": false, + "required": [ + "header", + "table" + ] +}""" + + +class AllLinspaceTests(unittest.TestCase): + + def setUp(self): + self.test_classes_list = [member for name, member in inspect.getmembers(linspace_tests, inspect.isclass) + if issubclass(member, unittest.TestCase) and not member.__name__=="TestCase"] + + def test_all(self): + + for test_class in self.test_classes_list: + + test_obj = test_class() + test_obj.setUp() + + test_program, channels, markers = test_obj.return_program() + + awg = types.SimpleNamespace(num_channels=2, MAX_SAMPLE_RATE_DIVIDER=13, sample_rate_divider=0, + master_device=types.SimpleNamespace(sample_clock=2.4) + ) + + test_manager = HDAWGProgramManager(awg, lambda idx: tuple([CT_SCHEMA_2405 for i in idx])) + + try: + test_manager.add_program("test", test_program, channels, markers, + np.ones(len(channels)),np.zeros(len(channels)), + [None,]*len(channels), TimeType.from_fraction(24,10)) + except Exception as e: + self.fail(f"{test_class.__name__} raised an exception: {e}") + \ No newline at end of file diff --git a/tests/program/linspace_tests.py b/tests/program/linspace_tests.py index 03a5b2971..fcf3d0472 100644 --- a/tests/program/linspace_tests.py +++ b/tests/program/linspace_tests.py @@ -5,6 +5,7 @@ from qupulse.pulses import * from qupulse.program.linspace import * from qupulse.program.transformation import * +from qupulse.program import Program class SingleRampTest(TestCase): def setUp(self): @@ -40,6 +41,9 @@ def test_program(self): def test_commands(self): commands = to_increment_commands([self.program]) self.assertEqual(self.commands, commands) + + def return_program(self) -> Tuple[Program,int,int]: + return self.pulse_template.create_program(program_builder=LinSpaceBuilder(('a',))), ('a',), tuple() class PlainCSDTest(TestCase): @@ -94,7 +98,10 @@ def test_program(self): def test_increment_commands(self): commands = to_increment_commands([self.program]) self.assertEqual(self.commands, commands) - + + def return_program(self) -> Tuple[Program,int,int]: + return self.pulse_template.create_program(program_builder=LinSpaceBuilder(('a','b'))), ('a','b'), tuple() + class TiltedCSDTest(TestCase): def setUp(self): @@ -167,6 +174,9 @@ def test_repeated_increment_commands(self): commands = to_increment_commands([self.repeated_program]) self.assertEqual(self.repeated_commands, commands) + def return_program(self) -> Tuple[Program,int,int]: + return self.repeated_pt.create_program(program_builder=LinSpaceBuilder(('a','b'))), ('a','b'), tuple() + class SingletLoadProcessing(TestCase): def setUp(self): @@ -256,6 +266,9 @@ def test_singlet_scan_commands(self): commands = to_increment_commands([self.program]) self.assertEqual(self.commands, commands) + def return_program(self) -> Tuple[Program,int,int]: + return self.pulse_template.create_program(program_builder=LinSpaceBuilder(('a','b'))), ('a','b'), tuple() + class TransformedRampTest(TestCase): def setUp(self): @@ -287,3 +300,9 @@ def test_local_trafo_program(self): global_transformation=self.transformation, to_single_waveform={self.pulse_template}) self.assertEqual([self.program], program) + + def return_program(self) -> Tuple[Program,int,int]: + return self.pulse_template.create_program(program_builder=LinSpaceBuilder(('a',)), + global_transformation=self.transformation),\ + ('a',), tuple() + \ No newline at end of file From ab0424c2229e6d70dc355dfb3556db49f41eb311 Mon Sep 17 00:00:00 2001 From: Nomos11 <82180697+Nomos11@users.noreply.github.com> Date: Tue, 28 May 2024 22:59:47 +0200 Subject: [PATCH 203/441] Revert "linspacebuilder tests on hdawg programentrys" This reverts commit 54a0e30cb1a61f06bcd4eddc62280ecdb834f204. --- tests/hardware/hdawg_tests.py | 313 -------------------------------- tests/program/linspace_tests.py | 21 +-- 2 files changed, 1 insertion(+), 333 deletions(-) delete mode 100644 tests/hardware/hdawg_tests.py diff --git a/tests/hardware/hdawg_tests.py b/tests/hardware/hdawg_tests.py deleted file mode 100644 index 9d01b19fb..000000000 --- a/tests/hardware/hdawg_tests.py +++ /dev/null @@ -1,313 +0,0 @@ -import unittest -import inspect -import numpy as np -import types - -try: - import qupulse_hdawg -except ImportError as err: - raise unittest.SkipTest("qupulse_hdawg not present") from err - -import tests.program.linspace_tests as linspace_tests -from qupulse_hdawg.seqc import HDAWGProgramManager -from qupulse.utils.types import TimeType - - -CT_SCHEMA_2405 = r"""{ - "$schema": "https://json-schema.org/draft-07/schema#", - "title": "AWG Command Table Schema", - "description": "Schema for ZI HDAWG AWG Command Table", - "version": "1.2.1", - "definitions": { - "header": { - "type": "object", - "properties": { - "version": { - "type": "string", - "pattern": "^1\\.[0-2](\\.[0-9]+)?$", - "description": "File format version (Major.Minor / Major.Minor.Patch). This version must match with the relevant schema version." - }, - "partial": { - "description": "Set to true for incremental table updates", - "type": "boolean", - "default": false - }, - "userString": { - "description": "User-definable label", - "type": "string", - "maxLength": 30 - } - }, - "required": [ - "version" - ] - }, - "table": { - "type": "array", - "items": { - "$ref": "#/definitions/entry" - }, - "minItems": 0, - "maxItems": 1024 - }, - "entry": { - "type": "object", - "properties": { - "index": { - "$ref": "#/definitions/tableindex" - }, - "waveform": { - "$ref": "#/definitions/waveform" - }, - "phase0": { - "$ref": "#/definitions/phase" - }, - "phase1": { - "$ref": "#/definitions/phase" - }, - "amplitude0": { - "$ref": "#/definitions/amplitude" - }, - "amplitude1": { - "$ref": "#/definitions/amplitude" - } - }, - "additionalProperties": false, - "anyOf": [ - { - "required": [ - "index", - "waveform" - ] - }, - { - "required": [ - "index", - "phase0" - ] - }, - { - "required": [ - "index", - "phase1" - ] - }, - { - "required": [ - "index", - "amplitude0" - ] - }, - { - "required": [ - "index", - "amplitude1" - ] - } - ] - }, - "tableindex": { - "type": "integer", - "minimum": 0, - "maximum": 1023 - }, - "waveform": { - "type": "object", - "properties": { - "index": { - "$ref": "#/definitions/waveformindex" - }, - "length": { - "$ref": "#/definitions/waveformlength" - }, - "samplingRateDivider": { - "$ref": "#/definitions/samplingratedivider" - }, - "awgChannel0": { - "$ref": "#/definitions/awgchannel" - }, - "awgChannel1": { - "$ref": "#/definitions/awgchannel" - }, - "precompClear": { - "$ref": "#/definitions/precompclear" - }, - "playZero": { - "$ref": "#/definitions/playzero" - }, - "playHold": { - "$ref": "#/definitions/playhold" - } - }, - "additionalProperties": false, - "oneOf": [ - { - "required": [ - "index" - ] - }, - { - "required": [ - "playZero", - "length" - ] - }, - { - "required": [ - "playHold", - "length" - ] - } - ] - }, - "waveformindex": { - "description": "Index of the waveform to play as defined with the assignWaveIndex sequencer instruction", - "type": "integer", - "minimum": 0, - "maximum": 15999 - }, - "waveformlength": { - "description": "The length of the waveform in samples", - "type": "integer", - "multipleOf": 16, - "minimum": 32 - }, - "samplingratedivider": { - "descpription": "Integer exponent n of the sample rate divider: SampleRate / 2^n, n in range 0 ... 13", - "type": "integer", - "minimum": 0, - "maximum": 13 - }, - "awgchannel": { - "description": "Assign the given AWG channel to signal output 0 & 1", - "type": "array", - "minItems": 1, - "maxItems": 2, - "uniqueItems": true, - "items": [ - { - "type": "string", - "enum": [ - "sigout0", - "sigout1" - ] - } - ] - }, - "precompclear": { - "description": "Set to true to clear the precompensation filters", - "type": "boolean", - "default": false - }, - "playzero": { - "description": "Play a zero-valued waveform for specified length of waveform, equivalent to the playZero sequencer instruction", - "type": "boolean", - "default": false - }, - "playhold": { - "description": "Hold the last played value for the specified number of samples, equivalent to the playHold sequencer instruction", - "type": "boolean", - "default": false - }, - "phase": { - "type": "object", - "properties": { - "value": { - "description": "Phase value of the given sine generator in degree", - "type": "number" - }, - "increment": { - "description": "Set to true for incremental phase value, or to false for absolute", - "type": "boolean", - "default": false - } - }, - "additionalProperties": false, - "required": [ - "value" - ] - }, - "amplitude": { - "type": "object", - "properties": { - "value": { - "description": "Amplitude scaling factor of the given AWG channel", - "type": "number", - "minimum": -1.0, - "maximum": 1.0 - }, - "increment": { - "description": "Set to true for incremental amplitude value, or to false for absolute", - "type": "boolean", - "default": false - }, - "register": { - "description": "Index of amplitude register that is selected for scaling the pulse amplitude.", - "type": "integer", - "minimum": 0, - "maximum": 3 - } - }, - "additionalProperties": false, - "anyOf": [ - { - "required": [ - "value" - ] - }, - { - "required": [ - "register" - ] - } - ] - } - }, - "type": "object", - "properties": { - "$schema": { - "type": "string" - }, - "header": { - "$ref": "#/definitions/header" - }, - "table": { - "$ref": "#/definitions/table" - } - }, - "additionalProperties": false, - "required": [ - "header", - "table" - ] -}""" - - -class AllLinspaceTests(unittest.TestCase): - - def setUp(self): - self.test_classes_list = [member for name, member in inspect.getmembers(linspace_tests, inspect.isclass) - if issubclass(member, unittest.TestCase) and not member.__name__=="TestCase"] - - def test_all(self): - - for test_class in self.test_classes_list: - - test_obj = test_class() - test_obj.setUp() - - test_program, channels, markers = test_obj.return_program() - - awg = types.SimpleNamespace(num_channels=2, MAX_SAMPLE_RATE_DIVIDER=13, sample_rate_divider=0, - master_device=types.SimpleNamespace(sample_clock=2.4) - ) - - test_manager = HDAWGProgramManager(awg, lambda idx: tuple([CT_SCHEMA_2405 for i in idx])) - - try: - test_manager.add_program("test", test_program, channels, markers, - np.ones(len(channels)),np.zeros(len(channels)), - [None,]*len(channels), TimeType.from_fraction(24,10)) - except Exception as e: - self.fail(f"{test_class.__name__} raised an exception: {e}") - \ No newline at end of file diff --git a/tests/program/linspace_tests.py b/tests/program/linspace_tests.py index fcf3d0472..03a5b2971 100644 --- a/tests/program/linspace_tests.py +++ b/tests/program/linspace_tests.py @@ -5,7 +5,6 @@ from qupulse.pulses import * from qupulse.program.linspace import * from qupulse.program.transformation import * -from qupulse.program import Program class SingleRampTest(TestCase): def setUp(self): @@ -41,9 +40,6 @@ def test_program(self): def test_commands(self): commands = to_increment_commands([self.program]) self.assertEqual(self.commands, commands) - - def return_program(self) -> Tuple[Program,int,int]: - return self.pulse_template.create_program(program_builder=LinSpaceBuilder(('a',))), ('a',), tuple() class PlainCSDTest(TestCase): @@ -98,10 +94,7 @@ def test_program(self): def test_increment_commands(self): commands = to_increment_commands([self.program]) self.assertEqual(self.commands, commands) - - def return_program(self) -> Tuple[Program,int,int]: - return self.pulse_template.create_program(program_builder=LinSpaceBuilder(('a','b'))), ('a','b'), tuple() - + class TiltedCSDTest(TestCase): def setUp(self): @@ -174,9 +167,6 @@ def test_repeated_increment_commands(self): commands = to_increment_commands([self.repeated_program]) self.assertEqual(self.repeated_commands, commands) - def return_program(self) -> Tuple[Program,int,int]: - return self.repeated_pt.create_program(program_builder=LinSpaceBuilder(('a','b'))), ('a','b'), tuple() - class SingletLoadProcessing(TestCase): def setUp(self): @@ -266,9 +256,6 @@ def test_singlet_scan_commands(self): commands = to_increment_commands([self.program]) self.assertEqual(self.commands, commands) - def return_program(self) -> Tuple[Program,int,int]: - return self.pulse_template.create_program(program_builder=LinSpaceBuilder(('a','b'))), ('a','b'), tuple() - class TransformedRampTest(TestCase): def setUp(self): @@ -300,9 +287,3 @@ def test_local_trafo_program(self): global_transformation=self.transformation, to_single_waveform={self.pulse_template}) self.assertEqual([self.program], program) - - def return_program(self) -> Tuple[Program,int,int]: - return self.pulse_template.create_program(program_builder=LinSpaceBuilder(('a',)), - global_transformation=self.transformation),\ - ('a',), tuple() - \ No newline at end of file From 8ab2fe5fa9f1d08b9d014d6e903ac747efdf9f1b Mon Sep 17 00:00:00 2001 From: Nomos11 <82180697+Nomos11@users.noreply.github.com> Date: Tue, 28 May 2024 23:01:50 +0200 Subject: [PATCH 204/441] silence print --- qupulse/hardware/awgs/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qupulse/hardware/awgs/base.py b/qupulse/hardware/awgs/base.py index 48a06f1ae..38b410378 100644 --- a/qupulse/hardware/awgs/base.py +++ b/qupulse/hardware/awgs/base.py @@ -261,7 +261,7 @@ def _transform_linspace_commands(self, command_list: List[Command]) -> List[Comm trafos_by_channel_idx = {ch: (v,a,o) for ch,v,a,o in zip(_channelname_to_idx.values(),self._voltage_transformations,self._amplitudes,self._offsets)} #the channels are now in idx in the commands. - print(trafos_by_channel_idx) + # print(trafos_by_channel_idx) for command in command_list: if isinstance(command,Union[LoopLabel, LoopJmp, Play, Wait]): From 0d796d0fc65f7b52fc73a190ecd958324be00cd6 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Wed, 29 May 2024 09:49:52 +0200 Subject: [PATCH 205/441] Cleanup _transform_linspace_commands --- qupulse/hardware/awgs/base.py | 47 ++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/qupulse/hardware/awgs/base.py b/qupulse/hardware/awgs/base.py index 38b410378..faa861c04 100644 --- a/qupulse/hardware/awgs/base.py +++ b/qupulse/hardware/awgs/base.py @@ -9,7 +9,7 @@ from abc import abstractmethod from numbers import Real -from typing import Set, Tuple, Callable, Optional, Mapping, Sequence, List, Union +from typing import Set, Tuple, Callable, Optional, Mapping, Sequence, List, Union, NamedTuple from collections import OrderedDict from enum import Enum # from itertools import chain @@ -174,6 +174,12 @@ class _ProgramType(Enum): Linspace = 1 +class ChannelTransformation(NamedTuple): + amplitude: float + offset: float + voltage_transformation: Optional[callable] + + class ProgramEntry: """This is a helper class for implementing awgs drivers. A driver can subclass it to help organizing sampled waveforms""" @@ -250,28 +256,35 @@ def _sample_empty_channel(self, time: numpy.ndarray) -> Optional[numpy.ndarray]: def _sample_empty_marker(self, time: numpy.ndarray) -> Optional[numpy.ndarray]: """Override this in derived class to change how empty channels are handled""" return None + + def _channel_transformations(self) -> Mapping[ChannelID, ChannelTransformation]: + return {ch: ChannelTransformation(amplitude, offset, trafo) + for ch, amplitude, offset, trafo in zip(self._channels, + self._voltage_transformations, + self._amplitudes, + self._offsets)} def _transform_linspace_commands(self, command_list: List[Command]) -> List[Command]: - # all commands = Union[Increment, Set, LoopLabel, LoopJmp, Wait, Play] - if any(self._voltage_transformations): - raise NotImplementedError('how to handle this?') - - _channelname_to_idx = {name: idx for idx, name in enumerate(self._channels)} - trafos_by_channel_idx = {ch: (v,a,o) for ch,v,a,o in zip(_channelname_to_idx.values(),self._voltage_transformations,self._amplitudes,self._offsets)} - #the channels are now in idx in the commands. - - # print(trafos_by_channel_idx) - + trafos_by_channel_idx = list(self._channel_transformations().values()) + for command in command_list: - if isinstance(command,Union[LoopLabel, LoopJmp, Play, Wait]): + if isinstance(command, (LoopLabel, LoopJmp, Play, Wait)): + # play is handled by transforming the sampled waveform continue - elif isinstance(command,Increment): - command.value = command.value / trafos_by_channel_idx[command.channel][1] - elif isinstance(command,LSPSet): - command.value = (command.value - trafos_by_channel_idx[command.channel][2]) / trafos_by_channel_idx[command.channel][1] + elif isinstance(command, Increment): + ch_trafo = trafos_by_channel_idx[command.channel] + if ch_trafo.voltage_transformation: + raise RuntimeError("Cannot apply a voltage transformation to a linspace increment command") + command.value /= ch_trafo.amplitude + elif isinstance(command, LSPSet): + ch_trafo = trafos_by_channel_idx[command.channel] + if ch_trafo.voltage_transformation: + command.value = float(ch_trafo.voltage_transformation(command.value)) + command.value -= ch_trafo.offset + command.value /= ch_trafo.amplitude else: - raise NotImplementedError() + raise NotImplementedError(command) return command_list From 2678b0b2f0427ebf4d01c089ce9cff55affcca03 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Wed, 29 May 2024 09:55:13 +0200 Subject: [PATCH 206/441] Make loop / program relation clearer --- qupulse/hardware/awgs/base.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/qupulse/hardware/awgs/base.py b/qupulse/hardware/awgs/base.py index faa861c04..f45ede269 100644 --- a/qupulse/hardware/awgs/base.py +++ b/qupulse/hardware/awgs/base.py @@ -216,8 +216,7 @@ def __init__(self, program: AllowedProgramTypes, self._sample_rate = sample_rate self._program_type = program_type - self._program = program #non-normalized - self.__loop = program + self._program = program if program_type == _ProgramType.Linspace: self._transformed_commands = self._transform_linspace_commands(to_increment_commands(program)) @@ -240,14 +239,16 @@ def __init__(self, program: AllowedProgramTypes, self._waveforms = OrderedDict() @property - def _loop(self,) -> _ProgramType: - if self._program_type is not _ProgramType.Loop and self._program_type is not _ProgramType.FSP: - raise DeprecationWarning() - return self.__loop + def _loop(self,) -> Loop: + if self._program_type not in (_ProgramType.Loop, _ProgramType.FSP): + raise AttributeError("The _loop attribute can only be get on loop-like program entries.") + return self._program @_loop.setter - def _loop(self,program:_ProgramType): - self.__loop = program + def _loop(self, program: Loop): + if self._program_type not in (_ProgramType.Loop, _ProgramType.FSP): + raise AttributeError("The _loop attribute can only be set on loop-like program entries.") + self._program = program def _sample_empty_channel(self, time: numpy.ndarray) -> Optional[numpy.ndarray]: """Override this in derived class to change how empty channels are handled""" From b2b8061f74a9adeba44ce90633817c96d00d3f7c Mon Sep 17 00:00:00 2001 From: Nomos11 <82180697+Nomos11@users.noreply.github.com> Date: Wed, 29 May 2024 10:43:34 +0200 Subject: [PATCH 207/441] fix ordering in zip() --- qupulse/hardware/awgs/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qupulse/hardware/awgs/base.py b/qupulse/hardware/awgs/base.py index faa861c04..91203c64d 100644 --- a/qupulse/hardware/awgs/base.py +++ b/qupulse/hardware/awgs/base.py @@ -259,7 +259,7 @@ def _sample_empty_marker(self, time: numpy.ndarray) -> Optional[numpy.ndarray]: def _channel_transformations(self) -> Mapping[ChannelID, ChannelTransformation]: return {ch: ChannelTransformation(amplitude, offset, trafo) - for ch, amplitude, offset, trafo in zip(self._channels, + for ch, trafo, amplitude, offset in zip(self._channels, self._voltage_transformations, self._amplitudes, self._offsets)} From 100cd94de4d3bd5655c73a54b4d6e5d1c39dd77a Mon Sep 17 00:00:00 2001 From: Nomos11 <82180697+Nomos11@users.noreply.github.com> Date: Wed, 29 May 2024 13:14:05 +0200 Subject: [PATCH 208/441] ParallelChannelTransformation: return true float if time has no len in --- qupulse/program/transformation.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qupulse/program/transformation.py b/qupulse/program/transformation.py index 784e8e193..a12f44995 100644 --- a/qupulse/program/transformation.py +++ b/qupulse/program/transformation.py @@ -328,7 +328,8 @@ def __call__(self, time: Union[np.ndarray, float], def _instantiated_values(self, time): scope = {'t': time} - return {channel: value.evaluate_in_scope(scope) if hasattr(value, 'evaluate_in_scope') else np.full_like(time, fill_value=value, dtype=float) + array_or_float = lambda x: np.full_like(time, fill_value=x, dtype=float) if hasattr(time, '__len__') and len(time)>1 else x + return {channel: value.evaluate_in_scope(scope) if hasattr(value, 'evaluate_in_scope') else array_or_float(value) for channel, value in self._channels.items()} @property From 0e430e22232b468d50424f25ab33f6a03cd0f3e5 Mon Sep 17 00:00:00 2001 From: Nomos11 <82180697+Nomos11@users.noreply.github.com> Date: Wed, 29 May 2024 14:29:52 +0200 Subject: [PATCH 209/441] remove len>1 check --- qupulse/program/transformation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qupulse/program/transformation.py b/qupulse/program/transformation.py index a12f44995..1797215b6 100644 --- a/qupulse/program/transformation.py +++ b/qupulse/program/transformation.py @@ -328,7 +328,7 @@ def __call__(self, time: Union[np.ndarray, float], def _instantiated_values(self, time): scope = {'t': time} - array_or_float = lambda x: np.full_like(time, fill_value=x, dtype=float) if hasattr(time, '__len__') and len(time)>1 else x + array_or_float = lambda x: np.full_like(time, fill_value=x, dtype=float) if hasattr(time, '__len__') else x return {channel: value.evaluate_in_scope(scope) if hasattr(value, 'evaluate_in_scope') else array_or_float(value) for channel, value in self._channels.items()} From ea88576ea5993d9dae3b9b410e6dd96f86cd0f1c Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 3 Jun 2024 14:14:34 +0200 Subject: [PATCH 210/441] Move pytest config to pyproject and use default path --- pyproject.toml | 18 ++++++++++++++++++ setup.cfg | 11 ----------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 994c5e81e..cd673d74b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,3 +8,21 @@ package_dir = "./qupulse" filename = "RELEASE_NOTES.rst" name = "qupulse" issue_format = "`#{issue} `_" + +[tool.pytest.ini_options] +minversion = "6.0" +python_files = [ + "*_tests.py", + "*_bug.py" +] +filterwarnings = [ + # syntax is action:message_regex:category:module_regex:lineno + # we fail on all with a whitelist because a dependency might mess-up passing the correct stacklevel + "error::SyntaxWarning", + "error::DeprecationWarning", + # pytest uses readline which uses collections.abc + # "ignore:Using or importing the ABCs from 'collections' instead of from 'collections\.abc\' is deprecated:DeprecationWarning:.*readline.*" +] + + + diff --git a/setup.cfg b/setup.cfg index a77b1e909..fceb89d5a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -70,17 +70,6 @@ qupulse = qctoolkit = *.pyi -[tool:pytest] -testpaths = tests tests/pulses tests/hardware tests/backward_compatibility -python_files=*_tests.py *_bug.py -filterwarnings = -# syntax is action:message_regex:category:module_regex:lineno -# we fail on all with a whitelist because a dependency might mess-up passing the correct stacklevel - error::SyntaxWarning - error::DeprecationWarning -# pytest uses readline which uses collections.abc - ignore:Using or importing the ABCs from \'collections\' instead of from \'collections\.abc\' is deprecated:DeprecationWarning:.*readline.* - [build_sphinx] project = 'qupulse' version = 0.9 From 2342c57736bec846cf3c7f55184b8ecc0af8b077 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 4 Jun 2024 13:49:00 +0200 Subject: [PATCH 211/441] Only trigger import hacking on manual request --- tests/utils/time_type_tests.py | 381 +++++++++++++++++---------------- 1 file changed, 193 insertions(+), 188 deletions(-) diff --git a/tests/utils/time_type_tests.py b/tests/utils/time_type_tests.py index 93e118325..cdb9ccffd 100644 --- a/tests/utils/time_type_tests.py +++ b/tests/utils/time_type_tests.py @@ -5,6 +5,8 @@ import importlib import fractions import random +import os + from unittest import mock try: @@ -18,6 +20,9 @@ import qupulse.utils.types as qutypes +MOCK_GMPY2_AS_MISSING = bool(os.getenv("QUPULSE_TESTS_MOCK_GMPY2_AS_MISSING")) + + @contextlib.contextmanager def mock_missing_module(module_name: str): exit_stack = contextlib.ExitStack() @@ -43,21 +48,191 @@ def mock_import(name, *args, **kwargs): yield -class TestTimeType(unittest.TestCase): - """The fallback test is here for convenience while developing. The fallback is also tested by the CI explicitly""" +@unittest.skipIf(gmpy2 and not MOCK_GMPY2_AS_MISSING, "Not explicitly included. " + "Define QUPULSE_TESTS_MOCK_GMPY2_AS_MISSING to include.") +class TestTimeTypeDevFallback(unittest.TestCase): + @classmethod + def setUpClass(cls): + with mock_missing_module('gmpy2'): + cls.fallback_qutypes = importlib.reload(qutypes) + + def test_fraction_fallback(self): + self.assertIs(fractions.Fraction, self.fallback_qutypes.TimeType._InternalType) - _fallback_qutypes = None + def test_fraction_time_from_fraction_fallback(self): + assert_from_fraction_works(self, self.fallback_qutypes.TimeType) - @property - def fallback_qutypes(self): - if not self._fallback_qutypes: - if gmpy2: - with mock_missing_module('gmpy2'): - self._fallback_qutypes = importlib.reload(qutypes) + def test_fraction_time_from_float_exact_fallback(self): + assert_from_float_exact_works(self, self.fallback_qutypes.TimeType) - else: - self._fallback_qutypes = qutypes - return self._fallback_qutypes + def test_fraction_time_from_float_with_precision_fallback(self): + assert_fraction_time_from_float_with_precision_works(self, self.fallback_qutypes.TimeType) + + def test_from_float_no_extra_args_fallback(self): + assert_from_float_no_extra_args_works(self, self.fallback_qutypes.TimeType) + + def test_try_from_any_fallback(self): + assert_try_from_any_works(self, self.fallback_qutypes.TimeType) + + def test_comparisons_work_fallback(self): + assert_comparisons_work(self, self.fallback_qutypes.TimeType) + + +def assert_from_fraction_works(test: unittest.TestCase, time_type): + t = time_type.from_fraction(43, 12) + test.assertIsInstance(t, time_type) + test.assertEqual(t, fractions.Fraction(43, 12)) + + +def assert_from_float_exact_works(test: unittest.TestCase, time_type): + test.assertEqual(time_type.from_float(123 / 931, 0), + fractions.Fraction(123 / 931)) + + +def assert_fraction_time_from_float_with_precision_works(test: unittest.TestCase, time_type): + test.assertEqual(time_type.from_float(1000000 / 1000001, 1e-5), + fractions.Fraction(1)) + test.assertEqual(time_type.from_float(2.50000000000008, absolute_error=1e-10), + time_type.from_fraction(5, 2)) + test.assertEqual(time_type.from_float(9926.666666667, absolute_error=1e-9), + time_type.from_fraction(29780, 3)) + + +def assert_from_float_no_extra_args_works(test: unittest.TestCase, time_type): + # test that float(from_float(x)) == x + base_floats = [4/5, 1, 1000, 0, np.pi, 1.23456789**99, 1e-100, 2**53] + n_steps = 10**2 + + def float_generator(): + for f in base_floats: + for _ in range(n_steps): + yield f + f = np.nextafter(f, float('inf')) + + for f in base_floats: + for _ in range(n_steps): + yield f + f = np.nextafter(f, float('-inf')) + + for x in float_generator(): + t = time_type.from_float(x) + t2x = float(t) + test.assertEqual(x, t2x) + test.assertGreater(t, np.nextafter(x, float('-inf'))) + test.assertLess(t, np.nextafter(x, float('inf'))) + + +def assert_try_from_any_works(test: unittest.TestCase, time_type): + try_from_any = time_type._try_from_any + + # these duck types are here because isinstance(, numbers.) is version dependent + class DuckTypeWrapper: + def __init__(self, value): + self.value = value + + def __repr__(self): + return f'{type(self)}({self.value})' + + class DuckInt(DuckTypeWrapper): + def __int__(self): + return int(self.value) + + class DuckFloat(DuckTypeWrapper): + def __float__(self): + return float(self.value) + + class DuckIntFloat(DuckFloat): + def __int__(self): + return int(self.value) + + class DuckRational: + def __init__(self, numerator, denominator): + self.numerator = numerator + self.denominator = denominator + + def __repr__(self): + return f'{type(self)}({self.numerator}, {self.denominator})' + + for_array_tests = [] + + signed_int_types = [int, sympy.Integer, np.int8, np.int16, np.int32, np.int64, DuckInt, DuckIntFloat] + if gmpy2: + signed_int_types.append(gmpy2.mpz) + + for s_t in signed_int_types: + for val in (1, 17, -17): + any_val = s_t(val) + expected_val = time_type.from_fraction(int(val), 1) + test.assertEqual(expected_val, try_from_any(any_val)) + for_array_tests.append((expected_val, any_val)) + + unsigned_int_types = [np.uint8, np.uint16, np.uint32, np.uint] + for u_t in unsigned_int_types: + for val in (1, 17): + any_val = u_t(val) + expected_val = time_type.from_fraction(int(val), 1) + test.assertEqual(expected_val, try_from_any(any_val)) + for_array_tests.append((expected_val, any_val)) + + rational_types = [fractions.Fraction, sympy.Rational, time_type.from_fraction, DuckRational] + if gmpy2: + rational_types.append(gmpy2.mpq) + for r_t in rational_types: + for num, den in ((1, 3), (-3, 8), (17, 5)): + any_val = r_t(num, den) + expected_val = time_type.from_fraction(num, den) + test.assertEqual(expected_val, try_from_any(any_val)) + for_array_tests.append((expected_val, any_val)) + + float_types = [float, sympy.Float, DuckFloat, DuckIntFloat] + if gmpy2: + float_types.append(gmpy2.mpfr) + for f_t in float_types: + for val in (3.4, -3., 1.): + any_val = f_t(val) + expected_val = time_type.from_float(val) + test.assertEqual(expected_val, try_from_any(any_val)) + for_array_tests.append((expected_val, any_val)) + + arr = np.array(for_array_tests, dtype='O') + any_arr = arr[:, 1] + expected_arr = arr[:, 0] + np.testing.assert_equal(expected_arr, try_from_any(any_arr)) + + +def assert_comparisons_work(test: unittest.TestCase, time_type): + tt = time_type.from_float(1.1) + + test.assertLess(tt, 4) + test.assertLess(tt, 4.) + test.assertLess(tt, time_type.from_float(4.)) + test.assertLess(tt, float('inf')) + + test.assertLessEqual(tt, 4) + test.assertLessEqual(tt, 4.) + test.assertLessEqual(tt, time_type.from_float(4.)) + test.assertLessEqual(tt, float('inf')) + + test.assertGreater(tt, 1) + test.assertGreater(tt, 1.) + test.assertGreater(tt, time_type.from_float(1.)) + test.assertGreater(tt, float('-inf')) + + test.assertGreaterEqual(tt, 1) + test.assertGreaterEqual(tt, 1.) + test.assertGreaterEqual(tt, time_type.from_float(1.)) + test.assertGreaterEqual(tt, float('-inf')) + + test.assertFalse(tt == float('nan')) + test.assertFalse(tt <= float('nan')) + test.assertFalse(tt >= float('nan')) + test.assertFalse(tt < float('nan')) + test.assertFalse(tt > float('nan')) + + +class TestTimeType(unittest.TestCase): + """The fallback test is here for convenience while developing and only triggered if the environment variable is set. + The fallback is also tested by the CI explicitly""" def test_non_finite_float(self): with self.assertRaisesRegex(ValueError, 'Cannot represent'): @@ -67,76 +242,17 @@ def test_non_finite_float(self): with self.assertRaisesRegex(ValueError, 'Cannot represent'): qutypes.TimeType.from_float(float('nan')) - def test_fraction_fallback(self): - self.assertIs(fractions.Fraction, self.fallback_qutypes.TimeType._InternalType) - - def assert_from_fraction_works(self, time_type): - t = time_type.from_fraction(43, 12) - self.assertIsInstance(t, time_type) - self.assertEqual(t, fractions.Fraction(43, 12)) - def test_fraction_time_from_fraction(self): - self.assert_from_fraction_works(qutypes.TimeType) - - @unittest.skipIf(gmpy2 is None, "fallback already tested") - def test_fraction_time_from_fraction_fallback(self): - self.assert_from_fraction_works(self.fallback_qutypes.TimeType) - - def assert_from_float_exact_works(self, time_type): - self.assertEqual(time_type.from_float(123 / 931, 0), - fractions.Fraction(123 / 931)) + assert_from_fraction_works(self, qutypes.TimeType) def test_fraction_time_from_float_exact(self): - self.assert_from_float_exact_works(qutypes.TimeType) - - @unittest.skipIf(gmpy2 is None, "fallback already tested") - def test_fraction_time_from_float_exact_fallback(self): - self.assert_from_float_exact_works(self.fallback_qutypes.TimeType) - - def assert_fraction_time_from_float_with_precision_works(self, time_type): - self.assertEqual(time_type.from_float(1000000 / 1000001, 1e-5), - fractions.Fraction(1)) - self.assertEqual(time_type.from_float(2.50000000000008, absolute_error=1e-10), - time_type.from_fraction(5, 2)) - self.assertEqual(time_type.from_float(9926.666666667, absolute_error=1e-9), - time_type.from_fraction(29780, 3)) + assert_from_float_exact_works(self, qutypes.TimeType) def test_fraction_time_from_float_with_precision(self): - self.assert_fraction_time_from_float_with_precision_works(qutypes.TimeType) - - @unittest.skipIf(gmpy2 is None, "fallback already tested") - def test_fraction_time_from_float_with_precision_fallback(self): - self.assert_fraction_time_from_float_with_precision_works(self.fallback_qutypes.TimeType) - - def assert_from_float_no_extra_args_works(self, time_type): - # test that float(from_float(x)) == x - base_floats = [4/5, 1, 1000, 0, np.pi, 1.23456789**99, 1e-100, 2**53] - n_steps = 10**2 - - def float_generator(): - for f in base_floats: - for _ in range(n_steps): - yield f - f = np.nextafter(f, float('inf')) - - for f in base_floats: - for _ in range(n_steps): - yield f - f = np.nextafter(f, float('-inf')) - - for x in float_generator(): - t = time_type.from_float(x) - t2x = float(t) - self.assertEqual(x, t2x) - self.assertGreater(t, np.nextafter(x, float('-inf'))) - self.assertLess(t, np.nextafter(x, float('inf'))) + assert_fraction_time_from_float_with_precision_works(self, qutypes.TimeType) def test_from_float_no_extra_args(self): - self.assert_from_float_exact_works(qutypes.TimeType) - - @unittest.skipIf(gmpy2 is None, "fallback already tested") - def test_from_float_no_extra_args_fallback(self): - self.assert_from_float_exact_works(self.fallback_qutypes.TimeType) + assert_from_float_exact_works(self, qutypes.TimeType) def test_from_float_exceptions(self): with self.assertRaisesRegex(ValueError, '> 0'): @@ -145,122 +261,11 @@ def test_from_float_exceptions(self): with self.assertRaisesRegex(ValueError, '<= 1'): qutypes.time_from_float(.8, 2) - def assert_try_from_any_works(self, time_type): - try_from_any = time_type._try_from_any - - # these duck types are here because isinstance(, numbers.) is version dependent - class DuckTypeWrapper: - def __init__(self, value): - self.value = value - - def __repr__(self): - return f'{type(self)}({self.value})' - - class DuckInt(DuckTypeWrapper): - def __int__(self): - return int(self.value) - - class DuckFloat(DuckTypeWrapper): - def __float__(self): - return float(self.value) - - class DuckIntFloat(DuckFloat): - def __int__(self): - return int(self.value) - - class DuckRational: - def __init__(self, numerator, denominator): - self.numerator = numerator - self.denominator = denominator - - def __repr__(self): - return f'{type(self)}({self.numerator}, {self.denominator})' - - for_array_tests = [] - - signed_int_types = [int, sympy.Integer, np.int8, np.int16, np.int32, np.int64, DuckInt, DuckIntFloat] - if gmpy2: - signed_int_types.append(gmpy2.mpz) - - for s_t in signed_int_types: - for val in (1, 17, -17): - any_val = s_t(val) - expected_val = time_type.from_fraction(int(val), 1) - self.assertEqual(expected_val, try_from_any(any_val)) - for_array_tests.append((expected_val, any_val)) - - unsigned_int_types = [np.uint8, np.uint16, np.uint32, np.uint] - for u_t in unsigned_int_types: - for val in (1, 17): - any_val = u_t(val) - expected_val = time_type.from_fraction(int(val), 1) - self.assertEqual(expected_val, try_from_any(any_val)) - for_array_tests.append((expected_val, any_val)) - - rational_types = [fractions.Fraction, sympy.Rational, time_type.from_fraction, DuckRational] - if gmpy2: - rational_types.append(gmpy2.mpq) - for r_t in rational_types: - for num, den in ((1, 3), (-3, 8), (17, 5)): - any_val = r_t(num, den) - expected_val = time_type.from_fraction(num, den) - self.assertEqual(expected_val, try_from_any(any_val)) - for_array_tests.append((expected_val, any_val)) - - float_types = [float, sympy.Float, DuckFloat, DuckIntFloat] - if gmpy2: - float_types.append(gmpy2.mpfr) - for f_t in float_types: - for val in (3.4, -3., 1.): - any_val = f_t(val) - expected_val = time_type.from_float(val) - self.assertEqual(expected_val, try_from_any(any_val)) - for_array_tests.append((expected_val, any_val)) - - arr = np.array(for_array_tests, dtype='O') - any_arr = arr[:, 1] - expected_arr = arr[:, 0] - np.testing.assert_equal(expected_arr, try_from_any(any_arr)) - def test_try_from_any(self): - self.assert_try_from_any_works(qutypes.TimeType) - self.assert_try_from_any_works(self.fallback_qutypes.TimeType) - - def assert_comparisons_work(self, time_type): - tt = time_type.from_float(1.1) - - self.assertLess(tt, 4) - self.assertLess(tt, 4.) - self.assertLess(tt, time_type.from_float(4.)) - self.assertLess(tt, float('inf')) - - self.assertLessEqual(tt, 4) - self.assertLessEqual(tt, 4.) - self.assertLessEqual(tt, time_type.from_float(4.)) - self.assertLessEqual(tt, float('inf')) - - self.assertGreater(tt, 1) - self.assertGreater(tt, 1.) - self.assertGreater(tt, time_type.from_float(1.)) - self.assertGreater(tt, float('-inf')) - - self.assertGreaterEqual(tt, 1) - self.assertGreaterEqual(tt, 1.) - self.assertGreaterEqual(tt, time_type.from_float(1.)) - self.assertGreaterEqual(tt, float('-inf')) - - self.assertFalse(tt == float('nan')) - self.assertFalse(tt <= float('nan')) - self.assertFalse(tt >= float('nan')) - self.assertFalse(tt < float('nan')) - self.assertFalse(tt > float('nan')) + assert_try_from_any_works(self, qutypes.TimeType) def test_comparisons_work(self): - self.assert_comparisons_work(qutypes.TimeType) - - @unittest.skipIf(gmpy2 is None, "fallback already tested") - def test_comparisons_work_fallback(self): - self.assert_comparisons_work(self.fallback_qutypes.TimeType) + assert_comparisons_work(self, qutypes.TimeType) def get_some_floats(seed=42, n=1000): From 4f4721dd5ee9131ef3b72f4868c6a2cb35d4dff3 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 3 Jun 2024 13:57:13 +0200 Subject: [PATCH 212/441] Make Waveform comparison __slots__based --- qupulse/program/waveforms.py | 56 +++++++----------------------------- 1 file changed, 11 insertions(+), 45 deletions(-) diff --git a/qupulse/program/waveforms.py b/qupulse/program/waveforms.py index 94c395532..8765ee5d4 100644 --- a/qupulse/program/waveforms.py +++ b/qupulse/program/waveforms.py @@ -52,7 +52,7 @@ def _to_time_type(duration: Real) -> TimeType: return time_from_float(float(duration), absolute_error=PULSE_TO_WAVEFORM_ERROR) -class Waveform(Comparable, metaclass=ABCMeta): +class Waveform(metaclass=ABCMeta): """Represents an instantiated PulseTemplate which can be sampled to retrieve arrays of voltage values for the hardware.""" @@ -143,6 +143,16 @@ def get_sampled(self, output_array[:] = constant_value return output_array + def __hash__(self): + return hash(tuple(getattr(self, slot) for slot in self.__slots__)) + + def __eq__(self, other): + slots = self.__slots__ + if slots is getattr(other, '__slots__', None): + return all(getattr(self, slot) == getattr(other, slot) for slot in slots) + # The other class might be more lenient + return NotImplemented + @property @abstractmethod def defined_channels(self) -> AbstractSet[ChannelID]: @@ -350,10 +360,6 @@ def from_table(cls, channel: ChannelID, table: Sequence[EntryInInit]) -> Union[' else: return TableWaveform(channel, tuple(table)) - @property - def compare_key(self) -> Any: - return self._channel_id, self._table - def unsafe_sample(self, channel: ChannelID, sample_times: np.ndarray, @@ -434,10 +440,6 @@ def defined_channels(self) -> AbstractSet[ChannelID]: return {self._channel} - @property - def compare_key(self) -> Tuple[Any, ...]: - return self._duration, self._amplitude, self._channel - def unsafe_sample(self, channel: ChannelID, sample_times: np.ndarray, @@ -506,10 +508,6 @@ def constant_value_dict(self) -> Optional[Mapping[ChannelID, float]]: def defined_channels(self) -> AbstractSet[ChannelID]: return {self._channel_id} - @property - def compare_key(self) -> Any: - return self._channel_id, self._expression, self._duration - @property def duration(self) -> TimeType: return self._duration @@ -636,10 +634,6 @@ def unsafe_sample(self, time = end return output_array - @property - def compare_key(self) -> Tuple[Waveform]: - return self._sequenced_waveforms - @property def duration(self) -> TimeType: return self._duration @@ -784,11 +778,6 @@ def __getitem__(self, key: ChannelID) -> Waveform: def defined_channels(self) -> AbstractSet[ChannelID]: return self._defined_channels - @property - def compare_key(self) -> Any: - # sort with channels - return self._sub_waveforms - def unsafe_sample(self, channel: ChannelID, sample_times: np.ndarray, @@ -853,10 +842,6 @@ def unsafe_sample(self, time = end return output_array - @property - def compare_key(self) -> Tuple[Any, int]: - return self._body.compare_key, self._repetition_count - def unsafe_get_subset_for_channels(self, channels: AbstractSet[ChannelID]) -> Waveform: return RepetitionWaveform.from_repetition_count( body=self._body.unsafe_get_subset_for_channels(channels), @@ -928,10 +913,6 @@ def transformation(self) -> Transformation: def defined_channels(self) -> AbstractSet[ChannelID]: return self.transformation.get_output_channels(self.inner_waveform.defined_channels) - @property - def compare_key(self) -> Tuple[Waveform, Transformation]: - return self.inner_waveform, self.transformation - def unsafe_get_subset_for_channels(self, channels: Set[ChannelID]) -> 'SubsetWaveform': return SubsetWaveform(self, channel_subset=channels) @@ -977,10 +958,6 @@ def inner_waveform(self) -> Waveform: def defined_channels(self) -> FrozenSet[ChannelID]: return self._channel_subset - @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) @@ -1128,10 +1105,6 @@ def unsafe_get_subset_for_channels(self, channels: Set[ChannelID]) -> Waveform: # TODO: optimization possible return SubsetWaveform(self, channels) - @property - def compare_key(self) -> Tuple[str, Waveform, Waveform]: - return self._arithmetic_operator, self._lhs, self._rhs - class FunctorWaveform(Waveform): # TODO: Use Protocol to enforce that it accepts second argument has the keyword out @@ -1188,9 +1161,6 @@ def unsafe_get_subset_for_channels(self, channels: Set[ChannelID]) -> Waveform: self._inner_waveform.unsafe_get_subset_for_channels(channels), {ch: self._functor[ch] for ch in channels}) - @property - def compare_key(self) -> Tuple[Waveform, FrozenSet]: - return self._inner_waveform, frozenset(self._functor.items()) class ReversedWaveform(Waveform): @@ -1229,9 +1199,5 @@ def defined_channels(self) -> AbstractSet[ChannelID]: def unsafe_get_subset_for_channels(self, channels: AbstractSet[ChannelID]) -> 'Waveform': return ReversedWaveform.from_to_reverse(self._inner.unsafe_get_subset_for_channels(channels)) - @property - def compare_key(self) -> Hashable: - return self._inner.compare_key - def reversed(self) -> 'Waveform': return self._inner From 839e1143f1b1a71c291ca08ac15bb4ed08c11e3b Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 4 Jun 2024 14:19:35 +0200 Subject: [PATCH 213/441] Re-introduce compare_key with deprecation warning --- qupulse/program/waveforms.py | 65 ++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/qupulse/program/waveforms.py b/qupulse/program/waveforms.py index 8765ee5d4..437cbfe70 100644 --- a/qupulse/program/waveforms.py +++ b/qupulse/program/waveforms.py @@ -360,6 +360,12 @@ def from_table(cls, channel: ChannelID, table: Sequence[EntryInInit]) -> Union[' else: return TableWaveform(channel, tuple(table)) + @property + def compare_key(self) -> Any: + warnings.warn("Waveform.compare_key is deprecated since 0.11 and will be removed in 0.12", + DeprecationWarning, stacklevel=2) + return self._channel_id, self._table + def unsafe_sample(self, channel: ChannelID, sample_times: np.ndarray, @@ -440,6 +446,12 @@ def defined_channels(self) -> AbstractSet[ChannelID]: return {self._channel} + @property + def compare_key(self) -> Tuple[Any, ...]: + warnings.warn("Waveform.compare_key is deprecated since 0.11 and will be removed in 0.12", + DeprecationWarning, stacklevel=2) + return self._duration, self._amplitude, self._channel + def unsafe_sample(self, channel: ChannelID, sample_times: np.ndarray, @@ -508,6 +520,12 @@ def constant_value_dict(self) -> Optional[Mapping[ChannelID, float]]: def defined_channels(self) -> AbstractSet[ChannelID]: return {self._channel_id} + @property + def compare_key(self) -> Any: + warnings.warn("Waveform.compare_key is deprecated since 0.11 and will be removed in 0.12", + DeprecationWarning, stacklevel=2) + return self._channel_id, self._expression, self._duration + @property def duration(self) -> TimeType: return self._duration @@ -634,6 +652,12 @@ def unsafe_sample(self, time = end return output_array + @property + def compare_key(self) -> Tuple[Waveform]: + warnings.warn("Waveform.compare_key is deprecated since 0.11 and will be removed in 0.12", + DeprecationWarning, stacklevel=2) + return self._sequenced_waveforms + @property def duration(self) -> TimeType: return self._duration @@ -778,6 +802,12 @@ def __getitem__(self, key: ChannelID) -> Waveform: def defined_channels(self) -> AbstractSet[ChannelID]: return self._defined_channels + @property + def compare_key(self) -> Any: + warnings.warn("Waveform.compare_key is deprecated since 0.11 and will be removed in 0.12", + DeprecationWarning, stacklevel=2) + return self._sub_waveforms + def unsafe_sample(self, channel: ChannelID, sample_times: np.ndarray, @@ -842,6 +872,12 @@ def unsafe_sample(self, time = end return output_array + @property + def compare_key(self) -> Tuple[Any, int]: + warnings.warn("Waveform.compare_key is deprecated since 0.11 and will be removed in 0.12", + DeprecationWarning, stacklevel=2) + return self._body.compare_key, self._repetition_count + def unsafe_get_subset_for_channels(self, channels: AbstractSet[ChannelID]) -> Waveform: return RepetitionWaveform.from_repetition_count( body=self._body.unsafe_get_subset_for_channels(channels), @@ -913,6 +949,12 @@ def transformation(self) -> Transformation: def defined_channels(self) -> AbstractSet[ChannelID]: return self.transformation.get_output_channels(self.inner_waveform.defined_channels) + @property + def compare_key(self) -> Tuple[Waveform, Transformation]: + warnings.warn("Waveform.compare_key is deprecated since 0.11 and will be removed in 0.12", + DeprecationWarning, stacklevel=2) + return self.inner_waveform, self.transformation + def unsafe_get_subset_for_channels(self, channels: Set[ChannelID]) -> 'SubsetWaveform': return SubsetWaveform(self, channel_subset=channels) @@ -958,6 +1000,12 @@ def inner_waveform(self) -> Waveform: def defined_channels(self) -> FrozenSet[ChannelID]: return self._channel_subset + @property + def compare_key(self) -> Tuple[frozenset, Waveform]: + warnings.warn("Waveform.compare_key is deprecated since 0.11 and will be removed in 0.12", + DeprecationWarning, stacklevel=2) + 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) @@ -1105,6 +1153,12 @@ def unsafe_get_subset_for_channels(self, channels: Set[ChannelID]) -> Waveform: # TODO: optimization possible return SubsetWaveform(self, channels) + @property + def compare_key(self) -> Tuple[str, Waveform, Waveform]: + warnings.warn("Waveform.compare_key is deprecated since 0.11 and will be removed in 0.12", + DeprecationWarning, stacklevel=2) + return self._arithmetic_operator, self._lhs, self._rhs + class FunctorWaveform(Waveform): # TODO: Use Protocol to enforce that it accepts second argument has the keyword out @@ -1161,6 +1215,11 @@ def unsafe_get_subset_for_channels(self, channels: Set[ChannelID]) -> Waveform: self._inner_waveform.unsafe_get_subset_for_channels(channels), {ch: self._functor[ch] for ch in channels}) + @property + def compare_key(self) -> Tuple[Waveform, FrozenSet]: + warnings.warn("Waveform.compare_key is deprecated since 0.11 and will be removed in 0.12", + DeprecationWarning, stacklevel=2) + return self._inner_waveform, frozenset(self._functor.items()) class ReversedWaveform(Waveform): @@ -1199,5 +1258,11 @@ def defined_channels(self) -> AbstractSet[ChannelID]: def unsafe_get_subset_for_channels(self, channels: AbstractSet[ChannelID]) -> 'Waveform': return ReversedWaveform.from_to_reverse(self._inner.unsafe_get_subset_for_channels(channels)) + @property + def compare_key(self) -> Hashable: + warnings.warn("Waveform.compare_key is deprecated since 0.11 and will be removed in 0.12", + DeprecationWarning, stacklevel=2) + return self._inner.compare_key + def reversed(self) -> 'Waveform': return self._inner From 3d1ebbb3ed62653d3933f33ce80784d2aa72b1d6 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 4 Jun 2024 14:49:00 +0200 Subject: [PATCH 214/441] Include duration in Waveform comparison --- qupulse/program/waveforms.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/qupulse/program/waveforms.py b/qupulse/program/waveforms.py index 437cbfe70..a492169a3 100644 --- a/qupulse/program/waveforms.py +++ b/qupulse/program/waveforms.py @@ -144,12 +144,14 @@ def get_sampled(self, return output_array def __hash__(self): - return hash(tuple(getattr(self, slot) for slot in self.__slots__)) + if self.__class__.__base__ is not Waveform: + raise NotImplementedError("Waveforms __hash__ and __eq__ implementation requires direct inheritance") + return hash(tuple(getattr(self, slot) for slot in self.__slots__)) ^ hash(self._duration) def __eq__(self, other): slots = self.__slots__ if slots is getattr(other, '__slots__', None): - return all(getattr(self, slot) == getattr(other, slot) for slot in slots) + return self._duration == other._duration and all(getattr(self, slot) == getattr(other, slot) for slot in slots) # The other class might be more lenient return NotImplemented From 3a1042da7d5b51b26ed6452551c586cf38fb8c88 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 4 Jun 2024 14:49:34 +0200 Subject: [PATCH 215/441] Exclude cached attributes from TransformingWaveform comparison --- qupulse/program/waveforms.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/qupulse/program/waveforms.py b/qupulse/program/waveforms.py index a492169a3..ac077bb1f 100644 --- a/qupulse/program/waveforms.py +++ b/qupulse/program/waveforms.py @@ -911,6 +911,14 @@ def __init__(self, inner_waveform: Waveform, transformation: Transformation): self._cached_data = None self._cached_times = lambda: None + def __hash__(self): + return hash((self._inner_waveform, self._transformation)) + + def __eq__(self, other): + if getattr(other, '__slots__', None) is self.__slots__: + return self._inner_waveform == other._inner_waveform and self._transformation == other._transformation + return NotImplemented + @classmethod def from_transformation(cls, inner_waveform: Waveform, transformation: Transformation) -> Waveform: constant_values = inner_waveform.constant_value_dict() From 2c686e695adf77f8ee2af0cf5684c8a043bb4bea Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 4 Jun 2024 14:50:29 +0200 Subject: [PATCH 216/441] Update tests to reflect compare_key deprecation --- tests/_program/waveforms_tests.py | 55 +++++++++++++++++++----------- tests/pulses/sequencing_dummies.py | 7 +++- 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/tests/_program/waveforms_tests.py b/tests/_program/waveforms_tests.py index c62fceb3a..ece22dce1 100644 --- a/tests/_program/waveforms_tests.py +++ b/tests/_program/waveforms_tests.py @@ -207,7 +207,10 @@ def test_init_several_channels(self) -> None: dwf_c_valid = DummyWaveform(duration=2.2, defined_channels={'C'}) waveform_flat = MultiChannelWaveform.from_parallel((waveform, dwf_c_valid)) - self.assertEqual(len(waveform_flat.compare_key), 3) + self.assertEqual( + MultiChannelWaveform([dwf_a, dwf_b, dwf_c_valid]), + waveform_flat + ) def test_unsafe_sample(self) -> None: sample_times = numpy.linspace(98.5, 103.5, num=11) @@ -330,10 +333,17 @@ def test_defined_channels(self): body_wf = DummyWaveform(defined_channels={'a'}) self.assertIs(RepetitionWaveform(body_wf, 2).defined_channels, body_wf.defined_channels) - def test_compare_key(self): - body_wf = DummyWaveform(defined_channels={'a'}) - wf = RepetitionWaveform(body_wf, 2) - self.assertEqual(wf.compare_key, (body_wf.compare_key, 2)) + def test_equality(self): + body_wf_1 = DummyWaveform(defined_channels={'a'}) + wf_1 = RepetitionWaveform(body_wf_1, 2) + body_wf_2 = DummyWaveform(defined_channels={'a'}) + wf_2 = RepetitionWaveform(body_wf_2, 2) + wf_3 = RepetitionWaveform(body_wf_1, 3) + wf_1_equal = RepetitionWaveform(body_wf_1, 2) + self.assertEqual(wf_1_equal, wf_1) + self.assertNotEqual(wf_1, wf_2) + self.assertNotEqual(wf_1, wf_3) + self.assertEqual({wf_1, wf_2, wf_3}, {wf_1, wf_2, wf_3, wf_1_equal}) def test_unsafe_get_subset_for_channels(self): body_wf = DummyWaveform(defined_channels={'a', 'b'}) @@ -395,12 +405,11 @@ def test_init(self): swf1 = SequenceWaveform((dwf_ab, dwf_ab)) self.assertEqual(swf1.duration, 2*dwf_ab.duration) - self.assertEqual(len(swf1.compare_key), 2) + self.assertEqual(swf1.sequenced_waveforms, (dwf_ab, dwf_ab)) swf2 = SequenceWaveform((swf1, dwf_ab)) self.assertEqual(swf2.duration, 3 * dwf_ab.duration) - - self.assertEqual(len(swf2.compare_key), 2) + self.assertEqual(swf2.sequenced_waveforms, (swf1, dwf_ab)) def test_from_sequence(self): dwf = DummyWaveform(duration=1.1, defined_channels={'A'}) @@ -478,12 +487,12 @@ def test_unsafe_get_subset_for_channels(self): sub_wf = wf.unsafe_get_subset_for_channels(subset) self.assertIsInstance(sub_wf, SequenceWaveform) - self.assertEqual(len(sub_wf.compare_key), 2) - self.assertEqual(sub_wf.compare_key[0].defined_channels, subset) - self.assertEqual(sub_wf.compare_key[1].defined_channels, subset) + self.assertEqual(len(sub_wf.sequenced_waveforms), 2) + self.assertEqual(sub_wf.sequenced_waveforms[0].defined_channels, subset) + self.assertEqual(sub_wf.sequenced_waveforms[1].defined_channels, subset) - self.assertEqual(sub_wf.compare_key[0].duration, TimeType.from_float(2.2)) - self.assertEqual(sub_wf.compare_key[1].duration, TimeType.from_float(3.3)) + self.assertEqual(sub_wf.sequenced_waveforms[0].duration, TimeType.from_float(2.2)) + self.assertEqual(sub_wf.sequenced_waveforms[1].duration, TimeType.from_float(3.3)) def test_repr(self): cwf_2_a = ConstantWaveform(duration=1.1, amplitude=2.2, channel='A') @@ -714,7 +723,8 @@ def test_simple_properties(self): self.assertIs(trafo_wf.inner_waveform, inner_wf) self.assertIs(trafo_wf.transformation, trafo) - self.assertEqual(trafo_wf.compare_key, (inner_wf, trafo)) + with self.assertWarns(DeprecationWarning): + 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) @@ -804,7 +814,8 @@ def test_simple_properties(self): 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)) + with self.assertWarns(DeprecationWarning): + 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'}) @@ -891,8 +902,8 @@ def test_simple_properties(self): self.assertIs(rhs, arith.rhs) self.assertEqual('-', arith.arithmetic_operator) self.assertEqual(lhs.duration, arith.duration) - - self.assertEqual(('-', lhs, rhs), arith.compare_key) + with self.assertWarns(DeprecationWarning): + self.assertEqual(('-', lhs, rhs), arith.compare_key) def test_unsafe_get_subset_for_channels(self): lhs = DummyWaveform(duration=1.5, defined_channels={'a', 'b', 'c'}) @@ -944,10 +955,12 @@ def test_equality(self) -> None: wf1b = FunctionWaveform(ExpressionScalar('2*t'), 3, channel='A') wf3 = FunctionWaveform(ExpressionScalar('2*t+2'), 3, channel='A') wf4 = FunctionWaveform(ExpressionScalar('2*t'), 4, channel='A') + wf5 = FunctionWaveform(ExpressionScalar('2*t'), 3, channel='B') self.assertEqual(wf1a, wf1a) self.assertEqual(wf1a, wf1b) self.assertNotEqual(wf1a, wf3) self.assertNotEqual(wf1a, wf4) + self.assertNotEqual(wf1a, wf5) def test_defined_channels(self) -> None: wf = FunctionWaveform(ExpressionScalar('t'), 4, channel='A') @@ -1056,7 +1069,7 @@ def test_unsafe_get_subset_for_channels(self): wf.unsafe_get_subset_for_channels({'A'})) inner_subset.assert_called_once_with({'A'}) - def test_compare_key(self): + def test_comparison(self): inner_wf_1 = DummyWaveform(defined_channels={'A', 'B'}) inner_wf_2 = DummyWaveform(defined_channels={'A', 'B'}) functors_1 = dict(A=np.positive, B=np.negative) @@ -1067,7 +1080,8 @@ def test_compare_key(self): wf21 = FunctorWaveform(inner_wf_2, functors_1) wf22 = FunctorWaveform(inner_wf_2, functors_2) - self.assertEqual((inner_wf_1, frozenset(functors_1.items())), wf11.compare_key) + with self.assertWarns(DeprecationWarning): + self.assertEqual((inner_wf_1, frozenset(functors_1.items())), wf11.compare_key) self.assertEqual(wf11, wf11) self.assertEqual(wf11, FunctorWaveform(inner_wf_1, functors_1)) @@ -1083,7 +1097,8 @@ def test_simple_properties(self): self.assertEqual(dummy_wf.duration, reversed_wf.duration) self.assertEqual(dummy_wf.defined_channels, reversed_wf.defined_channels) - self.assertEqual(dummy_wf.compare_key, reversed_wf.compare_key) + with self.assertWarns(DeprecationWarning): + self.assertEqual(dummy_wf.compare_key, reversed_wf.compare_key) self.assertNotEqual(reversed_wf, dummy_wf) def test_reversed_sample(self): diff --git a/tests/pulses/sequencing_dummies.py b/tests/pulses/sequencing_dummies.py index 21c3c7e62..c70cd8d85 100644 --- a/tests/pulses/sequencing_dummies.py +++ b/tests/pulses/sequencing_dummies.py @@ -34,7 +34,6 @@ def normalize_measurement_windows(mw): class DummyWaveform(Waveform): - def __init__(self, duration: Union[float, TimeType]=0, sample_output: Union[numpy.ndarray, dict]=None, defined_channels=None) -> None: super().__init__(duration=duration if isinstance(duration, TimeType) else TimeType.from_float(duration)) self.sample_output = sample_output @@ -46,6 +45,12 @@ def __init__(self, duration: Union[float, TimeType]=0, sample_output: Union[nump self.defined_channels_ = defined_channels self.sample_calls = [] + def __hash__(self): + return hash(self.compare_key) + + def __eq__(self, other): + return isinstance(other, DummyWaveform) and self.compare_key == other.compare_key + @property def compare_key(self) -> Any: if self.sample_output is not None: From 216da480a6ab9752f7dfcb5e2b2af4d1b6740c27 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 4 Jun 2024 15:15:33 +0200 Subject: [PATCH 217/441] Missing waveform related test --- tests/pulses/sequence_pulse_template_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pulses/sequence_pulse_template_tests.py b/tests/pulses/sequence_pulse_template_tests.py index d2a7f5544..ff14a0ed5 100644 --- a/tests/pulses/sequence_pulse_template_tests.py +++ b/tests/pulses/sequence_pulse_template_tests.py @@ -76,7 +76,7 @@ def test_build_waveform(self): self.assertIs(pt.build_waveform_calls[0][0], parameters) self.assertIsInstance(wf, SequenceWaveform) - for wfa, wfb in zip(wf.compare_key, wfs): + for wfa, wfb in zip(wf.sequenced_waveforms, wfs): self.assertIs(wfa, wfb) def test_identifier(self) -> None: From 9f38a39eae00350947e260eced75f714d4e7801f Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 4 Jun 2024 15:16:07 +0200 Subject: [PATCH 218/441] Depreacte Comparable and cleanup some imports --- qupulse/comparable.py | 4 ++ qupulse/hardware/awgs/base.py | 12 +++- qupulse/program/transformation.py | 72 +++++++++++++++++-- qupulse/program/waveforms.py | 7 +- tests/_program/transformation_tests.py | 12 ++-- tests/comparable_tests.py | 5 +- .../pulses/arithmetic_pulse_template_tests.py | 11 ++- 7 files changed, 103 insertions(+), 20 deletions(-) diff --git a/qupulse/comparable.py b/qupulse/comparable.py index 2582faa8b..ce57c5aaa 100644 --- a/qupulse/comparable.py +++ b/qupulse/comparable.py @@ -1,6 +1,7 @@ """This module defines the abstract Comparable class.""" from abc import abstractmethod from typing import Hashable, Any +import warnings from qupulse.utils.types import DocStringABCMeta @@ -8,6 +9,9 @@ __all__ = ["Comparable"] +warnings.warn("qupulse.comparable is deprecated since 0.11 and will be removed in 0.12", DeprecationWarning) + + class Comparable(metaclass=DocStringABCMeta): """An object that can be queried for equality with other Comparable objects. diff --git a/qupulse/hardware/awgs/base.py b/qupulse/hardware/awgs/base.py index 5b1bb7c74..21b8a1cea 100644 --- a/qupulse/hardware/awgs/base.py +++ b/qupulse/hardware/awgs/base.py @@ -12,7 +12,7 @@ from typing import Set, Tuple, Callable, Optional, Mapping, Sequence, List, Union, NamedTuple from collections import OrderedDict from enum import Enum -# from itertools import chain +import warnings from qupulse.hardware.util import get_sample_times, not_none_indices from qupulse.utils.types import ChannelID @@ -20,7 +20,6 @@ Increment, Set as LSPSet, LoopLabel, LoopJmp, Wait, Play from qupulse.program.loop import Loop from qupulse.program.waveforms import Waveform -from qupulse.comparable import Comparable from qupulse.utils.types import TimeType import numpy @@ -39,7 +38,7 @@ class AWGAmplitudeOffsetHandling: _valid = [IGNORE_OFFSET, CONSIDER_OFFSET] -class AWG(Comparable): +class AWG: """An arbitrary waveform generator abstraction class. It represents a set of channels that have to have(hardware enforced) the same: @@ -142,6 +141,13 @@ def sample_rate(self) -> float: def compare_key(self) -> int: """Comparison and hashing is based on the id of the AWG so different devices with the same properties are ot equal""" + warnings.warn("AWG.compare_key is deprecated since 0.11 and will be removed in 0.12", + DeprecationWarning, stacklevel=2) + + def __eq__(self, other): + return self is other + + def __hash__(self): return id(self) @abstractmethod diff --git a/qupulse/program/transformation.py b/qupulse/program/transformation.py index 1d3c86879..c51c5fbb0 100644 --- a/qupulse/program/transformation.py +++ b/qupulse/program/transformation.py @@ -1,12 +1,12 @@ from typing import Any, Mapping, Set, Tuple, Sequence, AbstractSet, Union, TYPE_CHECKING, Hashable from abc import abstractmethod from numbers import Real +import warnings import numpy as np from qupulse import ChannelID -from qupulse.comparable import Comparable -from qupulse.utils.types import SingletonABCMeta, frozendict +from qupulse.utils.types import SingletonABCMeta, frozendict, DocStringABCMeta from qupulse.expressions import ExpressionScalar @@ -18,7 +18,9 @@ 'chain_transformations'] -class Transformation(Comparable): +class Transformation(metaclass=DocStringABCMeta): + __slots__ = () + _identity_singleton = None """Transforms numeric time-voltage values for multiple channels to other time-voltage values. The number and names of input and output channels might differ.""" @@ -58,6 +60,8 @@ def get_constant_output_channels(self, input_channels: AbstractSet[ChannelID]) - class IdentityTransformation(Transformation, metaclass=SingletonABCMeta): + __slots__ = () + def __call__(self, time: Union[np.ndarray, float], data: Mapping[ChannelID, Union[np.ndarray, float]]) -> Mapping[ChannelID, Union[np.ndarray, float]]: return data @@ -66,9 +70,17 @@ def get_output_channels(self, input_channels: AbstractSet[ChannelID]) -> Abstrac return input_channels @property - def compare_key(self) -> None: + def compare_key(self) -> type(None): + warnings.warn("Transformation.compare_key is deprecated since 0.11 and will be removed in 0.12", + DeprecationWarning, stacklevel=2) return None + def __hash__(self): + return 0x1234991 + + def __eq__(self, other): + return isinstance(other, IdentityTransformation) + def get_input_channels(self, output_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]: return output_channels @@ -87,6 +99,8 @@ def get_constant_output_channels(self, input_channels: AbstractSet[ChannelID]) - class ChainedTransformation(Transformation): + __slots__ = ('_transformations',) + def __init__(self, *transformations: Transformation): self._transformations = transformations @@ -112,8 +126,16 @@ def __call__(self, time: Union[np.ndarray, float], @property def compare_key(self) -> Tuple[Transformation, ...]: + warnings.warn("Transformation.compare_key is deprecated since 0.11 and will be removed in 0.12", + DeprecationWarning, stacklevel=2) return self._transformations + def __hash__(self): + return hash(self._transformations) + + def __eq__(self, other): + return self._transformations == getattr(other, '_transformations', None) + def chain(self, next_transformation) -> Transformation: return chain_transformations(*self.transformations, next_transformation) @@ -197,8 +219,20 @@ def get_input_channels(self, output_channels: AbstractSet[ChannelID]) -> Abstrac else: return forwarded | self._input_channels_set + def __hash__(self): + return hash((self._input_channels, self._output_channels, self._matrix.tobytes())) + + def __eq__(self, other): + if isinstance(other, type(self)): + return (self._input_channels == other._input_channels and + self._output_channels == other._output_channels and + np.array_equal(self._matrix, other._matrix)) + return False + @property def compare_key(self) -> Tuple[Tuple[ChannelID], Tuple[ChannelID], bytes]: + warnings.warn("Transformation.compare_key is deprecated since 0.11 and will be removed in 0.12", + DeprecationWarning, stacklevel=2) return self._input_channels, self._output_channels, self._matrix.tobytes() def __repr__(self): @@ -218,6 +252,8 @@ def get_constant_output_channels(self, input_channels: AbstractSet[ChannelID]) - class OffsetTransformation(Transformation): + __slots__ = ('_offsets',) + def __init__(self, offsets: Mapping[ChannelID, _TrafoValue]): """Adds an offset to each channel specified in offsets. @@ -241,8 +277,16 @@ def get_input_channels(self, output_channels: AbstractSet[ChannelID]) -> Abstrac def get_output_channels(self, input_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]: return input_channels + def __eq__(self, other): + return isinstance(other, OffsetTransformation) and self._offsets == other._offsets + + def __hash__(self): + return hash(self._offsets) + @property def compare_key(self) -> Hashable: + warnings.warn("Transformation.compare_key is deprecated since 0.11 and will be removed in 0.12", + DeprecationWarning, stacklevel=2) return self._offsets def __repr__(self): @@ -257,6 +301,8 @@ def get_constant_output_channels(self, input_channels: AbstractSet[ChannelID]) - class ScalingTransformation(Transformation): + __slots__ = ('_factors',) + def __init__(self, factors: Mapping[ChannelID, _TrafoValue]): self._factors = frozendict(factors) assert _are_valid_transformation_expressions(self._factors), f"Not valid transformation expressions: {self._factors}" @@ -273,8 +319,16 @@ def get_input_channels(self, output_channels: AbstractSet[ChannelID]) -> Abstrac def get_output_channels(self, input_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]: return input_channels + def __eq__(self, other): + return isinstance(other, ScalingTransformation) and self._factors == other._factors + + def __hash__(self): + return hash(self._factors) + @property def compare_key(self) -> Hashable: + warnings.warn("Transformation.compare_key is deprecated since 0.11 and will be removed in 0.12", + DeprecationWarning, stacklevel=2) return self._factors def __repr__(self): @@ -312,6 +366,8 @@ def linear_transformation_from_pandas(transformation: PandasDataFrameType) -> Li class ParallelChannelTransformation(Transformation): + __slots__ = ('_channels', ) + def __init__(self, channels: Mapping[ChannelID, _TrafoValue]): """Set channel values to given values regardless their former existence. The values can be time dependent expressions. @@ -333,8 +389,16 @@ def _instantiated_values(self, time): return {channel: value.evaluate_in_scope(scope) if hasattr(value, 'evaluate_in_scope') else array_or_float(value) for channel, value in self._channels.items()} + def __hash__(self): + return hash(self._channels) + + def __eq__(self, other): + return isinstance(other, ParallelChannelTransformation) and self._channels == other._channels + @property def compare_key(self) -> Hashable: + warnings.warn("Transformation.compare_key is deprecated since 0.11 and will be removed in 0.12", + DeprecationWarning, stacklevel=2) return self._channels def get_input_channels(self, output_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]: diff --git a/qupulse/program/waveforms.py b/qupulse/program/waveforms.py index ac077bb1f..15a33273e 100644 --- a/qupulse/program/waveforms.py +++ b/qupulse/program/waveforms.py @@ -17,18 +17,15 @@ import numpy as np from qupulse import ChannelID -from qupulse.program.transformation import Transformation -from qupulse.utils import checked_int_cast, isclose -from qupulse.utils.types import TimeType, time_from_float from qupulse.utils.performance import is_monotonic -from qupulse.comparable import Comparable from qupulse.expressions import ExpressionScalar from qupulse.pulses.interpolation import InterpolationStrategy from qupulse.utils import checked_int_cast, isclose -from qupulse.utils.types import TimeType, time_from_float, FrozenDict +from qupulse.utils.types import TimeType, time_from_float from qupulse.program.transformation import Transformation from qupulse.utils import pairwise + class ConstantFunctionPulseTemplateWarning(UserWarning): """ This warning indicates a constant waveform is constructed from a FunctionPulseTemplate """ pass diff --git a/tests/_program/transformation_tests.py b/tests/_program/transformation_tests.py index f9422f1f4..2d8e31918 100644 --- a/tests/_program/transformation_tests.py +++ b/tests/_program/transformation_tests.py @@ -64,10 +64,12 @@ def test_compare_key_and_init(self): matrix_2 = np.array([[1, 1, 1], [1, 0, -1]]) trafo_2 = LinearTransformation(matrix_2, in_chs_2, out_chs_2) - self.assertEqual(trafo.compare_key, trafo_2.compare_key) + with self.assertWarns(DeprecationWarning): + self.assertEqual(trafo.compare_key, trafo_2.compare_key) self.assertEqual(trafo, trafo_2) self.assertEqual(hash(trafo), hash(trafo_2)) - self.assertEqual(trafo.compare_key, (in_chs, out_chs, matrix.tobytes())) + with self.assertWarns(DeprecationWarning): + self.assertEqual(trafo.compare_key, (in_chs, out_chs, matrix.tobytes())) def test_from_pandas(self): try: @@ -175,7 +177,8 @@ def test_constant_propagation(self): class IdentityTransformationTests(unittest.TestCase): def test_compare_key(self): - self.assertIsNone(IdentityTransformation().compare_key) + with self.assertWarns(DeprecationWarning): + self.assertIsNone(IdentityTransformation().compare_key) def test_singleton(self): self.assertIs(IdentityTransformation(), IdentityTransformation()) @@ -216,7 +219,8 @@ def test_init_and_properties(self): chained = ChainedTransformation(*trafos) self.assertEqual(chained.transformations, trafos) - self.assertIs(chained.transformations, chained.compare_key) + with self.assertWarns(DeprecationWarning): + self.assertIs(chained.transformations, chained.compare_key) def test_get_output_channels(self): trafos = TransformationStub(), TransformationStub(), TransformationStub() diff --git a/tests/comparable_tests.py b/tests/comparable_tests.py index 0394c7b3a..5a0c489f3 100644 --- a/tests/comparable_tests.py +++ b/tests/comparable_tests.py @@ -1,7 +1,10 @@ import unittest from typing import Any +import warnings -from qupulse.comparable import Comparable +with warnings.catch_warnings(): + warnings.simplefilter(action='ignore', category=DeprecationWarning) + from qupulse.comparable import Comparable class DummyComparable(Comparable): diff --git a/tests/pulses/arithmetic_pulse_template_tests.py b/tests/pulses/arithmetic_pulse_template_tests.py index 66ff074c8..7f833d7ad 100644 --- a/tests/pulses/arithmetic_pulse_template_tests.py +++ b/tests/pulses/arithmetic_pulse_template_tests.py @@ -435,9 +435,11 @@ def test_internal_create_program(self): to_single_waveform = {'something_else'} program_builder = mock.Mock() - expected_transformation = mock.Mock(spec=IdentityTransformation()) + with self.assertWarns(DeprecationWarning): + expected_transformation = mock.Mock(spec=IdentityTransformation()) - inner_trafo = mock.Mock(spec=IdentityTransformation()) + with self.assertWarns(DeprecationWarning): + inner_trafo = mock.Mock(spec=IdentityTransformation()) inner_trafo.chain.return_value = expected_transformation with mock.patch.object(rhs, '_create_program') as inner_create_program: @@ -593,7 +595,10 @@ def test_build_waveform(self): channel_mapping = dict(a='u', b='v') inner_wf = DummyWaveform(duration=6, defined_channels={'a'}) - trafo = mock.Mock(spec=IdentityTransformation()) + with self.assertWarns(DeprecationWarning): + # mock will inspect alsod eprecated attributes + # TODO: remove assert as soon as attribute is removed + trafo = mock.Mock(spec=IdentityTransformation()) arith = ArithmeticPulseTemplate(pt, '-', 6) From f78b832e9047166bb09c3200534321dcbfa6d155 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 4 Jun 2024 15:33:16 +0200 Subject: [PATCH 219/441] Fix missing return value in AWG.compare_key --- qupulse/hardware/awgs/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qupulse/hardware/awgs/base.py b/qupulse/hardware/awgs/base.py index 21b8a1cea..43209c4db 100644 --- a/qupulse/hardware/awgs/base.py +++ b/qupulse/hardware/awgs/base.py @@ -143,6 +143,7 @@ def compare_key(self) -> int: are ot equal""" warnings.warn("AWG.compare_key is deprecated since 0.11 and will be removed in 0.12", DeprecationWarning, stacklevel=2) + return id(self) def __eq__(self, other): return self is other From fc0e5e762f50a6f7ab5b912199f1f58c01db9e4c Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 4 Jun 2024 15:50:00 +0200 Subject: [PATCH 220/441] Unify transformation comparison implementations --- qupulse/program/transformation.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/qupulse/program/transformation.py b/qupulse/program/transformation.py index c51c5fbb0..22a28e495 100644 --- a/qupulse/program/transformation.py +++ b/qupulse/program/transformation.py @@ -79,7 +79,7 @@ def __hash__(self): return 0x1234991 def __eq__(self, other): - return isinstance(other, IdentityTransformation) + return self is other def get_input_channels(self, output_channels: AbstractSet[ChannelID]) -> AbstractSet[ChannelID]: return output_channels @@ -134,7 +134,9 @@ def __hash__(self): return hash(self._transformations) def __eq__(self, other): - return self._transformations == getattr(other, '_transformations', None) + if isinstance(other, ChainedTransformation): + return self._transformations == other._transformations + return NotImplemented def chain(self, next_transformation) -> Transformation: return chain_transformations(*self.transformations, next_transformation) @@ -223,11 +225,11 @@ def __hash__(self): return hash((self._input_channels, self._output_channels, self._matrix.tobytes())) def __eq__(self, other): - if isinstance(other, type(self)): + if isinstance(other, LinearTransformation): return (self._input_channels == other._input_channels and self._output_channels == other._output_channels and np.array_equal(self._matrix, other._matrix)) - return False + return NotImplemented @property def compare_key(self) -> Tuple[Tuple[ChannelID], Tuple[ChannelID], bytes]: @@ -278,7 +280,9 @@ def get_output_channels(self, input_channels: AbstractSet[ChannelID]) -> Abstrac return input_channels def __eq__(self, other): - return isinstance(other, OffsetTransformation) and self._offsets == other._offsets + if isinstance(other, OffsetTransformation): + return self._offsets == other._offsets + return NotImplemented def __hash__(self): return hash(self._offsets) @@ -320,7 +324,9 @@ def get_output_channels(self, input_channels: AbstractSet[ChannelID]) -> Abstrac return input_channels def __eq__(self, other): - return isinstance(other, ScalingTransformation) and self._factors == other._factors + if isinstance(other, ScalingTransformation): + return self._factors == other._factors + return NotImplemented def __hash__(self): return hash(self._factors) @@ -393,7 +399,9 @@ def __hash__(self): return hash(self._channels) def __eq__(self, other): - return isinstance(other, ParallelChannelTransformation) and self._channels == other._channels + if isinstance(other, ParallelChannelTransformation): + return self._channels == other._channels + return NotImplemented @property def compare_key(self) -> Hashable: From b346ad79a5326627c0a0429d0c1c3ff12b9c2fdb Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Tue, 7 May 2024 17:38:47 +0200 Subject: [PATCH 221/441] Add zenodo metadata --- .zenodo.json | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 .zenodo.json diff --git a/.zenodo.json b/.zenodo.json new file mode 100644 index 000000000..69d6c7d28 --- /dev/null +++ b/.zenodo.json @@ -0,0 +1,74 @@ +{ + "creators": [ + { + "orcid": "0000-0002-9399-1055", + "affiliation": "RWTH Aachen University", + "name": "Humpohl, Simon" + }, + { + "orcid": "0000-0001-8678-961X", + "affiliation": "RWTH Aachen University", + "name": "Prediger, Lukas" + }, + { + "orcid": "0000-0002-8227-4018", + "affiliation": "RWTH Aachen University", + "name": "Cerfontaine, Pascal" + }, + { + "affiliation": "Forschungszentrum Jülich", + "name": "Papajewski, Benjamin" + }, + { + "orcid": "0000-0001-9927-3102", + "affiliation": "RWTH Aachen University", + "name": "Bethke, Patrick" + }, + { + "orcid": "0000-0003-2057-9913", + "affiliation": "Forschungszentrum Jülich", + "name": "Lankes, Lukas" + }, + { + "orcid": "0009-0006-9702-2979", + "affiliation": "Forschungszentrum Jülich", + "name": "Willmes, Alexander" + }, + { + "orcid": "0009-0000-3779-4711", + "affiliation": "Forschungszentrum Jülich", + "name": "Kammerloher, Eugen" + } + ], + + "contributors": [ + { + "orcid": "0000-0001-7018-1124", + "affiliation": "Netherlands Organisation for Applied Scientific Research TNO", + "name": "Eendebak, Pieter Thijs" + }, + { + "name": "Kreutz, Maike", + "affiliation": "RWTH Aachen University" + }, + { + "name": "Xue, Ran", + "affiliation": "RWTH Aachen University", + "orcid": "0000-0002-2009-6279" + } + ], + + "related_identifiers": [ + { + "identifier": "2128/24264", + "relation": "isDocumentedBy", + "resource_type": "publication-thesis" + } + ], + + "license": "GPL-3.0-or-later", + + "title": "qupulse: A Quantum compUting PULse parametrization and SEquencing framework", + + "keywords": ["quantum computing", "control pulse"] +} From f9ed9ffc4ad9e525cd20ccc3064ce488c8617135 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 10 Jun 2024 15:24:48 +0200 Subject: [PATCH 222/441] Do not execute hardware dependent notebook when building docs --- doc/source/examples/04ZurichInstrumentsSetup.ipynb | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/examples/04ZurichInstrumentsSetup.ipynb b/doc/source/examples/04ZurichInstrumentsSetup.ipynb index 851c806bc..21a77fe44 100644 --- a/doc/source/examples/04ZurichInstrumentsSetup.ipynb +++ b/doc/source/examples/04ZurichInstrumentsSetup.ipynb @@ -462,6 +462,7 @@ } ], "metadata": { + "nbsphinx": { "execute": "never" }, "kernelspec": { "display_name": "Python 3", "language": "python", From 787f2bc8d108775b7edf5a8d3a3d69fd3aa4bf41 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 10 Jun 2024 15:31:10 +0200 Subject: [PATCH 223/441] Include LoopBuilder explicitly in public interface --- qupulse/program/loop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qupulse/program/loop.py b/qupulse/program/loop.py index 5f127fbef..0e5dccc3e 100644 --- a/qupulse/program/loop.py +++ b/qupulse/program/loop.py @@ -25,7 +25,7 @@ from qupulse.utils.tree import Node from qupulse.utils.types import TimeType, MeasurementWindow -__all__ = ['Loop', 'make_compatible', 'MakeCompatibleWarning', 'to_waveform'] +__all__ = ['Loop', 'make_compatible', 'MakeCompatibleWarning', 'to_waveform', 'LoopBuilder'] DurationStructure = Tuple[int, Union[TimeType, 'DurationStructure']] From 3be8559ec3356cc166999e3d5a3689fc98f10971 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 10 Jun 2024 16:02:46 +0200 Subject: [PATCH 224/441] Fix several docstrings and improve program concept documentation --- doc/source/concepts/program.rst | 31 +++++++++++-------------------- qupulse/program/__init__.py | 9 ++++++--- qupulse/pulses/plotting.py | 2 +- 3 files changed, 18 insertions(+), 24 deletions(-) diff --git a/doc/source/concepts/program.rst b/doc/source/concepts/program.rst index 82208ac15..f607f2e99 100644 --- a/doc/source/concepts/program.rst +++ b/doc/source/concepts/program.rst @@ -3,26 +3,17 @@ Instantiated Pulse: Program --------------------------- -In qupulse an instantiated pulse template is called a program as it is something that an arbitrary waveform generator -(AWG) can execute/playback. -It is created by the ``create_program`` method of the pulse template which returns a hardware -independent representation which is of type ``Loop`` by default. The method takes a ``program_builder`` keyword argument -which is passed through the pulse template tree and thereby implements the visitor pattern. If the argument is not -passed ``default_program_builder()`` is used instead which is ``LoopBuilder`` by default. - -The ``Loop`` default program is the root node of a tree ``Loop``s of arbitrary depth. -Each node consists of a repetition count and either a waveform or a sequence of nodes which are repeated that many times. -Iterations like the ```ForLoopPT`` cannot be represented natively but are unrolled into a sequence of items. -The repetition count is currently the only property of a program that can be defined as volatile. This means that the AWG driver tries to upload the program in a way, where the repetition count can quickly be changed. This is implemented via the ```VolatileRepetitionCount`` class. - -There is no description of the details of the program object here to avoid duplicated and outdated documentation. -The documentation is in the docstrings of the source code. -The program can be thought of as compact representation of a mapping :math:`\{t | 0 \le t \le t_{\texttt{duration}}} \rightarrow \mathbb{R}^n` from the time while the program lasts :math:´t´ to an n-dimensional voltage space :math:´\mathbb{R}^n´. +In qupulse an instantiated pulse template is called a program as it is something that an arbitrary waveform generator (AWG) can execute/playback. +It can be thought of as compact representation of a mapping :math:`\{t | 0 \le t \le t_{\texttt{duration}}\} \rightarrow \mathbb{R}^n` from the time while the program lasts :math:`t` to an n-dimensional voltage space :math:`\mathbb{R}^n`. The dimensions are named by the channel names. -The ``Loop`` class and its constituents ``Waveform`` and ``VolatileRepetitionCount`` are defined in the ``qupulse.program`` subpackage and it's submodules. -The private subpackage ``qupulse._program`` contains AWG driver internals that can change with any release, for example a -transpiler to Zurich Instruments sequencing C in ``qupulse._program.seqc``. +Programs are created by the :meth:`~.PulseTemplate.create_program` method of `PulseTemplate` which returns a hardware independent and un-parameterized representation. +The method takes a ``program_builder`` keyword argument that is propagated through the pulse template tree and thereby implements the visitor pattern. +If the argument is not passed :py:func:`~qupulse.program.default_program_builder()` is used instead which is :class:`.LoopBuilder` by default, i.e. the program created by default is of type :class:`.Loop`. The available program builders, programs and their constituents like :class:`.Waveform` and :class:`.VolatileRepetitionCount` are defined in th :mod:`qupulse.program` subpackage and it's submodules. There is a private ``qupulse._program`` subpackage that was used for more rapid iteration development and is slowly phased out. It still contains the hardware specific program representation for the tabor electronics AWG driver. Zurich instrument specific code has been factored into the separate package ``qupulse-hdawg``. Please refer to the reference and the docstrings for exact interfaces and implementation details. + +The :class:`.Loop` default program is the root node of a tree of loop objects of arbitrary depth. +Each node consists of a repetition count and either a waveform or a sequence of nodes which are repeated that many times. +Iterations like the :class:`.ForLoopPT` cannot be represented natively but are unrolled into a sequence of items. +The repetition count is currently the only property of a program that can be defined as volatile. This means that the AWG driver tries to upload the program in a way, where the repetition count can quickly be changed. This is implemented via the ``VolatileRepetitionCount`` class. -Another program format that is currently under development is ``LinSpaceProgram`` which efficiently encodes linearly -spaced sweeps in voltage space. However, the status of this is preliminary and not yet documented here. +A much more capable program format is :class:`.LinSpaceNode` which efficiently encodes linearly spaced sweeps in voltage space by utilizing increment commands. It is build via :class:`.LinSpaceBuilder`. Increment commands are available in the HDAWG command table. diff --git a/qupulse/program/__init__.py b/qupulse/program/__init__.py index 644cf9bd2..cd578dd5c 100644 --- a/qupulse/program/__init__.py +++ b/qupulse/program/__init__.py @@ -109,7 +109,8 @@ def evaluate_in_scope_(self, *args, **kwargs): @runtime_checkable class Program(Protocol): - """This protocol is used to inspect and or manipulate programs""" + """This protocol is used to inspect and or manipulate programs. As you can see the functionality is very limited + because most of a program class' capability are specific to the implementation.""" @property def duration(self) -> TimeType: @@ -117,11 +118,12 @@ def duration(self) -> TimeType: class ProgramBuilder(Protocol): - """This protocol is used by PulseTemplate to build the program via the visitor pattern. + """This protocol is used by :py:meth:`.PulseTemplate.create_program` to build a program via the visitor pattern. There is a default implementation which is the loop class. - Other hardware backends can use this protocol to implement easy translation of pulse templates.""" + Other hardware backends can use this protocol to implement easy translation of pulse templates into a hardware + compatible format.""" def inner_scope(self, scope: Scope) -> Scope: """This function is necessary to inject program builder specific parameter implementations into the build @@ -167,6 +169,7 @@ def to_program(self) -> Optional[Program]: def default_program_builder() -> ProgramBuilder: + """This function returns an instance of the default program builder class `LoopBuilder`""" from qupulse.program.loop import LoopBuilder return LoopBuilder() diff --git a/qupulse/pulses/plotting.py b/qupulse/pulses/plotting.py index 907629fe4..b6f29a4cd 100644 --- a/qupulse/pulses/plotting.py +++ b/qupulse/pulses/plotting.py @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: GPL-3.0-or-later -"""Deprecated plotting location. Was moved to :py:`qupulse.plotting`. +"""Deprecated plotting location. Was moved to :py:mod:`qupulse.plotting`. No deprecation warning because we will keep it around forever.""" from qupulse.plotting import * From bd99d286727fed9f2054f9b58b57c3144e0c4129 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 10 Jun 2024 16:51:45 +0200 Subject: [PATCH 225/441] Fix license classifier --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index d40206f1d..fd4cef6be 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,7 +10,7 @@ license_files = LICENSE/GPL-3.0-or-later.txt keywords = quantum, physics, control pulse, qubit classifiers = Programming Language :: Python :: 3 - OSI Approved :: GNU General Public License v3 or later (GPLv3+) + License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+) Operating System :: OS Independent Topic :: Scientific/Engineering Intended Audience :: Science/Research From f154f1b24afb2b9be0d6dd19b6e2f42246fb2b9c Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 17 Jun 2024 12:53:16 +0200 Subject: [PATCH 226/441] Make gmpy2 a hard dependency --- .github/workflows/pythontest.yaml | 6 -- qupulse/hardware/awgs/tabor.py | 6 +- qupulse/utils/types.py | 24 ++----- setup.cfg | 4 +- tests/utils/time_type_tests.py | 105 +++++------------------------- 5 files changed, 28 insertions(+), 117 deletions(-) diff --git a/.github/workflows/pythontest.yaml b/.github/workflows/pythontest.yaml index 64a7c27cd..e4300f978 100644 --- a/.github/workflows/pythontest.yaml +++ b/.github/workflows/pythontest.yaml @@ -23,17 +23,11 @@ jobs: fail-fast: false matrix: python-version: ["3.8", "3.9", "3.10"] - time-type: ["fractions", "gmpy2"] env: INSTALL_EXTRAS: tests,plotting,zurich-instruments,tektronix,tabor-instruments steps: - - name: Add gmpy extra feature - if: ${{ matrix.time-type }} == 'gmpy2' - run: | - echo "INSTALL_EXTRAS=${{ env.INSTALL_EXTRAS }},Faster-fractions" >> $GITHUB_ENV - - name: Checkout repository uses: actions/checkout@v2 diff --git a/qupulse/hardware/awgs/tabor.py b/qupulse/hardware/awgs/tabor.py index b500f3645..7711773d5 100644 --- a/qupulse/hardware/awgs/tabor.py +++ b/qupulse/hardware/awgs/tabor.py @@ -2,9 +2,7 @@ # # SPDX-License-Identifier: GPL-3.0-or-later -import fractions import functools -import warnings import weakref import logging import numbers @@ -14,7 +12,7 @@ import tabor_control.device import numpy as np -from qupulse.utils.types import ChannelID +from qupulse.utils.types import ChannelID, TimeType from qupulse.program.loop import Loop, make_compatible from qupulse.hardware.util import voltage_to_uint16, traced from qupulse.hardware.awgs.base import AWG, AWGAmplitudeOffsetHandling @@ -470,7 +468,7 @@ def upload(self, name: str, make_compatible(program, minimal_waveform_length=192, waveform_quantum=16, - sample_rate=fractions.Fraction(sample_rate, 10**9)) + sample_rate=TimeType.from_fraction(sample_rate, 10**9)) if name in self._known_programs: if force: diff --git a/qupulse/utils/types.py b/qupulse/utils/types.py index ca51d1965..83dce2a0f 100644 --- a/qupulse/utils/types.py +++ b/qupulse/utils/types.py @@ -14,6 +14,7 @@ import numpy import sympy +import gmpy2 try: from frozendict import frozendict @@ -30,16 +31,6 @@ MeasurementWindow = typing.Tuple[str, numbers.Real, numbers.Real] ChannelID = typing.Union[str, int] -try: - import gmpy2 - qupulse_numeric.FractionType = gmpy2.mpq - -except ImportError: - gmpy2 = None - - warnings.warn('gmpy2 not found. Using fractions.Fraction as fallback. Install gmpy2 for better performance.' - 'time_from_float might produce slightly different results') - def _with_other_as_time_type(fn): """This is decorator to convert the other argument and the result into a :class:`TimeType`""" @@ -57,17 +48,16 @@ def wrapper(self, other) -> 'TimeType': class TimeType: - """This type represents a rational number with arbitrary precision. - - Internally it uses :func:`gmpy2.mpq` (if available) or :class:`fractions.Fraction` - """ __slots__ = ('_value',) - _InternalType = fractions.Fraction if gmpy2 is None else type(gmpy2.mpq()) - _to_internal = fractions.Fraction if gmpy2 is None else gmpy2.mpq + _InternalType = type(gmpy2.mpq()) + _to_internal = gmpy2.mpq def __init__(self, value: typing.Union[numbers.Rational, int] = 0., denominator: typing.Optional[int] = None): - """ + """This type represents a rational number with arbitrary precision. + + Internally it uses :func:`gmpy2.mpq` which is considered an implementation detail. + Args: value: interpreted as Rational if denominator is None. interpreted as numerator otherwise denominator: Denominator of the Fraction if not None diff --git a/setup.cfg b/setup.cfg index fd4cef6be..0d4a6d23d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,6 +29,7 @@ install_requires = typing-extensions;python_version<'3.8' frozendict lazy_loader + gmpy2 test_suite = tests [options.extras_require] @@ -46,7 +47,6 @@ tabor-instruments = zurich-instruments = qupulse-hdawg-legacy;python_version<'3.9' qupulse-hdawg;python_version>='3.9' -Faster-fractions = gmpy2 tektronix = tek_awg>=0.2.1 autologging = autologging # sadly not open source for external legal reasons @@ -55,7 +55,7 @@ autologging = autologging faster-sampling = numba # Everything besides awg drivers default = - qupulse[tests,docs,plotting,Faster-fractions,autologging,faster-sampling] + qupulse[tests,docs,plotting,autologging,faster-sampling] pandas [options.packages.find] diff --git a/tests/utils/time_type_tests.py b/tests/utils/time_type_tests.py index cdb9ccffd..b069cacf4 100644 --- a/tests/utils/time_type_tests.py +++ b/tests/utils/time_type_tests.py @@ -1,81 +1,12 @@ -import sys import unittest -import builtins -import contextlib -import importlib import fractions import random -import os - -from unittest import mock - -try: - import gmpy2 -except ImportError: - gmpy2 = None import numpy as np import sympy +import gmpy2 -import qupulse.utils.types as qutypes - - -MOCK_GMPY2_AS_MISSING = bool(os.getenv("QUPULSE_TESTS_MOCK_GMPY2_AS_MISSING")) - - -@contextlib.contextmanager -def mock_missing_module(module_name: str): - exit_stack = contextlib.ExitStack() - - if module_name in sys.modules: - # temporarily remove gmpy2 from the imported modules - - temp_modules = sys.modules.copy() - del temp_modules[module_name] - exit_stack.enter_context(mock.patch.dict(sys.modules, temp_modules)) - - original_import = builtins.__import__ - - def mock_import(name, *args, **kwargs): - if name == module_name: - raise ImportError(name) - else: - return original_import(name, *args, **kwargs) - - exit_stack.enter_context(mock.patch('builtins.__import__', mock_import)) - - with exit_stack: - yield - - -@unittest.skipIf(gmpy2 and not MOCK_GMPY2_AS_MISSING, "Not explicitly included. " - "Define QUPULSE_TESTS_MOCK_GMPY2_AS_MISSING to include.") -class TestTimeTypeDevFallback(unittest.TestCase): - @classmethod - def setUpClass(cls): - with mock_missing_module('gmpy2'): - cls.fallback_qutypes = importlib.reload(qutypes) - - def test_fraction_fallback(self): - self.assertIs(fractions.Fraction, self.fallback_qutypes.TimeType._InternalType) - - def test_fraction_time_from_fraction_fallback(self): - assert_from_fraction_works(self, self.fallback_qutypes.TimeType) - - def test_fraction_time_from_float_exact_fallback(self): - assert_from_float_exact_works(self, self.fallback_qutypes.TimeType) - - def test_fraction_time_from_float_with_precision_fallback(self): - assert_fraction_time_from_float_with_precision_works(self, self.fallback_qutypes.TimeType) - - def test_from_float_no_extra_args_fallback(self): - assert_from_float_no_extra_args_works(self, self.fallback_qutypes.TimeType) - - def test_try_from_any_fallback(self): - assert_try_from_any_works(self, self.fallback_qutypes.TimeType) - - def test_comparisons_work_fallback(self): - assert_comparisons_work(self, self.fallback_qutypes.TimeType) +from qupulse.utils.types import TimeType, time_from_float def assert_from_fraction_works(test: unittest.TestCase, time_type): @@ -155,9 +86,7 @@ def __repr__(self): for_array_tests = [] - signed_int_types = [int, sympy.Integer, np.int8, np.int16, np.int32, np.int64, DuckInt, DuckIntFloat] - if gmpy2: - signed_int_types.append(gmpy2.mpz) + signed_int_types = [int, sympy.Integer, np.int8, np.int16, np.int32, np.int64, DuckInt, DuckIntFloat, gmpy2.mpz] for s_t in signed_int_types: for val in (1, 17, -17): @@ -231,41 +160,41 @@ def assert_comparisons_work(test: unittest.TestCase, time_type): class TestTimeType(unittest.TestCase): - """The fallback test is here for convenience while developing and only triggered if the environment variable is set. - The fallback is also tested by the CI explicitly""" + """Tests the TimeType class. The layout of this test is in this way for historic reasons, i.e. to allow testing + different internal representations for the time type. Right now only gmpy.mpq is implemented and tested.""" def test_non_finite_float(self): with self.assertRaisesRegex(ValueError, 'Cannot represent'): - qutypes.TimeType.from_float(float('inf')) + TimeType.from_float(float('inf')) with self.assertRaisesRegex(ValueError, 'Cannot represent'): - qutypes.TimeType.from_float(float('-inf')) + TimeType.from_float(float('-inf')) with self.assertRaisesRegex(ValueError, 'Cannot represent'): - qutypes.TimeType.from_float(float('nan')) + TimeType.from_float(float('nan')) def test_fraction_time_from_fraction(self): - assert_from_fraction_works(self, qutypes.TimeType) + assert_from_fraction_works(self, TimeType) def test_fraction_time_from_float_exact(self): - assert_from_float_exact_works(self, qutypes.TimeType) + assert_from_float_exact_works(self, TimeType) def test_fraction_time_from_float_with_precision(self): - assert_fraction_time_from_float_with_precision_works(self, qutypes.TimeType) + assert_fraction_time_from_float_with_precision_works(self, TimeType) def test_from_float_no_extra_args(self): - assert_from_float_exact_works(self, qutypes.TimeType) + assert_from_float_exact_works(self, TimeType) def test_from_float_exceptions(self): with self.assertRaisesRegex(ValueError, '> 0'): - qutypes.time_from_float(.8, -1) + time_from_float(.8, -1) with self.assertRaisesRegex(ValueError, '<= 1'): - qutypes.time_from_float(.8, 2) + time_from_float(.8, 2) def test_try_from_any(self): - assert_try_from_any_works(self, qutypes.TimeType) + assert_try_from_any_works(self, TimeType) def test_comparisons_work(self): - assert_comparisons_work(self, qutypes.TimeType) + assert_comparisons_work(self, TimeType) def get_some_floats(seed=42, n=1000): @@ -274,7 +203,7 @@ def get_some_floats(seed=42, n=1000): def get_from_float(fs): - return [qutypes.time_from_float(f) for f in fs] + return [time_from_float(f) for f in fs] def do_additions(xs, ys): From 850786e0a360443dad0b96bde4fa58fec083bc3a Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 17 Jun 2024 12:54:14 +0200 Subject: [PATCH 227/441] Delete .travis.yml Remove completetely outdated CI configuration --- .travis.yml | 40 ---------------------------------------- 1 file changed, 40 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index df66b4e0b..000000000 --- a/.travis.yml +++ /dev/null @@ -1,40 +0,0 @@ -language: python -python: - - 3.7 - - 3.8 -env: - - INSTALL_EXTRAS=[plotting,zurich-instruments,tektronix,tabor-instruments] - - INSTALL_EXTRAS=[plotting,zurich-instruments,tektronix,tabor-instruments,Faster-fractions,faster-sampling] - -#use container based infrastructure -sudo: false - -#these directories are persistent -cache: pip - -# install dependencies for gmpy2 -addons: - apt: - update: true - - sources: - # newer compiler for zhinst - - ubuntu-toolchain-r-test - - packages: - - libgmp-dev - - libmpfr-dev - - libmpc-dev - -before_install: - - eval "CC=gcc-8 && GXX=g++-8" - - pip install coverage coveralls -install: - - pip install .$INSTALL_EXTRAS -script: - - "coverage run --source=qupulse --rcfile=coverage.ini setup.py test" -after_success: - - coveralls - -notifications: - email: false From 3968eb57a27d9e7d7283b86c01a3ff22652617ec Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 17 Jun 2024 13:03:00 +0200 Subject: [PATCH 228/441] Add news fragment --- changes.d/845.removal | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes.d/845.removal diff --git a/changes.d/845.removal b/changes.d/845.removal new file mode 100644 index 000000000..b47bfa1fb --- /dev/null +++ b/changes.d/845.removal @@ -0,0 +1 @@ +Fallback for a missing `gmpy2` via `fractions` was removed. \ No newline at end of file From 5cb794ead18c7b70f7e6af31f1a82ce6006677c3 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 17 Jun 2024 13:29:13 +0200 Subject: [PATCH 229/441] Migrate to hatch # Conflicts: # setup.cfg # Conflicts: # setup.cfg --- pyproject.toml | 82 +++++++++++++++++++++++++++++++++++++++++++++++++- setup.cfg | 79 ------------------------------------------------ setup.py | 5 --- 3 files changed, 81 insertions(+), 85 deletions(-) delete mode 100644 setup.cfg delete mode 100644 setup.py diff --git a/pyproject.toml b/pyproject.toml index cd673d74b..ff43e4937 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,85 @@ [build-system] -requires = ["setuptools", "wheel"] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "qupulse" +dynamic = ["version"] +description = "A Quantum compUting PULse parametrization and SEquencing framework" +readme = "README.md" +license = "GPL-3.0-or-later" +requires-python = ">=3.8" +authors = [ + { name = "Quantum Technology Group and Chair of Software Engineering" }, + { name = "RWTH Aachen University" }, +] +keywords = [ + "control", + "physics", + "pulse", + "quantum", + "qubit", +] +classifiers = [ + "Intended Audience :: Science/Research", + "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Topic :: Scientific/Engineering", +] +dependencies = [ + "frozendict", + "lazy_loader", + "numpy", + "sympy>=1.1.1", + "gmpy2", +] + +[project.optional-dependencies] +autologging = [ + "autologging", +] +default = [ + "pandas", + "qupulse[tests,docs,plotting,autologging,faster-sampling]", +] +docs = [ + "ipykernel", + "nbsphinx", + "pyvisa", + "sphinx>=4", +] +faster-sampling = [ + "numba", +] +plotting = [ + "matplotlib", +] +tabor-instruments = [ + "tabor_control>=0.1.1", +] +tektronix = [ + "tek_awg>=0.2.1", +] +tests = [ + "pytest", + "pytest_benchmark", +] +zurich-instruments = [ + "qupulse-hdawg-legacy;python_version<'3.9'", + "qupulse-hdawg;python_version>='3.9'", +] + +[project.urls] +Homepage = "https://github.com/qutech/qupulse" + +[tool.hatch.version] +path = "qupulse/__init__.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/qupulse", +] [tool.towncrier] directory = "changes.d" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 0d4a6d23d..000000000 --- a/setup.cfg +++ /dev/null @@ -1,79 +0,0 @@ -[metadata] -name = qupulse -version = attr: qupulse.__version__ -description = A Quantum compUting PULse parametrization and SEquencing framework -long_description = file: README.md -long_description_content_type = text/markdown -author = Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University -license = GPL-3.0-or-later -license_files = LICENSE/GPL-3.0-or-later.txt -keywords = quantum, physics, control pulse, qubit -classifiers = - Programming Language :: Python :: 3 - License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+) - Operating System :: OS Independent - Topic :: Scientific/Engineering - Intended Audience :: Science/Research -url = https://github.com/qutech/qupulse - -[options] -packages = find: -package_dir = - qupulse=qupulse - qctoolkit=qctoolkit -python_requires = >=3.8 -install_requires = - sympy>=1.1.1 - numpy - cached_property;python_version<'3.8' - typing-extensions;python_version<'3.8' - frozendict - lazy_loader - gmpy2 -test_suite = tests - -[options.extras_require] -tests = - pytest - pytest_benchmark -docs = - sphinx>=4 - nbsphinx - ipykernel - pyvisa -plotting = matplotlib -tabor-instruments = - tabor_control>=0.1.1 -zurich-instruments = - qupulse-hdawg-legacy;python_version<'3.9' - qupulse-hdawg;python_version>='3.9' -tektronix = tek_awg>=0.2.1 -autologging = autologging -# sadly not open source for external legal reasons -# commented out because pypi does not allow direct dependencies -# atsaverage = atsaverage @ git+ssh://git@git.rwth-aachen.de/qutech/cpp-atsaverage.git@master#egg=atsaverage&subdirectory=python_source -faster-sampling = numba -# Everything besides awg drivers -default = - qupulse[tests,docs,plotting,autologging,faster-sampling] - pandas - -[options.packages.find] -include = - qupulse - qupulse.* - qctoolkit - -[options.package_data] -qupulse = - *.pyi -qctoolkit = - *.pyi - -[build_sphinx] -project = 'qupulse' -version = 0.9 -release = 0.9 -source-dir = ./doc/source -build-dir = ./doc/build -fresh-env = 1 diff --git a/setup.py b/setup.py deleted file mode 100644 index 5ff9a65c8..000000000 --- a/setup.py +++ /dev/null @@ -1,5 +0,0 @@ -from setuptools import setup - -if __name__ == '__main__': - # reads from setup.cfg - setup() From a1f834d53414b460effa64a57cf7edd9c8587105 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 10 Jun 2024 18:05:20 +0200 Subject: [PATCH 230/441] Switch to hatch as unified documentation build interface --- doc/Makefile | 20 -------------------- doc/README.md | 4 ++-- doc/make.bat | 35 ----------------------------------- doc/requirements.txt | 3 --- pyproject.toml | 31 +++++++++++++++++++++++++++++-- 5 files changed, 31 insertions(+), 62 deletions(-) delete mode 100644 doc/Makefile delete mode 100644 doc/make.bat delete mode 100644 doc/requirements.txt diff --git a/doc/Makefile b/doc/Makefile deleted file mode 100644 index d0c3cbf10..000000000 --- a/doc/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = source -BUILDDIR = build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/doc/README.md b/doc/README.md index 4aab70678..014783155 100644 --- a/doc/README.md +++ b/doc/README.md @@ -8,6 +8,6 @@ You may either build the documentation yourself or read it on [readthedocs](http In the subdirectory *examples* you can find various [Jupyter notebook](http://jupyter.org/) files providing some step-by-step examples of how qupulse can be used. These can be explored in an interactive fashion by running the *Jupyter notebook* application inside the folder. However, a static version will also be included in the documentation created with *sphinx*. ## Building the Documentation -To build the documentation, you will need [sphinx](http://www.sphinx-doc.org/en/stable/) and [nbsphinx](https://nbsphinx.readthedocs.org/) which, in turn, requires [pandoc](http://pandoc.org/). +To build the documentation, you will need [sphinx](http://www.sphinx-doc.org/en/stable/) and [nbsphinx](https://nbsphinx.readthedocs.org/) which, in turn, requires [pandoc](http://pandoc.org/) which must be installed separately. -The documentation is built by invoking `make ` inside the */doc* directory, where `` is an output format supported by *sphinx*, e.g., `html`. The output will then be found in `/doc/build/`. +You can use hatch to build the documentation locally via `hatch run docs:build ` or a bit more concise `hatch run docs:html`. The output will then be found in `/doc/build/`. diff --git a/doc/make.bat b/doc/make.bat deleted file mode 100644 index 9534b0181..000000000 --- a/doc/make.bat +++ /dev/null @@ -1,35 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=source -set BUILDDIR=build - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd diff --git a/doc/requirements.txt b/doc/requirements.txt deleted file mode 100644 index 575c546ab..000000000 --- a/doc/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -sphinx==4.4.0 -nbsphinx==0.8.8 -ipykernel==6.9.1 diff --git a/pyproject.toml b/pyproject.toml index ff43e4937..5b4d1f00c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,8 +10,7 @@ readme = "README.md" license = "GPL-3.0-or-later" requires-python = ">=3.8" authors = [ - { name = "Quantum Technology Group and Chair of Software Engineering" }, - { name = "RWTH Aachen University" }, + { name = "Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University" }, ] keywords = [ "control", @@ -81,6 +80,34 @@ include = [ "/qupulse", ] +[tool.hatch.envs.docs] +dependencies = [ + "sphinx", + "nbsphinx", + "sphinx-rtd-theme" +] +[tool.hatch.envs.docs.scripts] +# This is a hack to achieve cross-platform version extraction until https://github.com/pypa/hatch/issues/1006 +build = """ + python -c "import subprocess, os; \ + result = subprocess.run(['hatch', 'version'], capture_output=True, text=True); \ + version = result.stdout.strip(); \ + subprocess.run(['sphinx-build', '-b', '{args:0}', 'doc/source', 'doc/build', '-D', 'version=%s' % version, '-D', 'release=%s' % version])" +""" +latex = """ + python -c "import subprocess, os; \ + result = subprocess.run(['hatch', 'version'], capture_output=True, text=True); \ + version = result.stdout.strip(); \ + subprocess.run(['sphinx-build', '-b', 'latex', 'doc/source', 'doc/build', '-D', 'version=%s' % version, '-D', 'release=%s' % version])" +""" +html = """ + python -c "import subprocess, os; \ + result = subprocess.run(['hatch', 'version'], capture_output=True, text=True); \ + version = result.stdout.strip(); \ + subprocess.run(['sphinx-build', '-b', 'html', 'doc/source', 'doc/build', '-D', 'version=%s' % version, '-D', 'release=%s' % version])" +""" + + [tool.towncrier] directory = "changes.d" package = "qupulse" From 2e583593d0b915db3ef093c4ea3fd93d1381e87d Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 10 Jun 2024 18:25:41 +0200 Subject: [PATCH 231/441] Make build path even more confused --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5b4d1f00c..14128685a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,19 +92,19 @@ build = """ python -c "import subprocess, os; \ result = subprocess.run(['hatch', 'version'], capture_output=True, text=True); \ version = result.stdout.strip(); \ - subprocess.run(['sphinx-build', '-b', '{args:0}', 'doc/source', 'doc/build', '-D', 'version=%s' % version, '-D', 'release=%s' % version])" + subprocess.run(['sphinx-build', '-b', '{args:0}', 'doc/source', 'doc/build/{args:0}', '-d', 'doc/build/.doctrees', '-D', 'version=%s' % version, '-D', 'release=%s' % version])" """ latex = """ python -c "import subprocess, os; \ result = subprocess.run(['hatch', 'version'], capture_output=True, text=True); \ version = result.stdout.strip(); \ - subprocess.run(['sphinx-build', '-b', 'latex', 'doc/source', 'doc/build', '-D', 'version=%s' % version, '-D', 'release=%s' % version])" + subprocess.run(['sphinx-build', '-b', 'latex', 'doc/source', 'doc/build/latex', '-D', 'version=%s' % version, '-D', 'release=%s' % version])" """ html = """ python -c "import subprocess, os; \ result = subprocess.run(['hatch', 'version'], capture_output=True, text=True); \ version = result.stdout.strip(); \ - subprocess.run(['sphinx-build', '-b', 'html', 'doc/source', 'doc/build', '-D', 'version=%s' % version, '-D', 'release=%s' % version])" + subprocess.run(['sphinx-build', '-b', 'html', 'doc/source', 'doc/build/html', '-D', 'version=%s' % version, '-D', 'release=%s' % version])" """ From b9d726ba19a843ef66c2d5ad16379fd9cbeb5bae Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 10 Jun 2024 18:26:08 +0200 Subject: [PATCH 232/441] Use default requirements to build docs --- readthedocs.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/readthedocs.yml b/readthedocs.yml index 903c17d30..3c8203900 100644 --- a/readthedocs.yml +++ b/readthedocs.yml @@ -3,13 +3,14 @@ version: 2 build: os: ubuntu-22.04 tools: - python: "3.9" + python: "3.11" python: install: - - requirements: doc/requirements.txt - - method: setuptools + - method: pip path: . + extra_requirements: + - default sphinx: builder: html From 147676db4336eff714cf028f93bbe4f73e61a999 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 17 Jun 2024 13:26:19 +0200 Subject: [PATCH 233/441] Add towncrier integration and update README accordingly --- README.md | 11 ++++++++++- pyproject.toml | 9 +++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4dfe22973..ac8825e34 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Alternatively, the current development version of qupulse can be installed by ex ```sh python -m pip install -e git+https://github.com/qutech/qupulse.git#egg=qupulse[default] ``` -which will clone the github repository to `./src/qupulse` and do an editable/development install. +which will clone the github repository to `./src/qupulse` and do an editable/development install. ### Requirements and dependencies qupulse requires at least Python 3.8 and is tested on 3.8, 3.9 and 3.10. It relies on some external Python packages as dependencies. @@ -63,6 +63,15 @@ The repository primarily consists of the folders `qupulse` (toolkit core code) a Contents of `tests` mirror the structure of `qupulse`. For every `` somewhere in `qupulse` there should exist a `Tests.py` in the corresponding subdirectory of `tests`. +## Development + +`qupulse` uses `hatch` as development tool which provides a convenient interface for most development tasks. The following should work. + + - `hatch build`: Build wheel and source tarball + - `hatch version X.X.X`: Set version + - `hatch run docs:html`: Build documentation (requires pandoc) + - `hatch run changelog:draft` and `hatch run changelog:release` to preview or update the changelog. + ## License The current version of qupulse is available under the `GPL-3.0-or-later` license. Versions up to and including 0.10 were licensed under the MIT license. If you require different licensing terms, please contact us to discuss your needs. diff --git a/pyproject.toml b/pyproject.toml index 14128685a..b0d59c055 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -107,6 +107,15 @@ html = """ subprocess.run(['sphinx-build', '-b', 'html', 'doc/source', 'doc/build/html', '-D', 'version=%s' % version, '-D', 'release=%s' % version])" """ +[tool.hatch.envs.changelog] +detached = true +dependencies = [ + "towncrier", +] + +[tool.hatch.envs.changelog.scripts] +draft = "towncrier build --version main --draft" +release = "towncrier build --yes --version {args}" [tool.towncrier] directory = "changes.d" From cc4e7b8efbf5b770f92e971c7a8ed58c9aa88fec Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 17 Jun 2024 13:27:45 +0200 Subject: [PATCH 234/441] Update python setup --- .github/workflows/pythontest.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/pythontest.yaml b/.github/workflows/pythontest.yaml index e4300f978..b67a02074 100644 --- a/.github/workflows/pythontest.yaml +++ b/.github/workflows/pythontest.yaml @@ -32,12 +32,10 @@ jobs: uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - # supported since 2.3 cache: pip - cache-dependency-path: setup.cfg - name: Install dependencies run: | From c74c87100f49a3e4a0083c1fca65f4f065e4a457 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 20 Jun 2024 12:39:59 +0200 Subject: [PATCH 235/441] Properly include ZHINST example --- doc/source/examples/examples.rst | 1 + doc/source/learners_guide.rst | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/source/examples/examples.rst b/doc/source/examples/examples.rst index c543712d4..3226464d5 100644 --- a/doc/source/examples/examples.rst +++ b/doc/source/examples/examples.rst @@ -45,6 +45,7 @@ All examples are provided as static text in this documentation and, additionally :name: hardware_examples 02CreatePrograms + 04ZurichInstrumentsSetup The ``/doc/source/examples`` directory also contains some outdated examples for features and functionality that has been changed. These examples start with an underscore i.e. ``_*.ipynb`` and are currently left only for reference purposes. If you are just learning how to get around in qupulse please ignore them. \ No newline at end of file diff --git a/doc/source/learners_guide.rst b/doc/source/learners_guide.rst index ee728dd2a..f21b329d0 100644 --- a/doc/source/learners_guide.rst +++ b/doc/source/learners_guide.rst @@ -97,4 +97,4 @@ Read :ref:`program` and :ref:`hardware`. Setup an experiment ^^^^^^^^^^^^^^^^^^^ -This section is under construction. There is currently an outdated example :ref:`/examples/_HardwareSetup.ipynb` +This process is not fully documented yet. qupulse gives you tools for very flexible setup configurations. However, there is an example setup with Zurich Instruments devices in :ref:`/examples/04ZurichInstrumentsSetup.ipynb`. From a229bf235b260c2216bf629dc94e4b55e19778a2 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 20 Jun 2024 12:44:20 +0200 Subject: [PATCH 236/441] Specify expression path --- doc/source/concepts/pulsetemplates.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/concepts/pulsetemplates.rst b/doc/source/concepts/pulsetemplates.rst index 9c1463917..85b7853ad 100644 --- a/doc/source/concepts/pulsetemplates.rst +++ b/doc/source/concepts/pulsetemplates.rst @@ -8,7 +8,7 @@ qupulse represents pulses as abstract pulse templates. A pulse template can be u There are multiple types of different pulse template classes, briefly explained in the following. :class:`.TablePulseTemplate`, :class:`.PointPulseTemplate` and :class:`.FunctionPulseTemplate` are used to define the atomic building blocks of pulses in the following ways: :class:`.TablePulseTemplate` and :class:`.PointPulseTemplate` allow the user to specify pairs of time and voltage values and choose an interpolation strategy between neighbouring points. Both templates support multiple channels but :class:`.TablePulseTemplate` allows for different time values for different channels meaning that the channels can change their voltages at different times. :class:`.PointPulseTemplate` restricts this to switches at the same time by interpreting the voltage as a vector and provides a more convenient interface for this case. -:class:`.FunctionPulseTemplate` accepts any mathematical function that maps time to voltage values. Internally it uses :class:`.Expression` for function evaluation. +:class:`.FunctionPulseTemplate` accepts any mathematical function that maps time to voltage values. Internally it uses :class:`qupulse.expressions.Expression` for function evaluation. All other pulse template classes are then used to construct arbitrarily complex pulse templates by combining existing ones into new structures [#tree]_: :class:`.SequencePulseTemplate` enables the user to specify a sequence of existing pulse templates (subtemplates) and modify parameter values using a mapping function. From a707b906e4a7ee71f2fe9fd196265a9fd62bb226 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 20 Jun 2024 12:44:57 +0200 Subject: [PATCH 237/441] Include zhinst as dependency for documentation --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index b0d59c055..c79768ccb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,6 +82,7 @@ include = [ [tool.hatch.envs.docs] dependencies = [ + "qupulse[default,zurich-instruments]", "sphinx", "nbsphinx", "sphinx-rtd-theme" From 168f0a6c39cef037e5930745ba8abb2d9fef0a1e Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 20 Jun 2024 12:45:53 +0200 Subject: [PATCH 238/441] Include docs:clean command --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index c79768ccb..fd747e3eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -107,6 +107,9 @@ html = """ version = result.stdout.strip(); \ subprocess.run(['sphinx-build', '-b', 'html', 'doc/source', 'doc/build/html', '-D', 'version=%s' % version, '-D', 'release=%s' % version])" """ +clean= """ +python -c "import shutil; shutil.rmtree('doc/build')" +""" [tool.hatch.envs.changelog] detached = true From 3294b649e7fae86ab28ad891c80e5a61c0b2b358 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 20 Jun 2024 12:49:13 +0200 Subject: [PATCH 239/441] Remove outdated sentence in documentation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ac8825e34..358bb1c93 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ which will clone the github repository to `./src/qupulse` and do an editable/dev qupulse requires at least Python 3.8 and is tested on 3.8, 3.9 and 3.10. It relies on some external Python packages as dependencies. We intentionally did not restrict versions of dependencies in the install scripts to not unnecessarily prevent usage of newer releases of dependencies that might be compatible. However, if qupulse does encounter problems with a particular dependency version please file an issue. -The backend for TaborAWGs requires packages that can be found [here](https://git.rwth-aachen.de/qutech/python-TaborDriver). As a shortcut you can install it from the python interpreter via `qupulse.hardware.awgs.install_requirements('tabor')`. +The backend for TaborAWGs requires packages that can be found [here](https://git.rwth-aachen.de/qutech/python-TaborDriver). The data acquisition backend for AlazarTech cards needs a package that unfortunately is not open source (yet). If you need it or have questions contact . From 5a724a95cd263709192abe20e256dd718c2584c8 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 20 Jun 2024 14:55:33 +0200 Subject: [PATCH 240/441] Remove "unexpected indentation" --- doc/source/concepts/awgs.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/doc/source/concepts/awgs.rst b/doc/source/concepts/awgs.rst index 0b3497652..accd604d3 100644 --- a/doc/source/concepts/awgs.rst +++ b/doc/source/concepts/awgs.rst @@ -8,9 +8,10 @@ This section is supposed to help you understand how qupulse sees AWGs and by ext When a program is uploaded to an arbitrary waveform generator (AWG) it needs to brought in a form that the hardware understands. Most AWGs consist of three significant parts: - * The actual digital to analog converter (DAC) that outputs samples at a (semi-) fixed rate [1]_ - * A sequencer which tells the DAC what to do - * Waveform memory which contains sampled waveforms in a format that the DAC understands + +* The actual digital to analog converter (DAC) that outputs samples at a (semi-) fixed rate [1]_, +* a sequencer which tells the DAC what to do, +* waveform memory which contains sampled waveforms in a format that the DAC understands. The sequencer feeds the data from the waveform memory to the DAC in the correct order. Uploading a qupulse pulse to an AWG requires to sample the program, upload waveforms to the memory From c00dd7b69f229ac661f4f9e840a8b8d66f32d638 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 20 Jun 2024 15:05:02 +0200 Subject: [PATCH 241/441] Push minimal supported python version --- .github/workflows/pythontest.yaml | 2 +- README.md | 2 +- changes.d/835.removal | 1 + pyproject.toml | 5 ++--- 4 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 changes.d/835.removal diff --git a/.github/workflows/pythontest.yaml b/.github/workflows/pythontest.yaml index b67a02074..38cadd540 100644 --- a/.github/workflows/pythontest.yaml +++ b/.github/workflows/pythontest.yaml @@ -22,7 +22,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10"] + python-version: ["3.10", "3.11", "3.12"] env: INSTALL_EXTRAS: tests,plotting,zurich-instruments,tektronix,tabor-instruments diff --git a/README.md b/README.md index 358bb1c93..1ad4ccb7b 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ python -m pip install -e git+https://github.com/qutech/qupulse.git#egg=qupulse[d which will clone the github repository to `./src/qupulse` and do an editable/development install. ### Requirements and dependencies -qupulse requires at least Python 3.8 and is tested on 3.8, 3.9 and 3.10. It relies on some external Python packages as dependencies. +qupulse requires at least Python 3.10 and is tested on 3.10, 3.11 and 3.12. It relies on some external Python packages as dependencies. We intentionally did not restrict versions of dependencies in the install scripts to not unnecessarily prevent usage of newer releases of dependencies that might be compatible. However, if qupulse does encounter problems with a particular dependency version please file an issue. The backend for TaborAWGs requires packages that can be found [here](https://git.rwth-aachen.de/qutech/python-TaborDriver). diff --git a/changes.d/835.removal b/changes.d/835.removal new file mode 100644 index 000000000..cc2f7a5f0 --- /dev/null +++ b/changes.d/835.removal @@ -0,0 +1 @@ +Remove python 3.8 and 3.9 support. Version 3.10 is now the minimal supported version. \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index fd747e3eb..f0697b235 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ dynamic = ["version"] description = "A Quantum compUting PULse parametrization and SEquencing framework" readme = "README.md" license = "GPL-3.0-or-later" -requires-python = ">=3.8" +requires-python = ">=3.10" authors = [ { name = "Quantum Technology Group and Chair of Software Engineering, RWTH Aachen University" }, ] @@ -65,8 +65,7 @@ tests = [ "pytest_benchmark", ] zurich-instruments = [ - "qupulse-hdawg-legacy;python_version<'3.9'", - "qupulse-hdawg;python_version>='3.9'", + "qupulse-hdawg", ] [project.urls] From 21cd24bc4fb7adb18fd5055dafab614b14d4498b Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 20 Jun 2024 15:25:12 +0200 Subject: [PATCH 242/441] Allow gmpy2 release candidate installation for python 3.12 --- pyproject.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f0697b235..32f04eb74 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,9 @@ dependencies = [ "lazy_loader", "numpy", "sympy>=1.1.1", - "gmpy2", + # This is required because there is no 3.12 compatible gmpy2 stable release as of 2024.06.20 + "gmpy2;python_version<'3.12'", + "gmpy2>=2.2.0rc1;python_version>='3.12'" ] [project.optional-dependencies] From e50211606ec97854bc4496786d92d92f9e18a664 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 20 Jun 2024 15:32:28 +0200 Subject: [PATCH 243/441] Do not used removed assertEquals alias --- tests/serialization_tests.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/serialization_tests.py b/tests/serialization_tests.py index 1ebd93b8e..537e642c2 100644 --- a/tests/serialization_tests.py +++ b/tests/serialization_tests.py @@ -1527,8 +1527,8 @@ def test_convert_stored_pulse_in_storage_dest_not_empty_id_overlap(self) -> None with self.assertRaises(ValueError): convert_stored_pulse_in_storage('hugos_parent', source_backend, destination_backend) - self.assertEquals('already_existing_data', destination_backend['hugo']) - self.assertEquals(1, len(destination_backend.stored_items)) + self.assertEqual('already_existing_data', destination_backend['hugo']) + self.assertEqual(1, len(destination_backend.stored_items)) def test_convert_stored_pulse_in_storage_dest_not_empty_no_id_overlap(self) -> None: with warnings.catch_warnings(): @@ -1548,7 +1548,7 @@ def test_convert_stored_pulse_in_storage_dest_not_empty_no_id_overlap(self) -> N destination_backend.put('ilse', 'already_existing_data') convert_stored_pulse_in_storage('hugos_parent', source_backend, destination_backend) - self.assertEquals('already_existing_data', destination_backend['ilse']) + self.assertEqual('already_existing_data', destination_backend['ilse']) pulse_storage = PulseStorage(destination_backend) deserialized = pulse_storage['hugos_parent'] self.assertEqual(serializable, deserialized) @@ -1607,8 +1607,8 @@ def test_convert_stored_pulses_dest_not_empty_id_overlap(self) -> None: with self.assertRaises(ValueError): convert_pulses_in_storage(source_backend, destination_backend) - self.assertEquals('already_existing_data', destination_backend['hugo']) - self.assertEquals(1, len(destination_backend.stored_items)) + self.assertEqual('already_existing_data', destination_backend['hugo']) + self.assertEqual(1, len(destination_backend.stored_items)) def test_convert_stored_pulses_dest_not_empty_no_id_overlap(self) -> None: with warnings.catch_warnings(): From bce97bc46f815bdf2da224150a04a186e4409bfb Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Thu, 20 Jun 2024 16:06:30 +0200 Subject: [PATCH 244/441] Remove dead custom frozendict implementations --- changes.d/853.misc | 1 + qupulse/utils/types.py | 128 +------------------------------------ tests/utils/types_tests.py | 102 +---------------------------- 3 files changed, 5 insertions(+), 226 deletions(-) create mode 100644 changes.d/853.misc diff --git a/changes.d/853.misc b/changes.d/853.misc new file mode 100644 index 000000000..cf8b7ff32 --- /dev/null +++ b/changes.d/853.misc @@ -0,0 +1 @@ +Remove private and unused frozendict fallback implementations `_FrozenDictByInheritance` and `_FrozenDictByWrapping`. \ No newline at end of file diff --git a/qupulse/utils/types.py b/qupulse/utils/types.py index 83dce2a0f..915fe8832 100644 --- a/qupulse/utils/types.py +++ b/qupulse/utils/types.py @@ -15,18 +15,12 @@ import numpy import sympy import gmpy2 - -try: - from frozendict import frozendict -except ImportError: - warnings.warn("The frozendict package is not installed. We currently also ship a fallback frozendict which " - "will be removed in a future release.", category=DeprecationWarning) - frozendict = None +from frozendict import frozendict import qupulse.utils.numeric as qupulse_numeric __all__ = ["MeasurementWindow", "ChannelID", "HashableNumpyArray", "TimeType", "time_from_float", "DocStringABCMeta", - "SingletonABCMeta", "SequenceProxy", "frozendict"] + "SingletonABCMeta", "SequenceProxy"] MeasurementWindow = typing.Tuple[str, numbers.Real, numbers.Real] ChannelID = typing.Union[str, int] @@ -399,121 +393,7 @@ def has_type_interface(obj: typing.Any, type_obj: typing.Type) -> bool: _T_co_hash = typing.TypeVar('_T_co_hash', bound=typing.Hashable, covariant=True) # Any type covariant containers. FrozenMapping = typing.Mapping[_KT_hash, _T_co_hash] - - -class _FrozenDictByInheritance(dict): - """This is non mutable, hashable dict. It violates the Liskov substitution principle but is faster than wrapping. - It is not used by default and may be removed in the future. - """ - def __setitem__(self, key, value): - raise TypeError('FrozenDict is immutable') - - def __delitem__(self, key): - raise TypeError('FrozenDict is immutable') - - def update(self, *args, **kwargs): - raise TypeError('FrozenDict is immutable') - - def setdefault(self, *args, **kwargs): - raise TypeError('FrozenDict is immutable') - - def clear(self): - raise TypeError('FrozenDict is immutable') - - def pop(self, *args, **kwargs): - raise TypeError('FrozenDict is immutable') - - def popitem(self, *args, **kwargs): - raise TypeError('FrozenDict is immutable') - - def copy(self): - return self - - def to_dict(self) -> typing.Dict[_KT_hash, _T_co_hash]: - return super().copy() - - def __hash__(self): - # faster than functools.reduce(operator.xor, map(hash, self.items())) but takes more memory - # TODO: investigate caching - return hash(frozenset(self.items())) - - -class _FrozenDictByWrapping(FrozenMapping): - """Immutable dict like type. - - There are the following possibilities in pure python: - - subclass dict (violates the Liskov substitution principle) - - wrap dict (slow construction and method indirection) - - abuse MappingProxyType (hard to add hash and make mutation difficult) - - - - Wrapper around builtin dict without the mutating methods. - - Hot path methods in __slots__ are the bound methods of the dict object. The other methods are wrappers. - - Why not subclass dict and overwrite mutating methods: - roughly the same speed for __slot__ methods (a bit slower than native dict) - dict subclass always implements MutableMapping which makes type annotations useless - caching the hash value is slightly slower for the subclass - - Only downside: This wrapper class needs to implement __init__ and copy the __slot__ methods which is an overhead of - ~10 i.e. 250ns for empty subclass init vs. 4µs for empty wrapper init - """ - # made concessions in code style due to performance - _HOT_PATH_METHODS = ('keys', 'items', 'values', 'get', '__getitem__') - _PRIVATE_ATTRIBUTES = ('_hash', '_dict') - __slots__ = _HOT_PATH_METHODS + _PRIVATE_ATTRIBUTES - - def __new__(cls, *args, **kwds): - """Overwriting __new__ saves a factor of two for initialization. This is the relevant line from - Generic.__new__""" - return object.__new__(cls) - - def __init__(self, *args, **kwargs): - inner_dict = dict(*args, **kwargs) - self._dict = inner_dict # type: typing.Dict[_KT_hash, _T_co_hash] - self._hash = None - - self.__getitem__ = inner_dict.__getitem__ - self.keys = inner_dict.keys - self.items = inner_dict.items - self.values = inner_dict.values - self.get = inner_dict.get - - def __contains__(self, item: _KT_hash) -> bool: - return item in self._dict - - def __iter__(self) -> typing.Iterator[_KT_hash]: - return iter(self._dict) - - def __len__(self) -> int: - return len(self._dict) - - def __repr__(self): - return '%s(%r)' % (self.__class__.__name__, self._dict) - - def __hash__(self) -> int: - # use the local variable h to minimize getattr calls to minimum and reduce caching overhead - h = self._hash - if h is None: - self._hash = h = functools.reduce(operator.xor, map(hash, self.items()), 0xABCD0) - return h - - def __eq__(self, other: typing.Mapping): - return other == self._dict - - def copy(self): - return self - - def to_dict(self) -> typing.Dict[_KT_hash, _T_co_hash]: - return self._dict.copy() - - -if frozendict is None: - FrozenDict = _FrozenDictByWrapping -else: - FrozenDict = frozendict +FrozenDict = frozendict class SequenceProxy(collections.abc.Sequence): @@ -550,5 +430,3 @@ def __eq__(self, other): and all(x == y for x, y in zip(self, other))) else: return NotImplemented - - diff --git a/tests/utils/types_tests.py b/tests/utils/types_tests.py index e2271d2fe..5a2a53ef1 100644 --- a/tests/utils/types_tests.py +++ b/tests/utils/types_tests.py @@ -1,10 +1,8 @@ import unittest -import inspect import numpy as np -from qupulse.utils.types import (HashableNumpyArray, SequenceProxy, _FrozenDictByWrapping, - _FrozenDictByInheritance) +from qupulse.utils.types import (HashableNumpyArray, SequenceProxy,) class HashableNumpyArrayTest(unittest.TestCase): @@ -36,101 +34,3 @@ def test_sequence_proxy(self): with self.assertRaises(TypeError): p[1] = 7 - - -class FrozenDictTests(unittest.TestCase): - FrozenDictType = _FrozenDictByWrapping - - """This class can test general non mutable mappings""" - def setUp(self) -> None: - self.d = {'a': 1, 'b': 2} - self.f = self.FrozenDictType(self.d) - self.prev_state = dict(self.f) - - def tearDown(self) -> None: - self.assertEqual(self.prev_state, dict(self.f)) - - def test_init(self): - d = {'a': 1, 'b': 2} - - f1 = self.FrozenDictType(d) - f2 = self.FrozenDictType(**d) - f3 = self.FrozenDictType(d.items()) - - self.assertEqual(d, f1) - self.assertEqual(d, f2) - self.assertEqual(d, f3) - - self.assertEqual(d.keys(), f1.keys()) - self.assertEqual(d.keys(), f2.keys()) - self.assertEqual(d.keys(), f3.keys()) - - self.assertEqual(set(d.items()), set(f1.items())) - self.assertEqual(set(d.items()), set(f2.items())) - self.assertEqual(set(d.items()), set(f3.items())) - - def test_mapping(self): - d = {'a': 1, 'b': 2} - f = self.FrozenDictType(d) - - self.assertEqual(len(d), len(f)) - self.assertIn('a', f) - self.assertIn('b', f) - self.assertNotIn('c', f) - - self.assertEqual(1, f['a']) - self.assertEqual(2, f['b']) - - with self.assertRaisesRegex(KeyError, 'c'): - _ = f['c'] - - with self.assertRaises(TypeError): - f['a'] = 9 - - with self.assertRaises(TypeError): - del f['a'] - - def test_copy(self): - d = {'a': 1, 'b': 2} - f = self.FrozenDictType(d) - self.assertIs(f, f.copy()) - - def test_eq_and_hash(self): - d = {'a': 1, 'b': 2} - - f1 = self.FrozenDictType(d) - f2 = self.FrozenDictType({'a': 1, 'b': 2}) - f3 = self.FrozenDictType({'a': 1, 'c': 3}) - - self.assertEqual(f1, f2) - self.assertEqual(hash(f1), hash(f2)) - - self.assertNotEqual(f1, f3) - - -class FrozenDictByInheritanceTests(FrozenDictTests): - FrozenDictType = _FrozenDictByInheritance - - def test_update(self): - with self.assertRaisesRegex(TypeError, 'immutable'): - self.f.update(d=5) - - def test_setdefault(self): - with self.assertRaisesRegex(TypeError, 'immutable'): - self.f.setdefault('c', 3) - with self.assertRaisesRegex(TypeError, 'immutable'): - self.f.setdefault('a', 2) - - def test_clear(self): - with self.assertRaisesRegex(TypeError, 'immutable'): - self.f.clear() - - def test_pop(self): - with self.assertRaisesRegex(TypeError, 'immutable'): - self.f.pop() - with self.assertRaisesRegex(TypeError, 'immutable'): - self.f.pop('a') - - def test_popitem(self): - with self.assertRaisesRegex(TypeError, 'immutable'): - self.f.popitem() From ed91fb1d0bcebfcfd5f257af7d302f5597abadee Mon Sep 17 00:00:00 2001 From: Nomos11 <82180697+Nomos11@users.noreply.github.com> Date: Thu, 20 Jun 2024 21:58:58 +0200 Subject: [PATCH 245/441] fix jump back in linspace --- qupulse/program/linspace.py | 2 +- tests/program/linspace_tests.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/qupulse/program/linspace.py b/qupulse/program/linspace.py index 43d01113e..c15202088 100644 --- a/qupulse/program/linspace.py +++ b/qupulse/program/linspace.py @@ -341,7 +341,7 @@ def _add_iteration_node(self, node: LinSpaceIter): self.add_node(node.body) if node.length > 1: - self.iterations[-1] = node.length + self.iterations[-1] = node.length - 1 label, jmp = self.new_loop(node.length - 1) self.commands.append(label) self.add_node(node.body) diff --git a/tests/program/linspace_tests.py b/tests/program/linspace_tests.py index 03a5b2971..60acd136c 100644 --- a/tests/program/linspace_tests.py +++ b/tests/program/linspace_tests.py @@ -74,7 +74,7 @@ def setUp(self): LoopLabel(1, 99), - Increment(0, -2.0, key_0), + Increment(0, -1.99, key_0), Increment(1, 0.02, key_1), Wait(TimeType(10 ** 6)), @@ -131,8 +131,8 @@ def setUp(self): LoopLabel(1, 99), - Increment(0, 1e-3 + -200 * 1e-2, key_0), - Increment(1, 0.02 + -200 * -3e-3, key_1), + Increment(0, 1e-3 + -199 * 1e-2, key_0), + Increment(1, 0.02 + -199 * -3e-3, key_1), Wait(TimeType(10 ** 6)), LoopLabel(2, 199), @@ -223,7 +223,7 @@ def setUp(self): Set(0, -0.4), Set(1, -0.3), Wait(TimeType(10 ** 5)), - Increment(0, -2.0, key_0), + Increment(0, -1.99, key_0), Increment(1, 0.02, key_1), Wait(TimeType(10 ** 6)), Set(0, 0.05), From 06da4f7dbe85c917d94ec24dde29393d92f7116b Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 21 Jun 2024 10:23:47 +0200 Subject: [PATCH 246/441] Add numpy to test matrix --- .github/workflows/pythontest.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/pythontest.yaml b/.github/workflows/pythontest.yaml index 38cadd540..f64dfb63f 100644 --- a/.github/workflows/pythontest.yaml +++ b/.github/workflows/pythontest.yaml @@ -23,6 +23,7 @@ jobs: fail-fast: false matrix: python-version: ["3.10", "3.11", "3.12"] + numpy-version: [">=1.24,<2.0", ">=2.0"] env: INSTALL_EXTRAS: tests,plotting,zurich-instruments,tektronix,tabor-instruments @@ -42,6 +43,9 @@ jobs: python -m pip install --upgrade pip python -m pip install coverage coveralls + - name: Install numpy ${{ matrix.numpy-version }} + run: python -m pip install "numpy${{ matrix.numpy-version }}" + - name: Install package run: | python -m pip install .[${{ env.INSTALL_EXTRAS }}] From ed23e610c67c6c401d9faf3ff0cbbc5fd49ad433 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 21 Jun 2024 10:28:13 +0200 Subject: [PATCH 247/441] Execute tests on workflow changes --- .github/workflows/pythontest.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pythontest.yaml b/.github/workflows/pythontest.yaml index f64dfb63f..5b1c9c3f6 100644 --- a/.github/workflows/pythontest.yaml +++ b/.github/workflows/pythontest.yaml @@ -15,6 +15,7 @@ on: - 'tests/**' - 'setup.*' - 'pyproject.toml' + - '.github/workflows/*' jobs: test: From d70643bae7b21ce9a1b34958eb6cde3ebb901842 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 21 Jun 2024 12:15:41 +0200 Subject: [PATCH 248/441] Add linspace VM --- qupulse/program/linspace.py | 69 +++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/qupulse/program/linspace.py b/qupulse/program/linspace.py index 43d01113e..ec1f790ac 100644 --- a/qupulse/program/linspace.py +++ b/qupulse/program/linspace.py @@ -412,3 +412,72 @@ def to_increment_commands(linspace_nodes: Sequence[LinSpaceNode]) -> List[Comman state.add_node(linspace_nodes) return state.commands + +class LinSpaceVM: + def __init__(self, channels: int): + self.current_values = [np.nan] * channels + self.time = TimeType(0) + self.registers = tuple({} for _ in range(channels)) + + self.history: List[Tuple[TimeType, Tuple[float, ...]]] = [] + + self.commands = None + self.label_targets = None + self.label_counts = None + self.current_command = None + + def change_state(self, cmd: Union[Set, Increment, Wait, Play]): + if isinstance(cmd, Play): + raise NotImplementedError("TODO: Implement arbitrary waveform simulation") + elif isinstance(cmd, Wait): + self.history.append( + (self.time, self.current_values.copy()) + ) + self.time += cmd.duration + elif isinstance(cmd, Set): + self.current_values[cmd.channel] = cmd.value + self.registers[cmd.channel][cmd.key] = cmd.value + elif isinstance(cmd, Increment): + value = self.registers[cmd.channel][cmd.dependency_key] + value += cmd.value + self.registers[cmd.channel][cmd.dependency_key] = value + self.current_values[cmd.channel] = value + else: + raise NotImplementedError(cmd) + + def set_commands(self, commands: Sequence[Command]): + self.commands = [] + self.label_targets = {} + self.label_counts = {} + self.current_command = None + + for cmd in commands: + self.commands.append(cmd) + if isinstance(cmd, LoopLabel): + # a loop label signifies a reset count followed by the actual label that targets the following command + assert cmd.idx not in self.label_targets + self.label_targets[cmd.idx] = len(self.commands) + + self.current_command = 0 + + def step(self): + cmd = self.commands[self.current_command] + if isinstance(cmd, LoopJmp): + if self.label_counts[cmd.idx] > 0: + self.label_counts[cmd.idx] -= 1 + self.current_command = self.label_targets[cmd.idx] + else: + # ignore jump + self.current_command += 1 + elif isinstance(cmd, LoopLabel): + self.label_counts[cmd.idx] = cmd.count + self.current_command += 1 + else: + self.change_state(cmd) + self.current_command += 1 + + def run(self): + while self.current_command < len(self.commands): + self.step() + + From 025ba8a0d14c48d556f8488379f931c040bf9301 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 21 Jun 2024 12:15:59 +0200 Subject: [PATCH 249/441] Add test that fails due to known bug --- tests/program/linspace_tests.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/program/linspace_tests.py b/tests/program/linspace_tests.py index 03a5b2971..4c2dc59ed 100644 --- a/tests/program/linspace_tests.py +++ b/tests/program/linspace_tests.py @@ -32,6 +32,10 @@ def setUp(self): LoopJmp(0) ] + self.output = [ + (TimeType(10**6 * idx), [sum([-1.0] + [0.01] * idx)]) for idx in range(200) + ] + def test_program(self): program_builder = LinSpaceBuilder(('a',)) program = self.pulse_template.create_program(program_builder=program_builder) @@ -41,6 +45,12 @@ def test_commands(self): commands = to_increment_commands([self.program]) self.assertEqual(self.commands, commands) + def test_output(self): + vm = LinSpaceVM(1) + vm.set_commands(commands=self.commands) + vm.run() + self.assertEqual(self.output, vm.history) + class PlainCSDTest(TestCase): def setUp(self): From 6aa2e03a718073ac90afe7f92f7a649d2b7b4f17 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 21 Jun 2024 13:04:51 +0200 Subject: [PATCH 250/441] Fix VM loop count handling --- qupulse/program/linspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qupulse/program/linspace.py b/qupulse/program/linspace.py index 3ddc5a713..600a6f82b 100644 --- a/qupulse/program/linspace.py +++ b/qupulse/program/linspace.py @@ -470,7 +470,7 @@ def step(self): # ignore jump self.current_command += 1 elif isinstance(cmd, LoopLabel): - self.label_counts[cmd.idx] = cmd.count + self.label_counts[cmd.idx] = cmd.count - 1 self.current_command += 1 else: self.change_state(cmd) From e598e8c054e3587926d68659960a377a909601dc Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 21 Jun 2024 13:05:42 +0200 Subject: [PATCH 251/441] Add working VM tests --- tests/program/linspace_tests.py | 58 +++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/tests/program/linspace_tests.py b/tests/program/linspace_tests.py index a68f62ecc..604f82378 100644 --- a/tests/program/linspace_tests.py +++ b/tests/program/linspace_tests.py @@ -6,6 +6,16 @@ from qupulse.program.linspace import * from qupulse.program.transformation import * + +def assert_vm_output_almost_equal(test: TestCase, expected, actual): + test.assertEqual(len(expected), len(actual)) + for idx, ((t_e, vals_e), (t_a, vals_a)) in enumerate(zip(expected, actual)): + test.assertEqual(t_e, t_a, f"Differing times in {idx} element") + test.assertEqual(len(vals_e), len(vals_a), f"Differing channel count in {idx} element") + for ch, (val_e, val_a) in enumerate(zip(vals_e, vals_a)): + test.assertAlmostEqual(val_e, val_a, msg=f"Differing values in {idx} element channel {ch}") + + class SingleRampTest(TestCase): def setUp(self): hold = ConstantPT(10 ** 6, {'a': '-1. + idx * 0.01'}) @@ -50,6 +60,7 @@ def test_output(self): vm.set_commands(commands=self.commands) vm.run() self.assertEqual(self.output, vm.history) + assert_vm_output_almost_equal(self, self.output, vm.history) class PlainCSDTest(TestCase): @@ -96,6 +107,16 @@ def setUp(self): LoopJmp(1), ] + a_values = [sum([-1.] + [0.01] * i) for i in range(200)] + b_values = [sum([-.5] + [0.02] * j) for j in range(100)] + + self.output = [ + ( + TimeType(10 ** 6 * (i + 200 * j)), + [a_values[i], b_values[j]] + ) for j in range(100) for i in range(200) + ] + def test_program(self): program_builder = LinSpaceBuilder(('a', 'b')) program = self.pulse_template.create_program(program_builder=program_builder) @@ -105,13 +126,20 @@ def test_increment_commands(self): commands = to_increment_commands([self.program]) self.assertEqual(self.commands, commands) + def test_output(self): + vm = LinSpaceVM(2) + vm.set_commands(self.commands) + vm.run() + assert_vm_output_almost_equal(self, self.output, vm.history) + class TiltedCSDTest(TestCase): def setUp(self): + repetition_count = 3 hold = ConstantPT(10**6, {'a': '-1. + idx_a * 0.01 + idx_b * 1e-3', 'b': '-.5 + idx_b * 0.02 - 3e-3 * idx_a'}) scan_a = hold.with_iteration('idx_a', 200) self.pulse_template = scan_a.with_iteration('idx_b', 100) - self.repeated_pt = self.pulse_template.with_repetition(42) + self.repeated_pt = self.pulse_template.with_repetition(repetition_count) self.program = LinSpaceIter(length=100, body=(LinSpaceIter( length=200, @@ -123,7 +151,7 @@ def setUp(self): duration_factors=None ),) ),)) - self.repeated_program = LinSpaceRepeat(body=(self.program,), count=42) + self.repeated_program = LinSpaceRepeat(body=(self.program,), count=repetition_count) key_0 = DepKey.from_voltages((1e-3, 0.01,), DEFAULT_INCREMENT_RESOLUTION) key_1 = DepKey.from_voltages((0.02, -3e-3), DEFAULT_INCREMENT_RESOLUTION) @@ -157,7 +185,19 @@ def setUp(self): for cmd in inner_commands: if hasattr(cmd, 'idx'): cmd.idx += 1 - self.repeated_commands = [LoopLabel(0, 42)] + inner_commands + [LoopJmp(0)] + self.repeated_commands = [LoopLabel(0, repetition_count)] + inner_commands + [LoopJmp(0)] + + self.output = [ + ( + TimeType(10 ** 6 * (i + 200 * j)), + [-1. + i * 0.01 + j * 1e-3, -.5 + j * 0.02 - 3e-3 * i] + ) for j in range(100) for i in range(200) + ] + self.repeated_output = [ + (t + TimeType(10**6) * (n * 100 * 200), vals) + for n in range(repetition_count) + for t, vals in self.output + ] def test_program(self): program_builder = LinSpaceBuilder(('a', 'b')) @@ -177,6 +217,18 @@ def test_repeated_increment_commands(self): commands = to_increment_commands([self.repeated_program]) self.assertEqual(self.repeated_commands, commands) + def test_output(self): + vm = LinSpaceVM(2) + vm.set_commands(self.commands) + vm.run() + assert_vm_output_almost_equal(self, self.output, vm.history) + + def test_repeated_output(self): + vm = LinSpaceVM(2) + vm.set_commands(self.repeated_commands) + vm.run() + assert_vm_output_almost_equal(self, self.repeated_output, vm.history) + class SingletLoadProcessing(TestCase): def setUp(self): From e2801ffa3c316b2949e07a7078726954b5d59747 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Fri, 21 Jun 2024 13:11:09 +0200 Subject: [PATCH 252/441] Add singlet reload test --- tests/program/linspace_tests.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/tests/program/linspace_tests.py b/tests/program/linspace_tests.py index 604f82378..23d050ac2 100644 --- a/tests/program/linspace_tests.py +++ b/tests/program/linspace_tests.py @@ -8,6 +8,7 @@ def assert_vm_output_almost_equal(test: TestCase, expected, actual): + """Compare two vm outputs with default TestCase.assertAlmostEqual accuracy""" test.assertEqual(len(expected), len(actual)) for idx, ((t_e, vals_e), (t_a, vals_a)) in enumerate(zip(expected, actual)): test.assertEqual(t_e, t_a, f"Differing times in {idx} element") @@ -59,7 +60,6 @@ def test_output(self): vm = LinSpaceVM(1) vm.set_commands(commands=self.commands) vm.run() - self.assertEqual(self.output, vm.history) assert_vm_output_almost_equal(self, self.output, vm.history) @@ -309,6 +309,23 @@ def setUp(self): LoopJmp(1), ] + self.output = [] + time = TimeType(0) + for idx_b in range(100): + for idx_a in range(200): + self.output.append( + (time, [-.4, -.3]) + ) + time += 10 ** 5 + self.output.append( + (time, [-1. + idx_a * 0.01, -.5 + idx_b * 0.02]) + ) + time += 10 ** 6 + self.output.append( + (time, [0.05, 0.06]) + ) + time += 10 ** 5 + def test_singlet_scan_program(self): program_builder = LinSpaceBuilder(('a', 'b')) program = self.pulse_template.create_program(program_builder=program_builder) @@ -318,6 +335,12 @@ def test_singlet_scan_commands(self): commands = to_increment_commands([self.program]) self.assertEqual(self.commands, commands) + def test_singlet_scan_output(self): + vm = LinSpaceVM(2) + vm.set_commands(self.commands) + vm.run() + assert_vm_output_almost_equal(self, self.output, vm.history) + class TransformedRampTest(TestCase): def setUp(self): From 62c61f512e8b02add33535788652697cf437150a Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 1 Jul 2024 17:57:25 +0200 Subject: [PATCH 253/441] Improve arithmetic atomic pt warning level --- qupulse/pulses/arithmetic_pulse_template.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qupulse/pulses/arithmetic_pulse_template.py b/qupulse/pulses/arithmetic_pulse_template.py index 19b9b45d2..727b6456c 100644 --- a/qupulse/pulses/arithmetic_pulse_template.py +++ b/qupulse/pulses/arithmetic_pulse_template.py @@ -70,11 +70,13 @@ def __init__(self, "If they evaluate to different values on instantiation this will result in an error. " "(%r != %r) for ALL inputs " "(it may be unequal only for fringe cases)" % (lhs.duration, rhs.duration), + stacklevel=2, category=UnequalDurationWarningInArithmeticPT) if not silent_atomic and not (lhs._is_atomic() and rhs._is_atomic()): warnings.warn("ArithmeticAtomicPulseTemplate treats all operands as if they are atomic. " "You can silence this warning by passing `silent_atomic=True` or by ignoring this category.", + stacklevel=2, category=ImplicitAtomicityInArithmeticPT) self._lhs = lhs From 418e0cd2ab5fa9d8b1839a55d9c890796cd3d92a Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 1 Jul 2024 17:57:56 +0200 Subject: [PATCH 254/441] Add clean notebook command --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 32f04eb74..89edff880 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -111,6 +111,7 @@ html = """ clean= """ python -c "import shutil; shutil.rmtree('doc/build')" """ +clean-notebooks = "jupyter nbconvert --ClearOutputPreprocessor.enabled=True --ClearMetadataPreprocessor.enabled=True --to=notebook --inplace --log-level=ERROR doc/source/examples/*.ipynb" [tool.hatch.envs.changelog] detached = true From 3e4cee6e367c5e71681430e76dcbe0d8823da7e5 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 1 Jul 2024 18:13:20 +0200 Subject: [PATCH 255/441] Add failing test --- .../time_reversal_pulse_template_tests.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/pulses/time_reversal_pulse_template_tests.py b/tests/pulses/time_reversal_pulse_template_tests.py index 0ded84236..ad012e5f8 100644 --- a/tests/pulses/time_reversal_pulse_template_tests.py +++ b/tests/pulses/time_reversal_pulse_template_tests.py @@ -1,5 +1,9 @@ import unittest +import numpy as np + +from qupulse.pulses import ConstantPT, FunctionPT +from qupulse.plotting import render from qupulse.pulses.time_reversal_pulse_template import TimeReversalPulseTemplate from qupulse.utils.types import TimeType from qupulse.expressions import ExpressionScalar @@ -25,6 +29,19 @@ def test_simple_properties(self): self.assertEqual(reversed_pt.identifier, 'reverse') + def test_time_reversal_program(self): + inner = ConstantPT(4, {'a': 3}) @ FunctionPT('sin(t)', 5, channel='a') + manual_reverse = FunctionPT('sin(5 - t)', 5, channel='a') @ ConstantPT(4, {'a': 3}) + time_reversed = TimeReversalPulseTemplate(inner) + + program = time_reversed.create_program() + manual_program = manual_reverse.create_program() + + t, data, _ = render(program, 9 / 10) + _, manual_data, _ = render(manual_program, 9 / 10) + + np.testing.assert_allclose(data['a'], manual_data['a']) + class TimeReversalPulseTemplateSerializationTests(unittest.TestCase, SerializableTests): @property From 2fd223e8c12fc90b9b3f60a9039dc42ac5192019 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 1 Jul 2024 18:24:56 +0200 Subject: [PATCH 256/441] Add time_reversed to LoopBuilder --- qupulse/program/__init__.py | 3 +++ qupulse/program/loop.py | 10 ++++++++++ qupulse/program/waveforms.py | 2 +- qupulse/pulses/time_reversal_pulse_template.py | 11 ++++------- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/qupulse/program/__init__.py b/qupulse/program/__init__.py index cd578dd5c..63dc4f60e 100644 --- a/qupulse/program/__init__.py +++ b/qupulse/program/__init__.py @@ -164,6 +164,9 @@ def with_iteration(self, index_name: str, rng: range, measurements: Optional[Sequence[MeasurementWindow]] = None) -> Iterable['ProgramBuilder']: pass + def time_reversed(self) -> ContextManager['ProgramBuilder']: + pass + def to_program(self) -> Optional[Program]: """Further addition of new elements might fail after finalizing the program.""" diff --git a/qupulse/program/loop.py b/qupulse/program/loop.py index 0e5dccc3e..9e59f9d32 100644 --- a/qupulse/program/loop.py +++ b/qupulse/program/loop.py @@ -817,6 +817,16 @@ def with_iteration(self, index_name: str, rng: range, top_frame.iterating = (index_name, value) yield self + @contextmanager + def time_reversed(self) -> ContextManager['LoopBuilder']: + inner_builder = LoopBuilder() + yield inner_builder + inner_program = inner_builder.to_program() + + if inner_program: + inner_program.reverse_inplace() + self._try_append(inner_program, None) + @contextmanager def with_sequence(self, measurements: Optional[Sequence[MeasurementWindow]] = None) -> ContextManager['ProgramBuilder']: top_frame = StackFrame(LoopGuard(self._top, measurements), None) diff --git a/qupulse/program/waveforms.py b/qupulse/program/waveforms.py index 08b544115..1e35bc1b1 100644 --- a/qupulse/program/waveforms.py +++ b/qupulse/program/waveforms.py @@ -1257,7 +1257,7 @@ def unsafe_sample(self, channel: ChannelID, sample_times: np.ndarray, else: inner_output_array = output_array[::-1] inner_output_array = self._inner.unsafe_sample(channel, inner_sample_times, output_array=inner_output_array) - if inner_output_array.base not in (output_array, output_array.base): + if id(inner_output_array.base) not in (id(output_array), id(output_array.base)): # TODO: is there a guarantee by numpy we never end up here? output_array[:] = inner_output_array[::-1] return output_array diff --git a/qupulse/pulses/time_reversal_pulse_template.py b/qupulse/pulses/time_reversal_pulse_template.py index 5dc9fcabd..47bec2322 100644 --- a/qupulse/pulses/time_reversal_pulse_template.py +++ b/qupulse/pulses/time_reversal_pulse_template.py @@ -5,7 +5,7 @@ from typing import Optional, Set, Dict, Union from qupulse import ChannelID -from qupulse.program.loop import Loop +from qupulse.program import ProgramBuilder from qupulse.program.waveforms import Waveform from qupulse.serialization import PulseRegistryType from qupulse.expressions import ExpressionScalar @@ -50,12 +50,9 @@ def defined_channels(self) -> Set['ChannelID']: def integral(self) -> Dict[ChannelID, ExpressionScalar]: return self._inner.integral - def _internal_create_program(self, *, parent_loop: Loop, **kwargs) -> None: - inner_loop = Loop() - self._inner._internal_create_program(parent_loop=inner_loop, **kwargs) - inner_loop.reverse_inplace() - - parent_loop.append_child(inner_loop) + def _internal_create_program(self, *, program_builder: ProgramBuilder, **kwargs) -> None: + with program_builder.time_reversed() as reversed_builder: + self._inner._internal_create_program(program_builder=reversed_builder, **kwargs) def build_waveform(self, *args, **kwargs) -> Optional[Waveform]: From 54a7921e1377c1e2e738ef34db25a67d6d153d34 Mon Sep 17 00:00:00 2001 From: Simon Humpohl Date: Mon, 1 Jul 2024 21:46:01 +0200 Subject: [PATCH 257/441] Clean execution state of nearly all notebooks --- .../examples/00AbstractPulseTemplate.ipynb | 24 +- .../examples/00AdvancedTablePulse.ipynb | 2387 +--------- .../00ArithmeticWithPulseTemplates.ipynb | 193 +- doc/source/examples/00ComposedPulses.ipynb | 3953 +--------------- .../examples/00ConstantPulseTemplate.ipynb | 22 +- doc/source/examples/00FunctionPulse.ipynb | 1602 +------ doc/source/examples/00MappingTemplate.ipynb | 818 +--- .../examples/00MultiChannelTemplates.ipynb | 2433 +--------- doc/source/examples/00PointPulse.ipynb | 803 +--- ...RetrospectiveConstantChannelAddition.ipynb | 808 +--- doc/source/examples/00SimpleTablePulse.ipynb | 2377 +--------- doc/source/examples/00TimeReversal.ipynb | 111 +- doc/source/examples/01Measurements.ipynb | 20 +- .../examples/01ParameterConstraints.ipynb | 811 +--- doc/source/examples/01PulseStorage.ipynb | 27 +- doc/source/examples/02CreatePrograms.ipynb | 58 +- .../03DynamicNuclearPolarisation.ipynb | 52 +- .../03FreeInductionDecayExample.ipynb | 1638 +------ .../examples/03GateConfigurationExample.ipynb | 3957 +---------------- doc/source/examples/03SnakeChargeScan.ipynb | 80 +- .../examples/04ZurichInstrumentsSetup.ipynb | 225 +- 21 files changed, 399 insertions(+), 22000 deletions(-) diff --git a/doc/source/examples/00AbstractPulseTemplate.ipynb b/doc/source/examples/00AbstractPulseTemplate.ipynb index 050e81f91..7c2899b9e 100644 --- a/doc/source/examples/00AbstractPulseTemplate.ipynb +++ b/doc/source/examples/00AbstractPulseTemplate.ipynb @@ -14,9 +14,7 @@ { "cell_type": "code", "execution_count": 1, - "metadata": { - "scrolled": true - }, + "metadata": {}, "outputs": [], "source": [ "from qupulse.pulses import AbstractPT, FunctionPT, AtomicMultiChannelPT, PointPT\n", @@ -46,7 +44,7 @@ "output_type": "stream", "text": [ "The integral has been declared so we can get it\n", - "{'Y': Expression('a*b + sin(t_manip)'), 'X': Expression('t_init - cos(t_manip) + 2')}\n", + "{'X': ExpressionScalar('t_init/2 - cos(t_manip) + 2'), 'Y': ExpressionScalar('a*b + t_init/2 + sin(t_manip)')}\n", "\n", "We get an error that for the pulse \"readout\" the property \"duration\" was not specified:\n", "NotSpecifiedError('readout', 'duration')\n" @@ -84,7 +82,7 @@ "text": [ "With wrong integral value:\n", "RuntimeError('Cannot link to target. Wrong value of property \"integral\"')\n", - "the linking worked. The new experiment has now a defined duration of Expression('t_init + t_manip + t_read') .\n" + "the linking worked. The new experiment has now a defined duration of ExpressionScalar('t_init + t_manip + t_read') .\n" ] } ], @@ -107,22 +105,8 @@ } ], "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.2" + "name": "python" } }, "nbformat": 4, diff --git a/doc/source/examples/00AdvancedTablePulse.ipynb b/doc/source/examples/00AdvancedTablePulse.ipynb index de87a5d57..1ea295c7b 100644 --- a/doc/source/examples/00AdvancedTablePulse.ipynb +++ b/doc/source/examples/00AdvancedTablePulse.ipynb @@ -18,797 +18,13 @@ { "cell_type": "code", "execution_count": 1, - "metadata": { - "scrolled": false - }, + "metadata": {}, "outputs": [ { "data": { - "application/javascript": [ - "/* Put everything inside the global mpl namespace */\n", - "window.mpl = {};\n", - "\n", - "\n", - "mpl.get_websocket_type = function() {\n", - " if (typeof(WebSocket) !== 'undefined') {\n", - " return WebSocket;\n", - " } else if (typeof(MozWebSocket) !== 'undefined') {\n", - " return MozWebSocket;\n", - " } else {\n", - " alert('Your browser does not have WebSocket support.' +\n", - " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", - " 'Firefox 4 and 5 are also supported but you ' +\n", - " 'have to enable WebSockets in about:config.');\n", - " };\n", - "}\n", - "\n", - "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n", - " this.id = figure_id;\n", - "\n", - " this.ws = websocket;\n", - "\n", - " this.supports_binary = (this.ws.binaryType != undefined);\n", - "\n", - " if (!this.supports_binary) {\n", - " var warnings = document.getElementById(\"mpl-warnings\");\n", - " if (warnings) {\n", - " warnings.style.display = 'block';\n", - " warnings.textContent = (\n", - " \"This browser does not support binary websocket messages. \" +\n", - " \"Performance may be slow.\");\n", - " }\n", - " }\n", - "\n", - " this.imageObj = new Image();\n", - "\n", - " this.context = undefined;\n", - " this.message = undefined;\n", - " this.canvas = undefined;\n", - " this.rubberband_canvas = undefined;\n", - " this.rubberband_context = undefined;\n", - " this.format_dropdown = undefined;\n", - "\n", - " this.image_mode = 'full';\n", - "\n", - " this.root = $('
');\n", - " this._root_extra_style(this.root)\n", - " this.root.attr('style', 'display: inline-block');\n", - "\n", - " $(parent_element).append(this.root);\n", - "\n", - " this._init_header(this);\n", - " this._init_canvas(this);\n", - " this._init_toolbar(this);\n", - "\n", - " var fig = this;\n", - "\n", - " this.waiting = false;\n", - "\n", - " this.ws.onopen = function () {\n", - " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n", - " fig.send_message(\"send_image_mode\", {});\n", - " if (mpl.ratio != 1) {\n", - " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n", - " }\n", - " fig.send_message(\"refresh\", {});\n", - " }\n", - "\n", - " this.imageObj.onload = function() {\n", - " if (fig.image_mode == 'full') {\n", - " // Full images could contain transparency (where diff images\n", - " // almost always do), so we need to clear the canvas so that\n", - " // there is no ghosting.\n", - " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", - " }\n", - " fig.context.drawImage(fig.imageObj, 0, 0);\n", - " };\n", - "\n", - " this.imageObj.onunload = function() {\n", - " fig.ws.close();\n", - " }\n", - "\n", - " this.ws.onmessage = this._make_on_message_function(this);\n", - "\n", - " this.ondownload = ondownload;\n", - "}\n", - "\n", - "mpl.figure.prototype._init_header = function() {\n", - " var titlebar = $(\n", - " '
');\n", - " var titletext = $(\n", - " '
');\n", - " titlebar.append(titletext)\n", - " this.root.append(titlebar);\n", - " this.header = titletext[0];\n", - "}\n", - "\n", - "\n", - "\n", - "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n", - "\n", - "}\n", - "\n", - "\n", - "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n", - "\n", - "}\n", - "\n", - "mpl.figure.prototype._init_canvas = function() {\n", - " var fig = this;\n", - "\n", - " var canvas_div = $('
');\n", - "\n", - " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n", - "\n", - " function canvas_keyboard_event(event) {\n", - " return fig.key_event(event, event['data']);\n", - " }\n", - "\n", - " canvas_div.keydown('key_press', canvas_keyboard_event);\n", - " canvas_div.keyup('key_release', canvas_keyboard_event);\n", - " this.canvas_div = canvas_div\n", - " this._canvas_extra_style(canvas_div)\n", - " this.root.append(canvas_div);\n", - "\n", - " var canvas = $('');\n", - " canvas.addClass('mpl-canvas');\n", - " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n", - "\n", - " this.canvas = canvas[0];\n", - " this.context = canvas[0].getContext(\"2d\");\n", - "\n", - " var backingStore = this.context.backingStorePixelRatio ||\n", - "\tthis.context.webkitBackingStorePixelRatio ||\n", - "\tthis.context.mozBackingStorePixelRatio ||\n", - "\tthis.context.msBackingStorePixelRatio ||\n", - "\tthis.context.oBackingStorePixelRatio ||\n", - "\tthis.context.backingStorePixelRatio || 1;\n", - "\n", - " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n", - "\n", - " var rubberband = $('');\n", - " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n", - "\n", - " var pass_mouse_events = true;\n", - "\n", - " canvas_div.resizable({\n", - " start: function(event, ui) {\n", - " pass_mouse_events = false;\n", - " },\n", - " resize: function(event, ui) {\n", - " fig.request_resize(ui.size.width, ui.size.height);\n", - " },\n", - " stop: function(event, ui) {\n", - " pass_mouse_events = true;\n", - " fig.request_resize(ui.size.width, ui.size.height);\n", - " },\n", - " });\n", - "\n", - " function mouse_event_fn(event) {\n", - " if (pass_mouse_events)\n", - " return fig.mouse_event(event, event['data']);\n", - " }\n", - "\n", - " rubberband.mousedown('button_press', mouse_event_fn);\n", - " rubberband.mouseup('button_release', mouse_event_fn);\n", - " // Throttle sequential mouse events to 1 every 20ms.\n", - " rubberband.mousemove('motion_notify', mouse_event_fn);\n", - "\n", - " rubberband.mouseenter('figure_enter', mouse_event_fn);\n", - " rubberband.mouseleave('figure_leave', mouse_event_fn);\n", - "\n", - " canvas_div.on(\"wheel\", function (event) {\n", - " event = event.originalEvent;\n", - " event['data'] = 'scroll'\n", - " if (event.deltaY < 0) {\n", - " event.step = 1;\n", - " } else {\n", - " event.step = -1;\n", - " }\n", - " mouse_event_fn(event);\n", - " });\n", - "\n", - " canvas_div.append(canvas);\n", - " canvas_div.append(rubberband);\n", - "\n", - " this.rubberband = rubberband;\n", - " this.rubberband_canvas = rubberband[0];\n", - " this.rubberband_context = rubberband[0].getContext(\"2d\");\n", - " this.rubberband_context.strokeStyle = \"#000000\";\n", - "\n", - " this._resize_canvas = function(width, height) {\n", - " // Keep the size of the canvas, canvas container, and rubber band\n", - " // canvas in synch.\n", - " canvas_div.css('width', width)\n", - " canvas_div.css('height', height)\n", - "\n", - " canvas.attr('width', width * mpl.ratio);\n", - " canvas.attr('height', height * mpl.ratio);\n", - " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n", - "\n", - " rubberband.attr('width', width);\n", - " rubberband.attr('height', height);\n", - " }\n", - "\n", - " // Set the figure to an initial 600x600px, this will subsequently be updated\n", - " // upon first draw.\n", - " this._resize_canvas(600, 600);\n", - "\n", - " // Disable right mouse context menu.\n", - " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n", - " return false;\n", - " });\n", - "\n", - " function set_focus () {\n", - " canvas.focus();\n", - " canvas_div.focus();\n", - " }\n", - "\n", - " window.setTimeout(set_focus, 100);\n", - "}\n", - "\n", - "mpl.figure.prototype._init_toolbar = function() {\n", - " var fig = this;\n", - "\n", - " var nav_element = $('
')\n", - " nav_element.attr('style', 'width: 100%');\n", - " this.root.append(nav_element);\n", - "\n", - " // Define a callback function for later on.\n", - " function toolbar_event(event) {\n", - " return fig.toolbar_button_onclick(event['data']);\n", - " }\n", - " function toolbar_mouse_event(event) {\n", - " return fig.toolbar_button_onmouseover(event['data']);\n", - " }\n", - "\n", - " for(var toolbar_ind in mpl.toolbar_items) {\n", - " var name = mpl.toolbar_items[toolbar_ind][0];\n", - " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", - " var image = mpl.toolbar_items[toolbar_ind][2];\n", - " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", - "\n", - " if (!name) {\n", - " // put a spacer in here.\n", - " continue;\n", - " }\n", - " var button = $('');\n", - " button.click(method_name, toolbar_event);\n", - " button.mouseover(tooltip, toolbar_mouse_event);\n", - " nav_element.append(button);\n", - " }\n", - "\n", - " // Add the status bar.\n", - " var status_bar = $('');\n", - " nav_element.append(status_bar);\n", - " this.message = status_bar[0];\n", - "\n", - " // Add the close button to the window.\n", - " var buttongrp = $('
');\n", - " var button = $('');\n", - " button.click(function (evt) { fig.handle_close(fig, {}); } );\n", - " button.mouseover('Stop Interaction', toolbar_mouse_event);\n", - " buttongrp.append(button);\n", - " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n", - " titlebar.prepend(buttongrp);\n", - "}\n", - "\n", - "mpl.figure.prototype._root_extra_style = function(el){\n", - " var fig = this\n", - " el.on(\"remove\", function(){\n", - "\tfig.close_ws(fig, {});\n", - " });\n", - "}\n", - "\n", - "mpl.figure.prototype._canvas_extra_style = function(el){\n", - " // this is important to make the div 'focusable\n", - " el.attr('tabindex', 0)\n", - " // reach out to IPython and tell the keyboard manager to turn it's self\n", - " // off when our div gets focus\n", - "\n", - " // location in version 3\n", - " if (IPython.notebook.keyboard_manager) {\n", - " IPython.notebook.keyboard_manager.register_events(el);\n", - " }\n", - " else {\n", - " // location in version 2\n", - " IPython.keyboard_manager.register_events(el);\n", - " }\n", - "\n", - "}\n", - "\n", - "mpl.figure.prototype._key_event_extra = function(event, name) {\n", - " var manager = IPython.notebook.keyboard_manager;\n", - " if (!manager)\n", - " manager = IPython.keyboard_manager;\n", - "\n", - " // Check for shift+enter\n", - " if (event.shiftKey && event.which == 13) {\n", - " this.canvas_div.blur();\n", - " event.shiftKey = false;\n", - " // Send a \"J\" for go to next cell\n", - " event.which = 74;\n", - " event.keyCode = 74;\n", - " manager.command_mode();\n", - " manager.handle_keydown(event);\n", - " }\n", - "}\n", - "\n", - "mpl.figure.prototype.handle_save = function(fig, msg) {\n", - " fig.ondownload(fig, null);\n", - "}\n", - "\n", - "\n", - "mpl.find_output_cell = function(html_output) {\n", - " // Return the cell and output element which can be found *uniquely* in the notebook.\n", - " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n", - " // IPython event is triggered only after the cells have been serialised, which for\n", - " // our purposes (turning an active figure into a static one), is too late.\n", - " var cells = IPython.notebook.get_cells();\n", - " var ncells = cells.length;\n", - " for (var i=0; i= 3 moved mimebundle to data attribute of output\n", - " data = data.data;\n", - " }\n", - " if (data['text/html'] == html_output) {\n", - " return [cell, data, j];\n", - " }\n", - " }\n", - " }\n", - " }\n", - "}\n", - "\n", - "// Register the function which deals with the matplotlib target/channel.\n", - "// The kernel may be null if the page has been refreshed.\n", - "if (IPython.notebook.kernel != null) {\n", - " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n", - "}\n" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "" - ], + "image/png": "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0KBBqlfP+XpFfn6+fagoNDRULVq00FNPPaUpU6a4Nd4L2QxXJo1bTH5+vmJjY5WXl6eYmBizwwEAuOinn37SgQMH1KpVK4WFhZkdDtzoYv+2rnx+M/QDAAAsi0QFAABYFokKAACwLJppAX9lGFJpYcXz4AjplzUcAMCXkKgA/sgwpLcGSkc2V2wnXyndvZJkBYDPYegH8EelheeSFEk6skkqOFGRwACADyFRAQLF/BTprUEkKwB8CokK4O8S0s89p7ICwMeQqAD+LmOlNHn/uW0qK4DHHTx4UDabTdu3bzc7FKf06dNHEyZMMDsMh0hUAH9ns0mR8RUNtZWorACohaKiIsXFxSk+Pl7FxcVeeU8SFSAQ2GwVs36orACog+XLl6tDhw5KS0vTBx984JX3JFEBAgWVFcCtysvLNW/ePKWkpCg0NFTNmzfXrFmzqhzz7bff6vrrr1dERIQ6d+6sjRs32l87efKkRowYoWbNmikiIkLp6elaunRpla/v06ePxo8fr0cffVRxcXFKSEjQE088UeUYm82mN954Q7feeqsiIiKUmpqqDz/8sMoxu3bt0uDBgxUVFaWmTZtq1KhROnHihMvf85tvvqmRI0dq5MiRLt0pui5IVIBAQmUFvsAwpJICcx4u/B5MnTpVf/zjHzV9+nTt3r1b7777rpo2bVrlmMcff1yTJ0/W9u3b1bZtW40YMUI///yzpIqb9nXr1k0fffSRdu3apfvvv1+jRo3SF198UeUcixcvVmRkpDZv3qx58+bpqaee0urVq6sc8+STT2rYsGHasWOHbrjhBt155506deqUJOn06dPq27evunTpoq1bt2rlypU6duyYhg0b5tI/S3Z2tjZu3Khhw4Zp2LBhWrdunQ4dOuTSOWqDuycD/qikQJqdVPH8saNSSGTV1w2jIjk5suncvsn7KyouLAoHL3J4h93zf369zdHviwNnzpxR48aNtXDhQt17773VXj948KBatWqlN954Q/fcc48kaffu3erQoYP27NmjtLQ0h+e96aablJaWpvnz50uqqKiUlZVp3bp19mN69uypvn376o9//KOkiorKtGnT9PTTT0uSCgoKFBUVpY8//liDBg3SM888o3Xr1mnVqlX2c3z33XdKTk7W3r171bZtW/Xp00dXXHGFFixYUOP3/Pjjj2v37t1asWKFJGno0KG64oor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", "text/plain": [ - "" + "
" ] }, "metadata": {}, @@ -1668,9 +99,7 @@ { "cell_type": "code", "execution_count": 3, - "metadata": { - "scrolled": false - }, + "metadata": {}, "outputs": [ { "name": "stdout", @@ -1681,791 +110,9 @@ }, { "data": { - "application/javascript": [ - "/* Put everything inside the global mpl namespace */\n", - "window.mpl = {};\n", - "\n", - "\n", - "mpl.get_websocket_type = function() {\n", - " if (typeof(WebSocket) !== 'undefined') {\n", - " return WebSocket;\n", - " } else if (typeof(MozWebSocket) !== 'undefined') {\n", - " return MozWebSocket;\n", - " } else {\n", - " alert('Your browser does not have WebSocket support.' +\n", - " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", - " 'Firefox 4 and 5 are also supported but you ' +\n", - " 'have to enable WebSockets in about:config.');\n", - " };\n", - "}\n", - "\n", - "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n", - " this.id = figure_id;\n", - "\n", - " this.ws = websocket;\n", - "\n", - " this.supports_binary = (this.ws.binaryType != undefined);\n", - "\n", - " if (!this.supports_binary) {\n", - " var warnings = document.getElementById(\"mpl-warnings\");\n", - " if (warnings) {\n", - " warnings.style.display = 'block';\n", - " warnings.textContent = (\n", - " \"This browser does not support binary websocket messages. \" +\n", - " \"Performance may be slow.\");\n", - " }\n", - " }\n", - "\n", - " this.imageObj = new Image();\n", - "\n", - " this.context = undefined;\n", - " this.message = undefined;\n", - " this.canvas = undefined;\n", - " this.rubberband_canvas = undefined;\n", - " this.rubberband_context = undefined;\n", - " this.format_dropdown = undefined;\n", - "\n", - " this.image_mode = 'full';\n", - "\n", - " this.root = $('
');\n", - " this._root_extra_style(this.root)\n", - " this.root.attr('style', 'display: inline-block');\n", - "\n", - " $(parent_element).append(this.root);\n", - "\n", - " this._init_header(this);\n", - " this._init_canvas(this);\n", - " this._init_toolbar(this);\n", - "\n", - " var fig = this;\n", - "\n", - " this.waiting = false;\n", - "\n", - " this.ws.onopen = function () {\n", - " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n", - " fig.send_message(\"send_image_mode\", {});\n", - " if (mpl.ratio != 1) {\n", - " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n", - " }\n", - " fig.send_message(\"refresh\", {});\n", - " }\n", - "\n", - " this.imageObj.onload = function() {\n", - " if (fig.image_mode == 'full') {\n", - " // Full images could contain transparency (where diff images\n", - " // almost always do), so we need to clear the canvas so that\n", - " // there is no ghosting.\n", - " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", - " }\n", - " fig.context.drawImage(fig.imageObj, 0, 0);\n", - " };\n", - "\n", - " this.imageObj.onunload = function() {\n", - " fig.ws.close();\n", - " }\n", - "\n", - " this.ws.onmessage = this._make_on_message_function(this);\n", - "\n", - " this.ondownload = ondownload;\n", - "}\n", - "\n", - "mpl.figure.prototype._init_header = function() {\n", - " var titlebar = $(\n", - " '
');\n", - " var titletext = $(\n", - " '
');\n", - " titlebar.append(titletext)\n", - " this.root.append(titlebar);\n", - " this.header = titletext[0];\n", - "}\n", - "\n", - "\n", - "\n", - "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n", - "\n", - "}\n", - "\n", - "\n", - "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n", - "\n", - "}\n", - "\n", - "mpl.figure.prototype._init_canvas = function() {\n", - " var fig = this;\n", - "\n", - " var canvas_div = $('
');\n", - "\n", - " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n", - "\n", - " function canvas_keyboard_event(event) {\n", - " return fig.key_event(event, event['data']);\n", - " }\n", - "\n", - " canvas_div.keydown('key_press', canvas_keyboard_event);\n", - " canvas_div.keyup('key_release', canvas_keyboard_event);\n", - " this.canvas_div = canvas_div\n", - " this._canvas_extra_style(canvas_div)\n", - " this.root.append(canvas_div);\n", - "\n", - " var canvas = $('');\n", - " canvas.addClass('mpl-canvas');\n", - " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n", - "\n", - " this.canvas = canvas[0];\n", - " this.context = canvas[0].getContext(\"2d\");\n", - "\n", - " var backingStore = this.context.backingStorePixelRatio ||\n", - "\tthis.context.webkitBackingStorePixelRatio ||\n", - "\tthis.context.mozBackingStorePixelRatio ||\n", - "\tthis.context.msBackingStorePixelRatio ||\n", - "\tthis.context.oBackingStorePixelRatio ||\n", - "\tthis.context.backingStorePixelRatio || 1;\n", - "\n", - " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n", - "\n", - " var rubberband = $('');\n", - " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n", - "\n", - " var pass_mouse_events = true;\n", - "\n", - " canvas_div.resizable({\n", - " start: function(event, ui) {\n", - " pass_mouse_events = false;\n", - " },\n", - " resize: function(event, ui) {\n", - " fig.request_resize(ui.size.width, ui.size.height);\n", - " },\n", - " stop: function(event, ui) {\n", - " pass_mouse_events = true;\n", - " fig.request_resize(ui.size.width, ui.size.height);\n", - " },\n", - " });\n", - "\n", - " function mouse_event_fn(event) {\n", - " if (pass_mouse_events)\n", - " return fig.mouse_event(event, event['data']);\n", - " }\n", - "\n", - " rubberband.mousedown('button_press', mouse_event_fn);\n", - " rubberband.mouseup('button_release', mouse_event_fn);\n", - " // Throttle sequential mouse events to 1 every 20ms.\n", - " rubberband.mousemove('motion_notify', mouse_event_fn);\n", - "\n", - " rubberband.mouseenter('figure_enter', mouse_event_fn);\n", - " rubberband.mouseleave('figure_leave', mouse_event_fn);\n", - "\n", - " canvas_div.on(\"wheel\", function (event) {\n", - " event = event.originalEvent;\n", - " event['data'] = 'scroll'\n", - " if (event.deltaY < 0) {\n", - " event.step = 1;\n", - " } else {\n", - " event.step = -1;\n", - " }\n", - " mouse_event_fn(event);\n", - " });\n", - "\n", - " canvas_div.append(canvas);\n", - " canvas_div.append(rubberband);\n", - "\n", - " this.rubberband = rubberband;\n", - " this.rubberband_canvas = rubberband[0];\n", - " this.rubberband_context = rubberband[0].getContext(\"2d\");\n", - " this.rubberband_context.strokeStyle = \"#000000\";\n", - "\n", - " this._resize_canvas = function(width, height) {\n", - " // Keep the size of the canvas, canvas container, and rubber band\n", - " // canvas in synch.\n", - " canvas_div.css('width', width)\n", - " canvas_div.css('height', height)\n", - "\n", - " canvas.attr('width', width * mpl.ratio);\n", - " canvas.attr('height', height * mpl.ratio);\n", - " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n", - "\n", - " rubberband.attr('width', width);\n", - " rubberband.attr('height', height);\n", - " }\n", - "\n", - " // Set the figure to an initial 600x600px, this will subsequently be updated\n", - " // upon first draw.\n", - " this._resize_canvas(600, 600);\n", - "\n", - " // Disable right mouse context menu.\n", - " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n", - " return false;\n", - " });\n", - "\n", - " function set_focus () {\n", - " canvas.focus();\n", - " canvas_div.focus();\n", - " }\n", - "\n", - " window.setTimeout(set_focus, 100);\n", - "}\n", - "\n", - "mpl.figure.prototype._init_toolbar = function() {\n", - " var fig = this;\n", - "\n", - " var nav_element = $('
')\n", - " nav_element.attr('style', 'width: 100%');\n", - " this.root.append(nav_element);\n", - "\n", - " // Define a callback function for later on.\n", - " function toolbar_event(event) {\n", - " return fig.toolbar_button_onclick(event['data']);\n", - " }\n", - " function toolbar_mouse_event(event) {\n", - " return fig.toolbar_button_onmouseover(event['data']);\n", - " }\n", - "\n", - " for(var toolbar_ind in mpl.toolbar_items) {\n", - " var name = mpl.toolbar_items[toolbar_ind][0];\n", - " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", - " var image = mpl.toolbar_items[toolbar_ind][2];\n", - " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", - "\n", - " if (!name) {\n", - " // put a spacer in here.\n", - " continue;\n", - " }\n", - " var button = $('');\n", - " button.click(method_name, toolbar_event);\n", - " button.mouseover(tooltip, toolbar_mouse_event);\n", - " nav_element.append(button);\n", - " }\n", - "\n", - " // Add the status bar.\n", - " var status_bar = $('');\n", - " nav_element.append(status_bar);\n", - " this.message = status_bar[0];\n", - "\n", - " // Add the close button to the window.\n", - " var buttongrp = $('
');\n", - " var button = $('');\n", - " button.click(function (evt) { fig.handle_close(fig, {}); } );\n", - " button.mouseover('Stop Interaction', toolbar_mouse_event);\n", - " buttongrp.append(button);\n", - " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n", - " titlebar.prepend(buttongrp);\n", - "}\n", - "\n", - "mpl.figure.prototype._root_extra_style = function(el){\n", - " var fig = this\n", - " el.on(\"remove\", function(){\n", - "\tfig.close_ws(fig, {});\n", - " });\n", - "}\n", - "\n", - "mpl.figure.prototype._canvas_extra_style = function(el){\n", - " // this is important to make the div 'focusable\n", - " el.attr('tabindex', 0)\n", - " // reach out to IPython and tell the keyboard manager to turn it's self\n", - " // off when our div gets focus\n", - "\n", - " // location in version 3\n", - " if (IPython.notebook.keyboard_manager) {\n", - " IPython.notebook.keyboard_manager.register_events(el);\n", - " }\n", - " else {\n", - " // location in version 2\n", - " IPython.keyboard_manager.register_events(el);\n", - " }\n", - "\n", - "}\n", - "\n", - "mpl.figure.prototype._key_event_extra = function(event, name) {\n", - " var manager = IPython.notebook.keyboard_manager;\n", - " if (!manager)\n", - " manager = IPython.keyboard_manager;\n", - "\n", - " // Check for shift+enter\n", - " if (event.shiftKey && event.which == 13) {\n", - " this.canvas_div.blur();\n", - " event.shiftKey = false;\n", - " // Send a \"J\" for go to next cell\n", - " event.which = 74;\n", - " event.keyCode = 74;\n", - " manager.command_mode();\n", - " manager.handle_keydown(event);\n", - " }\n", - "}\n", - "\n", - "mpl.figure.prototype.handle_save = function(fig, msg) {\n", - " fig.ondownload(fig, null);\n", - "}\n", - "\n", - "\n", - "mpl.find_output_cell = function(html_output) {\n", - " // Return the cell and output element which can be found *uniquely* in the notebook.\n", - " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n", - " // IPython event is triggered only after the cells have been serialised, which for\n", - " // our purposes (turning an active figure into a static one), is too late.\n", - " var cells = IPython.notebook.get_cells();\n", - " var ncells = cells.length;\n", - " for (var i=0; i= 3 moved mimebundle to data attribute of output\n", - " data = data.data;\n", - " }\n", - " if (data['text/html'] == html_output) {\n", - " return [cell, data, j];\n", - " }\n", - " }\n", - " }\n", - " }\n", - "}\n", - "\n", - "// Register the function which deals with the matplotlib target/channel.\n", - "// The kernel may be null if the page has been refreshed.\n", - "if (IPython.notebook.kernel != null) {\n", - " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n", - "}\n" - ], + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkIAAAGwCAYAAABFFQqPAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAAA3fUlEQVR4nO3dfVxUZf7/8feAOIDAKCkCird4f0OY2qKWN5mmLq27rbqWgqlttmip3RjV2tpusrqaXys3t9LMXVtNTWu7M/IG0kXTkk2zNBXFXNDUBAEXFc7vjx7MLxJwBmcYhvN6Ph7zeDBnrnPmc3EaeXeu6zpjMQzDEAAAgAn5eLoAAAAATyEIAQAA0yIIAQAA0yIIAQAA0yIIAQAA0yIIAQAA0yIIAQAA06rn6QJqWmlpqf773/8qODhYFovF0+UAAAAHGIahCxcuKDIyUj4+rruOY7og9N///ldRUVGeLgMAAFTDiRMn1Lx5c5cdz3RBKDg4WNIPv8iQkBAPVwMAAByRn5+vqKgo+99xVzFdECobDgsJCSEIAQDgZVw9rYXJ0gAAwLQIQgAAwLQIQgAAwLRMN0fIUSUlJbp8+bKny4AL+Pn5ydfX19NlAABqIYLQTxiGodzcXJ0/f97TpcCFGjZsqPDwcO4dBQAohyD0E2UhKCwsTIGBgfzh9HKGYaioqEinT5+WJEVERHi4IgBAbUIQ+pGSkhJ7CLrhhhs8XQ5cJCAgQJJ0+vRphYWFMUwGALBjsvSPlM0JCgwM9HAlcLWyc8q8LwDAjxGEKsBwWN3DOQUAVIQgBAAATIsgBAAATIsgZALHjh2TxWJRZmamp0txyIABAzR9+nRPlwEAMAGCELzWxYsXFRoaqsaNG6u4uNjT5QAAvBBBCF5r/fr16tKlizp27KiNGzd6uhwAgBciCF2DYRgqunTFIw/DMByus7S0VPPnz1d0dLSsVqtatGihZ599tlybo0ePauDAgQoMDFRMTIwyMjLsr509e1Zjx45Vs2bNFBgYqG7duumf//xnuf0HDBigBx98UI899phCQ0MVHh6uP/zhD+XaWCwWvfrqq/rlL3+pwMBAtWvXTu+88065Nvv379ewYcMUFBSkpk2bavz48Tpz5ozDfS2zbNkyjRs3TuPGjdOyZcuc3h8AAG6oeA0XL5eo8+xNHnnvA88MVWB9x05RcnKyXnnlFS1atEj9+vVTTk6Ovv7663JtnnzySS1YsEDt2rXTk08+qbFjx+rw4cOqV6+e/ve//+mmm27SrFmzFBISovfee0/jx49X27Zt1bt3b/sxXn/9dc2cOVO7du1SRkaGJkyYoL59++r222+3t5kzZ47mz5+vv/zlL3rhhRd0zz336Pjx4woNDdX58+c1aNAgTZ48WYsWLdLFixc1a9YsjR49Wlu2bHH4d3PkyBFlZGTorbfekmEYmjFjho4fP66WLVs6fAwAALgiVAdcuHBBixcv1vz585WYmKi2bduqX79+mjx5crl2jzzyiEaMGKH27dtrzpw5On78uA4fPixJatasmR555BHdeOONatOmjaZNm6Y77rhDb775ZrljdO/eXU8//bTatWunhIQE9ezZU5s3by7XZsKECRo7dqyio6M1d+5cFRQU6NNPP5Ukvfjii4qNjdXcuXPVsWNHxcbGavny5dq6dasOHTrkcJ+XL1+uYcOGqVGjRgoNDdXQoUP12muvVefXBwAwMa4IXUOAn68OPDPUY+/tiK+++krFxcW67bbbqmzXvXt3+89l37l1+vRpdezYUSUlJZo7d67efPNNnTx5UpcuXVJxcfFVd9n+8THKjlP2PV4VtWnQoIFCQkLsbf7zn/9o69atCgoKuqq+I0eOqH379tfsb0lJiV5//XUtXrzYvm3cuHF65JFHNHv2bPn4kO8BAI4hCF2DxWJxeHjKU8q+S+ta/Pz87D+X3Wm5tLRUkvSXv/xFixcv1v/93/+pW7duatCggaZPn65Lly5Veoyy45Qdw5E2BQUFio+P17x5866qz9EvRN20aZNOnjypMWPGlNteUlKizZs3lxumAwCgKrX7Lzwc0q5dOwUEBGjz5s1XDYc5aseOHfrFL36hcePGSfohIB06dEidO3d2Zanq0aOH1q9fr1atWqlever957ds2TL95je/0ZNPPllu+7PPPqtly5YRhAAADmMMoQ7w9/fXrFmz9Nhjj2nlypU6cuSIdu7c6dRKqnbt2ik1NVX//ve/9dVXX+n+++/XqVOnXF5rUlKSzp07p7Fjx2r37t06cuSINm3apHvvvVclJSXX3P+7777Tv/71LyUmJqpr167lHgkJCdq4caPOnTvn8roBAHUTQaiO+P3vf6+HH35Ys2fPVqdOnTRmzJir5u5U5amnnlKPHj00dOhQDRgwQOHh4Ro5cqTL64yMjNSOHTtUUlKiIUOGqFu3bpo+fboaNmzo0NyelStXqkGDBhXOh7rtttsUEBCgf/zjHy6vGwBQN1kMZ25WUwfk5+fLZrMpLy9PISEh5V773//+p6ysLLVu3Vr+/v4eqhDuwLkFAO9W1d/v68EVIQAAYFoeDUIpKSnq1auXgoODFRYWppEjR+rgwYMO77969WpZLBa3DOEAAIC6z6NBKC0tTUlJSdq5c6dSU1N1+fJlDRkyRIWFhdfc99ixY3rkkUd0yy231EClAACgLvLo8vkPP/yw3PMVK1YoLCxMn332mW699dZK9yspKdE999yjOXPm6JNPPtH58+ddWpfJpk2ZgtnOqWEYunj52qvwAHivAD9f+z3hUH216j5CeXl5kqTQ0NAq2z3zzDMKCwvTpEmT9Mknn1TZtri4WMXFxfbn+fn5lbYtuxFgUVGRwzcphHcoKiqSdPXNHusiwzD066UZ+uz4954uBYAbOfN9lKhcrfkNlpaWavr06erbt6+6du1aabvt27dr2bJlyszMdOi4KSkpmjNnjkNtfX191bBhQ/uy88DAQNK2lzMMQ0VFRTp9+rQaNmwoX1/HvrbEm128XEIIAgAH1ZoglJSUpP3792v79u2Vtrlw4YLGjx+vV155RY0bN3bouMnJyZo5c6b9eX5+vqKioiptHx4eLklO3YMHtV/Dhg3t59ZM9jw1WIH16374A8zI0e+jRNVqRRCaOnWq3n33XaWnp6t58+aVtjty5IiOHTum+Ph4+7ay77CqV6+eDh48qLZt25bbx2q1ymq1OlyLxWJRRESEwsLCdPnyZSd7gtrIz8/PFFeCKhJY35dL5wBQBY/+C2kYhqZNm6YNGzZo27Ztat26dZXtO3bsqH379pXb9tRTT+nChQtavHhxlVd6nOXr62vaP54AAJiFR4NQUlKS3njjDb399tsKDg5Wbm6uJMlms9knKyckJKhZs2ZKSUmRv7//VfOHGjZsKElVzisCAACoiEeD0EsvvSRJGjBgQLntr732miZMmCBJys7Odug7qAAAAJzl8aGxa9m2bVuVr69YscI1xQAAANPhUgsAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtghAAADAtjwahlJQU9erVS8HBwQoLC9PIkSN18ODBKvd55ZVXdMstt6hRo0Zq1KiRBg8erE8//bSGKgYAAHWJR4NQWlqakpKStHPnTqWmpury5csaMmSICgsLK91n27ZtGjt2rLZu3aqMjAxFRUVpyJAhOnnyZA1WDgAA6gKLYRiGp4so89133yksLExpaWm69dZbHdqnpKREjRo10osvvqiEhIRrts/Pz5fNZlNeXp5CQkKut2Sg1im6dEWdZ2+SJB14ZqgC69fzcEUAcP3c9fe7Vv0LmZeXJ0kKDQ11eJ+ioiJdvny50n2Ki4tVXFxsf56fn399RQIAgDqj1kyWLi0t1fTp09W3b1917drV4f1mzZqlyMhIDR48uMLXU1JSZLPZ7I+oqChXlQwAALxcrQlCSUlJ2r9/v1avXu3wPn/+85+1evVqbdiwQf7+/hW2SU5OVl5env1x4sQJV5UMAAC8XK0YGps6dareffddpaenq3nz5g7ts2DBAv35z3/Wxx9/rO7du1fazmq1ymq1uqpUAABQh3g0CBmGoWnTpmnDhg3atm2bWrdu7dB+8+fP17PPPqtNmzapZ8+ebq4SAADUVR4NQklJSXrjjTf09ttvKzg4WLm5uZIkm82mgIAASVJCQoKaNWumlJQUSdK8efM0e/ZsvfHGG2rVqpV9n6CgIAUFBXmmIwAAwCt5dI7QSy+9pLy8PA0YMEARERH2x5o1a+xtsrOzlZOTU26fS5cu6de//nW5fRYsWOCJLgAAAC/m8aGxa9m2bVu558eOHXNPMQAAwHRqzaoxAACAmkYQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQ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ALiB00Fo2bJlCg0N1U033SSr1Sqr1aqePXsqNDRUy5YtkyQFBQVp4cKFLi8WAADAlZyeIxQeHq7U1FR9/fXXOnTokCSpQ4cO6tChg73NwIEDXVchAACAm1T7hoodO3ZUx44dXVkLAABAjapWEPr222/1zjvvKDs7W5cuXSr32nPPPeeSwgAAANzN6SC0efNm3XnnnWrTpo2+/vprde3aVceOHZNhGOrRo4c7agQAAHALpydLJycn65FHHtG+ffvk7++v9evX68SJE+rfv79GjRrljhoBAADcwukg9NVXXykhIUGSVK9ePV28eFFBQUF65plnNG/ePKeOlZ6ervj4eEVGRspisWjjxo3X3Ke4uFhPPvmkWrZsKavVqlatWmn58uXOdgMAAMD5obEGDRrY5wVFREToyJEj6tKliyTpzJkzTh2rsLBQMTExmjhxon71q185tM/o0aN16tQpLVu2TNHR0crJyVFpaalznQAAAFA1gtDPfvYzbd++XZ06ddLw4cP18MMPa9++fXrrrbf0s5/9zKljDRs2TMOGDXO4/Ycffqi0tDQdPXpUoaGhkqRWrVo59Z4AAABlnB4ae+6553TzzTdLkubMmaPbbrtNa9asUatWrew3VHSXd955Rz179tT8+fPVrFkztW/fXo888oguXrxY6T7FxcXKz88v9wAAAJCqcUWoTZs29p8bNGigpUuXurSgqhw9elTbt2+Xv7+/NmzYoDNnzuh3v/udzp49q9dee63CfVJSUjRnzpwaqxEAAHgPp68ItWnTRmfPnr1q+/nz58uFJHcoLS2VxWLRqlWr1Lt3bw0fPlzPPfecXn/99UqvCiUnJysvL8/+OHHihFtrBAAA3sPpK0LHjh1TSUnJVduLi4t18uRJlxRVmYiICDVr1kw2m82+rVOnTjIMQ99++63atWt31T5l34cGAADwUw4HoXfeecf+86ZNm8qFkZKSEm3evNntE5f79u2rtWvXqqCgQEFBQZKkQ4cOycfHR82bN3frewMAgLrH4SA0cuRISZLFYlFiYmK51/z8/NSqVSunv3G+oKBAhw8ftj/PyspSZmamQkND1aJFCyUnJ+vkyZNauXKlJOnuu+/WH//4R917772aM2eOzpw5o0cffVQTJ05UQECAU+8NAADgcBAqu1dP69attXv3bjVu3Pi633zPnj3lvql+5syZkqTExEStWLFCOTk5ys7Otr8eFBSk1NRUTZs2TT179tQNN9yg0aNH609/+tN11wIAAMzHYhiG4ekialJ+fr5sNpvy8vIUEhLi6XIAlyu6dEWdZ2+SJB14ZqgC61fru5UBoFZx199vh/6FfP755x0+4IMPPljtYgAAAGqSQ0Fo0aJFDh3MYrEQhAAAgNdwKAhlZWW5uw4AAIAa5/QNFX/MMAyZbIoRAACoQ6oVhFauXKlu3bopICBAAQEB6t69u/7+97+7ujYAAAC3cno5yXPPPaff//73mjp1qvr27StJ2r59u6ZMmaIzZ85oxowZLi8SAADAHZwOQi+88IJeeuklJSQk2Lfdeeed6tKli/7whz8QhAAAgNdwemgsJydHffr0uWp7nz59lJOT45KiAAAAaoLTQSg6OlpvvvnmVdvXrFlT4ZeeAgAA1FZOD43NmTNHY8aMUXp6un2O0I4dO7R58+YKAxIAAEBt5fAVof3790uS7rrrLu3atUuNGzfWxo0btXHjRjVu3FiffvqpfvnLX7qtUAAAAFdz+IpQ9+7d1atXL02ePFm/+c1v9I9//MOddQEAALidw1eE0tLS1KVLFz388MOKiIjQhAkT9Mknn7izNgAAALdyOAjdcsstWr58uXJycvTCCy8oKytL/fv3V/v27TVv3jzl5ua6s04AAACXc3rVWIMGDXTvvfcqLS1Nhw4d0qhRo7RkyRK1aNFCd955pztqBAAAcIvr+q6x6OhoPfHEE3rqqacUHBys9957z1V1AQAAuJ3Ty+fLpKena/ny5Vq/fr18fHw0evRoTZo0yZW1AQAAuJVTQei///2vVqxYoRUrVujw4cPq06ePnn/+eY0ePVoNGjRwV40AAABu4XAQGjZsmD7++GM1btxYCQkJmjhxojp06ODO2gAAANzK4SDk5+endevW6ec//7l8fX3dWRMAAECNcDgIvfPOO+6sAwAAoMZd16oxAAAAb0YQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApkUQAgAApuXRIJSenq74+HhFRkbKYrFo48aNDu+7Y8cO1atXTzfeeKPb6gMAAHWbR4NQYWGhYmJitGTJEqf2O3/+vBISEnTbbbe5qTIAAGAG9Tz55sOGDdOwYcOc3m/KlCm6++675evr69RVJAAAgB/zujlCr732mo4ePaqnn37aofbFxcXKz88v9wDqMsPwdAUA4D28Kgh98803evzxx/WPf/xD9eo5djErJSVFNpvN/oiKinJzlYDnlJYa+vkL2z1dBgB4Da8JQiUlJbr77rs1Z84ctW/f3uH9kpOTlZeXZ3+cOHHCjVUCNc8wDBVduqLC4iu67bk0ZZ0plCR1jghRgJ+vh6sDgNrNo3OEnHHhwgXt2bNHe/fu1dSpUyVJpaWlMgxD9erV00cffaRBgwZdtZ/VapXVaq3pcoEaUXYF6EBO+SHf1o0b6N1p/WSxWDxUGQB4B68JQiEhIdq3b1+5bX/961+1ZcsWrVu3Tq1bt/ZQZYBnlJYa5a4AlekcEaJ3p/WTjw8hCACuxaNBqKCgQIcPH7Y/z8rKUmZmpkJDQ9WiRQslJyfr5MmTWrlypXx8fNS1a9dy+4eFhcnf3/+q7UBd99MQ9P+vAEkBfr5cCQIAB3k0CO3Zs0cDBw60P585c6YkKTExUStWrFBOTo6ys7M9VR5Q6/wwH6hEP39he7kQtHlmf64AAUA1WAzDXItt8/PzZbPZlJeXp5CQEE+XAzisovlAhCAAZuGuv99eM0cIMLOK5gMxFwgArh9BCKjFKhsKe3daPwXWZy4QAFwvghBQC5UFoFFLMxgKAwA3IggBtUxl9wZiKAwAXI8gBNQilc0FWjsljqEwAHADghBQS1R2byACEAC4D0EI8DDuDQQAnkMQAjyIewMBgGcRhAAP4d5AAOB5BCGgBhmGoYuXS2QY4t5AAFALEISAGlLZsniGwgDAcwhCQA2oaBhMYigMADyNIAS4WWXL4i0WKcCPoTAA8CSCEOAmLIsHgNqPIAS4AcviAcA7EIQAF2NZPAB4D4IQ4AIsiwcA70QQAq4Ty+IBwHsRhIDrwLJ4APBuBCGgmlgWDwDejyAEOIll8QBQdxCEACewLB4A6haCEOAglsUDQN1DEAKuobKhMJbFA4D3IwgBlSgLQKOWZjAUBgB1FEEIqEBl9wZiKAwA6haCEPATlc0FWjsljqEwAKhjCELAj1R2byACEADUTQQhQNwbCADMiiAE0+PeQABgXgQhmBr3BgIAcyMIwXQMw9DFyyUyDHFvIAAwOYIQTKWyZfEMhQGAOfl48s3T09MVHx+vyMhIWSwWbdy4scr2b731lm6//XY1adJEISEhiouL06ZNm2qmWHi9smGwiu4NRAgCAHPyaBAqLCxUTEyMlixZ4lD79PR03X777Xr//ff12WefaeDAgYqPj9fevXvdXCm8XUXL4r+cM1QHnhmq9x5kPhAAmJXFMAzD00VIksVi0YYNGzRy5Ein9uvSpYvGjBmj2bNnO9Q+Pz9fNptNeXl5CgkJqUal8CYsiweAusFdf7+9eo5QaWmpLly4oNDQ0ErbFBcXq7i42P48Pz+/0raoW1gWDwC4Fo8OjV2vBQsWqKCgQKNHj660TUpKimw2m/0RFRVVgxXCUyqaD8RcIADAT3ntFaE33nhDc+bM0dtvv62wsLBK2yUnJ2vmzJn25/n5+YShOopl8QAAZ3llEFq9erUmT56stWvXavDgwVW2tVqtslqtNVQZPIVl8QCA6vC6IPTPf/5TEydO1OrVqzVixAhPl4NaoKK7Q0vcIRoAcG0eDUIFBQU6fPiw/XlWVpYyMzMVGhqqFi1aKDk5WSdPntTKlSsl/TAclpiYqMWLF+vmm29Wbm6uJCkgIEA2m80jfYBnVfZt8RaLFODHUBgAoGoenSy9Z88excbGKjY2VpI0c+ZMxcbG2pfC5+TkKDs7297+5Zdf1pUrV5SUlKSIiAj746GHHvJI/fAcwzBUWHzlqhC0eWZ/NbDWU2D9eoQgAMA11Zr7CNUU7iPk/VgWDwDmw32EAPFt8QAA1yIIwStUdodolsUDAK4HQQi1WlkAGrU0g6EwAIDLEYRQa1V2byCGwgAArkIQQq1U2VygtVPiGAoDALgMQQi1TmX3BiIAAQBcjSCEWqOyCdHMBQIAuAtBCLUC9wYCAHgCQQgex72BAACeQhCCRxiGoYuXS2QY4t5AAACPIQihxlW2LJ6hMABATSMIoUZVNAwmMRQGAPAMghBqTGXL4i0WKcCPoTAAQM0jCMHtWBYPAKitCEJwK5bFAwBqM4IQ3IZl8QCA2o4gBJdiWTwAwJsQhOAyLIsHAHgbghBcgmXxAABvRBDCdWNZPADAWxGEUG0siwcAeDuCEKqFZfEAgLqAIASnsSweAFBXEITgsMqGwlgWDwDwVgQhXFNZABq1NIOhMABAnUIQQpUquzcQQ2EAgLqAIIRKVTYXaO2UOIbCAAB1AkEIFars3kAEIABAXUIQQjncGwgAYCYEIdhxbyAAgNkQhCCJewMBAMyJIGRihmHo4uUSGYa4NxAAwJQIQiZV2bJ4hsIAAGbi48k3T09PV3x8vCIjI2WxWLRx48Zr7rNt2zb16NFDVqtV0dHRWrFihdvrrGvKhsEqujcQIQgAYCYevSJUWFiomJgYTZw4Ub/61a+u2T4rK0sjRozQlClTtGrVKm3evFmTJ09WRESEhg4dWgMVe7/KlsVbLFKAH0NhAABz8WgQGjZsmIYNG+Zw+6VLl6p169ZauHChJKlTp07avn27Fi1aRBCqQlVzgbgCBAAwM6+aI5SRkaHBgweX2zZ06FBNnz690n2Ki4tVXFxsf56fn19p27rq4uUSdZ69qdw2QhAAAB6eI+Ss3NxcNW3atNy2pk2bKj8/XxcvXqxwn5SUFNlsNvsjKiqqJkqt1ZgLBADAD7zqilB1JCcna+bMmfbn+fn5pgtDAX6+OvDM0HLPmQsEAICXBaHw8HCdOnWq3LZTp04pJCREAQEBFe5jtVpltVprorxay2KxKLC+V51qAABqhFcNjcXFxWnz5s3ltqWmpiouLs5DFQEAAG/m0SBUUFCgzMxMZWZmSvpheXxmZqays7Ml/TCslZCQYG8/ZcoUHT16VI899pi+/vpr/fWvf9Wbb76pGTNmeKJ8AADg5TwahPbs2aPY2FjFxsZKkmbOnKnY2FjNnj1bkpSTk2MPRZLUunVrvffee0pNTVVMTIwWLlyoV199laXzAACgWiyGYRieLqIm5efny2azKS8vTyEhIZ4uBwAAOMBdf7+9ao4QAACAKxGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAaRGEAACAadXzdAE1zTAMSVJ+fr6HKwEAAI4q+7td9nfcVUwXhM6ePStJioqK8nAlAADAWWfPnpXNZnPZ8UwXhEJDQyVJ2dnZLv1F1nb5+fmKiorSiRMnFBIS4ulyagz9pt9mQL/ptxnk5eWpRYsW9r/jrmK6IOTj88O0KJvNZqr/gMqEhITQbxOh3+ZCv83FrP0u+zvusuO59GgAAABehCAEAABMy3RByGq16umnn5bVavV0KTWKftNvM6Df9NsM6Ldr+20xXL0ODQAAwEuY7ooQAABAGYIQAAAwLYIQAAAwLYIQAAAwLVMEoXPnzumee+5RSEiIGjZsqEmTJqmgoKDKfQYMGCCLxVLuMWXKlBqquHqWLFmiVq1ayd/fXzfffLM+/fTTKtuvXbtWHTt2lL+/v7p166b333+/hip1LWf6vWLFiqvOq7+/fw1W6xrp6emKj49XZGSkLBaLNm7ceM19tm3bph49eshqtSo6OlorVqxwe52u5my/t23bdtX5tlgsys3NrZmCXSAlJUW9evVScHCwwsLCNHLkSB08ePCa+3n757s6/a4rn++XXnpJ3bt3t98wMS4uTh988EGV+3j7+Zac77erzrcpgtA999yjL7/8UqmpqXr33XeVnp6u3/72t9fc77777lNOTo79MX/+/BqotnrWrFmjmTNn6umnn9bnn3+umJgYDR06VKdPn66w/b///W+NHTtWkyZN0t69ezVy5EiNHDlS+/fvr+HKr4+z/ZZ+uBvrj8/r8ePHa7Bi1ygsLFRMTIyWLFniUPusrCyNGDFCAwcOVGZmpqZPn67Jkydr06ZNbq7UtZztd5mDBw+WO+dhYWFuqtD10tLSlJSUpJ07dyo1NVWXL1/WkCFDVFhYWOk+deHzXZ1+S3Xj8928eXP9+c9/1meffaY9e/Zo0KBB+sUvfqEvv/yywvZ14XxLzvdbctH5Nuq4AwcOGJKM3bt327d98MEHhsViMU6ePFnpfv379zceeuihGqjQNXr37m0kJSXZn5eUlBiRkZFGSkpKhe1Hjx5tjBgxoty2m2++2bj//vvdWqerOdvv1157zbDZbDVUXc2QZGzYsKHKNo899pjRpUuXctvGjBljDB061I2VuZcj/d66dashyfj+++9rpKaacPr0aUOSkZaWVmmbuvL5/jFH+l0XP99lGjVqZLz66qsVvlYXz3eZqvrtqvNd568IZWRkqGHDhurZs6d92+DBg+Xj46Ndu3ZVue+qVavUuHFjde3aVcnJySoqKnJ3udVy6dIlffbZZxo8eLB9m4+PjwYPHqyMjIwK98nIyCjXXpKGDh1aafvaqDr9lqSCggK1bNlSUVFR1/y/jbqiLpzv63HjjTcqIiJCt99+u3bs2OHpcq5LXl6eJFX5xZN18Xw70m+p7n2+S0pKtHr1ahUWFiouLq7CNnXxfDvSb8k157vOf+lqbm7uVZfB69Wrp9DQ0CrnCdx9991q2bKlIiMj9cUXX2jWrFk6ePCg3nrrLXeX7LQzZ86opKRETZs2Lbe9adOm+vrrryvcJzc3t8L23jR3ojr97tChg5YvX67u3bsrLy9PCxYsUJ8+ffTll1+qefPmNVG2R1R2vvPz83Xx4kUFBAR4qDL3ioiI0NKlS9WzZ08VFxfr1Vdf1YABA7Rr1y716NHD0+U5rbS0VNOnT1ffvn3VtWvXStvVhc/3jzna77r0+d63b5/i4uL0v//9T0FBQdqwYYM6d+5cYdu6dL6d6berzrfXBqHHH39c8+bNq7LNV199Ve3j/3gOUbdu3RQREaHbbrtNR44cUdu2bat9XHhWXFxcuf+76NOnjzp16qS//e1v+uMf/+jByuAOHTp0UIcOHezP+/TpoyNHjmjRokX6+9//7sHKqicpKUn79+/X9u3bPV1KjXK033Xp892hQwdlZmYqLy9P69atU2JiotLS0ioNBXWFM/121fn22iD08MMPa8KECVW2adOmjcLDw6+aOHvlyhWdO3dO4eHhDr/fzTffLEk6fPhwrQtCjRs3lq+vr06dOlVu+6lTpyrtY3h4uFPta6Pq9Pun/Pz8FBsbq8OHD7ujxFqjsvMdEhJSZ68GVaZ3795eGSSmTp1qX+xxrf/brQuf7zLO9PunvPnzXb9+fUVHR0uSbrrpJu3evVuLFy/W3/72t6va1qXz7Uy/f6q659tr5wg1adJEHTt2rPJRv359xcXF6fz58/rss8/s+27ZskWlpaX2cOOIzMxMST9caq9t6tevr5tuukmbN2+2bystLdXmzZsrHVuNi4sr116SUlNTqxyLrW2q0++fKikp0b59+2rleXWlunC+XSUzM9OrzrdhGJo6dao2bNigLVu2qHXr1tfcpy6c7+r0+6fq0ue7tLRUxcXFFb5WF853Zarq909V+3xf93RrL3DHHXcYsbGxxq5du4zt27cb7dq1M8aOHWt//dtvvzU6dOhg7Nq1yzAMwzh8+LDxzDPPGHv27DGysrKMt99+22jTpo1x6623eqoL17R69WrDarUaK1asMA4cOGD89re/NRo2bGjk5uYahmEY48ePNx5//HF7+x07dhj16tUzFixYYHz11VfG008/bfj5+Rn79u3zVBeqxdl+z5kzx9i0aZNx5MgR47PPPjN+85vfGP7+/saXX37pqS5Uy4ULF4y9e/cae/fuNSQZzz33nLF3717j+PHjhmEYxuOPP26MHz/e3v7o0aNGYGCg8eijjxpfffWVsWTJEsPX19f48MMPPdWFanG234sWLTI2btxofPPNN8a+ffuMhx56yPDx8TE+/vhjT3XBaQ888IBhs9mMbdu2GTk5OfZHUVGRvU1d/HxXp9915fP9+OOPG2lpaUZWVpbxxRdfGI8//rhhsViMjz76yDCMunm+DcP5frvqfJsiCJ09e9YYO3asERQUZISEhBj33nuvceHCBfvrWVlZhiRj69athmEYRnZ2tnHrrbcaoaGhhtVqNaKjo41HH33UyMvL81APHPPCCy8YLVq0MOrXr2/07t3b2Llzp/21/v37G4mJieXav/nmm0b79u2N+vXrG126dDHee++9Gq7YNZzp9/Tp0+1tmzZtagwfPtz4/PPPPVD19SlbFv7TR1lfExMTjf79+1+1z4033mjUr1/faNOmjfHaa6/VeN3Xy9l+z5s3z2jbtq3h7+9vhIaGGgMGDDC2bNnimeKrqaL+Sip3/uri57s6/a4rn++JEycaLVu2NOrXr280adLEuO222+xhwDDq5vk2DOf77arzbTEMw3DuGhIAAEDd4LVzhAAAAK4XQQgAAJgWQQgAAJgWQQgAAJgWQQgAAJgWQQgAAJgWQQgAAJgWQQgAAJgWQQhAjZswYYJGjhzpsfcfP3685s6d65JjXbp0Sa1atdKePXtccjwANYs7SwNwKYvFUuXrTz/9tGbMmCHDMNSwYcOaKepH/vOf/2jQoEE6fvy4goKCXHLMF198URs2bLjqiy8B1H4EIQAulZuba/95zZo1mj17tg4ePGjfFhQU5LIAUh2TJ09WvXr1tHTpUpcd8/vvv1d4eLg+//xzdenSxWXHBeB+DI0BcKnw8HD7w2azyWKxlNsWFBR01dDYgAEDNG3aNE2fPl2NGjVS06ZN9corr6iwsFD33nuvgoODFR0drQ8++KDce+3fv1/Dhg1TUFCQmjZtqvHjx+vMmTOV1lZSUqJ169YpPj6+3PZWrVpp7ty5mjhxooKDg9WiRQu9/PLL9tcvXbqkqVOnKiIiQv7+/mrZsqVSUlLsrzdq1Eh9+/bV6tWrr/O3B6CmEYQA1Aqvv/66GjdurE8//VTTpk3TAw88oFGjRqlPnz76/PPPNWTIEI0fP15FRUWSpPPnz2vQoEGKjY3Vnj179OGHH+rUqVMaPXp0pe/xxRdfKC8vTz179rzqtYULF6pnz57au3evfve73+mBBx6wX8l6/vnn9c477+jNN9/UwYMHtWrVKrVq1arc/r1799Ynn3ziul8IgBpBEAJQK8TExOipp55Su3btlJycLH9/fzVu3Fj33Xef2rVrp9mzZ+vs2bP64osvJP0wLyc2NlZz585Vx44dFRsbq+XLl2vr1q06dOhQhe9x/Phx+fr6Kiws7KrXhg8frt/97neKjo7WrFmz1LhxY23dulWSlJ2drXbt2qlfv35q2bKl+vXrp7Fjx5bbPzIyUsePH3fxbwWAuxGEANQK3bt3t//s6+urG264Qd26dbNva9q0qSTp9OnTkn6Y9Lx161b7nKOgoCB17NhRknTkyJEK3+PixYuyWq0VTuj+8fuXDeeVvdeECROUmZmpDh066MEHH9RHH3101f4BAQH2q1UAvEc9TxcAAJLk5+dX7rnFYim3rSy8lJaWSpIKCgoUHx+vefPmXXWsiIiICt+jcePGKioq0qVLl1S/fv1rvn/Ze/Xo0UNZWVn64IMP9PHHH2v06NEaPHiw1q1bZ29/7tw5NWnSxNHuAqglCEIAvFKPHj20fv16tWrVSvXqOfZP2Y033ihJOnDggP1nR4WEhGjMmDEaM2aMfv3rX+uOO+7QuXPnFBoaKumHiduxsbFOHROA5zE0BsArJSUl6dy5cxo7dqx2796tI0eOaNOmTbr33ntVUlJS4T5NmjRRjx49tH37dqfe67nnntM///lPff311zp06JDWrl2r8PDwcvdB+uSTTzRkyJDr6RIADyAIAfBKkZGR2rFjh0pKSjRkyBB169ZN06dPV8OGDeXjU/k/bZMnT9aqVauceq/g4GDNnz9fPXv2VK9evXTs2DG9//779vfJyMhQXl6efv3rX19XnwDUPG6oCMBULl68qA4dOmjNmjWKi4tzyTHHjBmjmJgYPfHEEy45HoCawxUhAKYSEBCglStXVnnjRWdcunRJ3bp104wZM1xyPAA1iytCAADAtLgiBAAATIsgBAAATIsgBAAATIsgBAAATIsgBAAATIsgBAAATIsgBAAATIsgBAAATIsgBAAATOv/AdkP52/heJpSAAAAAElFTkSuQmCC", "text/plain": [ - "" + "
" ] }, "metadata": {}, @@ -851,11 +81,9 @@ }, { "data": { - "text/html": [ - "" - ], + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkIAAAGwCAYAAABFFQqPAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAAA+lElEQVR4nO3dfVhUdf7/8dcMCCgCSYhAoWKa5k2GYUVSWpHmXev+WjW3JCvbNNTMbtn8arSlq5v1LXPX3Vaz+9RKc7XN9S4NQ4uSTbM0lcQMvEEFARcUzu8PvzMLAjKDcz/Px3XNdcnhnJk3R5x5eT6f8/6YDMMwBAAA4IfM7i4AAADAXQhCAADAbxGEAACA3yIIAQAAv0UQAgAAfosgBAAA/BZBCAAA+K1AdxfgatXV1frll18UFhYmk8nk7nIAAIANDMPQyZMnFRcXJ7PZcddx/C4I/fLLL4qPj3d3GQAAoAkOHDigSy+91GHP53dBKCwsTNLZExkeHu7magAAgC1KSkoUHx9v/Rx3FL8LQpbhsPDwcIIQAABextHTWpgsDQAA/BZBCAAA+C2CEAAA8Ft+N0cIAOA7qqurVVlZ6e4y4CBBQUEOvTXeFgQhAIBXqqysVF5enqqrq91dChzEbDYrISFBQUFBLntNghAAwOsYhqGCggIFBAQoPj7e5VcR4HiWhscFBQVq27aty5oeE4QAAF7nzJkzKi8vV1xcnFq0aOHucuAgrVu31i+//KIzZ86oWbNmLnlNIjQAwOtUVVVJkkuHUOB8lr9Py9+vKxCEAABeizUjfYs7/j4JQgAAwG8RhAAAgN8iCAEA4AF++uknmUwm5ebmursUm/Tr10+TJ092dxkXjCAEAACc5tSpU4qMjFRUVJQqKircXU4dBCEAAOA0H374obp166YuXbpo+fLl7i6nDoIQAMDrGYah8sozbnkYhmFzndXV1Zo9e7Y6duyo4OBgtW3bVs8//3ytffbt26ebbrpJLVq0UM+ePZWdnW39XlFRkUaNGqVLLrlELVq0UI8ePfTee+/VOr5fv36aNGmSnnjiCUVGRiomJkbPPPNMrX1MJpP+/ve/69e//rVatGihTp06acWKFbX22bFjhwYOHKiWLVuqTZs2Gj16tI4ePWrzz2qxYMEC3X333br77ru1YMECu493NhoqAgC83qnTVeo6bbVbXnvnswPUIsi2j9OMjAy99tpreumll5SSkqKCggL98MMPtfZ5+umn9cILL6hTp056+umnNWrUKO3Zs0eBgYH6z3/+o6uvvlpPPvmkwsPDtWrVKo0ePVqXXXaZrrnmGutzvPHGG5oyZYq2bt2q7OxsjRkzRn369NGtt95q3SczM1OzZ8/Wn/70J82dO1d33XWX9u/fr8jISJ04cUI333yzxo4dq5deekmnTp3Sk08+qREjRmj9+vU2n5u9e/cqOztbH330kQzD0COPPKL9+/erXbt2Nj+Hs3FFCAAAFzh58qRefvllzZ49W/fcc48uu+wypaSkaOzYsbX2e+yxxzR48GBdfvnlyszM1P79+7Vnzx5J0iWXXKLHHntMV111lTp06KCJEyfqtttu05IlS2o9x5VXXqnp06erU6dOSktLU1JSktatW1drnzFjxmjUqFHq2LGjZsyYodLSUn355ZeSpFdffVWJiYmaMWOGunTposTERC1cuFAbNmzQ7t27bf6ZFy5cqIEDB6pVq1aKjIzUgAED9Prrrzfl9DkNV4QAAF6vebMA7Xx2gNte2xbff/+9KioqdMstt5x3vyuvvNL659jYWEnS4cOH1aVLF1VVVWnGjBlasmSJDh48qMrKSlVUVNRZZqTmc1ie5/Dhww3uExoaqvDwcOs+//73v7Vhwwa1bNmyTn179+7V5Zdf3ujPW1VVpTfeeEMvv/yyddvdd9+txx57TNOmTfOY9eEIQgAAr2cymWwennKX5s2b27RfzTW2LJ2Wq6urJUl/+tOf9PLLL+t///d/1aNHD4WGhmry5MmqrKxs8Dksz2N5Dlv2KS0t1dChQzVr1qw69VnCWWNWr16tgwcPauTIkbW2V1VVad26dbWG6dzJs39rAADwEZ06dVLz5s21bt26OsNhttq8ebN+9atf6e6775Z0NiDt3r1bXbt2dWSp6tWrlz788EO1b99egYFNiwoLFizQnXfeqaeffrrW9ueff14LFizwmCDkGdelAADwcSEhIXryySf1xBNP6M0339TevXu1ZcsWu+6k6tSpk9asWaMvvvhC33//vR588EEdOnTI4bWmp6fr2LFjGjVqlL766ivt3btXq1ev1r333mvTgqhHjhzRP/7xD91zzz3q3r17rUdaWpqWL1+uY8eOObzupiAIAQDgIv/zP/+jRx99VNOmTdMVV1yhkSNH1pm7cz5T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nDihm2++WYmJicrJydGnn36qQ4cOacSIEQ2+xrfffqvi4mIlJSXV+d6cOXOUlJSkbdu26aGHHtL48eOtV7JeeeUVrVixQkuWLNGuXbv0zjvvqH379rWOv+aaa/T555877oQAcAmCEACP0LNnT02dOlWdOnVSRkaGQkJCFBUVpQceeECdOnXStGnTVFRUpG+//VbS2Xk5iYmJmjFjhrp06aLExEQtXLhQGzZs0O7du+t9jf379ysgIEDR0dF1vjdo0CA99NBD6tixo5588klFRUVpw4YNkqT8/Hx16tRJKSkpateunVJSUjRq1Khax8fFxWn//v0OPisAnI0gBMAjXHnlldY/BwQE6OKLL1aPHj2s29q0aSNJOnz4sKSzk543bNhgnXPUsmVLdenSRZK0d+/eel/j1KlTCg4OrndCd83XtwznWV5rzJgxys3NVefOnTVp0iT961//qnN88+bNrVerAHiPQHcXAACS1KxZs1pfm0ymWtss4aW6ulqSVFpaqqFDh2rWrFl1nis2Nrbe14iKilJ5ebkqKysVFBTU6OtbXqtXr17Ky8vTP//5T61du1YjRoxQamqqPvjgA+v+x44dU+vWrW39cQF4CIIQAK/Uq1cvffjhh2rfvr0CA217K7vqqqskSTt37rT+2Vbh4eEaOXKkRo4cqd/85je67bbbdOzYMUVGRko6O3E7MTHRrucE4H4MjQHwSunp6Tp27JhGjRqlr776Snv37tXq1at17733qqqqqt5jWrdurV69eikrK8uu13rxxRf13nvv6YcfftDu3bu1dOlSxcTE1OqD9Pnnn6t///4X8iMBcAOCEACvFBcXp82bN6uqqkr9+/dXjx49NHnyZF100UUymxt+axs7dqzeeecdu14rLCxMs2fPVlJSknr37q2ffvpJn3zyifV1srOzVVxcrN/85jcX9DMBcD0aKgLwK6dOnVLnzp21ePFiJScnO+Q5R44cqZ49e+r3v/+9Q54PgOtwRQiAX2nevLnefPPN8zZetEdlZaV69OihRx55xCHPB8C1uCIEAAD8FleEAACA3yIIAQAAv0UQAgAAfosgBAAA/BZBCAAA+C2CEAAA8FsEIQAA4LcIQgAAwG8RhAAAgN/6/87odjf0xUZaAAAAAElFTkSuQmCC", "text/plain": [ - "" + "
" ] }, "metadata": {}, @@ -863,1583 +91,9 @@ }, { "data": { - "application/javascript": [ - "/* Put everything inside the global mpl namespace */\n", - "window.mpl = {};\n", - "\n", - "\n", - "mpl.get_websocket_type = function() {\n", - " if (typeof(WebSocket) !== 'undefined') {\n", - " return WebSocket;\n", - " } else if (typeof(MozWebSocket) !== 'undefined') {\n", - " return MozWebSocket;\n", - " } else {\n", - " alert('Your browser does not have WebSocket support.' +\n", - " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", - " 'Firefox 4 and 5 are also supported but you ' +\n", - " 'have to enable WebSockets in about:config.');\n", - " };\n", - "}\n", - "\n", - "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n", - " this.id = figure_id;\n", - "\n", - " this.ws = websocket;\n", - "\n", - " this.supports_binary = (this.ws.binaryType != undefined);\n", - "\n", - " if (!this.supports_binary) {\n", - " var warnings = document.getElementById(\"mpl-warnings\");\n", - " if (warnings) {\n", - " warnings.style.display = 'block';\n", - " warnings.textContent = (\n", - " \"This browser does not support binary websocket messages. \" +\n", - " \"Performance may be slow.\");\n", - " }\n", - " }\n", - "\n", - " this.imageObj = new Image();\n", - "\n", - " this.context = undefined;\n", - " this.message = undefined;\n", - " this.canvas = undefined;\n", - " this.rubberband_canvas = undefined;\n", - " this.rubberband_context = undefined;\n", - " this.format_dropdown = undefined;\n", - "\n", - " this.image_mode = 'full';\n", - "\n", - " this.root = $('
');\n", - " this._root_extra_style(this.root)\n", - " this.root.attr('style', 'display: inline-block');\n", - "\n", - " $(parent_element).append(this.root);\n", - "\n", - " this._init_header(this);\n", - " this._init_canvas(this);\n", - " this._init_toolbar(this);\n", - "\n", - " var fig = this;\n", - "\n", - " this.waiting = false;\n", - "\n", - " this.ws.onopen = function () {\n", - " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n", - " fig.send_message(\"send_image_mode\", {});\n", - " if (mpl.ratio != 1) {\n", - " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n", - " }\n", - " fig.send_message(\"refresh\", {});\n", - " }\n", - "\n", - " this.imageObj.onload = function() {\n", - " if (fig.image_mode == 'full') {\n", - " // Full images could contain transparency (where diff images\n", - " // almost always do), so we need to clear the canvas so that\n", - " // there is no ghosting.\n", - " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", - " }\n", - " fig.context.drawImage(fig.imageObj, 0, 0);\n", - " };\n", - "\n", - " this.imageObj.onunload = function() {\n", - " fig.ws.close();\n", - " }\n", - "\n", - " this.ws.onmessage = this._make_on_message_function(this);\n", - "\n", - " this.ondownload = ondownload;\n", - "}\n", - "\n", - "mpl.figure.prototype._init_header = function() {\n", - " var titlebar = $(\n", - " '
');\n", - " var titletext = $(\n", - " '
');\n", - " titlebar.append(titletext)\n", - " this.root.append(titlebar);\n", - " this.header = titletext[0];\n", - "}\n", - "\n", - "\n", - "\n", - "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n", - "\n", - "}\n", - "\n", - "\n", - "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n", - "\n", - "}\n", - "\n", - "mpl.figure.prototype._init_canvas = function() {\n", - " var fig = this;\n", - "\n", - " var canvas_div = $('
');\n", - "\n", - " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n", - "\n", - " function canvas_keyboard_event(event) {\n", - " return fig.key_event(event, event['data']);\n", - " }\n", - "\n", - " canvas_div.keydown('key_press', canvas_keyboard_event);\n", - " canvas_div.keyup('key_release', canvas_keyboard_event);\n", - " this.canvas_div = canvas_div\n", - " this._canvas_extra_style(canvas_div)\n", - " this.root.append(canvas_div);\n", - "\n", - " var canvas = $('');\n", - " canvas.addClass('mpl-canvas');\n", - " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n", - "\n", - " this.canvas = canvas[0];\n", - " this.context = canvas[0].getContext(\"2d\");\n", - "\n", - " var backingStore = this.context.backingStorePixelRatio ||\n", - "\tthis.context.webkitBackingStorePixelRatio ||\n", - "\tthis.context.mozBackingStorePixelRatio ||\n", - "\tthis.context.msBackingStorePixelRatio ||\n", - "\tthis.context.oBackingStorePixelRatio ||\n", - "\tthis.context.backingStorePixelRatio || 1;\n", - "\n", - " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n", - "\n", - " var rubberband = $('');\n", - " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n", - "\n", - " var pass_mouse_events = true;\n", - "\n", - " canvas_div.resizable({\n", - " start: function(event, ui) {\n", - " pass_mouse_events = false;\n", - " },\n", - " resize: function(event, ui) {\n", - " fig.request_resize(ui.size.width, ui.size.height);\n", - " },\n", - " stop: function(event, ui) {\n", - " pass_mouse_events = true;\n", - " fig.request_resize(ui.size.width, ui.size.height);\n", - " },\n", - " });\n", - "\n", - " function mouse_event_fn(event) {\n", - " if (pass_mouse_events)\n", - " return fig.mouse_event(event, event['data']);\n", - " }\n", - "\n", - " rubberband.mousedown('button_press', mouse_event_fn);\n", - " rubberband.mouseup('button_release', mouse_event_fn);\n", - " // Throttle sequential mouse events to 1 every 20ms.\n", - " rubberband.mousemove('motion_notify', mouse_event_fn);\n", - "\n", - " rubberband.mouseenter('figure_enter', mouse_event_fn);\n", - " rubberband.mouseleave('figure_leave', mouse_event_fn);\n", - "\n", - " canvas_div.on(\"wheel\", function (event) {\n", - " event = event.originalEvent;\n", - " event['data'] = 'scroll'\n", - " if (event.deltaY < 0) {\n", - " event.step = 1;\n", - " } else {\n", - " event.step = -1;\n", - " }\n", - " mouse_event_fn(event);\n", - " });\n", - "\n", - " canvas_div.append(canvas);\n", - " canvas_div.append(rubberband);\n", - "\n", - " this.rubberband = rubberband;\n", - " this.rubberband_canvas = rubberband[0];\n", - " this.rubberband_context = rubberband[0].getContext(\"2d\");\n", - " this.rubberband_context.strokeStyle = \"#000000\";\n", - "\n", - " this._resize_canvas = function(width, height) {\n", - " // Keep the size of the canvas, canvas container, and rubber band\n", - " // canvas in synch.\n", - " canvas_div.css('width', width)\n", - " canvas_div.css('height', height)\n", - "\n", - " canvas.attr('width', width * mpl.ratio);\n", - " canvas.attr('height', height * mpl.ratio);\n", - " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n", - "\n", - " rubberband.attr('width', width);\n", - " rubberband.attr('height', height);\n", - " }\n", - "\n", - " // Set the figure to an initial 600x600px, this will subsequently be updated\n", - " // upon first draw.\n", - " this._resize_canvas(600, 600);\n", - "\n", - " // Disable right mouse context menu.\n", - " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n", - " return false;\n", - " });\n", - "\n", - " function set_focus () {\n", - " canvas.focus();\n", - " canvas_div.focus();\n", - " }\n", - "\n", - " window.setTimeout(set_focus, 100);\n", - "}\n", - "\n", - "mpl.figure.prototype._init_toolbar = function() {\n", - " var fig = this;\n", - "\n", - " var nav_element = $('
')\n", - " nav_element.attr('style', 'width: 100%');\n", - " this.root.append(nav_element);\n", - "\n", - " // Define a callback function for later on.\n", - " function toolbar_event(event) {\n", - " return fig.toolbar_button_onclick(event['data']);\n", - " }\n", - " function toolbar_mouse_event(event) {\n", - " return fig.toolbar_button_onmouseover(event['data']);\n", - " }\n", - "\n", - " for(var toolbar_ind in mpl.toolbar_items) {\n", - " var name = mpl.toolbar_items[toolbar_ind][0];\n", - " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", - " var image = mpl.toolbar_items[toolbar_ind][2];\n", - " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", - "\n", - " if (!name) {\n", - " // put a spacer in here.\n", - " continue;\n", - " }\n", - " var button = $('');\n", - " button.click(method_name, toolbar_event);\n", - " button.mouseover(tooltip, toolbar_mouse_event);\n", - " nav_element.append(button);\n", - " }\n", - "\n", - " // Add the status bar.\n", - " var status_bar = $('');\n", - " nav_element.append(status_bar);\n", - " this.message = status_bar[0];\n", - "\n", - " // Add the close button to the window.\n", - " var buttongrp = $('
');\n", - " var button = $('');\n", - " button.click(function (evt) { fig.handle_close(fig, {}); } );\n", - " button.mouseover('Stop Interaction', toolbar_mouse_event);\n", - " buttongrp.append(button);\n", - " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n", - " titlebar.prepend(buttongrp);\n", - "}\n", - "\n", - "mpl.figure.prototype._root_extra_style = function(el){\n", - " var fig = this\n", - " el.on(\"remove\", function(){\n", - "\tfig.close_ws(fig, {});\n", - " });\n", - "}\n", - "\n", - "mpl.figure.prototype._canvas_extra_style = function(el){\n", - " // this is important to make the div 'focusable\n", - " el.attr('tabindex', 0)\n", - " // reach out to IPython and tell the keyboard manager to turn it's self\n", - " // off when our div gets focus\n", - "\n", - " // location in version 3\n", - " if (IPython.notebook.keyboard_manager) {\n", - " IPython.notebook.keyboard_manager.register_events(el);\n", - " }\n", - " else {\n", - " // location in version 2\n", - " IPython.keyboard_manager.register_events(el);\n", - " }\n", - "\n", - "}\n", - "\n", - "mpl.figure.prototype._key_event_extra = function(event, name) {\n", - " var manager = IPython.notebook.keyboard_manager;\n", - " if (!manager)\n", - " manager = IPython.keyboard_manager;\n", - "\n", - " // Check for shift+enter\n", - " if (event.shiftKey && event.which == 13) {\n", - " this.canvas_div.blur();\n", - " event.shiftKey = false;\n", - " // Send a \"J\" for go to next cell\n", - " event.which = 74;\n", - " event.keyCode = 74;\n", - " manager.command_mode();\n", - " manager.handle_keydown(event);\n", - " }\n", - "}\n", - "\n", - "mpl.figure.prototype.handle_save = function(fig, msg) {\n", - " fig.ondownload(fig, null);\n", - "}\n", - "\n", - "\n", - "mpl.find_output_cell = function(html_output) {\n", - " // Return the cell and output element which can be found *uniquely* in the notebook.\n", - " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n", - " // IPython event is triggered only after the cells have been serialised, which for\n", - " // our purposes (turning an active figure into a static one), is too late.\n", - " var cells = IPython.notebook.get_cells();\n", - " var ncells = cells.length;\n", - " for (var i=0; i= 3 moved mimebundle to data attribute of output\n", - " data = data.data;\n", - " }\n", - " if (data['text/html'] == html_output) {\n", - " return [cell, data, j];\n", - " }\n", - " }\n", - " }\n", - " }\n", - "}\n", - "\n", - "// Register the function which deals with the matplotlib target/channel.\n", - "// The kernel may be null if the page has been refreshed.\n", - "if (IPython.notebook.kernel != null) {\n", - " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n", - "}\n" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "" - ], - "text/plain": [ - "" + "
" ] }, "metadata": {}, @@ -2447,7 +101,6 @@ } ], "source": [ - "%matplotlib notebook\n", "from qupulse.pulses.plotting import plot\n", "\n", "parameters = dict(t=3,\n", @@ -2478,797 +131,15 @@ "name": "stdout", "output_type": "stream", "text": [ - "repetition parameters: {'v_1', 'n_rep', 'v_0', 't'}\n", + "repetition parameters: {'t', 'v_0', 'v_1', 'n_rep'}\n", "repetition measurements: {'M'}\n" ] }, { "data": { - "application/javascript": [ - "/* Put everything inside the global mpl namespace */\n", - "window.mpl = {};\n", - "\n", - "\n", - "mpl.get_websocket_type = function() {\n", - " if (typeof(WebSocket) !== 'undefined') {\n", - " return WebSocket;\n", - " } else if (typeof(MozWebSocket) !== 'undefined') {\n", - " return MozWebSocket;\n", - " } else {\n", - " alert('Your browser does not have WebSocket support.' +\n", - " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", - " 'Firefox 4 and 5 are also supported but you ' +\n", - " 'have to enable WebSockets in about:config.');\n", - " };\n", - "}\n", - "\n", - "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n", - " this.id = figure_id;\n", - "\n", - " this.ws = websocket;\n", - "\n", - " this.supports_binary = (this.ws.binaryType != undefined);\n", - "\n", - " if (!this.supports_binary) {\n", - " var warnings = document.getElementById(\"mpl-warnings\");\n", - " if (warnings) {\n", - " warnings.style.display = 'block';\n", - " warnings.textContent = (\n", - " \"This browser does not support binary websocket messages. \" +\n", - " \"Performance may be slow.\");\n", - " }\n", - " }\n", - "\n", - " this.imageObj = new Image();\n", - "\n", - " this.context = undefined;\n", - " this.message = undefined;\n", - " this.canvas = undefined;\n", - " this.rubberband_canvas = undefined;\n", - " this.rubberband_context = undefined;\n", - " this.format_dropdown = undefined;\n", - "\n", - " this.image_mode = 'full';\n", - "\n", - " this.root = $('
');\n", - " this._root_extra_style(this.root)\n", - " this.root.attr('style', 'display: inline-block');\n", - "\n", - " $(parent_element).append(this.root);\n", - "\n", - " this._init_header(this);\n", - " this._init_canvas(this);\n", - " this._init_toolbar(this);\n", - "\n", - " var fig = this;\n", - "\n", - " this.waiting = false;\n", - "\n", - " this.ws.onopen = function () {\n", - " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n", - " fig.send_message(\"send_image_mode\", {});\n", - " if (mpl.ratio != 1) {\n", - " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n", - " }\n", - " fig.send_message(\"refresh\", {});\n", - " }\n", - "\n", - " this.imageObj.onload = function() {\n", - " if (fig.image_mode == 'full') {\n", - " // Full images could contain transparency (where diff images\n", - " // almost always do), so we need to clear the canvas so that\n", - " // there is no ghosting.\n", - " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", - " }\n", - " fig.context.drawImage(fig.imageObj, 0, 0);\n", - " };\n", - "\n", - " this.imageObj.onunload = function() {\n", - " fig.ws.close();\n", - " }\n", - "\n", - " this.ws.onmessage = this._make_on_message_function(this);\n", - "\n", - " this.ondownload = ondownload;\n", - "}\n", - "\n", - "mpl.figure.prototype._init_header = function() {\n", - " var titlebar = $(\n", - " '
');\n", - " var titletext = $(\n", - " '
');\n", - " titlebar.append(titletext)\n", - " this.root.append(titlebar);\n", - " this.header = titletext[0];\n", - "}\n", - "\n", - "\n", - "\n", - "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n", - "\n", - "}\n", - "\n", - "\n", - "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n", - "\n", - "}\n", - "\n", - "mpl.figure.prototype._init_canvas = function() {\n", - " var fig = this;\n", - "\n", - " var canvas_div = $('
');\n", - "\n", - " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n", - "\n", - " function canvas_keyboard_event(event) {\n", - " return fig.key_event(event, event['data']);\n", - " }\n", - "\n", - " canvas_div.keydown('key_press', canvas_keyboard_event);\n", - " canvas_div.keyup('key_release', canvas_keyboard_event);\n", - " this.canvas_div = canvas_div\n", - " this._canvas_extra_style(canvas_div)\n", - " this.root.append(canvas_div);\n", - "\n", - " var canvas = $('');\n", - " canvas.addClass('mpl-canvas');\n", - " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n", - "\n", - " this.canvas = canvas[0];\n", - " this.context = canvas[0].getContext(\"2d\");\n", - "\n", - " var backingStore = this.context.backingStorePixelRatio ||\n", - "\tthis.context.webkitBackingStorePixelRatio ||\n", - "\tthis.context.mozBackingStorePixelRatio ||\n", - "\tthis.context.msBackingStorePixelRatio ||\n", - "\tthis.context.oBackingStorePixelRatio ||\n", - "\tthis.context.backingStorePixelRatio || 1;\n", - "\n", - " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n", - "\n", - " var rubberband = $('');\n", - " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n", - "\n", - " var pass_mouse_events = true;\n", - "\n", - " canvas_div.resizable({\n", - " start: function(event, ui) {\n", - " pass_mouse_events = false;\n", - " },\n", - " resize: function(event, ui) {\n", - " fig.request_resize(ui.size.width, ui.size.height);\n", - " },\n", - " stop: function(event, ui) {\n", - " pass_mouse_events = true;\n", - " fig.request_resize(ui.size.width, ui.size.height);\n", - " },\n", - " });\n", - "\n", - " function mouse_event_fn(event) {\n", - " if (pass_mouse_events)\n", - " return fig.mouse_event(event, event['data']);\n", - " }\n", - "\n", - " rubberband.mousedown('button_press', mouse_event_fn);\n", - " rubberband.mouseup('button_release', mouse_event_fn);\n", - " // Throttle sequential mouse events to 1 every 20ms.\n", - " rubberband.mousemove('motion_notify', mouse_event_fn);\n", - "\n", - " rubberband.mouseenter('figure_enter', mouse_event_fn);\n", - " rubberband.mouseleave('figure_leave', mouse_event_fn);\n", - "\n", - " canvas_div.on(\"wheel\", function (event) {\n", - " event = event.originalEvent;\n", - " event['data'] = 'scroll'\n", - " if (event.deltaY < 0) {\n", - " event.step = 1;\n", - " } else {\n", - " event.step = -1;\n", - " }\n", - " mouse_event_fn(event);\n", - " });\n", - "\n", - " canvas_div.append(canvas);\n", - " canvas_div.append(rubberband);\n", - "\n", - " this.rubberband = rubberband;\n", - " this.rubberband_canvas = rubberband[0];\n", - " this.rubberband_context = rubberband[0].getContext(\"2d\");\n", - " this.rubberband_context.strokeStyle = \"#000000\";\n", - "\n", - " this._resize_canvas = function(width, height) {\n", - " // Keep the size of the canvas, canvas container, and rubber band\n", - " // canvas in synch.\n", - " canvas_div.css('width', width)\n", - " canvas_div.css('height', height)\n", - "\n", - " canvas.attr('width', width * mpl.ratio);\n", - " canvas.attr('height', height * mpl.ratio);\n", - " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n", - "\n", - " rubberband.attr('width', width);\n", - " rubberband.attr('height', height);\n", - " }\n", - "\n", - " // Set the figure to an initial 600x600px, this will subsequently be updated\n", - " // upon first draw.\n", - " this._resize_canvas(600, 600);\n", - "\n", - " // Disable right mouse context menu.\n", - " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n", - " return false;\n", - " });\n", - "\n", - " function set_focus () {\n", - " canvas.focus();\n", - " canvas_div.focus();\n", - " }\n", - "\n", - " window.setTimeout(set_focus, 100);\n", - "}\n", - "\n", - "mpl.figure.prototype._init_toolbar = function() {\n", - " var fig = this;\n", - "\n", - " var nav_element = $('
')\n", - " nav_element.attr('style', 'width: 100%');\n", - " this.root.append(nav_element);\n", - "\n", - " // Define a callback function for later on.\n", - " function toolbar_event(event) {\n", - " return fig.toolbar_button_onclick(event['data']);\n", - " }\n", - " function toolbar_mouse_event(event) {\n", - " return fig.toolbar_button_onmouseover(event['data']);\n", - " }\n", - "\n", - " for(var toolbar_ind in mpl.toolbar_items) {\n", - " var name = mpl.toolbar_items[toolbar_ind][0];\n", - " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", - " var image = mpl.toolbar_items[toolbar_ind][2];\n", - " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", - "\n", - " if (!name) {\n", - " // put a spacer in here.\n", - " continue;\n", - " }\n", - " var button = $('');\n", - " button.click(method_name, toolbar_event);\n", - " button.mouseover(tooltip, toolbar_mouse_event);\n", - " nav_element.append(button);\n", - " }\n", - "\n", - " // Add the status bar.\n", - " var status_bar = $('');\n", - " nav_element.append(status_bar);\n", - " this.message = status_bar[0];\n", - "\n", - " // Add the close button to the window.\n", - " var buttongrp = $('
');\n", - " var button = $('');\n", - " button.click(function (evt) { fig.handle_close(fig, {}); } );\n", - " button.mouseover('Stop Interaction', toolbar_mouse_event);\n", - " buttongrp.append(button);\n", - " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n", - " titlebar.prepend(buttongrp);\n", - "}\n", - "\n", - "mpl.figure.prototype._root_extra_style = function(el){\n", - " var fig = this\n", - " el.on(\"remove\", function(){\n", - "\tfig.close_ws(fig, {});\n", - " });\n", - "}\n", - "\n", - "mpl.figure.prototype._canvas_extra_style = function(el){\n", - " // this is important to make the div 'focusable\n", - " el.attr('tabindex', 0)\n", - " // reach out to IPython and tell the keyboard manager to turn it's self\n", - " // off when our div gets focus\n", - "\n", - " // location in version 3\n", - " if (IPython.notebook.keyboard_manager) {\n", - " IPython.notebook.keyboard_manager.register_events(el);\n", - " }\n", - " else {\n", - " // location in version 2\n", - " IPython.keyboard_manager.register_events(el);\n", - " }\n", - "\n", - "}\n", - "\n", - "mpl.figure.prototype._key_event_extra = function(event, name) {\n", - " var manager = IPython.notebook.keyboard_manager;\n", - " if (!manager)\n", - " manager = IPython.keyboard_manager;\n", - "\n", - " // Check for shift+enter\n", - " if (event.shiftKey && event.which == 13) {\n", - " this.canvas_div.blur();\n", - " event.shiftKey = false;\n", - " // Send a \"J\" for go to next cell\n", - " event.which = 74;\n", - " event.keyCode = 74;\n", - " manager.command_mode();\n", - " manager.handle_keydown(event);\n", - " }\n", - "}\n", - "\n", - "mpl.figure.prototype.handle_save = function(fig, msg) {\n", - " fig.ondownload(fig, null);\n", - "}\n", - "\n", - "\n", - "mpl.find_output_cell = function(html_output) {\n", - " // Return the cell and output element which can be found *uniquely* in the notebook.\n", - " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n", - " // IPython event is triggered only after the cells have been serialised, which for\n", - " // our purposes (turning an active figure into a static one), is too late.\n", - " var cells = IPython.notebook.get_cells();\n", - " var ncells = cells.length;\n", - " for (var i=0; i= 3 moved mimebundle to data attribute of output\n", - " data = data.data;\n", - " }\n", - " if (data['text/html'] == html_output) {\n", - " return [cell, data, j];\n", - " }\n", - " }\n", - " }\n", - " }\n", - "}\n", - "\n", - "// Register the function which deals with the matplotlib target/channel.\n", - "// The kernel may be null if the page has been refreshed.\n", - "if (IPython.notebook.kernel != null) {\n", - " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n", - "}\n" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "" - ], + "image/png": "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oDo8TnReeOEFvfzyyxo7dqxr39VXX62uXbvqscceI9EBAAB1hsdNV9nZ2br44ovP2X/xxRcrOzvbK0EBAAB4g8eJTvv27fXee++ds3/u3LkVLqoJAABgFo+brqZOnarRo0dr2bJlrj46K1as0KJFiypMgAAAAMzido3O5s2bJUnXXnutvv32WzVv3lwLFy7UwoUL1bx5c61evVq/+93vfBYoAACAp9yu0enRo4f69Omj2267TX/84x/1n//8x5dxAQAA1JjbNTpLly5V165ddf/99ysqKkrjx4/XN99848vYAAAAasTtROfSSy/VG2+8oezsbL3wwgvKzMzU5Zdfrg4dOuipp57S/v37fRknAACAxzweddW4cWPdfPPNWrp0qbZt26brrrtOL730ktq0aaOrr77aFzECAABUS43Wumrfvr0efvhhPfLIIwoNDdWnn37qrbgAAABqzOPh5U7Lli3TG2+8ofnz58vPz0+jRo3Srbfe6s3YAAAAasSjROfnn39WWlqa0tLStH37dl188cV6/vnnNWrUKDVu3NhXMQIAAFSL24lOUlKSvvrqKzVv3lxjx47VLbfcoo4dO/oyNgAAgBpxO9Fp2LCh3n//ff2///f/1KBBA1/GBAAA4BVuJzofffSRL+MAAADwuhqNugIAAKjLSHQAAIBtkegAAADbItEBAAC2RaIDAABsi0QHAADYFokOAACwLRIdAABgWyQ6AADAtkh0AACAbZHoAAAA2yLRAQAAtmVqorNs2TKNGDFC0dHRcjgcWrhwodvnrlixQv7+/rrooot8Fh8AALA2UxOdgoICJSQk6KWXXvLovGPHjmns2LEaPHiwjyIDAAB24G/mmyclJSkpKcnj8+68805df/31atCggUe1QAAAoH6xXB+dN998Uzt37tSjjz7q1vFFRUXKzc0t9/CGUsPwynVQd1HG5jtxssTsEOBjxylj+JilEp2ffvpJf/nLX/Sf//xH/v7uVUalpqYqLCzM9YiJifFKLF/+cECSdb8MD+UXSZJKLBp/bfjv5v2SrFvGdrA+65gkysDONu87/eOTzyL4imUSnZKSEl1//fWaOnWqOnTo4PZ5KSkpysnJcT327NnjlXh+zD79x/lLXrFXrlfb1u0+Kknis6VymYcKJEmHC6xZxnYS3NDUVnb4iFHmA6hJUEMTI4GdWebTIy8vT2vX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NdeYEk9tOHjwoCHJWLp0qWEYp8u0YcOGxrx581zH/Pjjj4YkY+XKlWaF6TW/vl/DMIzLL7/cuPfee80LqhY0a9bMeP31121fvk7O+zUM+5ZvXl6eER8fb6Snp5e7R2+Wcb2v0SkuLta6des0ZMgQ1z4/Pz8NGTJEK1euNDEy3/rpp58UHR2tdu3a6YYbblBWVpbZIdWKzMxM7d+/v1x5h4WFqV+/frYub0lasmSJIiIi1LFjR9111106fPiw2SF5TU5OjiQpPDxckrRu3TqdPHmyXDl36tRJbdq0sUU5//p+nd5++201b95c3bp1U0pKigoLC80Iz+tKSko0Z84cFRQUqH///rYv31/fr5Mdyzc5OVnDhw8vV5aSd/+G692inr926NAhlZSUqGXLluX2t2zZUlu2bDEpKt/q16+f0tLS1LFjR2VnZ2vq1Km69NJLtXnzZoWGhpodnk/t379fkiosb+drdnTllVfq97//veLi4rRjxw49/PDDSkpK0sqVK9WgQQOzw6uR0tJSTZo0SQMGDFC3bt0knS7ngIAANW3atNyxdijniu5Xkq6//nq1bdtW0dHR2rhxox566CFt3bpVH3zwgYnR1symTZvUv39/nThxQiEhIVqwYIG6dOmijIwMW5ZvZfcr2bN858yZo/Xr12vNmjXnvObNv+F6n+jUR0lJSa7tHj16qF+/fmrbtq3ee+893XrrrSZGBl/54x//6Nru3r27evTooQsvvFBLlizR4MGDTYys5pKTk7V582Zb9TM7n8ru909/+pNru3v37oqKitLgwYO1Y8cOXXjhhbUdpld07NhRGRkZysnJ0fvvv69x48Zp6dKlZoflM5Xdb5cuXWxXvnv27NG9996r9PR0BQUF+fS96n3TVfPmzdWgQYNzenIfOHBAkZGRJkVVu5o2baoOHTpo+/btZofic84yrc/lLUnt2rVT8+bNLV/mEyZM0CeffKLFixerdevWrv2RkZEqLi7WsWPHyh1v9XKu7H4r0q9fP0mydBkHBASoffv26tWrl1JTU5WQkKCZM2fatnwru9+KWL18161bp4MHD6pnz57y9/eXv7+/li5dqueff17+/v5q2bKl18q43ic6AQEB6tWrlxYtWuTaV1paqkWLFpVrG7Wz/Px87dixQ1FRUWaH4nNxcXGKjIwsV965ubn69ttv6015S9LevXt1+PBhy5a5YRiaMGGCFixYoK+//lpxcXHlXu/Vq5caNmxYrpy3bt2qrKwsS5ZzVfdbkYyMDEmybBlXpLS0VEVFRbYr38o477ciVi/fwYMHa9OmTcrIyHA9evfurRtuuMG17bUy9l7faeuaM2eOERgYaKSlpRk//PCD8ac//clo2rSpsX//frND84n777/fWLJkiZGZmWmsWLHCGDJkiNG8eXPj4MGDZofmFXl5ecaGDRuMDRs2GJKMZ5991tiwYYOxe/duwzAM48knnzSaNm1qfPjhh8bGjRuNa665xoiLizOOHz9ucuTVd757zsvLMx544AFj5cqVRmZmpvHVV18ZPXv2NOLj440TJ06YHXq13HXXXUZYWJixZMkSIzs72/UoLCx0HXPnnXcabdq0Mb7++mtj7dq1Rv/+/Y3+/fubGHX1VXW/27dvNx5//HFj7dq1RmZmpvHhhx8a7dq1My677DKTI6++v/zlL8bSpUuNzMxMY+PGjcZf/vIXw+FwGF9++aVhGPYqX8M4//3asXwr8uuRZd4qYxKdM1544QWjTZs2RkBAgNG3b19j1apVZofkM6NHjzaioqKMgIAAo1WrVsbo0aON7du3mx2W1yxevNiQdM5j3LhxhmGcHmL+t7/9zWjZsqURGBhoDB482Ni6dau5QdfQ+e65sLDQGDp0qNGiRQujYcOGRtu2bY3bb7/d0ol8RfcqyXjzzTddxxw/fty4++67jWbNmhmNGjUyfve73xnZ2dnmBV0DVd1vVlaWcdlllxnh4eFGYGCg0b59e+PBBx80cnJyzA28Bm655Rajbdu2RkBAgNGiRQtj8ODBriTHMOxVvoZx/vu1Y/lW5NeJjrfK2GEYhlHNmicAAIA6rd730QEAAPZFogMAAGyLRAcAANgWiQ4AALAtEh0AAGBbJDoAAMC2SHQAAIBtkegAAADbItEBUOvGjx+vkSNHmvb+N910k6ZNm+aVaxUXFys2NlZr1671yvUAeBczIwPwKofDcd7XH330Ud13330yDENNmzatnaDK+O677zRo0CDt3r1bISEhXrnmiy++qAULFpRbgBBA3UCiA8Cr9u/f79qeO3eupkyZoq1bt7r2hYSEeC3BqI7bbrtN/v7+euWVV7x2zaNHjyoyMlLr169X165dvXZdADVH0xUAr4qMjHQ9wsLC5HA4yu0LCQk5p+lq4MCBmjhxoiZNmqRmzZqpZcuWeu2111RQUKCbb75ZoaGhat++vf773/+We6/NmzcrKSlJISEhatmypW666SYdOnSo0thKSkr0/vvva8SIEeX2x8bGatq0abrlllsUGhqqNm3a6NVXX3W9XlxcrAkTJigqKkpBQUFq27atUlNTXa83a9ZMAwYM0Jw5c2r4rwfA20h0ANQJb731lpo3b67Vq1dr4sSJuuuuu3Tdddfp4osv1vr16zV06FDddNNNKiwslCQdO3ZMgwYNUmJiotauXavPP/9cBw4c0KhRoyp9j40bNyonJ0e9e/c+57UZM2aod+/e2rBhg+6++27dddddrpqo559/Xh999JHee+89bd26VW+//bZiY2PLnd+3b19988033vsHAeAVJDoA6oSEhAQ98sgjio+PV0pKioKCgtS8eXPdfvvtio+P15QpU3T48GFt3LhR0ul+MYmJiZo2bZo6deqkxMREvfHGG1q8eLG2bdtW4Xvs3r1bDRo0UERExDmvXXXVVbr77rvVvn17PfTQQ2revLkWL14sScrKylJ8fLwuueQStW3bVpdcconGjBlT7vzo6Gjt3r3by/8qAGqKRAdAndCjRw/XdoMGDXTBBReoe/furn0tW7aUJB08eFDS6U7FixcvdvX5CQkJUadOnSRJO3bsqPA9jh8/rsDAwAo7TJd9f2dzm/O9xo8fr4yMDHXs2FH33HOPvvzyy3PODw4OdtU2Aag7/M0OAAAkqWHDhuWeOxyOcvucyUlpaakkKT8/XyNGjNBTTz11zrWioqIqfI/mzZursLBQxcXFCggIqPL9ne/Vs2dPZWZm6r///a+++uorjRo1SkOGDNH777/vOv7IkSNq0aKFu7cLoJaQ6ACwpJ49e2r+/PmKjY2Vv797H2UXXXSRJOmHH35wbburSZMmGj16tEaPHq0//OEPuvLKK3XkyBGFh4dLOt0xOjEx0aNrAvA9mq4AWFJycrKOHDmiMWPGaM2aNdqxY4e++OIL3XzzzSopKanwnBYtWqhnz55avny5R+/17LPP6t1339WWLVu0bds2zZs3T5GRkeXmAfrmm280dOjQmtwSAB8g0QFgSdHR0VqxYoVKSko0dOhQde/eXZMmTVLTpk3l51f5R9ttt92mt99+26P3Cg0N1fTp09W7d2/16dNHu3bt0meffeZ6n5UrVyonJ0d/+MMfanRPALyPCQMB1CvHjx9Xx44dNXfuXPXv398r1xw9erQSEhL08MMPe+V6ALyHGh0A9UpwcLBmz5593okFPVFcXKzu3bvrvvvu88r1AHgXNToAAMC2qNEBAAC2RaIDAABsi0QHAADYFokOAACwLRIdAABgWyQ6AADAtkh0AACAbZHoAAAA2yLRAQAAtvX/ATmezirBcyO9AAAAAElFTkSuQmCC", "text/plain": [ - "" + "
" ] }, "metadata": {}, @@ -4142,22 +231,8 @@ } ], "metadata": { - "kernelspec": { - "display_name": "Python [default]", - "language": "python", - "name": "python3" - }, "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.0" + "name": "python" } }, "nbformat": 4, diff --git a/doc/source/examples/00ConstantPulseTemplate.ipynb b/doc/source/examples/00ConstantPulseTemplate.ipynb index 41b71020a..0dafbe501 100644 --- a/doc/source/examples/00ConstantPulseTemplate.ipynb +++ b/doc/source/examples/00ConstantPulseTemplate.ipynb @@ -46,8 +46,10 @@ "outputs": [ { "data": { - "text/plain": "
", - "image/png": "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SzFSUVGRFR8fb6WlpVm33HKL9fjjj3u7JKOMHTvWuvHGG71dhvF69eplDR061Kntr3/9qzVw4EAvVWQmSdbHH3/seG+3262oqCjrpZdecrT98ccfVkREhPXWW29VeT3M3FygkpISpaenKykpyak9KSlJW7du9VJVl4aCggJJUp06dbxciZlGjBihXr16qWvXrt4uxUirVq1S+/btdc8996hBgwa65pprtGDBAm+XZZwbb7xR69at0+7duyVJ//u//6vNmzerZ8+eXq7MbFlZWTpy5IjTsTEgIEC33HJLtRwbL7kHZ3ra0aNHVVZWpsjISKf2yMhIHTlyxEtVmc+yLI0ePVo33nij2rRp4+1yjPPBBx/o22+/1fbt271dirH279+vefPmafTo0Ro/fry2bdumkSNHKiAgQIMHD/Z2ecYYO3asCgoK1KpVK/n6+qqsrEwvvviiBgwY4O3SjHbm+FfRsfHgwYNVvn/CjYfYbDan95ZllWuD5zz66KP67rvvtHnzZm+XYpycnBw9/vjjWrt2rQIDA71djrHsdrvat2+vqVOnSpKuueYa/fjjj5o3bx7hxoNSU1P1H//xH1q+fLmuvPJKZWZmKiUlRTExMRoyZIi3yzOet46NhJsLVK9ePfn6+pabpcnPzy+XWOEZjz32mFatWqWNGzeqUaNG3i7HOOnp6crPz1diYqKjraysTBs3btScOXNUXFwsX19fL1ZohujoaF1xxRVOba1bt9aKFSu8VJGZnnzyST399NO69957JUlt27bVwYMHNW3aNMJNFYqKipJ0egYnOjra0V5dx0bW3Fwgf39/JSYmKi0tzak9LS1NHTp08FJVZrIsS48++qhWrlypzz//XHFxcd4uyUi33nqrvv/+e2VmZjpe7du31/3336/MzEyCjYd07Nix3K0Mdu/erSZNmnipIjOdPHlSPj7OhzpfX18uBa9icXFxioqKcjo2lpSUaMOGDdVybGTmxgNGjx6tQYMGqX379rrhhhs0f/58ZWdnKzk52dulGWXEiBFavny5/uu//kthYWGO2bKIiAgFBQV5uTpzhIWFlVvHFBISorp167K+yYNGjRqlDh06aOrUqerXr5+2bdum+fPna/78+d4uzSi9e/fWiy++qMaNG+vKK69URkaGZs6cqaFDh3q7tIve77//rr179zreZ2VlKTMzU3Xq1FHjxo2VkpKiqVOnKj4+XvHx8Zo6daqCg4N13333VX1xVX491iXizTfftJo0aWL5+/tbCQkJXJ5cBSRV+HrnnXe8XZrxuBS8anz66adWmzZtrICAAKtVq1bW/PnzvV2ScQoLC63HH3/caty4sRUYGGg1a9bMmjBhglVcXOzt0i5669evr/D/k4cMGWJZ1unLwZ977jkrKirKCggIsG6++Wbr+++/r5babJZlWVUfoQAAAKoHa24AAIBRCDcAAMAohBsAAGAUwg0AADAK4QYAABiFcAMAAIxCuAEAAEYh3AAAAKMQbgBUu0mTJunqq6/22v6fffZZPfTQQx4b7y9/+YtWrlzpsfEAXBjuUAzAo2w22zk/HzJkiOPp4nXr1q2mqv7Pzz//rPj4eH333Xdq2rSpR8ZctWqVxowZo59++qncQxoBVD/CDQCPOvNAU0lKTU3VxIkTnZ5+HRQUpIiICG+UJkmaOnWqNmzYoDVr1nhszLKyMsXExGjJkiXq0aOHx8YFUDn8JwYAj4qKinK8IiIiZLPZyrX9+2mpBx54QH379tXUqVMVGRmp2rVra/LkySotLdWTTz6pOnXqqFGjRlq8eLHTvnJzc9W/f39ddtllqlu3rvr06aMDBw6cs74PPvhAd9xxh1Nbp06dNHLkSD311FOqU6eOoqKiNGnSJKc+kyZNUuPGjRUQEKCYmBiNHDnS8Zmvr6969uyp999/v1K/GQDPItwAqBE+//xzHT58WBs3btTMmTM1adIk3X777brsssv09ddfKzk5WcnJycrJyZEknTx5Up07d1ZoaKg2btyozZs3KzQ0VLfddptKSkoq3Mevv/6qH374Qe3bty/32bvvvquQkBB9/fXXmjFjhqZMmaK0tDRJ0kcffaRZs2bp7bff1p49e/TJJ5+obdu2Tttfe+212rRpk4d/FQCVQbgBUCPUqVNHr7/+ulq2bKmhQ4eqZcuWOnnypMaPH6/4+HiNGzdO/v7+2rJli6TTMzA+Pj5auHCh2rZtq9atW+udd95Rdna2vvjiiwr3cfDgQVmWpZiYmHKftWvXTs8995zi4+M1ePBgtW/fXuvWrZMkZWdnKyoqSl27dlXjxo117bXX6sEHH3TavmHDhsrOzpbdbvfsDwPAbYQbADXClVde6bQYNzIy0ml2xNfXV3Xr1lV+fr4kKT09XXv37lVYWJhCQ0MVGhqqOnXq6I8//tC+ffsq3MepU6ckSYGBgeU+a9eundP76Ohox77uuecenTp1Ss2aNdODDz6ojz/+WKWlpU79g4KCZLfbVVxcXIlvD8CT/LxdAABIUq1atZze22y2CtvOzIzY7XYlJiZq2bJl5caqX79+hfuoV6+epNOnp/69z7n2FRsbq127diktLU3//Oc/9cgjj+jll1/Whg0bHNsdP35cwcHBCgoKcvUrA6gihBsAF6WEhASlpqaqQYMGCg8Pd2mbyy+/XOHh4dqxY4datGjh1v6CgoJ0xx136I477tCIESPUqlUrff/990pISJAk/fDDD45/A/AuTksBuCjdf//9qlevnvr06aNNmzYpKytLGzZs0OOPP65Dhw5VuI2Pj4+6du2qzZs3u7WvJUuWaNGiRfrhhx+0f/9+vffeewoKClKTJk0cfTZt2qSkpKQL+k4APINwA+CiFBwcrI0bN6px48b661//qtatW2vo0KE6derUOWdyHnroIX3wwQduLfytXbu2FixYoI4dO6pdu3Zat26dPv30U8dNCHNzc7V161b97W9/u+DvBeDCcRM/AJcUy7J0/fXXKyUlRQMGDPDImE8++aQKCgo0f/58j4wH4MIwcwPgkmKz2TR//vxyVztdiAYNGuj555/32HgALgwzNwAAwCjM3AAAAKMQbgAAgFEINwAAwCiEGwAAYBTCDQAAMArhBgAAGIVwAwAAjEK4AQAARiHcAAAAo/w/CSwDtMUQAowAAAAASUVORK5CYII=\n" + "image/png": "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8vA43Gzbtq3Eez8/P9WrV08zZsy47pVUAAAAFc3jcLNu3bqKqAMAAMAruIkfAAAwitfCzZgxYzgtBQAAfM5rD848evSojhw54q3hAAC4LofDoYsXL/q6DHhJYGCgVx7C7bVw8/rrr3trKAAArsmyLOXm5urMmTO+LgVe5Ofnp8TERAUGBt7QOF4LNwAAVJYrwSYqKkohISGy2Wy+Lgk3yOl06tixYzp+/LgaNGhwQ79pucJNYWGhPv30Ux0+fFjFxcUlPhsxYkS5iwEA4HocDocr2NStW9fX5cCL6tWrp2PHjunSpUuqUaNGuccp131u7rzzTp0/f16FhYWqU6eOTp06pZCQEEVFRRFuAAAV6socm5CQEB9XAm+7cjrK4XDcULjxeNbOyJEjlZGRoR9//FHBwcH68ssv9f3336tdu3Z6+eWXy10IAACe4FSUebz1m3ocbnJycvTHP/5Rfn5+8vf3V1FRkeLj4zVt2jSNGTPGK0UBAACUl8fhpkaNGq7LtKKionT48GFJUkREBJeCAwBQTocOHZLNZlNOTo6vS3HLbbfdpszMTF+XUSaPw01KSoo2b94sSeratavGjx+vN998U5mZmWrZsqXXCwQAANXPkiVLZLPZXK/Q0FC1a9dO77zzToWv2+NwM3nyZMXGxkqSJk2apNq1a+uxxx7TyZMnNX/+fK8XCAAAqqfw8HAdP35cx48f17Zt25Senq4+ffpoz549Fbpej8NNamqqunXrJunyaanVq1eroKBAW7ZsUXJystcLBADAFE6nU9OmTVPjxo1lt9vVoEEDTZo0qUSfAwcOqFu3bgoJCVFycrI2btzo+uyHH35Qv379VL9+fYWEhKhVq1Z66623Six/2223acSIEXr66adVp04dxcTE6LnnnivRx2azaeHChbrnnnsUEhKipKQkrVq1qkSfHTt2qFevXgoNDVV0dLQGDBigU6dOefR9bTabYmJiFBMTo6SkJL344ovy8/PT9u3bPRrHUzw4EwBQ7VmWpfPFl3zysizL7TpHjx6tl156SePGjdPOnTu1dOlSRUdHl+jz7LPP6qmnnlJOTo6aNGmifv366dKlS5Kkn376Se3atdP777+vHTt26NFHH9WAAQO0adOmEmO8/vrrqlmzpr766itNmzZNzz//vNauXVuiz8SJE9WnTx9t375dd955px544AGdPn1aknTmzBl1795dKSkp+vrrr7V69Wrl5eWpT58+5fl5JF2+vPvK0wzatm1b7nHc4dZ9bu644w4999xz+tWvfnXNfmfPntVrr72m0NBQDRs2zCsFAgBwPRcuOtR8/BqfrHvn8+kKCbz+7vTs2bN69dVXNWfOHA0aNEiSdPPNN6tLly4l+j311FO66667JF0OIC1atNC+ffvUrFkz1a9fX0899ZSr7xNPPKE1a9ZoxYoVat++vau9devWmjBhgiQpKSlJc+bMUXZ2tm6//XZXnwcffFD9+vWTdHnKyezZs7Vp0ybdcccdmjNnjlJSUjR58mRX/8WLFys+Pl579+5VkyZN3No2+fn5Cg0NlSRduHBBNWrU0Pz583XzzTe7tXx5uRVu7r//ft17772KiIhQRkaGUlNTFRcXp6CgIP3444/auXOn1q9frw8++EB33XWXpk+fXqFFAwBQ3ezatUtFRUXq0aPHNfu1bt3a9feVOa4nTpxQs2bN5HA4NHnyZK1YsUJHjx5VcXGxioqKSt3Q8OdjXBnnxIkTV+1Ts2ZNhYeHu/p88803WrdunSuY/Nz+/fvdDjdhYWHaunWrJOn8+fP6+OOPNXToUNWtW1cZGRlujVEeboWbhx9+WP3799df/vIXLV++XPPnz1d+fr6ky+fTmjdvrvT0dG3evFm33HJLhRULAEBZgmv4a+fz6T5bt1v9goPd6vfzO/Neuamd0+mUJE2fPl2vvvqqZs2apVatWqlmzZrKzMws9Sikf727r81mc43hTp9z584pIyNDU6dOLVXflcDlDj8/PzVu3Nj1vnXr1vroo480depU34cbSbLb7erfv7/69+8v6fKhpgsXLqhu3bo3dItkAABulM1mc+vUkC8lJSUpODhY2dnZGjJkSLnG2LBhg37729+69sVOp1N79+5V8+bNvVmq2rZtq5UrVyohIUEBAd7drv7+/rpw4YJXx/xX5Z5QHBERoZiYGIINAABuCAoK0qhRo/T000/rjTfe0P79+/Xll19q0aJFbo+RlJSktWvX6osvvtCuXbv0hz/8QXl5eV6vddiwYTp9+rT69eunzZs3a//+/VqzZo0GDx4sh8Ph9jiWZSk3N1e5ubk6ePCg5s+frzVr1ui3v/2t12v+uaodcwEAMMi4ceMUEBCg8ePH69ixY4qNjdXQoUPdXn7s2LE6cOCA0tPTFRISokcffVS9e/d2TRXxlri4OG3YsEGjRo1Sz549VVRUpIYNG+qOO+5wPaXAHQUFBa7TWHa7XQ0bNtTzzz+vUaNGebXef2WzPLmGzQAFBQWKiIhQfn6+wsPDfV0OAMBDP/30kw4ePKjExEQFBQX5uhx40bV+W0/239znBgAAGIVwAwAAjFKucHPmzBktXLhQo0ePdt3NcOvWrTp69KhXiwMAAPCUxxOKt2/frrS0NEVEROjQoUN65JFHVKdOHb3zzjs6fPiw3njjjYqoEwAAwC0eH7nJysrSgw8+qO+++67EZJ8777xTn332mVeLAwAA8JTH4Wbz5s36wx/+UKq9fv36ys3N9biAuXPnKiEhQUFBQerQoUOph3/9qzNnzmjYsGGKjY2V3W5XkyZN9MEHH3i8XgAAYCaPT0vZ7XYVFBSUat+7d6/q1avn0VjLly9XVlaW5s2bpw4dOmjWrFlKT0/Xnj17FBUVVap/cXGxbr/9dkVFRentt99W/fr19f3336tWrVqefg0AAGAoj4/c3H333Xr++ed18eJFSZdveX348GGNGjVK9957r0djzZw5U4888ogGDx6s5s2ba968eQoJCdHixYvL7L948WKdPn1a7733njp37qyEhAR17dpVycnJnn4NAABgKI/DzYwZM3Tu3DlFRUXpwoUL6tq1qxo3bqywsDBNmjTJ7XGKi4u1ZcsWpaWl/bMYPz+lpaVp48aNZS6zatUqdezYUcOGDVN0dLRatmypyZMnX/NW0EVFRSooKCjxAgAA5vL4tFRERITWrl2r9evXa/v27Tp37pzatm1bIqS449SpU3I4HIqOji7RHh0drd27d5e5zIEDB/T3v/9dDzzwgD744APt27dPjz/+uC5evKgJEyaUucyUKVM0ceJEj2oDAKCyHTp0SImJidq2bZvatGnj63Ku67bbblObNm00a9YsX5dSSrmfLdWlSxd16dLFm7Vcl9PpVFRUlObPny9/f3+1a9dOR48e1fTp068abkaPHq2srCzX+4KCAsXHx1dWyQAA/KJduHBB9evXl5+fn44ePSq73V7h6/Q43MyePbvMdpvNpqCgIDVu3Fj/7//9P/n7+19znMjISPn7+5d6mmleXp5iYmLKXCY2NlY1atQoMfYtt9yi3NxcFRcXKzAwsNQydru9UjYkAAAobeXKlWrRooUsy9J7772nvn37Vvg6PZ5z88orr2jMmDHKzMzUxIkTNXHiRGVmZmr06NEaN26cevTooaZNm+rIkSPXHCcwMFDt2rVTdna2q83pdCo7O1sdO3Ysc5nOnTtr3759cjqdrra9e/cqNja2zGADAEBV4nQ6NW3aNDVu3Fh2u10NGjQoNV/1wIED6tatm0JCQpScnFxiHuoPP/ygfv36qX79+goJCVGrVq301ltvlVj+tttu04gRI/T000+rTp06iomJ0XPPPVeij81m08KFC3XPPfcoJCRESUlJWrVqVYk+O3bsUK9evRQaGqro6GgNGDBAp06d8vg7L1q0SP3791f//v21aNEij5cvF8tDS5cutW677TZr3759rrbvvvvO6t69u7Vs2TLryJEjVufOna177733umMtW7bMstvt1pIlS6ydO3dajz76qFWrVi0rNzfXsizLGjBggPXMM8+4+h8+fNgKCwuzhg8fbu3Zs8f629/+ZkVFRVkvvvii2/Xn5+dbkqz8/HwPvjUAoKq4cOGCtXPnTuvChQv/bHQ6LavonG9eTqfbtT/99NNW7dq1rSVLllj79u2zPv/8c2vBggWWZVnWwYMHLUlWs2bNrL/97W/Wnj17rPvuu89q2LChdfHiRcuyLOsf//iHNX36dGvbtm3W/v37rdmzZ1v+/v7WV1995VpH165drfDwcOu5556z9u7da73++uuWzWazPvroI1cfSdZNN91kLV261Pruu++sESNGWKGhodYPP/xgWZZl/fjjj1a9evWs0aNHW7t27bK2bt1q3X777Va3bt1KrOfJJ5+85vfdt2+fZbfbrdOnT1s//PCDFRQUZB06dMiz3/b/eLL/tv3fl3TbzTffrJUrV5aa7LRt2zbde++9OnDggL744gvde++9On78+HXHmzNnjqZPn67c3Fy1adNGs2fPVocOHSRdTp8JCQlasmSJq//GjRs1cuRI5eTkqH79+nr44Yc1atSo654Gu8KTR6YDAKqen376SQcPHlRiYuI/75RfXChNjvNNQWOOSYE1r9vt7NmzqlevnubMmaMhQ4aU+vzKhOKFCxfq4YcfliTt3LlTLVq00K5du9SsWbMyx/3Nb36jZs2a6eWXX5Z0ed/pcDj0+eefu/q0b99e3bt310svvSTp8pGbsWPH6oUXXpAkFRYWKjQ0VB9++KHuuOMOvfjii/r888+1Zs0a1xj/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8EHl5OSoadOmGjFihD766KNSywcHB7uOKgHwrQBfFwAAklSjRo0S7202W4m2K4HE6XRKks6dO6eMjAxNnTq11FixsbFlriMyMlLnz59XcXGxAgMDr7v+K+tq27atDh48qA8//FAff/yx+vTpo7S0NL399tuu/qdPn1a9evXc/boAKhDhBkC11LZtW61cuVIJCQkKCHDvn7I2bdpIknbu3On6213h4eHq27ev+vbtq/vuu0933HGHTp8+rTp16ki6PLk5JSXFozEBVAxOSwGoloYNG6bTp0+rX79+2rx5s/bv3681a9Zo8ODBcjgcZS5Tr149tW3bVuvXr/doXTNnztRbb72l3bt3a+/evfrLX/6imJiYEvfp+fzzz9WzZ88b+UoAvIRwA6BaiouL04YNG+RwONSzZ0+1atVKmZmZqlWrlvz8rv5P25AhQ/Tmm296tK6wsDBNmzZNqampuvXWW3Xo0CF98MEHrvVs3LhR+fn5uu+++27oOwHwDm7iB+AX5cKFC2ratKmWL1+ujh07emXMvn37Kjk5WWPGjPHKeABuDEduAPyiBAcH64033rjmzf48UVxcrFatWmnkyJFeGQ/AjePIDQAAMApHbgAAgFEINwAAwCiEGwAAYBTCDQAAMArhBgAAGIVwAwAAjEK4AQAARiHcAAAAoxBuAACAUf4/vUa+J2Znxp4AAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] }, "metadata": {}, "output_type": "display_data" @@ -61,22 +63,8 @@ } ], "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.6" + "name": "python" } }, "nbformat": 4, diff --git a/doc/source/examples/00FunctionPulse.ipynb b/doc/source/examples/00FunctionPulse.ipynb index 2d0d53edb..1bb33ff91 100644 --- a/doc/source/examples/00FunctionPulse.ipynb +++ b/doc/source/examples/00FunctionPulse.ipynb @@ -18,791 +18,9 @@ "outputs": [ { "data": { - "application/javascript": [ - "/* Put everything inside the global mpl namespace */\n", - "window.mpl = {};\n", - "\n", - "\n", - "mpl.get_websocket_type = function() {\n", - " if (typeof(WebSocket) !== 'undefined') {\n", - " return WebSocket;\n", - " } else if (typeof(MozWebSocket) !== 'undefined') {\n", - " return MozWebSocket;\n", - " } else {\n", - " alert('Your browser does not have WebSocket support.' +\n", - " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", - " 'Firefox 4 and 5 are also supported but you ' +\n", - " 'have to enable WebSockets in about:config.');\n", - " };\n", - "}\n", - "\n", - "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n", - " this.id = figure_id;\n", - "\n", - " this.ws = websocket;\n", - "\n", - " this.supports_binary = (this.ws.binaryType != undefined);\n", - "\n", - " if (!this.supports_binary) {\n", - " var warnings = document.getElementById(\"mpl-warnings\");\n", - " if (warnings) {\n", - " warnings.style.display = 'block';\n", - " warnings.textContent = (\n", - " \"This browser does not support binary websocket messages. \" +\n", - " \"Performance may be slow.\");\n", - " }\n", - " }\n", - "\n", - " this.imageObj = new Image();\n", - "\n", - " this.context = undefined;\n", - " this.message = undefined;\n", - " this.canvas = undefined;\n", - " this.rubberband_canvas = undefined;\n", - " this.rubberband_context = undefined;\n", - " this.format_dropdown = undefined;\n", - "\n", - " this.image_mode = 'full';\n", - "\n", - " this.root = $('
');\n", - " this._root_extra_style(this.root)\n", - " this.root.attr('style', 'display: inline-block');\n", - "\n", - " $(parent_element).append(this.root);\n", - "\n", - " this._init_header(this);\n", - " this._init_canvas(this);\n", - " this._init_toolbar(this);\n", - "\n", - " var fig = this;\n", - "\n", - " this.waiting = false;\n", - "\n", - " this.ws.onopen = function () {\n", - " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n", - " fig.send_message(\"send_image_mode\", {});\n", - " if (mpl.ratio != 1) {\n", - " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n", - " }\n", - " fig.send_message(\"refresh\", {});\n", - " }\n", - "\n", - " this.imageObj.onload = function() {\n", - " if (fig.image_mode == 'full') {\n", - " // Full images could contain transparency (where diff images\n", - " // almost always do), so we need to clear the canvas so that\n", - " // there is no ghosting.\n", - " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", - " }\n", - " fig.context.drawImage(fig.imageObj, 0, 0);\n", - " };\n", - "\n", - " this.imageObj.onunload = function() {\n", - " fig.ws.close();\n", - " }\n", - "\n", - " this.ws.onmessage = this._make_on_message_function(this);\n", - "\n", - " this.ondownload = ondownload;\n", - "}\n", - "\n", - "mpl.figure.prototype._init_header = function() {\n", - " var titlebar = $(\n", - " '
');\n", - " var titletext = $(\n", - " '
');\n", - " titlebar.append(titletext)\n", - " this.root.append(titlebar);\n", - " this.header = titletext[0];\n", - "}\n", - "\n", - "\n", - "\n", - "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n", - "\n", - "}\n", - "\n", - "\n", - "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n", - "\n", - "}\n", - "\n", - "mpl.figure.prototype._init_canvas = function() {\n", - " var fig = this;\n", - "\n", - " var canvas_div = $('
');\n", - "\n", - " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n", - "\n", - " function canvas_keyboard_event(event) {\n", - " return fig.key_event(event, event['data']);\n", - " }\n", - "\n", - " canvas_div.keydown('key_press', canvas_keyboard_event);\n", - " canvas_div.keyup('key_release', canvas_keyboard_event);\n", - " this.canvas_div = canvas_div\n", - " this._canvas_extra_style(canvas_div)\n", - " this.root.append(canvas_div);\n", - "\n", - " var canvas = $('');\n", - " canvas.addClass('mpl-canvas');\n", - " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n", - "\n", - " this.canvas = canvas[0];\n", - " this.context = canvas[0].getContext(\"2d\");\n", - "\n", - " var backingStore = this.context.backingStorePixelRatio ||\n", - "\tthis.context.webkitBackingStorePixelRatio ||\n", - "\tthis.context.mozBackingStorePixelRatio ||\n", - "\tthis.context.msBackingStorePixelRatio ||\n", - "\tthis.context.oBackingStorePixelRatio ||\n", - "\tthis.context.backingStorePixelRatio || 1;\n", - "\n", - " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n", - "\n", - " var rubberband = $('');\n", - " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n", - "\n", - " var pass_mouse_events = true;\n", - "\n", - " canvas_div.resizable({\n", - " start: function(event, ui) {\n", - " pass_mouse_events = false;\n", - " },\n", - " resize: function(event, ui) {\n", - " fig.request_resize(ui.size.width, ui.size.height);\n", - " },\n", - " stop: function(event, ui) {\n", - " pass_mouse_events = true;\n", - " fig.request_resize(ui.size.width, ui.size.height);\n", - " },\n", - " });\n", - "\n", - " function mouse_event_fn(event) {\n", - " if (pass_mouse_events)\n", - " return fig.mouse_event(event, event['data']);\n", - " }\n", - "\n", - " rubberband.mousedown('button_press', mouse_event_fn);\n", - " rubberband.mouseup('button_release', mouse_event_fn);\n", - " // Throttle sequential mouse events to 1 every 20ms.\n", - " rubberband.mousemove('motion_notify', mouse_event_fn);\n", - "\n", - " rubberband.mouseenter('figure_enter', mouse_event_fn);\n", - " rubberband.mouseleave('figure_leave', mouse_event_fn);\n", - "\n", - " canvas_div.on(\"wheel\", function (event) {\n", - " event = event.originalEvent;\n", - " event['data'] = 'scroll'\n", - " if (event.deltaY < 0) {\n", - " event.step = 1;\n", - " } else {\n", - " event.step = -1;\n", - " }\n", - " mouse_event_fn(event);\n", - " });\n", - "\n", - " canvas_div.append(canvas);\n", - " canvas_div.append(rubberband);\n", - "\n", - " this.rubberband = rubberband;\n", - " this.rubberband_canvas = rubberband[0];\n", - " this.rubberband_context = rubberband[0].getContext(\"2d\");\n", - " this.rubberband_context.strokeStyle = \"#000000\";\n", - "\n", - " this._resize_canvas = function(width, height) {\n", - " // Keep the size of the canvas, canvas container, and rubber band\n", - " // canvas in synch.\n", - " canvas_div.css('width', width)\n", - " canvas_div.css('height', height)\n", - "\n", - " canvas.attr('width', width * mpl.ratio);\n", - " canvas.attr('height', height * mpl.ratio);\n", - " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n", - "\n", - " rubberband.attr('width', width);\n", - " rubberband.attr('height', height);\n", - " }\n", - "\n", - " // Set the figure to an initial 600x600px, this will subsequently be updated\n", - " // upon first draw.\n", - " this._resize_canvas(600, 600);\n", - "\n", - " // Disable right mouse context menu.\n", - " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n", - " return false;\n", - " });\n", - "\n", - " function set_focus () {\n", - " canvas.focus();\n", - " canvas_div.focus();\n", - " }\n", - "\n", - " window.setTimeout(set_focus, 100);\n", - "}\n", - "\n", - "mpl.figure.prototype._init_toolbar = function() {\n", - " var fig = this;\n", - "\n", - " var nav_element = $('
')\n", - " nav_element.attr('style', 'width: 100%');\n", - " this.root.append(nav_element);\n", - "\n", - " // Define a callback function for later on.\n", - " function toolbar_event(event) {\n", - " return fig.toolbar_button_onclick(event['data']);\n", - " }\n", - " function toolbar_mouse_event(event) {\n", - " return fig.toolbar_button_onmouseover(event['data']);\n", - " }\n", - "\n", - " for(var toolbar_ind in mpl.toolbar_items) {\n", - " var name = mpl.toolbar_items[toolbar_ind][0];\n", - " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", - " var image = mpl.toolbar_items[toolbar_ind][2];\n", - " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", - "\n", - " if (!name) {\n", - " // put a spacer in here.\n", - " continue;\n", - " }\n", - " var button = $('');\n", - " button.click(method_name, toolbar_event);\n", - " button.mouseover(tooltip, toolbar_mouse_event);\n", - " nav_element.append(button);\n", - " }\n", - "\n", - " // Add the status bar.\n", - " var status_bar = $('');\n", - " nav_element.append(status_bar);\n", - " this.message = status_bar[0];\n", - "\n", - " // Add the close button to the window.\n", - " var buttongrp = $('
');\n", - " var button = $('');\n", - " button.click(function (evt) { fig.handle_close(fig, {}); } );\n", - " button.mouseover('Stop Interaction', toolbar_mouse_event);\n", - " buttongrp.append(button);\n", - " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n", - " titlebar.prepend(buttongrp);\n", - "}\n", - "\n", - "mpl.figure.prototype._root_extra_style = function(el){\n", - " var fig = this\n", - " el.on(\"remove\", function(){\n", - "\tfig.close_ws(fig, {});\n", - " });\n", - "}\n", - "\n", - "mpl.figure.prototype._canvas_extra_style = function(el){\n", - " // this is important to make the div 'focusable\n", - " el.attr('tabindex', 0)\n", - " // reach out to IPython and tell the keyboard manager to turn it's self\n", - " // off when our div gets focus\n", - "\n", - " // location in version 3\n", - " if (IPython.notebook.keyboard_manager) {\n", - " IPython.notebook.keyboard_manager.register_events(el);\n", - " }\n", - " else {\n", - " // location in version 2\n", - " IPython.keyboard_manager.register_events(el);\n", - " }\n", - "\n", - "}\n", - "\n", - "mpl.figure.prototype._key_event_extra = function(event, name) {\n", - " var manager = IPython.notebook.keyboard_manager;\n", - " if (!manager)\n", - " manager = IPython.keyboard_manager;\n", - "\n", - " // Check for shift+enter\n", - " if (event.shiftKey && event.which == 13) {\n", - " this.canvas_div.blur();\n", - " event.shiftKey = false;\n", - " // Send a \"J\" for go to next cell\n", - " event.which = 74;\n", - " event.keyCode = 74;\n", - " manager.command_mode();\n", - " manager.handle_keydown(event);\n", - " }\n", - "}\n", - "\n", - "mpl.figure.prototype.handle_save = function(fig, msg) {\n", - " fig.ondownload(fig, null);\n", - "}\n", - "\n", - "\n", - "mpl.find_output_cell = function(html_output) {\n", - " // Return the cell and output element which can be found *uniquely* in the notebook.\n", - " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n", - " // IPython event is triggered only after the cells have been serialised, which for\n", - " // our purposes (turning an active figure into a static one), is too late.\n", - " var cells = IPython.notebook.get_cells();\n", - " var ncells = cells.length;\n", - " for (var i=0; i= 3 moved mimebundle to data attribute of output\n", - " data = data.data;\n", - " }\n", - " if (data['text/html'] == html_output) {\n", - " return [cell, data, j];\n", - " }\n", - " }\n", - " }\n", - " }\n", - "}\n", - "\n", - "// Register the function which deals with the matplotlib target/channel.\n", - "// The kernel may be null if the page has been refreshed.\n", - "if (IPython.notebook.kernel != null) {\n", - " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n", - "}\n" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" + "name": "stderr", + "output_type": "stream", + "text": [ + "C:\\Users\\Simon\\Documents\\git\\qupulse\\qupulse\\plotting.py:186: UserWarning: Sample count 6288/5 is not an integer. Will be rounded (this changes the sample rate).\n", + " times, voltages, measurements = render(program,\n" + ] }, { "data": { - "text/html": [ - "" - ], + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAksAAAGwCAYAAAC5ACFFAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAACRS0lEQVR4nO3dd3xT9foH8E/SvQfdrFJWQcqGslSEXuYVuS70hyKIoCh6Ab0iXkFFBcGN1yuK4roO3FsEKkORDWXIXmV1UEo3XUl+fyTn5KTNOElOVvt5v159cZqcnHMS2ubJ9/t8n0el0+l0ICIiIiKz1J6+ACIiIiJvxmCJiIiIyAoGS0RERERWMFgiIiIisoLBEhEREZEVDJaIiIiIrGCwRERERGSFv6cvoCnQarW4cOECIiIioFKpPH05REREJINOp0N5eTlSUlKgVlseP2KwpIALFy6gdevWnr4MIiIicsDZs2fRqlUri/czWFJAREQEAP2LHRkZ6eGrISIiIjnKysrQunVr8X3cEgZLChCm3iIjIxksERER+RhbKTRM8CYiIiKygsESERERkRUMloiIiIisYM4SERG5lUajQV1dnacvg5qBgIAA+Pn5OX0cBktEROQWOp0O+fn5KCkp8fSlUDMSHR2NpKQkp+ogMlgiIiK3EAKlhIQEhIaGsogvuZROp0NVVRUKCwsBAMnJyQ4fi8ESERG5nEajEQOlFi1aePpyqJkICQkBABQWFiIhIcHhKTkmeBMRkcsJOUqhoaEevhJqboSfOWfy5BgsERGR23DqjdxNiZ85BktEREREVjBYIiIiIrKCwRIREZGDTp8+DZVKhZycHE9fiixDhw7FrFmz7HrMU089hZ49e9r1mMOHD2PAgAEIDg62+7HWOHL9SmCwRERERIp68sknERYWhiNHjiA7O9tl50lNTcWrr77qsuMLWDqAiIiIFHXixAmMHTsWbdu29fSlKIIjS0RE5BE6nQ5VtfUe+dLpdLKvU6vVYunSpejQoQOCgoLQpk0bPPfccyb7nDx5Etdddx1CQ0PRo0cPbNmyRbzv0qVLuP3229GyZUuEhoYiIyMDn376qcnjhw4dioceegiPPvooYmNjkZSUhKeeespkH5VKhXfeeQf/+Mc/EBoaio4dO+L777832efAgQMYPXo0wsPDkZiYiDvvvBNFRUWynysAPP/880hMTERERASmTp2K6urqRvu888476NKlC4KDg5Geno7//ve/Jte5a9cuLFy4ECqVSnwec+fORadOnRAaGoq0tDTMnz/fZDn/5MmTMX78eJPzzJo1C0OHDjV7nUOHDkVubi5mz54NlUrl0pWWHFkiIiKPuFKnQdcFv3rk3AcXjkRooLy3wHnz5mHFihV45ZVXMGTIEOTl5eHw4cMm+/z73//Giy++iI4dO+Lf//43br/9dhw/fhz+/v6orq5Gnz59MHfuXERGRuKnn37CnXfeifbt26N///7iMT744APMmTMH27Ztw5YtWzB58mQMHjwYf/vb38R9nn76aSxduhQvvPACXn/9dUycOBG5ubmIjY1FSUkJhg0bhnvuuQevvPIKrly5grlz5+LWW2/Fb7/9Juu5fv7553jqqafwxhtvYMiQIfjoo4+wbNkypKWlift8/PHHWLBgAf7zn/+gV69e2LNnD6ZNm4awsDDcddddyMvLQ1ZWFkaNGoVHHnkE4eHhAICIiAi8//77SElJwf79+zFt2jRERETg0UcflXVtDX399dfo0aMHpk+fjmnTpjl0DLl8amRp06ZNuP7665GSkgKVSoVvv/3W5mM2bNiA3r17IygoCB06dMD777/faJ833ngDqampCA4ORmZmJrZv3678xRMRkc8pLy/Ha6+9hqVLl+Kuu+5C+/btMWTIENxzzz0m+z3yyCMYO3YsOnXqhKeffhq5ubk4fvw4AKBly5Z45JFH0LNnT6SlpeHBBx/EqFGj8Pnnn5sco3v37njyySfRsWNHTJo0CX379m2U7zN58mTcfvvt6NChAxYtWoSKigrxPUsIXhYtWoT09HT06tULK1euxPr163H06FFZz/fVV1/F1KlTMXXqVHTu3BnPPvssunbtarLPk08+iZdeegk33ngj2rVrhxtvvBGzZ8/GW2+9BQBISkqCv78/wsPDkZSUJAZLTzzxBAYNGoTU1FRcf/31eOSRRxq9BvaIjY2Fn58fIiIikJSUhKSkJIePZYtPjSxVVlaiR48euPvuu3HjjTfa3P/UqVMYO3Ys7rvvPnz88cfIzs7GPffcg+TkZIwcORIAsGrVKsyZMwfLly9HZmYmXn31VYwcORJHjhxBQkKCq58SEVGzFRLgh4MLR3rs3HIcOnQINTU1GD58uNX9unfvLm4LPcgKCwuRnp4OjUaDRYsW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", "text/plain": [ - "" + "
" ] }, "metadata": {}, @@ -1637,22 +77,8 @@ } ], "metadata": { - "kernelspec": { - "display_name": "Python [default]", - "language": "python", - "name": "python3" - }, "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.0" + "name": "python" } }, "nbformat": 4, diff --git a/doc/source/examples/00MappingTemplate.ipynb b/doc/source/examples/00MappingTemplate.ipynb index d5dc11078..b9fb1f9d1 100644 --- a/doc/source/examples/00MappingTemplate.ipynb +++ b/doc/source/examples/00MappingTemplate.ipynb @@ -59,7 +59,6 @@ "output_type": "stream", "text": [ "we expect an exception here:\n", - "MissingMappingException : The template needs a mapping function for parameter(s) {'omega', 'a'}\n", "\n", "no exception with allow_partial_parameter_mapping=True\n", "2*pi/omega\n", @@ -139,8 +138,8 @@ "remapped_sine channels: {'sin_channel'}\n", "remapped_sine measurements: {'M_sin'}\n", "\n", - "{'sin_channel', 'cos_channel'}\n", - "{'M_cos', 'M_sin'}\n" + "{'cos_channel', 'sin_channel'}\n", + "{'M_sin', 'M_cos'}\n" ] } ], @@ -177,792 +176,18 @@ "metadata": {}, "outputs": [ { - "data": { - "application/javascript": [ - "/* Put everything inside the global mpl namespace */\n", - "window.mpl = {};\n", - "\n", - "\n", - "mpl.get_websocket_type = function() {\n", - " if (typeof(WebSocket) !== 'undefined') {\n", - " return WebSocket;\n", - " } else if (typeof(MozWebSocket) !== 'undefined') {\n", - " return MozWebSocket;\n", - " } else {\n", - " alert('Your browser does not have WebSocket support.' +\n", - " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", - " 'Firefox 4 and 5 are also supported but you ' +\n", - " 'have to enable WebSockets in about:config.');\n", - " };\n", - "}\n", - "\n", - "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n", - " this.id = figure_id;\n", - "\n", - " this.ws = websocket;\n", - "\n", - " this.supports_binary = (this.ws.binaryType != undefined);\n", - "\n", - " if (!this.supports_binary) {\n", - " var warnings = document.getElementById(\"mpl-warnings\");\n", - " if (warnings) {\n", - " warnings.style.display = 'block';\n", - " warnings.textContent = (\n", - " \"This browser does not support binary websocket messages. \" +\n", - " \"Performance may be slow.\");\n", - " }\n", - " }\n", - "\n", - " this.imageObj = new Image();\n", - "\n", - " this.context = undefined;\n", - " this.message = undefined;\n", - " this.canvas = undefined;\n", - " this.rubberband_canvas = undefined;\n", - " this.rubberband_context = undefined;\n", - " this.format_dropdown = undefined;\n", - "\n", - " this.image_mode = 'full';\n", - "\n", - " this.root = $('
');\n", - " this._root_extra_style(this.root)\n", - " this.root.attr('style', 'display: inline-block');\n", - "\n", - " $(parent_element).append(this.root);\n", - "\n", - " this._init_header(this);\n", - " this._init_canvas(this);\n", - " this._init_toolbar(this);\n", - "\n", - " var fig = this;\n", - "\n", - " this.waiting = false;\n", - "\n", - " this.ws.onopen = function () {\n", - " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n", - " fig.send_message(\"send_image_mode\", {});\n", - " if (mpl.ratio != 1) {\n", - " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n", - " }\n", - " fig.send_message(\"refresh\", {});\n", - " }\n", - "\n", - " this.imageObj.onload = function() {\n", - " if (fig.image_mode == 'full') {\n", - " // Full images could contain transparency (where diff images\n", - " // almost always do), so we need to clear the canvas so that\n", - " // there is no ghosting.\n", - " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", - " }\n", - " fig.context.drawImage(fig.imageObj, 0, 0);\n", - " };\n", - "\n", - " this.imageObj.onunload = function() {\n", - " fig.ws.close();\n", - " }\n", - "\n", - " this.ws.onmessage = this._make_on_message_function(this);\n", - "\n", - " this.ondownload = ondownload;\n", - "}\n", - "\n", - "mpl.figure.prototype._init_header = function() {\n", - " var titlebar = $(\n", - " '
');\n", - " var titletext = $(\n", - " '
');\n", - " titlebar.append(titletext)\n", - " this.root.append(titlebar);\n", - " this.header = titletext[0];\n", - "}\n", - "\n", - "\n", - "\n", - "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n", - "\n", - "}\n", - "\n", - "\n", - "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n", - "\n", - "}\n", - "\n", - "mpl.figure.prototype._init_canvas = function() {\n", - " var fig = this;\n", - "\n", - " var canvas_div = $('
');\n", - "\n", - " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n", - "\n", - " function canvas_keyboard_event(event) {\n", - " return fig.key_event(event, event['data']);\n", - " }\n", - "\n", - " canvas_div.keydown('key_press', canvas_keyboard_event);\n", - " canvas_div.keyup('key_release', canvas_keyboard_event);\n", - " this.canvas_div = canvas_div\n", - " this._canvas_extra_style(canvas_div)\n", - " this.root.append(canvas_div);\n", - "\n", - " var canvas = $('');\n", - " canvas.addClass('mpl-canvas');\n", - " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n", - "\n", - " this.canvas = canvas[0];\n", - " this.context = canvas[0].getContext(\"2d\");\n", - "\n", - " var backingStore = this.context.backingStorePixelRatio ||\n", - "\tthis.context.webkitBackingStorePixelRatio ||\n", - "\tthis.context.mozBackingStorePixelRatio ||\n", - "\tthis.context.msBackingStorePixelRatio ||\n", - "\tthis.context.oBackingStorePixelRatio ||\n", - "\tthis.context.backingStorePixelRatio || 1;\n", - "\n", - " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n", - "\n", - " var rubberband = $('');\n", - " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n", - "\n", - " var pass_mouse_events = true;\n", - "\n", - " canvas_div.resizable({\n", - " start: function(event, ui) {\n", - " pass_mouse_events = false;\n", - " },\n", - " resize: function(event, ui) {\n", - " fig.request_resize(ui.size.width, ui.size.height);\n", - " },\n", - " stop: function(event, ui) {\n", - " pass_mouse_events = true;\n", - " fig.request_resize(ui.size.width, ui.size.height);\n", - " },\n", - " });\n", - "\n", - " function mouse_event_fn(event) {\n", - " if (pass_mouse_events)\n", - " return fig.mouse_event(event, event['data']);\n", - " }\n", - "\n", - " rubberband.mousedown('button_press', mouse_event_fn);\n", - " rubberband.mouseup('button_release', mouse_event_fn);\n", - " // Throttle sequential mouse events to 1 every 20ms.\n", - " rubberband.mousemove('motion_notify', mouse_event_fn);\n", - "\n", - " rubberband.mouseenter('figure_enter', mouse_event_fn);\n", - " rubberband.mouseleave('figure_leave', mouse_event_fn);\n", - "\n", - " canvas_div.on(\"wheel\", function (event) {\n", - " event = event.originalEvent;\n", - " event['data'] = 'scroll'\n", - " if (event.deltaY < 0) {\n", - " event.step = 1;\n", - " } else {\n", - " event.step = -1;\n", - " }\n", - " mouse_event_fn(event);\n", - " });\n", - "\n", - " canvas_div.append(canvas);\n", - " canvas_div.append(rubberband);\n", - "\n", - " this.rubberband = rubberband;\n", - " this.rubberband_canvas = rubberband[0];\n", - " this.rubberband_context = rubberband[0].getContext(\"2d\");\n", - " this.rubberband_context.strokeStyle = \"#000000\";\n", - "\n", - " this._resize_canvas = function(width, height) {\n", - " // Keep the size of the canvas, canvas container, and rubber band\n", - " // canvas in synch.\n", - " canvas_div.css('width', width)\n", - " canvas_div.css('height', height)\n", - "\n", - " canvas.attr('width', width * mpl.ratio);\n", - " canvas.attr('height', height * mpl.ratio);\n", - " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n", - "\n", - " rubberband.attr('width', width);\n", - " rubberband.attr('height', height);\n", - " }\n", - "\n", - " // Set the figure to an initial 600x600px, this will subsequently be updated\n", - " // upon first draw.\n", - " this._resize_canvas(600, 600);\n", - "\n", - " // Disable right mouse context menu.\n", - " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n", - " return false;\n", - " });\n", - "\n", - " function set_focus () {\n", - " canvas.focus();\n", - " canvas_div.focus();\n", - " }\n", - "\n", - " window.setTimeout(set_focus, 100);\n", - "}\n", - "\n", - "mpl.figure.prototype._init_toolbar = function() {\n", - " var fig = this;\n", - "\n", - " var nav_element = $('
')\n", - " nav_element.attr('style', 'width: 100%');\n", - " this.root.append(nav_element);\n", - "\n", - " // Define a callback function for later on.\n", - " function toolbar_event(event) {\n", - " return fig.toolbar_button_onclick(event['data']);\n", - " }\n", - " function toolbar_mouse_event(event) {\n", - " return fig.toolbar_button_onmouseover(event['data']);\n", - " }\n", - "\n", - " for(var toolbar_ind in mpl.toolbar_items) {\n", - " var name = mpl.toolbar_items[toolbar_ind][0];\n", - " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", - " var image = mpl.toolbar_items[toolbar_ind][2];\n", - " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", - "\n", - " if (!name) {\n", - " // put a spacer in here.\n", - " continue;\n", - " }\n", - " var button = $('');\n", - " button.click(method_name, toolbar_event);\n", - " button.mouseover(tooltip, toolbar_mouse_event);\n", - " nav_element.append(button);\n", - " }\n", - "\n", - " // Add the status bar.\n", - " var status_bar = $('');\n", - " nav_element.append(status_bar);\n", - " this.message = status_bar[0];\n", - "\n", - " // Add the close button to the window.\n", - " var buttongrp = $('
');\n", - " var button = $('');\n", - " button.click(function (evt) { fig.handle_close(fig, {}); } );\n", - " button.mouseover('Stop Interaction', toolbar_mouse_event);\n", - " buttongrp.append(button);\n", - " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n", - " titlebar.prepend(buttongrp);\n", - "}\n", - "\n", - "mpl.figure.prototype._root_extra_style = function(el){\n", - " var fig = this\n", - " el.on(\"remove\", function(){\n", - "\tfig.close_ws(fig, {});\n", - " });\n", - "}\n", - "\n", - "mpl.figure.prototype._canvas_extra_style = function(el){\n", - " // this is important to make the div 'focusable\n", - " el.attr('tabindex', 0)\n", - " // reach out to IPython and tell the keyboard manager to turn it's self\n", - " // off when our div gets focus\n", - "\n", - " // location in version 3\n", - " if (IPython.notebook.keyboard_manager) {\n", - " IPython.notebook.keyboard_manager.register_events(el);\n", - " }\n", - " else {\n", - " // location in version 2\n", - " IPython.keyboard_manager.register_events(el);\n", - " }\n", - "\n", - "}\n", - "\n", - "mpl.figure.prototype._key_event_extra = function(event, name) {\n", - " var manager = IPython.notebook.keyboard_manager;\n", - " if (!manager)\n", - " manager = IPython.keyboard_manager;\n", - "\n", - " // Check for shift+enter\n", - " if (event.shiftKey && event.which == 13) {\n", - " this.canvas_div.blur();\n", - " event.shiftKey = false;\n", - " // Send a \"J\" for go to next cell\n", - " event.which = 74;\n", - " event.keyCode = 74;\n", - " manager.command_mode();\n", - " manager.handle_keydown(event);\n", - " }\n", - "}\n", - "\n", - "mpl.figure.prototype.handle_save = function(fig, msg) {\n", - " fig.ondownload(fig, null);\n", - "}\n", - "\n", - "\n", - "mpl.find_output_cell = function(html_output) {\n", - " // Return the cell and output element which can be found *uniquely* in the notebook.\n", - " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n", - " // IPython event is triggered only after the cells have been serialised, which for\n", - " // our purposes (turning an active figure into a static one), is too late.\n", - " var cells = IPython.notebook.get_cells();\n", - " var ncells = cells.length;\n", - " for (var i=0; i= 3 moved mimebundle to data attribute of output\n", - " data = data.data;\n", - " }\n", - " if (data['text/html'] == html_output) {\n", - " return [cell, data, j];\n", - " }\n", - " }\n", - " }\n", - " }\n", - "}\n", - "\n", - "// Register the function which deals with the matplotlib target/channel.\n", - "// The kernel may be null if the page has been refreshed.\n", - "if (IPython.notebook.kernel != null) {\n", - " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n", - "}\n" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" + "name": "stdout", + "output_type": "stream", + "text": [ + "The number of channels in table_template is 2.\n" + ] }, { "data": { - "text/html": [ - "" - ], + "image/png": "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9EKkq66IjFLaJc/ZfeXG8uXLBT8/P0EulwvFixcXAgMDhf3796uXly9fXmjTpk2W+w4MDFS/fvPmjTBs2DChdOnSgqWlpVC/fn3h5MmTWuulXb68adMmje3FxcUJAIQVK1Zo7MPHx0dr3yEhIUL58uU1+lJTU4Vp06YJPj4+6vfi7+8vTJgwQUhISNB4P7m5pFsQBGHSpElCmTJlBBMTE43Lu3fs2CFUr15dsLCwENzd3YVp06YJy5cv17oEPO3Y7d27V6hevbogl8sFLy8vrfee+ZLuNBcuXBDat28vlChRQpDL5UL58uWFTp06CeHh4e+t/d27d8KAAQOEUqVKCTKZTOv3YfHixYK/v79gaWkp2NraCr6+vsKIESOEe/fuadWfGQChX79+Gn1pP78ZM2ao+0JCQgRra2shNjZWaNGihWBlZSU4OzsLoaGhWpf/I9Ml3c+fPxd69uwplCxZUrCxsRGCgoKEa9euZfnzW7JkieDh4SGYmppqHceIiAghKChIsLe3FywsLISKFSsKPXr0EM6ePfveY0hUUDJB0NEsMyIiElWPHj2wefPmLE+hERUFnFNDREREksBQQ0RERJLAUENERESSwDk1REREJAkcqSEiIiJJYKghIiIiSShSN99TKpW4d+8ebG1t83RrdyIiIhKPIAh4+fIlXF1dYWKS/XhMkQo19+7dy/ZBe0RERFS43blzB2XLls12eZEKNba2tgBUB8XOzk7kaoiIiCg3EhMT4ebmpv4cz06RCjVpp5zs7OwYaoiIiIzM+6aOcKIwERERSQJDDREREUkCQw0RERFJAkMNERERSQJDDREREUkCQw0RERFJAkMNERERSQJDDREREUkCQw0RERFJAkMNERERSQJDDREREUkCQw0RERFJAkMNERERSQJDDREREUkCQw0RERFJAkMNERERSQJDDREREUkCQw0RERFJAkMNERERSQJDDREREUkCQw0RERFJAkMNERERSQJDDREREUkCQw0RERFJgtGGmqlTp0Imk2Hw4MFil0JERESFgFGGmsjISCxatAjVq1cXuxQiIiIqJIqJXUBeJSUl4YsvvsCSJUvw008/iV2OUXv8MgUp7xRil0FERsbFzgLFTI3y/4lJ4owu1PTr1w9t2rRBs2bN3htqUlJSkJKSon6dmJio7/KMxqoTtxC64x+xyyAiI1SjrD3+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", "text/plain": [ - "" + "
" ] }, "metadata": {}, "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The number of channels in table_template is 2.\n" - ] } ], "source": [ "from qupulse.pulses import TablePT\n", + "from qupulse.plotting import plot\n", "\n", "table_template = TablePT(identifier='2-channel-table-template',\n", " entries={'first_channel' : [(0, 0),\n", @@ -832,9 +51,6 @@ " (9, 'bar', 'linear')]}\n", " )\n", "\n", - "# plot it\n", - "%matplotlib notebook\n", - "from qupulse.pulses.plotting import plot\n", "parameters = dict(\n", " foo=7,\n", " bar=-1.3\n", @@ -860,804 +76,22 @@ "metadata": {}, "outputs": [ { - "data": { - "application/javascript": [ - "/* Put everything inside the global mpl namespace */\n", - "window.mpl = {};\n", - "\n", - "\n", - "mpl.get_websocket_type = function() {\n", - " if (typeof(WebSocket) !== 'undefined') {\n", - " return WebSocket;\n", - " } else if (typeof(MozWebSocket) !== 'undefined') {\n", - " return MozWebSocket;\n", - " } else {\n", - " alert('Your browser does not have WebSocket support.' +\n", - " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", - " 'Firefox 4 and 5 are also supported but you ' +\n", - " 'have to enable WebSockets in about:config.');\n", - " };\n", - "}\n", - "\n", - "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n", - " this.id = figure_id;\n", - "\n", - " this.ws = websocket;\n", - "\n", - " this.supports_binary = (this.ws.binaryType != undefined);\n", - "\n", - " if (!this.supports_binary) {\n", - " var warnings = document.getElementById(\"mpl-warnings\");\n", - " if (warnings) {\n", - " warnings.style.display = 'block';\n", - " warnings.textContent = (\n", - " \"This browser does not support binary websocket messages. \" +\n", - " \"Performance may be slow.\");\n", - " }\n", - " }\n", - "\n", - " this.imageObj = new Image();\n", - "\n", - " this.context = undefined;\n", - " this.message = undefined;\n", - " this.canvas = undefined;\n", - " this.rubberband_canvas = undefined;\n", - " this.rubberband_context = undefined;\n", - " this.format_dropdown = undefined;\n", - "\n", - " this.image_mode = 'full';\n", - "\n", - " this.root = $('
');\n", - " this._root_extra_style(this.root)\n", - " this.root.attr('style', 'display: inline-block');\n", - "\n", - " $(parent_element).append(this.root);\n", - "\n", - " this._init_header(this);\n", - " this._init_canvas(this);\n", - " this._init_toolbar(this);\n", - "\n", - " var fig = this;\n", - "\n", - " this.waiting = false;\n", - "\n", - " this.ws.onopen = function () {\n", - " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n", - " fig.send_message(\"send_image_mode\", {});\n", - " if (mpl.ratio != 1) {\n", - " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n", - " }\n", - " fig.send_message(\"refresh\", {});\n", - " }\n", - "\n", - " this.imageObj.onload = function() {\n", - " if (fig.image_mode == 'full') {\n", - " // Full images could contain transparency (where diff images\n", - " // almost always do), so we need to clear the canvas so that\n", - " // there is no ghosting.\n", - " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", - " }\n", - " fig.context.drawImage(fig.imageObj, 0, 0);\n", - " };\n", - "\n", - " this.imageObj.onunload = function() {\n", - " fig.ws.close();\n", - " }\n", - "\n", - " this.ws.onmessage = this._make_on_message_function(this);\n", - "\n", - " this.ondownload = ondownload;\n", - "}\n", - "\n", - "mpl.figure.prototype._init_header = function() {\n", - " var titlebar = $(\n", - " '
');\n", - " var titletext = $(\n", - " '
');\n", - " titlebar.append(titletext)\n", - " this.root.append(titlebar);\n", - " this.header = titletext[0];\n", - "}\n", - "\n", - "\n", - "\n", - "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n", - "\n", - "}\n", - "\n", - "\n", - "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n", - "\n", - "}\n", - "\n", - "mpl.figure.prototype._init_canvas = function() {\n", - " var fig = this;\n", - "\n", - " var canvas_div = $('
');\n", - "\n", - " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n", - "\n", - " function canvas_keyboard_event(event) {\n", - " return fig.key_event(event, event['data']);\n", - " }\n", - "\n", - " canvas_div.keydown('key_press', canvas_keyboard_event);\n", - " canvas_div.keyup('key_release', canvas_keyboard_event);\n", - " this.canvas_div = canvas_div\n", - " this._canvas_extra_style(canvas_div)\n", - " this.root.append(canvas_div);\n", - "\n", - " var canvas = $('');\n", - " canvas.addClass('mpl-canvas');\n", - " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n", - "\n", - " this.canvas = canvas[0];\n", - " this.context = canvas[0].getContext(\"2d\");\n", - "\n", - " var backingStore = this.context.backingStorePixelRatio ||\n", - "\tthis.context.webkitBackingStorePixelRatio ||\n", - "\tthis.context.mozBackingStorePixelRatio ||\n", - "\tthis.context.msBackingStorePixelRatio ||\n", - "\tthis.context.oBackingStorePixelRatio ||\n", - "\tthis.context.backingStorePixelRatio || 1;\n", - "\n", - " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n", - "\n", - " var rubberband = $('');\n", - " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n", - "\n", - " var pass_mouse_events = true;\n", - "\n", - " canvas_div.resizable({\n", - " start: function(event, ui) {\n", - " pass_mouse_events = false;\n", - " },\n", - " resize: function(event, ui) {\n", - " fig.request_resize(ui.size.width, ui.size.height);\n", - " },\n", - " stop: function(event, ui) {\n", - " pass_mouse_events = true;\n", - " fig.request_resize(ui.size.width, ui.size.height);\n", - " },\n", - " });\n", - "\n", - " function mouse_event_fn(event) {\n", - " if (pass_mouse_events)\n", - " return fig.mouse_event(event, event['data']);\n", - " }\n", - "\n", - " rubberband.mousedown('button_press', mouse_event_fn);\n", - " rubberband.mouseup('button_release', mouse_event_fn);\n", - " // Throttle sequential mouse events to 1 every 20ms.\n", - " rubberband.mousemove('motion_notify', mouse_event_fn);\n", - "\n", - " rubberband.mouseenter('figure_enter', mouse_event_fn);\n", - " rubberband.mouseleave('figure_leave', mouse_event_fn);\n", - "\n", - " canvas_div.on(\"wheel\", function (event) {\n", - " event = event.originalEvent;\n", - " event['data'] = 'scroll'\n", - " if (event.deltaY < 0) {\n", - " event.step = 1;\n", - " } else {\n", - " event.step = -1;\n", - " }\n", - " mouse_event_fn(event);\n", - " });\n", - "\n", - " canvas_div.append(canvas);\n", - " canvas_div.append(rubberband);\n", - "\n", - " this.rubberband = rubberband;\n", - " this.rubberband_canvas = rubberband[0];\n", - " this.rubberband_context = rubberband[0].getContext(\"2d\");\n", - " this.rubberband_context.strokeStyle = \"#000000\";\n", - "\n", - " this._resize_canvas = function(width, height) {\n", - " // Keep the size of the canvas, canvas container, and rubber band\n", - " // canvas in synch.\n", - " canvas_div.css('width', width)\n", - " canvas_div.css('height', height)\n", - "\n", - " canvas.attr('width', width * mpl.ratio);\n", - " canvas.attr('height', height * mpl.ratio);\n", - " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n", - "\n", - " rubberband.attr('width', width);\n", - " rubberband.attr('height', height);\n", - " }\n", - "\n", - " // Set the figure to an initial 600x600px, this will subsequently be updated\n", - " // upon first draw.\n", - " this._resize_canvas(600, 600);\n", - "\n", - " // Disable right mouse context menu.\n", - " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n", - " return false;\n", - " });\n", - "\n", - " function set_focus () {\n", - " canvas.focus();\n", - " canvas_div.focus();\n", - " }\n", - "\n", - " window.setTimeout(set_focus, 100);\n", - "}\n", - "\n", - "mpl.figure.prototype._init_toolbar = function() {\n", - " var fig = this;\n", - "\n", - " var nav_element = $('
')\n", - " nav_element.attr('style', 'width: 100%');\n", - " this.root.append(nav_element);\n", - "\n", - " // Define a callback function for later on.\n", - " function toolbar_event(event) {\n", - " return fig.toolbar_button_onclick(event['data']);\n", - " }\n", - " function toolbar_mouse_event(event) {\n", - " return fig.toolbar_button_onmouseover(event['data']);\n", - " }\n", - "\n", - " for(var toolbar_ind in mpl.toolbar_items) {\n", - " var name = mpl.toolbar_items[toolbar_ind][0];\n", - " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", - " var image = mpl.toolbar_items[toolbar_ind][2];\n", - " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", - "\n", - " if (!name) {\n", - " // put a spacer in here.\n", - " continue;\n", - " }\n", - " var button = $('');\n", - " button.click(method_name, toolbar_event);\n", - " button.mouseover(tooltip, toolbar_mouse_event);\n", - " nav_element.append(button);\n", - " }\n", - "\n", - " // Add the status bar.\n", - " var status_bar = $('');\n", - " nav_element.append(status_bar);\n", - " this.message = status_bar[0];\n", - "\n", - " // Add the close button to the window.\n", - " var buttongrp = $('
');\n", - " var button = $('');\n", - " button.click(function (evt) { fig.handle_close(fig, {}); } );\n", - " button.mouseover('Stop Interaction', toolbar_mouse_event);\n", - " buttongrp.append(button);\n", - " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n", - " titlebar.prepend(buttongrp);\n", - "}\n", - "\n", - "mpl.figure.prototype._root_extra_style = function(el){\n", - " var fig = this\n", - " el.on(\"remove\", function(){\n", - "\tfig.close_ws(fig, {});\n", - " });\n", - "}\n", - "\n", - "mpl.figure.prototype._canvas_extra_style = function(el){\n", - " // this is important to make the div 'focusable\n", - " el.attr('tabindex', 0)\n", - " // reach out to IPython and tell the keyboard manager to turn it's self\n", - " // off when our div gets focus\n", - "\n", - " // location in version 3\n", - " if (IPython.notebook.keyboard_manager) {\n", - " IPython.notebook.keyboard_manager.register_events(el);\n", - " }\n", - " else {\n", - " // location in version 2\n", - " IPython.keyboard_manager.register_events(el);\n", - " }\n", - "\n", - "}\n", - "\n", - "mpl.figure.prototype._key_event_extra = function(event, name) {\n", - " var manager = IPython.notebook.keyboard_manager;\n", - " if (!manager)\n", - " manager = IPython.keyboard_manager;\n", - "\n", - " // Check for shift+enter\n", - " if (event.shiftKey && event.which == 13) {\n", - " this.canvas_div.blur();\n", - " event.shiftKey = false;\n", - " // Send a \"J\" for go to next cell\n", - " event.which = 74;\n", - " event.keyCode = 74;\n", - " manager.command_mode();\n", - " manager.handle_keydown(event);\n", - " }\n", - "}\n", - "\n", - "mpl.figure.prototype.handle_save = function(fig, msg) {\n", - " fig.ondownload(fig, null);\n", - "}\n", - "\n", - "\n", - "mpl.find_output_cell = function(html_output) {\n", - " // Return the cell and output element which can be found *uniquely* in the notebook.\n", - " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n", - " // IPython event is triggered only after the cells have been serialised, which for\n", - " // our purposes (turning an active figure into a static one), is too late.\n", - " var cells = IPython.notebook.get_cells();\n", - " var ncells = cells.length;\n", - " for (var i=0; i= 3 moved mimebundle to data attribute of output\n", - " data = data.data;\n", - " }\n", - " if (data['text/html'] == html_output) {\n", - " return [cell, data, j];\n", - " }\n", - " }\n", - " }\n", - " }\n", - "}\n", - "\n", - "// Register the function which deals with the matplotlib target/channel.\n", - "// The kernel may be null if the page has been refreshed.\n", - "if (IPython.notebook.kernel != null) {\n", - " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n", - "}\n" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" + "name": "stdout", + "output_type": "stream", + "text": [ + "The number of channels in sequence_template is 2.\n" + ] }, { "data": { - "text/html": [ - "" - ], + "image/png": "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", "text/plain": [ - "" + "
" ] }, "metadata": {}, "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The number of channels in sequence_template is 2.\n" - ] } ], "source": [ @@ -2590,22 +221,8 @@ } ], "metadata": { - "kernelspec": { - "display_name": "Python [default]", - "language": "python", - "name": "python3" - }, "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.0" + "name": "python" } }, "nbformat": 4, diff --git a/doc/source/examples/00PointPulse.ipynb b/doc/source/examples/00PointPulse.ipynb index 2eff02f84..e99cbc0a9 100644 --- a/doc/source/examples/00PointPulse.ipynb +++ b/doc/source/examples/00PointPulse.ipynb @@ -49,791 +49,9 @@ "outputs": [ { "data": { - "application/javascript": [ - "/* Put everything inside the global mpl namespace */\n", - "window.mpl = {};\n", - "\n", - "\n", - "mpl.get_websocket_type = function() {\n", - " if (typeof(WebSocket) !== 'undefined') {\n", - " return WebSocket;\n", - " } else if (typeof(MozWebSocket) !== 'undefined') {\n", - " return MozWebSocket;\n", - " } else {\n", - " alert('Your browser does not have WebSocket support.' +\n", - " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", - " 'Firefox 4 and 5 are also supported but you ' +\n", - " 'have to enable WebSockets in about:config.');\n", - " };\n", - "}\n", - "\n", - "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n", - " this.id = figure_id;\n", - "\n", - " this.ws = websocket;\n", - "\n", - " this.supports_binary = (this.ws.binaryType != undefined);\n", - "\n", - " if (!this.supports_binary) {\n", - " var warnings = document.getElementById(\"mpl-warnings\");\n", - " if (warnings) {\n", - " warnings.style.display = 'block';\n", - " warnings.textContent = (\n", - " \"This browser does not support binary websocket messages. \" +\n", - " \"Performance may be slow.\");\n", - " }\n", - " }\n", - "\n", - " this.imageObj = new Image();\n", - "\n", - " this.context = undefined;\n", - " this.message = undefined;\n", - " this.canvas = undefined;\n", - " this.rubberband_canvas = undefined;\n", - " this.rubberband_context = undefined;\n", - " this.format_dropdown = undefined;\n", - "\n", - " this.image_mode = 'full';\n", - "\n", - " this.root = $('
');\n", - " this._root_extra_style(this.root)\n", - " this.root.attr('style', 'display: inline-block');\n", - "\n", - " $(parent_element).append(this.root);\n", - "\n", - " this._init_header(this);\n", - " this._init_canvas(this);\n", - " this._init_toolbar(this);\n", - "\n", - " var fig = this;\n", - "\n", - " this.waiting = false;\n", - "\n", - " this.ws.onopen = function () {\n", - " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n", - " fig.send_message(\"send_image_mode\", {});\n", - " if (mpl.ratio != 1) {\n", - " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n", - " }\n", - " fig.send_message(\"refresh\", {});\n", - " }\n", - "\n", - " this.imageObj.onload = function() {\n", - " if (fig.image_mode == 'full') {\n", - " // Full images could contain transparency (where diff images\n", - " // almost always do), so we need to clear the canvas so that\n", - " // there is no ghosting.\n", - " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", - " }\n", - " fig.context.drawImage(fig.imageObj, 0, 0);\n", - " };\n", - "\n", - " this.imageObj.onunload = function() {\n", - " fig.ws.close();\n", - " }\n", - "\n", - " this.ws.onmessage = this._make_on_message_function(this);\n", - "\n", - " this.ondownload = ondownload;\n", - "}\n", - "\n", - "mpl.figure.prototype._init_header = function() {\n", - " var titlebar = $(\n", - " '
');\n", - " var titletext = $(\n", - " '
');\n", - " titlebar.append(titletext)\n", - " this.root.append(titlebar);\n", - " this.header = titletext[0];\n", - "}\n", - "\n", - "\n", - "\n", - "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n", - "\n", - "}\n", - "\n", - "\n", - "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n", - "\n", - "}\n", - "\n", - "mpl.figure.prototype._init_canvas = function() {\n", - " var fig = this;\n", - "\n", - " var canvas_div = $('
');\n", - "\n", - " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n", - "\n", - " function canvas_keyboard_event(event) {\n", - " return fig.key_event(event, event['data']);\n", - " }\n", - "\n", - " canvas_div.keydown('key_press', canvas_keyboard_event);\n", - " canvas_div.keyup('key_release', canvas_keyboard_event);\n", - " this.canvas_div = canvas_div\n", - " this._canvas_extra_style(canvas_div)\n", - " this.root.append(canvas_div);\n", - "\n", - " var canvas = $('');\n", - " canvas.addClass('mpl-canvas');\n", - " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n", - "\n", - " this.canvas = canvas[0];\n", - " this.context = canvas[0].getContext(\"2d\");\n", - "\n", - " var backingStore = this.context.backingStorePixelRatio ||\n", - "\tthis.context.webkitBackingStorePixelRatio ||\n", - "\tthis.context.mozBackingStorePixelRatio ||\n", - "\tthis.context.msBackingStorePixelRatio ||\n", - "\tthis.context.oBackingStorePixelRatio ||\n", - "\tthis.context.backingStorePixelRatio || 1;\n", - "\n", - " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n", - "\n", - " var rubberband = $('');\n", - " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n", - "\n", - " var pass_mouse_events = true;\n", - "\n", - " canvas_div.resizable({\n", - " start: function(event, ui) {\n", - " pass_mouse_events = false;\n", - " },\n", - " resize: function(event, ui) {\n", - " fig.request_resize(ui.size.width, ui.size.height);\n", - " },\n", - " stop: function(event, ui) {\n", - " pass_mouse_events = true;\n", - " fig.request_resize(ui.size.width, ui.size.height);\n", - " },\n", - " });\n", - "\n", - " function mouse_event_fn(event) {\n", - " if (pass_mouse_events)\n", - " return fig.mouse_event(event, event['data']);\n", - " }\n", - "\n", - " rubberband.mousedown('button_press', mouse_event_fn);\n", - " rubberband.mouseup('button_release', mouse_event_fn);\n", - " // Throttle sequential mouse events to 1 every 20ms.\n", - " rubberband.mousemove('motion_notify', mouse_event_fn);\n", - "\n", - " rubberband.mouseenter('figure_enter', mouse_event_fn);\n", - " rubberband.mouseleave('figure_leave', mouse_event_fn);\n", - "\n", - " canvas_div.on(\"wheel\", function (event) {\n", - " event = event.originalEvent;\n", - " event['data'] = 'scroll'\n", - " if (event.deltaY < 0) {\n", - " event.step = 1;\n", - " } else {\n", - " event.step = -1;\n", - " }\n", - " mouse_event_fn(event);\n", - " });\n", - "\n", - " canvas_div.append(canvas);\n", - " canvas_div.append(rubberband);\n", - "\n", - " this.rubberband = rubberband;\n", - " this.rubberband_canvas = rubberband[0];\n", - " this.rubberband_context = rubberband[0].getContext(\"2d\");\n", - " this.rubberband_context.strokeStyle = \"#000000\";\n", - "\n", - " this._resize_canvas = function(width, height) {\n", - " // Keep the size of the canvas, canvas container, and rubber band\n", - " // canvas in synch.\n", - " canvas_div.css('width', width)\n", - " canvas_div.css('height', height)\n", - "\n", - " canvas.attr('width', width * mpl.ratio);\n", - " canvas.attr('height', height * mpl.ratio);\n", - " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n", - "\n", - " rubberband.attr('width', width);\n", - " rubberband.attr('height', height);\n", - " }\n", - "\n", - " // Set the figure to an initial 600x600px, this will subsequently be updated\n", - " // upon first draw.\n", - " this._resize_canvas(600, 600);\n", - "\n", - " // Disable right mouse context menu.\n", - " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n", - " return false;\n", - " });\n", - "\n", - " function set_focus () {\n", - " canvas.focus();\n", - " canvas_div.focus();\n", - " }\n", - "\n", - " window.setTimeout(set_focus, 100);\n", - "}\n", - "\n", - "mpl.figure.prototype._init_toolbar = function() {\n", - " var fig = this;\n", - "\n", - " var nav_element = $('
')\n", - " nav_element.attr('style', 'width: 100%');\n", - " this.root.append(nav_element);\n", - "\n", - " // Define a callback function for later on.\n", - " function toolbar_event(event) {\n", - " return fig.toolbar_button_onclick(event['data']);\n", - " }\n", - " function toolbar_mouse_event(event) {\n", - " return fig.toolbar_button_onmouseover(event['data']);\n", - " }\n", - "\n", - " for(var toolbar_ind in mpl.toolbar_items) {\n", - " var name = mpl.toolbar_items[toolbar_ind][0];\n", - " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", - " var image = mpl.toolbar_items[toolbar_ind][2];\n", - " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", - "\n", - " if (!name) {\n", - " // put a spacer in here.\n", - " continue;\n", - " }\n", - " var button = $('');\n", - " button.click(method_name, toolbar_event);\n", - " button.mouseover(tooltip, toolbar_mouse_event);\n", - " nav_element.append(button);\n", - " }\n", - "\n", - " // Add the status bar.\n", - " var status_bar = $('');\n", - " nav_element.append(status_bar);\n", - " this.message = status_bar[0];\n", - "\n", - " // Add the close button to the window.\n", - " var buttongrp = $('
');\n", - " var button = $('');\n", - " button.click(function (evt) { fig.handle_close(fig, {}); } );\n", - " button.mouseover('Stop Interaction', toolbar_mouse_event);\n", - " buttongrp.append(button);\n", - " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n", - " titlebar.prepend(buttongrp);\n", - "}\n", - "\n", - "mpl.figure.prototype._root_extra_style = function(el){\n", - " var fig = this\n", - " el.on(\"remove\", function(){\n", - "\tfig.close_ws(fig, {});\n", - " });\n", - "}\n", - "\n", - "mpl.figure.prototype._canvas_extra_style = function(el){\n", - " // this is important to make the div 'focusable\n", - " el.attr('tabindex', 0)\n", - " // reach out to IPython and tell the keyboard manager to turn it's self\n", - " // off when our div gets focus\n", - "\n", - " // location in version 3\n", - " if (IPython.notebook.keyboard_manager) {\n", - " IPython.notebook.keyboard_manager.register_events(el);\n", - " }\n", - " else {\n", - " // location in version 2\n", - " IPython.keyboard_manager.register_events(el);\n", - " }\n", - "\n", - "}\n", - "\n", - "mpl.figure.prototype._key_event_extra = function(event, name) {\n", - " var manager = IPython.notebook.keyboard_manager;\n", - " if (!manager)\n", - " manager = IPython.keyboard_manager;\n", - "\n", - " // Check for shift+enter\n", - " if (event.shiftKey && event.which == 13) {\n", - " this.canvas_div.blur();\n", - " event.shiftKey = false;\n", - " // Send a \"J\" for go to next cell\n", - " event.which = 74;\n", - " event.keyCode = 74;\n", - " manager.command_mode();\n", - " manager.handle_keydown(event);\n", - " }\n", - "}\n", - "\n", - "mpl.figure.prototype.handle_save = function(fig, msg) {\n", - " fig.ondownload(fig, null);\n", - "}\n", - "\n", - "\n", - "mpl.find_output_cell = function(html_output) {\n", - " // Return the cell and output element which can be found *uniquely* in the notebook.\n", - " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n", - " // IPython event is triggered only after the cells have been serialised, which for\n", - " // our purposes (turning an active figure into a static one), is too late.\n", - " var cells = IPython.notebook.get_cells();\n", - " var ncells = cells.length;\n", - " for (var i=0; i= 3 moved mimebundle to data attribute of output\n", - " data = data.data;\n", - " }\n", - " if (data['text/html'] == html_output) {\n", - " return [cell, data, j];\n", - " }\n", - " }\n", - " }\n", - " }\n", - "}\n", - "\n", - "// Register the function which deals with the matplotlib target/channel.\n", - "// The kernel may be null if the page has been refreshed.\n", - "if (IPython.notebook.kernel != null) {\n", - " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n", - "}\n" - ], + "image/png": "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", "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "" - ], - "text/plain": [ - "" + "
" ] }, "metadata": {}, @@ -890,8 +108,6 @@ } ], "source": [ - "%matplotlib notebook\n", - "\n", "import json\n", "from qupulse.pulses.plotting import plot\n", "\n", @@ -903,22 +119,8 @@ } ], "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.2" + "name": "python" } }, "nbformat": 4, diff --git a/doc/source/examples/00SimpleTablePulse.ipynb b/doc/source/examples/00SimpleTablePulse.ipynb index 3da941c64..6c7c313c5 100644 --- a/doc/source/examples/00SimpleTablePulse.ipynb +++ b/doc/source/examples/00SimpleTablePulse.ipynb @@ -65,791 +65,9 @@ "outputs": [ { "data": { - "application/javascript": [ - "/* Put everything inside the global mpl namespace */\n", - "window.mpl = {};\n", - "\n", - "\n", - "mpl.get_websocket_type = function() {\n", - " if (typeof(WebSocket) !== 'undefined') {\n", - " return WebSocket;\n", - " } else if (typeof(MozWebSocket) !== 'undefined') {\n", - " return MozWebSocket;\n", - " } else {\n", - " alert('Your browser does not have WebSocket support.' +\n", - " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", - " 'Firefox 4 and 5 are also supported but you ' +\n", - " 'have to enable WebSockets in about:config.');\n", - " };\n", - "}\n", - "\n", - "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n", - " this.id = figure_id;\n", - "\n", - " this.ws = websocket;\n", - "\n", - " this.supports_binary = (this.ws.binaryType != undefined);\n", - "\n", - " if (!this.supports_binary) {\n", - " var warnings = document.getElementById(\"mpl-warnings\");\n", - " if (warnings) {\n", - " warnings.style.display = 'block';\n", - " warnings.textContent = (\n", - " \"This browser does not support binary websocket messages. \" +\n", - " \"Performance may be slow.\");\n", - " }\n", - " }\n", - "\n", - " this.imageObj = new Image();\n", - "\n", - " this.context = undefined;\n", - " this.message = undefined;\n", - " this.canvas = undefined;\n", - " this.rubberband_canvas = undefined;\n", - " this.rubberband_context = undefined;\n", - " this.format_dropdown = undefined;\n", - "\n", - " this.image_mode = 'full';\n", - "\n", - " this.root = $('
');\n", - " this._root_extra_style(this.root)\n", - " this.root.attr('style', 'display: inline-block');\n", - "\n", - " $(parent_element).append(this.root);\n", - "\n", - " this._init_header(this);\n", - " this._init_canvas(this);\n", - " this._init_toolbar(this);\n", - "\n", - " var fig = this;\n", - "\n", - " this.waiting = false;\n", - "\n", - " this.ws.onopen = function () {\n", - " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n", - " fig.send_message(\"send_image_mode\", {});\n", - " if (mpl.ratio != 1) {\n", - " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n", - " }\n", - " fig.send_message(\"refresh\", {});\n", - " }\n", - "\n", - " this.imageObj.onload = function() {\n", - " if (fig.image_mode == 'full') {\n", - " // Full images could contain transparency (where diff images\n", - " // almost always do), so we need to clear the canvas so that\n", - " // there is no ghosting.\n", - " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", - " }\n", - " fig.context.drawImage(fig.imageObj, 0, 0);\n", - " };\n", - "\n", - " this.imageObj.onunload = function() {\n", - " fig.ws.close();\n", - " }\n", - "\n", - " this.ws.onmessage = this._make_on_message_function(this);\n", - "\n", - " this.ondownload = ondownload;\n", - "}\n", - "\n", - "mpl.figure.prototype._init_header = function() {\n", - " var titlebar = $(\n", - " '
');\n", - " var titletext = $(\n", - " '
');\n", - " titlebar.append(titletext)\n", - " this.root.append(titlebar);\n", - " this.header = titletext[0];\n", - "}\n", - "\n", - "\n", - "\n", - "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n", - "\n", - "}\n", - "\n", - "\n", - "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n", - "\n", - "}\n", - "\n", - "mpl.figure.prototype._init_canvas = function() {\n", - " var fig = this;\n", - "\n", - " var canvas_div = $('
');\n", - "\n", - " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n", - "\n", - " function canvas_keyboard_event(event) {\n", - " return fig.key_event(event, event['data']);\n", - " }\n", - "\n", - " canvas_div.keydown('key_press', canvas_keyboard_event);\n", - " canvas_div.keyup('key_release', canvas_keyboard_event);\n", - " this.canvas_div = canvas_div\n", - " this._canvas_extra_style(canvas_div)\n", - " this.root.append(canvas_div);\n", - "\n", - " var canvas = $('');\n", - " canvas.addClass('mpl-canvas');\n", - " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n", - "\n", - " this.canvas = canvas[0];\n", - " this.context = canvas[0].getContext(\"2d\");\n", - "\n", - " var backingStore = this.context.backingStorePixelRatio ||\n", - "\tthis.context.webkitBackingStorePixelRatio ||\n", - "\tthis.context.mozBackingStorePixelRatio ||\n", - "\tthis.context.msBackingStorePixelRatio ||\n", - "\tthis.context.oBackingStorePixelRatio ||\n", - "\tthis.context.backingStorePixelRatio || 1;\n", - "\n", - " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n", - "\n", - " var rubberband = $('');\n", - " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n", - "\n", - " var pass_mouse_events = true;\n", - "\n", - " canvas_div.resizable({\n", - " start: function(event, ui) {\n", - " pass_mouse_events = false;\n", - " },\n", - " resize: function(event, ui) {\n", - " fig.request_resize(ui.size.width, ui.size.height);\n", - " },\n", - " stop: function(event, ui) {\n", - " pass_mouse_events = true;\n", - " fig.request_resize(ui.size.width, ui.size.height);\n", - " },\n", - " });\n", - "\n", - " function mouse_event_fn(event) {\n", - " if (pass_mouse_events)\n", - " return fig.mouse_event(event, event['data']);\n", - " }\n", - "\n", - " rubberband.mousedown('button_press', mouse_event_fn);\n", - " rubberband.mouseup('button_release', mouse_event_fn);\n", - " // Throttle sequential mouse events to 1 every 20ms.\n", - " rubberband.mousemove('motion_notify', mouse_event_fn);\n", - "\n", - " rubberband.mouseenter('figure_enter', mouse_event_fn);\n", - " rubberband.mouseleave('figure_leave', mouse_event_fn);\n", - "\n", - " canvas_div.on(\"wheel\", function (event) {\n", - " event = event.originalEvent;\n", - " event['data'] = 'scroll'\n", - " if (event.deltaY < 0) {\n", - " event.step = 1;\n", - " } else {\n", - " event.step = -1;\n", - " }\n", - " mouse_event_fn(event);\n", - " });\n", - "\n", - " canvas_div.append(canvas);\n", - " canvas_div.append(rubberband);\n", - "\n", - " this.rubberband = rubberband;\n", - " this.rubberband_canvas = rubberband[0];\n", - " this.rubberband_context = rubberband[0].getContext(\"2d\");\n", - " this.rubberband_context.strokeStyle = \"#000000\";\n", - "\n", - " this._resize_canvas = function(width, height) {\n", - " // Keep the size of the canvas, canvas container, and rubber band\n", - " // canvas in synch.\n", - " canvas_div.css('width', width)\n", - " canvas_div.css('height', height)\n", - "\n", - " canvas.attr('width', width * mpl.ratio);\n", - " canvas.attr('height', height * mpl.ratio);\n", - " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n", - "\n", - " rubberband.attr('width', width);\n", - " rubberband.attr('height', height);\n", - " }\n", - "\n", - " // Set the figure to an initial 600x600px, this will subsequently be updated\n", - " // upon first draw.\n", - " this._resize_canvas(600, 600);\n", - "\n", - " // Disable right mouse context menu.\n", - " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n", - " return false;\n", - " });\n", - "\n", - " function set_focus () {\n", - " canvas.focus();\n", - " canvas_div.focus();\n", - " }\n", - "\n", - " window.setTimeout(set_focus, 100);\n", - "}\n", - "\n", - "mpl.figure.prototype._init_toolbar = function() {\n", - " var fig = this;\n", - "\n", - " var nav_element = $('
')\n", - " nav_element.attr('style', 'width: 100%');\n", - " this.root.append(nav_element);\n", - "\n", - " // Define a callback function for later on.\n", - " function toolbar_event(event) {\n", - " return fig.toolbar_button_onclick(event['data']);\n", - " }\n", - " function toolbar_mouse_event(event) {\n", - " return fig.toolbar_button_onmouseover(event['data']);\n", - " }\n", - "\n", - " for(var toolbar_ind in mpl.toolbar_items) {\n", - " var name = mpl.toolbar_items[toolbar_ind][0];\n", - " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", - " var image = mpl.toolbar_items[toolbar_ind][2];\n", - " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", - "\n", - " if (!name) {\n", - " // put a spacer in here.\n", - " continue;\n", - " }\n", - " var button = $('');\n", - " button.click(method_name, toolbar_event);\n", - " button.mouseover(tooltip, toolbar_mouse_event);\n", - " nav_element.append(button);\n", - " }\n", - "\n", - " // Add the status bar.\n", - " var status_bar = $('');\n", - " nav_element.append(status_bar);\n", - " this.message = status_bar[0];\n", - "\n", - " // Add the close button to the window.\n", - " var buttongrp = $('
');\n", - " var button = $('');\n", - " button.click(function (evt) { fig.handle_close(fig, {}); } );\n", - " button.mouseover('Stop Interaction', toolbar_mouse_event);\n", - " buttongrp.append(button);\n", - " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n", - " titlebar.prepend(buttongrp);\n", - "}\n", - "\n", - "mpl.figure.prototype._root_extra_style = function(el){\n", - " var fig = this\n", - " el.on(\"remove\", function(){\n", - "\tfig.close_ws(fig, {});\n", - " });\n", - "}\n", - "\n", - "mpl.figure.prototype._canvas_extra_style = function(el){\n", - " // this is important to make the div 'focusable\n", - " el.attr('tabindex', 0)\n", - " // reach out to IPython and tell the keyboard manager to turn it's self\n", - " // off when our div gets focus\n", - "\n", - " // location in version 3\n", - " if (IPython.notebook.keyboard_manager) {\n", - " IPython.notebook.keyboard_manager.register_events(el);\n", - " }\n", - " else {\n", - " // location in version 2\n", - " IPython.keyboard_manager.register_events(el);\n", - " }\n", - "\n", - "}\n", - "\n", - "mpl.figure.prototype._key_event_extra = function(event, name) {\n", - " var manager = IPython.notebook.keyboard_manager;\n", - " if (!manager)\n", - " manager = IPython.keyboard_manager;\n", - "\n", - " // Check for shift+enter\n", - " if (event.shiftKey && event.which == 13) {\n", - " this.canvas_div.blur();\n", - " event.shiftKey = false;\n", - " // Send a \"J\" for go to next cell\n", - " event.which = 74;\n", - " event.keyCode = 74;\n", - " manager.command_mode();\n", - " manager.handle_keydown(event);\n", - " }\n", - "}\n", - "\n", - "mpl.figure.prototype.handle_save = function(fig, msg) {\n", - " fig.ondownload(fig, null);\n", - "}\n", - "\n", - "\n", - "mpl.find_output_cell = function(html_output) {\n", - " // Return the cell and output element which can be found *uniquely* in the notebook.\n", - " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n", - " // IPython event is triggered only after the cells have been serialised, which for\n", - " // our purposes (turning an active figure into a static one), is too late.\n", - " var cells = IPython.notebook.get_cells();\n", - " var ncells = cells.length;\n", - " for (var i=0; i= 3 moved mimebundle to data attribute of output\n", - " data = data.data;\n", - " }\n", - " if (data['text/html'] == html_output) {\n", - " return [cell, data, j];\n", - " }\n", - " }\n", - " }\n", - " }\n", - "}\n", - "\n", - "// Register the function which deals with the matplotlib target/channel.\n", - "// The kernel may be null if the page has been refreshed.\n", - "if (IPython.notebook.kernel != null) {\n", - " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n", - "}\n" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "" - ], + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjcAAAGwCAYAAABVdURTAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAAA5f0lEQVR4nO3de1xUdeL/8TcgVwW8clHxCok3EMULamlqmvm12G+Z63dLNG3T1Uqpr34xV6vdlcrMtFzNyrXLmlqmtVYqkpdUzCulmZlXvADeQVBBYX5/9HO2WcFmcOAMh9fz8ZjHg/nMOWfeMxW8O+czn3GzWCwWAQAAmIS70QEAAACciXIDAABMhXIDAABMhXIDAABMhXIDAABMhXIDAABMhXIDAABMpZrRASpacXGxTp06JX9/f7m5uRkdBwAA2MFisejSpUuqX7++3N1vfW6mypWbU6dOKSwszOgYAACgDI4fP66GDRvecpsqV278/f0l/fLmBAQEGJwGAADYIzc3V2FhYda/47dS5crNjUtRAQEBlBsAACoZe6aUMKEYAACYCuUGAACYCuUGAACYSpWbcwMAMJeioiJdu3bN6BhwAi8vr9/8mLc9KDcAgErJYrEoKytLFy9eNDoKnMTd3V1NmzaVl5fXbR2HcgMAqJRuFJugoCD5+fmxMGsld2OR3czMTDVq1Oi2/nlSbgAAlU5RUZG12NSpU8foOHCSevXq6dSpU7p+/bo8PT3LfBwmFAMAKp0bc2z8/PwMTgJnunE5qqio6LaOQ7kBAFRaXIoyF2f986TcAAAAU6HcAAAAU6HcAADgIo4ePSo3Nzelp6cbHcUuPXv21Lhx44yOcRPKDQAAKDfr169X+/bt5e3trfDwcC1cuLDcn5NyAwAAysWRI0c0YMAA3X333UpPT9e4ceM0cuRIrV69ulyfl3IDADAFi8Wiy4XXDblZLBa7cxYXF+uVV15ReHi4vL291ahRI/3tb3+z2ebw4cO6++675efnp+joaKWlpVkfO3funIYMGaIGDRrIz89Pbdu21UcffWSzf8+ePfXUU09pwoQJql27tkJCQvT888/bbOPm5qZ33nlHv/vd7+Tn56eIiAh9/vnnNtvs3btX/fv3V40aNRQcHKxHH31UZ8+etfu1zps3T02bNtWMGTPUsmVLjR07Vg899JBmzpxp9zHKgkX8AACmcOVakVpNKd8zAqXZ92I/+XnZ9yc1KSlJb7/9tmbOnKnu3bsrMzNT+/fvt9nmueee06uvvqqIiAg999xzGjJkiA4ePKhq1arp6tWr6tChgyZOnKiAgAB98cUXevTRR9W8eXN16tTJeoz33ntPiYmJ+vbbb5WWlqZhw4apW7duuueee6zbvPDCC3rllVc0ffp0vfHGG/rDH/6gY8eOqXbt2rp48aJ69eqlkSNHaubMmbpy5YomTpyohx9+WF9//bVdrzUtLU19+vSxGevXr1+5z9Oh3AAAUEEuXbqkWbNm6c0331RCQoIkqXnz5urevbvNds8++6wGDBgg6ZcC0rp1ax08eFCRkZFq0KCBnn32Weu2Tz75pFavXq2lS5falJuoqChNnTpVkhQREaE333xTqampNuVm2LBhGjJkiCRp2rRpmj17trZt26Z7771Xb775pmJiYjRt2jTr9gsWLFBYWJgOHDigO+644zdfb1ZWloKDg23GgoODlZubqytXrsjX19eu981RlBsAgCn4enpo34v9DHtue/z4448qKChQ7969b7ldVFSU9efQ0FBJ0unTpxUZGamioiJNmzZNS5cu1cmTJ1VYWKiCgoKbVmv+9TFuHOf06dOlblO9enUFBARYt/nuu++0bt061ahR46Z8hw4dsqvcGIVyAwAwBTc3N7svDRnF3jMVv/5epRur9hYXF0uSpk+frlmzZun1119X27ZtVb16dY0bN06FhYWlHuPGcW4cw55t8vLyNHDgQL388ss35btRuH5LSEiIsrOzbcays7MVEBBQbmdtJMoNAAAVJiIiQr6+vkpNTdXIkSPLdIzNmzfrgQce0COPPCLpl9Jz4MABtWrVyplR1b59ey1btkxNmjRRtWplqwtxcXH68ssvbcZSUlIUFxfnjIil4tNSAABUEB8fH02cOFETJkzQ+++/r0OHDmnr1q1699137T5GRESEUlJStGXLFv3444964oknbjo74gxjxozR+fPnNWTIEG3fvl2HDh3S6tWrNXz4cLu/2HLUqFE6fPiwJkyYoP379+vvf/+7li5dqvHjxzs9769x5gYAgAr05z//WdWqVdOUKVN06tQphYaGatSoUXbvP3nyZB0+fFj9+vWTn5+f/vjHPyo+Pl45OTlOzVm/fn1t3rxZEydOVN++fVVQUKDGjRvr3nvvlbu7fedGmjZtqi++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AwIAADjC4TM3sbGx1p+DgoK0atUqpwYCAAC4HSziBwAATMWucnPvvfdq69atv7ndpUuX9PLLL2vOnDm3HQwAAKAs7LosNWjQID344IMKDAzUwIEDFRsbq/r168vHx0cXLlzQvn37tGnTJn355ZcaMGCApk+fXt65AQAASmRXuRkxYoQeeeQRffzxx1qyZInmz5+vnJwcSZKbm5tatWqlfv36afv27WrZsmW5BgYAALgVuycUe3t765FHHtEjjzwiScrJydGVK1dUp04deXp6lltAAAAAR5TpizOlXxbtCwwMdGYWAACA28anpQAAgKlQbgAAgKlQbgAAgKkYWm42btyogQMHqn79+nJzc9OKFStuuf369evl5uZ20y0rK6tiAgMAAJdXpnJz8eJFvfPOO0pKStL58+clSbt27dLJkycdOk5+fr6io6MdXvTvp59+UmZmpvUWFBTk0P4AAMC8HP601Pfff68+ffooMDBQR48e1eOPP67atWvr008/VUZGht5//327j9W/f3/179/f0QgKCgpSzZo1Hd4PAACYn8NnbhITEzVs2DD9/PPP8vHxsY7fd9992rhxo1PDlaZdu3YKDQ3VPffco82bN99y24KCAuXm5trcAACAeTlcbrZv364nnnjipvEGDRqU+9yX0NBQzZs3T8uWLdOyZcsUFhamnj17ateuXaXuk5ycbF2TJzAwUGFhYeWaEQAAGMvhy1Le3t4lnv04cOCA6tWr55RQpWnRooVatGhhvd+1a1cdOnRIM2fO1AcffFDiPklJSUpMTLTez83NpeAAAGBiDp+5uf/++/Xiiy/q2rVrkn75bqmMjAxNnDhRDz74oNMD/pZOnTrp4MGDpT7u7e2tgIAAmxsAADAvh8vNjBkzlJeXp6CgIF25ckU9evRQeHi4/P399be//a08Mt5Senq6QkNDK/x5AQCAa3L4slRgYKBSUlK0adMmff/998rLy1P79u3Vp08fh588Ly/P5qzLkSNHlJ6ertq1a6tRo0ZKSkrSyZMnrZ/Aev3119W0aVO1bt1aV69e1TvvvKOvv/5aa9ascfi5AQCAOZX5izO7d++u7t2739aT79ixQ3fffbf1/o25MQkJCVq4cKEyMzOVkZFhfbywsFDPPPOMTp48KT8/P0VFRWnt2rU2xwAAAFWbw+Vm9uzZJY67ubnJx8dH4eHhuuuuu+Th4fGbx+rZs6csFkupjy9cuNDm/oQJEzRhwgSH8gIAgKrF4XIzc+ZMnTlzRpcvX1atWrUkSRcuXJCfn59q1Kih06dPq1mzZlq3bh2fSgIAABXO4QnF06ZNU8eOHfXzzz/r3LlzOnfunA4cOKDOnTtr1qxZysjIUEhIiMaPH18eeQEAAG7J4TM3kydP1rJly9S8eXPrWHh4uF599VU9+OCDOnz4sF555RVDPhYOAADg8JmbzMxMXb9+/abx69evW1corl+/vi5dunT76QAAABzkcLm5++679cQTT2j37t3Wsd27d2v06NHq1auXJGnPnj1q2rSp81ICAADYyeFy8+6776p27drq0KGDvL295e3trdjYWNWuXVvvvvuuJKlGjRqaMWOG08MCAAD8Fofn3ISEhCglJUX79+/XgQMHJN38nU+sOwMAAIxS5kX8IiMjFRkZ6cwsAAAAt61M5ebEiRP6/PPPlZGRocLCQpvHXnvtNacEAwAAKAuHy01qaqruv/9+NWvWTPv371ebNm109OhRWSwWtW/fvjwyAgAA2M3hCcVJSUl69tlntWfPHvn4+GjZsmU6fvy4evTooUGDBpVHRgAAALs5XG5+/PFHDR06VJJUrVo1XblyRTVq1NCLL76ol19+2ekBAQAAHOFwualevbp1nk1oaKgOHTpkfezs2bPOSwYAAFAGDs+56dKlizZt2qSWLVvqvvvu0zPPPKM9e/bo008/VZcuXcojIwAAgN0cLjevvfaa8vLyJEkvvPCC8vLytGTJEkVERPBJKQAAYDiHy02zZs2sP1evXl3z5s1zaiAAAIDb4fCcm2bNmuncuXM3jV+8eNGm+AAAABjB4XJz9OhRFRUV3TReUFCgkydPOiUUAABAWdl9Werzzz+3/rx69WoFBgZa7xcVFSk1NVVNmjRxajgAAABH2V1u4uPjJUlubm5KSEiweczT01NNmjThm8ABAIDh7C43xcXFkqSmTZtq+/btqlu3brmFAgAAKCuHPy115MiR8sgBAADgFHaVm9mzZ9t9wKeeeqrMYQAAAG6XXeVm5syZdh3Mzc2NcgMAAAxlV7nhUhQAAKgsHF7n5tcsFossFouzsgAAANy2MpWb999/X23btpWvr698fX0VFRWlDz74wNnZAAAAHFamL87885//rLFjx6pbt26SpE2bNmnUqFE6e/asxo8f7/SQAAAA9nK43LzxxhuaO3euhg4dah27//771bp1az3//POUGwAAYCiHL0tlZmaqa9euN4137dpVmZmZTgkFAABQVg6Xm/DwcC1duvSm8SVLligiIsIpoQAAAMrK4ctSL7zwggYPHqyNGzda59xs3rxZqampJZYeAACAimT3mZu9e/dKkh588EF9++23qlu3rlasWKEVK1aobt262rZtm373u9+VW1AAAAB72H3mJioqSh07dtTIkSP1+9//Xh9++GF55gIAACgTu8/cbNiwQa1bt9Yzzzyj0NBQDRs2TN988015ZgMAAHCY3eXmzjvv1IIFC5SZmak33nhDR44cUY8ePXTHHXfo5ZdfVlZWVnnmBAAAsIvDn5aqXr26hg8frg0bNujAgQMaNGiQ5syZo0aNGun+++8vj4wAAAB2u63vlgoPD9ekSZM0efJk+fv764svvnBWLgAAgDJx+KPgN2zcuFELFizQsmXL5O7urocfflgjRoxwZjYAAACHOVRuTp06pYULF2rhwoU6ePCgunbtqtmzZ+vhhx9W9erVyysjAACA3ey+LNW/f381btxYb7zxhn73u9/pxx9/1KZNmzR8+PAyF5uNGzdq4MCBql+/vtzc3LRixYrf3Gf9+vVq3769vL29FR4eroULF5bpuQEAgDnZXW48PT31ySef6MSJE3r55ZfVokWL237y/Px8RUdHa86cOXZtf+TIEQ0YMEB333230tPTNW7cOI0cOVKrV6++7SwAAMAc7L4s9fnnnzv9yfv376/+/fvbvf28efPUtGlTzZgxQ5LUsmVLbdq0STNnzlS/fv2cng8AAFQ+t/VpqYqWlpamPn362Iz169dPaWlppe5TUFCg3NxcmxsAADCvSlVusrKyFBwcbDMWHBys3NxcXblypcR9kpOTFRgYaL2FhYVVRFQAAGCQSlVuyiIpKUk5OTnW2/Hjx42OBAAAylGZ17kxQkhIiLKzs23GsrOzFRAQIF9f3xL38fb2lre3d0XEAwAALqBSnbmJi4tTamqqzVhKSori4uIMSgQAAFyNoeUmLy9P6enpSk9Pl/TLR73T09OVkZEh6ZdLSkOHDrVuP2rUKB0+fFgTJkzQ/v379fe//11Lly7V+PHjjYgPAABckKHlZseOHYqJiVFMTIwkKTExUTExMZoyZYokKTMz01p0JKlp06b64osvlJKS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2rXTqlWrrJOGMzIy5O7+7xNMXbt21aJFizR58mRNmjRJERERWrFihdq0aWPUSwAAAC7E8HVuKporrXMDAADs48jf78r3+S4AAIBboNwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTodwAAABTqWZ0gIpmsVgkSbm5uQYnAQAA9rrxd/vG3/FbqXLl5tKlS5KksLAwg5MAAABHXbp0SYGBgbfcxs1iTwUykeLiYp06dUr+/v5yc3MzOo5yc3MVFham48ePKyAgwOg4LoX3pmS8L6XjvSkd703peG9K50rvjcVi0aVLl1S/fn25u996Vk2VO3Pj7u6uhg0bGh3jJgEBAYb/i+OqeG9KxvtSOt6b0vHelI73pnSu8t781hmbG5hQDAAATIVyAwAATIVyYzBvb29NnTpV3t7eRkdxObw3JeN9KR3vTel4b0rHe1O6yvreVLkJxQAAwNw4cwMAAEyFcgMAAEyFcgMAAEyFcgMAAEyFcmOgOXPmqEmTJvLx8VHnzp21bds2oyO5hI0bN2rgwIGqX7++3NzctGLFCqMjuYTk5GR17NhR/v7+CgoKUnx8vH766SejY7mEuXPnKioqyrrQWFxcnL766iujY7mcl156SW5ubho3bpzRUVzC888/Lzc3N5tbZGSk0bFcwsmTJ/XII4+oTp068vX1Vdu2bbVjxw6jY9mNcmOQJUuWKDExUVOnTtWuXbsUHR2tfv366fTp00ZHM1x+fr6io6M1Z84co6O4lA0bNmjMmDHaunWrUlJSdO3aNfXt21f5+flGRzNcw4YN9dJLL2nnzp3asWOHevXqpQceeEA//PCD0dFcxvbt2/XWW28pKirK6CgupXXr1srMzLTeNm3aZHQkw124cEHdunWTp6envvrqK+3bt08zZsxQrVq1jI5mPwsM0alTJ8uYMWOs94uKiiz169e3JCcnG5jK9UiyLF++3OgYLun06dMWSZYNGzYYHcUl1apVy/LOO+8YHcMlXLp0yRIREWFJSUmx9OjRw/L0008bHcklTJ061RIdHW10DJczceJES/fu3Y2OcVs4c2OAwsJC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", "text/plain": [ - "" + "
" ] }, "metadata": {}, @@ -1741,791 +176,9 @@ "outputs": [ { "data": { - "application/javascript": [ - "/* Put everything inside the global mpl namespace */\n", - "window.mpl = {};\n", - "\n", - "\n", - "mpl.get_websocket_type = function() {\n", - " if (typeof(WebSocket) !== 'undefined') {\n", - " return WebSocket;\n", - " } else if (typeof(MozWebSocket) !== 'undefined') {\n", - " return MozWebSocket;\n", - " } else {\n", - " alert('Your browser does not have WebSocket support.' +\n", - " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", - " 'Firefox 4 and 5 are also supported but you ' +\n", - " 'have to enable WebSockets in about:config.');\n", - " };\n", - "}\n", - "\n", - "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n", - " this.id = figure_id;\n", - "\n", - " this.ws = websocket;\n", - "\n", - " this.supports_binary = (this.ws.binaryType != undefined);\n", - "\n", - " if (!this.supports_binary) {\n", - " var warnings = document.getElementById(\"mpl-warnings\");\n", - " if (warnings) {\n", - " warnings.style.display = 'block';\n", - " warnings.textContent = (\n", - " \"This browser does not support binary websocket messages. \" +\n", - " \"Performance may be slow.\");\n", - " }\n", - " }\n", - "\n", - " this.imageObj = new Image();\n", - "\n", - " this.context = undefined;\n", - " this.message = undefined;\n", - " this.canvas = undefined;\n", - " this.rubberband_canvas = undefined;\n", - " this.rubberband_context = undefined;\n", - " this.format_dropdown = undefined;\n", - "\n", - " this.image_mode = 'full';\n", - "\n", - " this.root = $('
');\n", - " this._root_extra_style(this.root)\n", - " this.root.attr('style', 'display: inline-block');\n", - "\n", - " $(parent_element).append(this.root);\n", - "\n", - " this._init_header(this);\n", - " this._init_canvas(this);\n", - " this._init_toolbar(this);\n", - "\n", - " var fig = this;\n", - "\n", - " this.waiting = false;\n", - "\n", - " this.ws.onopen = function () {\n", - " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n", - " fig.send_message(\"send_image_mode\", {});\n", - " if (mpl.ratio != 1) {\n", - " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n", - " }\n", - " fig.send_message(\"refresh\", {});\n", - " }\n", - "\n", - " this.imageObj.onload = function() {\n", - " if (fig.image_mode == 'full') {\n", - " // Full images could contain transparency (where diff images\n", - " // almost always do), so we need to clear the canvas so that\n", - " // there is no ghosting.\n", - " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", - " }\n", - " fig.context.drawImage(fig.imageObj, 0, 0);\n", - " };\n", - "\n", - " this.imageObj.onunload = function() {\n", - " fig.ws.close();\n", - " }\n", - "\n", - " this.ws.onmessage = this._make_on_message_function(this);\n", - "\n", - " this.ondownload = ondownload;\n", - "}\n", - "\n", - "mpl.figure.prototype._init_header = function() {\n", - " var titlebar = $(\n", - " '
');\n", - " var titletext = $(\n", - " '
');\n", - " titlebar.append(titletext)\n", - " this.root.append(titlebar);\n", - " this.header = titletext[0];\n", - "}\n", - "\n", - "\n", - "\n", - "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n", - "\n", - "}\n", - "\n", - "\n", - "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n", - "\n", - "}\n", - "\n", - "mpl.figure.prototype._init_canvas = function() {\n", - " var fig = this;\n", - "\n", - " var canvas_div = $('
');\n", - "\n", - " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n", - "\n", - " function canvas_keyboard_event(event) {\n", - " return fig.key_event(event, event['data']);\n", - " }\n", - "\n", - " canvas_div.keydown('key_press', canvas_keyboard_event);\n", - " canvas_div.keyup('key_release', canvas_keyboard_event);\n", - " this.canvas_div = canvas_div\n", - " this._canvas_extra_style(canvas_div)\n", - " this.root.append(canvas_div);\n", - "\n", - " var canvas = $('');\n", - " canvas.addClass('mpl-canvas');\n", - " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n", - "\n", - " this.canvas = canvas[0];\n", - " this.context = canvas[0].getContext(\"2d\");\n", - "\n", - " var backingStore = this.context.backingStorePixelRatio ||\n", - "\tthis.context.webkitBackingStorePixelRatio ||\n", - "\tthis.context.mozBackingStorePixelRatio ||\n", - "\tthis.context.msBackingStorePixelRatio ||\n", - "\tthis.context.oBackingStorePixelRatio ||\n", - "\tthis.context.backingStorePixelRatio || 1;\n", - "\n", - " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n", - "\n", - " var rubberband = $('');\n", - " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n", - "\n", - " var pass_mouse_events = true;\n", - "\n", - " canvas_div.resizable({\n", - " start: function(event, ui) {\n", - " pass_mouse_events = false;\n", - " },\n", - " resize: function(event, ui) {\n", - " fig.request_resize(ui.size.width, ui.size.height);\n", - " },\n", - " stop: function(event, ui) {\n", - " pass_mouse_events = true;\n", - " fig.request_resize(ui.size.width, ui.size.height);\n", - " },\n", - " });\n", - "\n", - " function mouse_event_fn(event) {\n", - " if (pass_mouse_events)\n", - " return fig.mouse_event(event, event['data']);\n", - " }\n", - "\n", - " rubberband.mousedown('button_press', mouse_event_fn);\n", - " rubberband.mouseup('button_release', mouse_event_fn);\n", - " // Throttle sequential mouse events to 1 every 20ms.\n", - " rubberband.mousemove('motion_notify', mouse_event_fn);\n", - "\n", - " rubberband.mouseenter('figure_enter', mouse_event_fn);\n", - " rubberband.mouseleave('figure_leave', mouse_event_fn);\n", - "\n", - " canvas_div.on(\"wheel\", function (event) {\n", - " event = event.originalEvent;\n", - " event['data'] = 'scroll'\n", - " if (event.deltaY < 0) {\n", - " event.step = 1;\n", - " } else {\n", - " event.step = -1;\n", - " }\n", - " mouse_event_fn(event);\n", - " });\n", - "\n", - " canvas_div.append(canvas);\n", - " canvas_div.append(rubberband);\n", - "\n", - " this.rubberband = rubberband;\n", - " this.rubberband_canvas = rubberband[0];\n", - " this.rubberband_context = rubberband[0].getContext(\"2d\");\n", - " this.rubberband_context.strokeStyle = \"#000000\";\n", - "\n", - " this._resize_canvas = function(width, height) {\n", - " // Keep the size of the canvas, canvas container, and rubber band\n", - " // canvas in synch.\n", - " canvas_div.css('width', width)\n", - " canvas_div.css('height', height)\n", - "\n", - " canvas.attr('width', width * mpl.ratio);\n", - " canvas.attr('height', height * mpl.ratio);\n", - " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n", - "\n", - " rubberband.attr('width', width);\n", - " rubberband.attr('height', height);\n", - " }\n", - "\n", - " // Set the figure to an initial 600x600px, this will subsequently be updated\n", - " // upon first draw.\n", - " this._resize_canvas(600, 600);\n", - "\n", - " // Disable right mouse context menu.\n", - " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n", - " return false;\n", - " });\n", - "\n", - " function set_focus () {\n", - " canvas.focus();\n", - " canvas_div.focus();\n", - " }\n", - "\n", - " window.setTimeout(set_focus, 100);\n", - "}\n", - "\n", - "mpl.figure.prototype._init_toolbar = function() {\n", - " var fig = this;\n", - "\n", - " var nav_element = $('
')\n", - " nav_element.attr('style', 'width: 100%');\n", - " this.root.append(nav_element);\n", - "\n", - " // Define a callback function for later on.\n", - " function toolbar_event(event) {\n", - " return fig.toolbar_button_onclick(event['data']);\n", - " }\n", - " function toolbar_mouse_event(event) {\n", - " return fig.toolbar_button_onmouseover(event['data']);\n", - " }\n", - "\n", - " for(var toolbar_ind in mpl.toolbar_items) {\n", - " var name = mpl.toolbar_items[toolbar_ind][0];\n", - " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", - " var image = mpl.toolbar_items[toolbar_ind][2];\n", - " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", - "\n", - " if (!name) {\n", - " // put a spacer in here.\n", - " continue;\n", - " }\n", - " var button = $('');\n", - " button.click(method_name, toolbar_event);\n", - " button.mouseover(tooltip, toolbar_mouse_event);\n", - " nav_element.append(button);\n", - " }\n", - "\n", - " // Add the status bar.\n", - " var status_bar = $('');\n", - " nav_element.append(status_bar);\n", - " this.message = status_bar[0];\n", - "\n", - " // Add the close button to the window.\n", - " var buttongrp = $('
');\n", - " var button = $('');\n", - " button.click(function (evt) { fig.handle_close(fig, {}); } );\n", - " button.mouseover('Stop Interaction', toolbar_mouse_event);\n", - " buttongrp.append(button);\n", - " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n", - " titlebar.prepend(buttongrp);\n", - "}\n", - "\n", - "mpl.figure.prototype._root_extra_style = function(el){\n", - " var fig = this\n", - " el.on(\"remove\", function(){\n", - "\tfig.close_ws(fig, {});\n", - " });\n", - "}\n", - "\n", - "mpl.figure.prototype._canvas_extra_style = function(el){\n", - " // this is important to make the div 'focusable\n", - " el.attr('tabindex', 0)\n", - " // reach out to IPython and tell the keyboard manager to turn it's self\n", - " // off when our div gets focus\n", - "\n", - " // location in version 3\n", - " if (IPython.notebook.keyboard_manager) {\n", - " IPython.notebook.keyboard_manager.register_events(el);\n", - " }\n", - " else {\n", - " // location in version 2\n", - " IPython.keyboard_manager.register_events(el);\n", - " }\n", - "\n", - "}\n", - "\n", - "mpl.figure.prototype._key_event_extra = function(event, name) {\n", - " var manager = IPython.notebook.keyboard_manager;\n", - " if (!manager)\n", - " manager = IPython.keyboard_manager;\n", - "\n", - " // Check for shift+enter\n", - " if (event.shiftKey && event.which == 13) {\n", - " this.canvas_div.blur();\n", - " event.shiftKey = false;\n", - " // Send a \"J\" for go to next cell\n", - " event.which = 74;\n", - " event.keyCode = 74;\n", - " manager.command_mode();\n", - " manager.handle_keydown(event);\n", - " }\n", - "}\n", - "\n", - "mpl.figure.prototype.handle_save = function(fig, msg) {\n", - " fig.ondownload(fig, null);\n", - "}\n", - "\n", - "\n", - "mpl.find_output_cell = function(html_output) {\n", - " // Return the cell and output element which can be found *uniquely* in the notebook.\n", - " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n", - " // IPython event is triggered only after the cells have been serialised, which for\n", - " // our purposes (turning an active figure into a static one), is too late.\n", - " var cells = IPython.notebook.get_cells();\n", - " var ncells = cells.length;\n", - " for (var i=0; i= 3 moved mimebundle to data attribute of output\n", - " data = data.data;\n", - " }\n", - " if (data['text/html'] == html_output) {\n", - " return [cell, data, j];\n", - " }\n", - " }\n", - " }\n", - " }\n", - "}\n", - "\n", - "// Register the function which deals with the matplotlib target/channel.\n", - "// The kernel may be null if the page has been refreshed.\n", - "if (IPython.notebook.kernel != null) {\n", - " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n", - "}\n" - ], + "image/png": "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", "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "" - ], - "text/plain": [ - "" + "
" ] }, "metadata": {}, @@ -808,9 +26,8 @@ } ], "source": [ - "%matplotlib notebook\n", "from qupulse.pulses import TablePT\n", - "from qupulse.pulses.plotting import plot\n", + "from qupulse.plotting import plot\n", "\n", "table_pulse = TablePT({'A': [(0, 'v_a'),\n", " ('t_ramp', 'v_b', 'linear')]})\n", @@ -833,7 +50,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "{'v_a', 'max_rate', 't_ramp', 'v_b'}\n", + "{'v_b', 'max_rate', 'v_a', 't_ramp'}\n", "Abs(v_a - v_b)/t_ramp < max_rate\n", "t_ramp > 1\n" ] @@ -864,7 +81,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "ParameterNotProvidedException: No value was provided for parameter ''max_rate''.\n" + "ParameterNotProvidedException: No value was provided for parameter 'max_rate'.\n" ] } ], @@ -892,7 +109,7 @@ "output_type": "stream", "text": [ "ParameterConstraintViolation: The constraint 'Abs(v_a - v_b)/t_ramp < max_rate' is not fulfilled.\n", - "Parameters: {'v_a': -1, 'max_rate': 0.1, 't_ramp': 10, 'v_b': 1}\n" + "Parameters: DictScope(values=frozendict.frozendict({'t_ramp': 10, 'v_a': -1, 'v_b': 1, 'max_rate': 0.1}))\n" ] } ], @@ -905,22 +122,8 @@ } ], "metadata": { - "kernelspec": { - "display_name": "Python [default]", - "language": "python", - "name": "python3" - }, "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.0" + "name": "python" } }, "nbformat": 4, diff --git a/doc/source/examples/01PulseStorage.ipynb b/doc/source/examples/01PulseStorage.ipynb index 437778796..b0889b321 100644 --- a/doc/source/examples/01PulseStorage.ipynb +++ b/doc/source/examples/01PulseStorage.ipynb @@ -113,21 +113,18 @@ }, { "data": { - "image/png": "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VYXPLVmyhPfff5/Zs2dz/PHHM3LkSOrUqcOLL75Io0aNWLFiBfvuuy/HH388APPmzeOZZ57hoYce4ic/+QnPP/88Z5xxBgDbtm1jypQpjB8/nuuvv56JEyfyyCOP0LhxYz7++GM2b97MAQccwJFHHlliw/eLL77InDlzmDFjBkuXLqVnz56cc845bN26lUsuuYRx48bRokULnn32Wa699loeffRRLrzwQho0aMBvf/tbAB599FGaNm3Kxo0b2XvvvTn55JNp1qxZmftx5MiR3HPPPdx+++3k5eUlvP+TQUVFRLLG22+/zciRI2nevDkATZs2LXzuhBNOoFq1avTs2ZOlS5cCwdHCNddcw6RJk6hWrRrfffdd4XMdO3akf//+AAwaNIiFCxcWbuukk07a5fEJEybw+eefFx4FrVmzhnnz5tG1a9dis06aNInTTjuN6tWr06ZNGw499FAA5syZwxdffMERRxwBBEdArVu3LnYbd911Fy+++CIA3377LfPmzUuoqEQp0qJiZo8CxwLL3H2Xli4L/gS4Ezga2ACMcvdP0ptSRIpT2hFFqrh7iUcGtWvX3mk9gKeffprly5czbdo0atasSYcOHQr7aMSvX7169Z1OEcWeq169Otu2bSvc5t13382wYcN2et/4YlRUcVndnV69ejF58uTSPirvvvsuEydOZPLkydSrV4+hQ4cWZo/fbqaNbhB1m8pjwFGlPD8c6BLeLgDuT0MmEclQhx12GM899xwrV64E2On0V3HWrFlDy5YtqVmzJu+88w6LFlV8dPdhw4Zx//33s3XrVgDmzp3L+vXrS1z/4IMPZsyYMWzfvp0lS5bwzjvvANCtWzeWL19eWFS2bt3KzJkzi83epEkT6tWrx+zZs/nwww8Ln2vVqhWzZs2ioKCg8EimqIYNG7J27doKf96KivRIxd0nmVmHUlYZATzhwZ8dH5rZbmbW2t2XpCWgZDR3Z9HKDSz9cRP1atWgfu3q7NmsPtWrqXNeVdWrVy+uvfZahgwZQvXq1RkwYACPPfZYieuffvrpHHfcceTl5dG/f3+6d+9e4fc+77zzWLhwIQMHDsTdadGiBS+99FKJ65944om8/fbb9OnTh65duzJkyBAAatWqxdixY7n00ktZs2YN27Zt4/LLL6dXr52P/I466igeeOAB+vbtS7du3dh3330Ln7vllls4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\n", + "image/png": "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", "text/plain": [ - "
" + "
" ] }, - "metadata": { - "needs_background": "light" - }, + "metadata": {}, "output_type": "display_data" } ], "source": [ - "%matplotlib inline\n", "import math\n", - "from qupulse.pulses.plotting import plot as plot\n", + "from qupulse.plotting import plot\n", "\n", "sine = file_pulse_storage['my_other_pulse']\n", "\n", @@ -389,22 +386,8 @@ } ], "metadata": { - "kernelspec": { - "display_name": "Python [default]", - "language": "python", - "name": "python3" - }, "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.0" + "name": "python" } }, "nbformat": 4, diff --git a/doc/source/examples/02CreatePrograms.ipynb b/doc/source/examples/02CreatePrograms.ipynb index 251769ad5..6e65cbb27 100644 --- a/doc/source/examples/02CreatePrograms.ipynb +++ b/doc/source/examples/02CreatePrograms.ipynb @@ -21,21 +21,19 @@ "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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1FRJV445161bVx07dtTmzZtVp06dEg0M4Bb4Bl7ZY2LUexdBQEBA0Tbn6+v42WKxSLoyF0eSXnrpJU2dOlWvv/66GjRooKCgIA0dOvSa27r8cRtXt3N1G0VZ5+LFi+rWrZsmT558Tb6rRetGli9frqNHj6pXr175lufm5l6z1wjA9RX5lG6r1eqYxCZJqampysrKUrly5a75Sw/ATVksRToEZKT4+HgFBARo5cqVevzxx29qG+vXr9e9997r+L7Ky8vTvn37VLduXWdGVePGjbVw4UJVq1ZNPj43dSs9zZo1S3/961/13HPP5Vs+YcIEzZo1i1IDFMNN79cMCwtTVFQUhQaAU/n7+2vUqFF65plnNG/ePKWkpOinn34q1hlB8fHxWrFihX788Uf9+uuveuqpp3Ty5EmnZx00aJDOnTun3r17a/PmzUpJSdHy5cv1yCOPKDc394avP336tL788kv169dP9evXz/fo27evFi9e7JiUDODGOFgLwO288MILGjFihMaMGaM6deqoV69e18x1uZ7nn39ejRs3VseOHdWmTRtFRUWpe/fuTs8ZHR2t9evXKzc3Vx06dFCDBg00dOhQlSlTpkhzYebNm6egoKAC5w+1b99eAQEB+uCDD5yeGzAri91ejPMsPVxaWprCwsKUmpqq0NBQQ7NkZmZq7ZotCgmprICAos01QOnmm3lU9b9rp1wvP9lGHrpm0uv/unTpkg4cOKDq1avL39/fRSnhCsX6s908S1o6XGfLNVde6xkKLOKcJZRumVlZSjl9VAnNm97wu8YVivrvN3tqAACAKXhcqXnjjTdUrVo1+fv76/bbb7/mYloAAKB0uqlSc+HCBb377rtKSkpyTGLbtm2bjh496tRw/2v+/PkaPny4xo4dq23btqlhw4bq2LFjsY61AwAAcyr2OYg7d+7UXXfdpbCwMB08eFBPPPGEwsPD9fnnn+vw4cOaN29eSeSUJL366qt64okn9Mgjj0iS3nzzTS1dulSzZ8/Ws88+W2LvWxLOnjisrLQTslzO02Xuxosi8Lc5/+wdlB65ly/p7JnD8vfj+wY3dinbposXTisz/YJbzKkpqmKXmuHDh6t///6aMmWKQkJCHMu7dOmiBx980Knh/ig7O1tbt25VUlKSY5mXl5fuuuuufDey+yObzSabzeZ4npaWVmL5iiv3/fvV3f4f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NDTfAwAAmJOhc2pGjBih/v37X3ed2NhY14QBAAAezdBSExkZqcjISCMjAAAAk/CYs58OHz6sc+fO6fDhw8rNzdWOHTskXbn/VHBwsLHhAACA4Tym1IwZM0Zz5851PG/UqJEkadWqVWrTpo1BqQAAgLvwmLOf5syZI7vdfs2DQgMAACQPKjUAAADXQ6kBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACmQKkBAACm4BGl5uDBg3rsscdUvXp1BQQEqEaNGho7dqyys7ONjgYAANyEj9EBimLv3r3Ky8vTW2+9pbi4OO3evVtPPPGEMjIy9PLLLxsdDwAAuAGPKDWdOnVSp06dHM9jY2P122+/aebMmZQalDqX8yRbTq4CjQ4CAG7GI0pNQVJTUxUeHn7ddWw2m2w2m+N5WlpaSccCSoyXxeL4udHENZKk13vdpntvi5blD2MAUFp5xJya/7V//35Nnz5dTz311HXXS05OVlhYmOMRExPjooSA85UL8FVc2YB8y4bO36HqSV+rRfJKbT983qBkAOAeDC01zz77rCwWy3Ufe/fuzfeao0ePqlOnTurRo4eeeOKJ624/KSlJqampjseRI0dK8j8HKFEWi0UvtqqmV1sGq3vDqHxjx1Iv6b7/+1HVnl2qPrM26kTqJYNSAoBxDD38NGLECPXv3/+668TGxjp+PnbsmNq2basWLVro7bffvuH2rVarrFbrrcYE3EqQr0UT762j13s30YEzGRr4wVbtPZHuGF/77zO6I3mlJKlv86oa3aWO/H29jYoLAC5jaKmJjIxUZGRkkdY9evSo2rZtqyZNmui9996Tl5dHHjkDnKp6RJCWDf2TJGnVb6f01Lytys7Nc4zP23BI8zYckiRNvK+B/to0Rl5ezL8BYE4eMVH46NGjatOmjapWraqXX35Zp0+fdoxF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", "text/plain": [ - "
" + "
" ] }, - "metadata": { - "needs_background": "light" - }, + "metadata": {}, "output_type": "display_data" } ], "source": [ - "%matplotlib inline\n", - "from qupulse.pulses.plotting import plot\n", + "from qupulse.plotting import plot\n", "from qupulse.pulses import TablePT\n", + "\n", "template = TablePT(entries={'A': [(0, 0),\n", " ('ta', 'va', 'hold'),\n", " ('tb', 'vb', 'linear'),\n", @@ -73,8 +71,8 @@ "output_type": "stream", "text": [ "LOOP 1 times:\n", - " ->EXEC 1 times\n", - "Defined on {'B', 'A'}\n", + " ->EXEC MultiChannelWaveform((TableWaveform(channel='A', waveform_table=(TableWaveformEntry(t=0.0, v=0, interp=), TableWaveformEntry(t=2, v=2, interp=), TableWaveformEntry(t=4, v=3, interp=), TableWaveformEntry(t=6, v=0, interp=))), TableWaveform(channel='B', waveform_table=(TableWaveformEntry(t=0.0, v=0, interp=), TableWaveformEntry(t=2, v=-2, interp=), TableWaveformEntry(t=4, v=-3, interp=), TableWaveformEntry(t=6, v=0, interp=))))) 1 times\n", + "Defined on frozenset({'B', 'A'})\n", "{'m': (array([0.]), array([2.])), 'n': (array([4.]), array([2.]))}\n" ] } @@ -109,7 +107,7 @@ "output_type": "stream", "text": [ "LOOP 1 times:\n", - " ->EXEC 1 times\n", + " ->EXEC TableWaveform(channel='Y', waveform_table=(TableWaveformEntry(t=0.0, v=0, interp=), TableWaveformEntry(t=2, v=-2, interp=), TableWaveformEntry(t=4, v=-3, interp=), TableWaveformEntry(t=6, v=0, interp=))) 1 times\n", "Defined on {'Y'}\n", "{'foo': (array([0.]), array([2.]))}\n" ] @@ -136,9 +134,7 @@ { "cell_type": "code", "execution_count": 4, - "metadata": { - "scrolled": false - }, + "metadata": {}, "outputs": [ { "name": "stdout", @@ -146,21 +142,27 @@ "text": [ "LOOP 1 times:\n", " ->LOOP 4 times:\n", - " ->EXEC 1 times\n", - " ->EXEC 1 times\n", + " ->EXEC MultiChannelWaveform((TableWaveform(channel='A', waveform_table=(TableWaveformEntry(t=0.0, v=0, interp=), TableWaveformEntry(t=2, v=2, interp=), TableWaveformEntry(t=4, v=3, interp=), TableWaveformEntry(t=6, v=0, interp=))), TableWaveform(channel='B', waveform_table=(TableWaveformEntry(t=0.0, v=0, interp=), TableWaveformEntry(t=2, v=-2, interp=), TableWaveformEntry(t=4, v=-3, interp=), TableWaveformEntry(t=6, v=0, interp=))))) 1 times\n", + " ->EXEC MultiChannelWaveform((FunctionWaveform(duration=TimeType(6283, 1000), expression=ExpressionScalar('sin(t)'), channel='A'), FunctionWaveform(duration=TimeType(6283, 1000), expression=ExpressionScalar('2*sin(t)'), channel='B'))) 1 times\n", "{'m': (array([ 0., 6., 12., 18.]), array([2., 2., 2., 2.])), 'n': (array([ 4., 10., 16., 22.]), array([2., 2., 2., 2.]))}\n" ] }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "C:\\Users\\Simon\\Documents\\git\\qupulse\\qupulse\\plotting.py:186: UserWarning: Sample count 30293/10 is not an integer. Will be rounded (this changes the sample rate).\n", + " times, voltages, measurements = render(program,\n" + ] + }, { "data": { - "image/png": "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\n", + "image/png": "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", "text/plain": [ - "
" + "
" ] }, - "metadata": { - "needs_background": "light" - }, + "metadata": {}, "output_type": "display_data" } ], @@ -198,22 +200,8 @@ } ], "metadata": { - "kernelspec": { - "display_name": "Python [default]", - "language": "python", - "name": "python3" - }, "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.0" + "name": "python" } }, "nbformat": 4, diff --git a/doc/source/examples/03DynamicNuclearPolarisation.ipynb b/doc/source/examples/03DynamicNuclearPolarisation.ipynb index 5f02c1219..f2abaed8f 100644 --- a/doc/source/examples/03DynamicNuclearPolarisation.ipynb +++ b/doc/source/examples/03DynamicNuclearPolarisation.ipynb @@ -44,7 +44,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -66,7 +66,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -89,20 +89,9 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "LOOP 1 times:\n", - " ->EXEC 3 times\n", - " ->EXEC 3 times\n", - " ->EXEC 3 times\n" - ] - } - ], + "outputs": [], "source": [ "hardware_setup.register_program('dnp', dnp_prog)\n", "hardware_setup.arm_program('dnp')\n", @@ -123,7 +112,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -139,20 +128,9 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "LOOP 1 times:\n", - " ->EXEC 3 times\n", - " ->EXEC 1 times\n", - " ->EXEC 5 times\n" - ] - } - ], + "outputs": [], "source": [ "used_awg.run_current_program()\n", "\n", @@ -168,22 +146,8 @@ } ], "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.4" + "name": "python" } }, "nbformat": 4, diff --git a/doc/source/examples/03FreeInductionDecayExample.ipynb b/doc/source/examples/03FreeInductionDecayExample.ipynb index eb5ab7458..365eff2aa 100644 --- a/doc/source/examples/03FreeInductionDecayExample.ipynb +++ b/doc/source/examples/03FreeInductionDecayExample.ipynb @@ -13,7 +13,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -69,7 +69,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -86,17 +86,9 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'max_ramp_speed', 't_meas_start', 'ST_jump', 't_ST_read', 'eps_J', 't_init', 'ST_plus', 'N_repetitions', 't_step', 't_start', 'op', 't_meas_duration', 'S_init', 't_meas_wait', 'N_fid_steps', 't_op', 'meas', 't_ST_prep'}\n" - ] - } - ], + "outputs": [], "source": [ "print(experiment.parameter_names)" ] @@ -112,802 +104,9 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/javascript": [ - "/* Put everything inside the global mpl namespace */\n", - "window.mpl = {};\n", - "\n", - "\n", - "mpl.get_websocket_type = function() {\n", - " if (typeof(WebSocket) !== 'undefined') {\n", - " return WebSocket;\n", - " } else if (typeof(MozWebSocket) !== 'undefined') {\n", - " return MozWebSocket;\n", - " } else {\n", - " alert('Your browser does not have WebSocket support.' +\n", - " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", - " 'Firefox 4 and 5 are also supported but you ' +\n", - " 'have to enable WebSockets in about:config.');\n", - " };\n", - "}\n", - "\n", - "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n", - " this.id = figure_id;\n", - "\n", - " this.ws = websocket;\n", - "\n", - " this.supports_binary = (this.ws.binaryType != undefined);\n", - "\n", - " if (!this.supports_binary) {\n", - " var warnings = document.getElementById(\"mpl-warnings\");\n", - " if (warnings) {\n", - " warnings.style.display = 'block';\n", - " warnings.textContent = (\n", - " \"This browser does not support binary websocket messages. \" +\n", - " \"Performance may be slow.\");\n", - " }\n", - " }\n", - "\n", - " this.imageObj = new Image();\n", - "\n", - " this.context = undefined;\n", - " this.message = undefined;\n", - " this.canvas = undefined;\n", - " this.rubberband_canvas = undefined;\n", - " this.rubberband_context = undefined;\n", - " this.format_dropdown = undefined;\n", - "\n", - " this.image_mode = 'full';\n", - "\n", - " this.root = $('
');\n", - " this._root_extra_style(this.root)\n", - " this.root.attr('style', 'display: inline-block');\n", - "\n", - " $(parent_element).append(this.root);\n", - "\n", - " this._init_header(this);\n", - " this._init_canvas(this);\n", - " this._init_toolbar(this);\n", - "\n", - " var fig = this;\n", - "\n", - " this.waiting = false;\n", - "\n", - " this.ws.onopen = function () {\n", - " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n", - " fig.send_message(\"send_image_mode\", {});\n", - " if (mpl.ratio != 1) {\n", - " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n", - " }\n", - " fig.send_message(\"refresh\", {});\n", - " }\n", - "\n", - " this.imageObj.onload = function() {\n", - " if (fig.image_mode == 'full') {\n", - " // Full images could contain transparency (where diff images\n", - " // almost always do), so we need to clear the canvas so that\n", - " // there is no ghosting.\n", - " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", - " }\n", - " fig.context.drawImage(fig.imageObj, 0, 0);\n", - " };\n", - "\n", - " this.imageObj.onunload = function() {\n", - " fig.ws.close();\n", - " }\n", - "\n", - " this.ws.onmessage = this._make_on_message_function(this);\n", - "\n", - " this.ondownload = ondownload;\n", - "}\n", - "\n", - "mpl.figure.prototype._init_header = function() {\n", - " var titlebar = $(\n", - " '
');\n", - " var titletext = $(\n", - " '
');\n", - " titlebar.append(titletext)\n", - " this.root.append(titlebar);\n", - " this.header = titletext[0];\n", - "}\n", - "\n", - "\n", - "\n", - "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n", - "\n", - "}\n", - "\n", - "\n", - "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n", - "\n", - "}\n", - "\n", - "mpl.figure.prototype._init_canvas = function() {\n", - " var fig = this;\n", - "\n", - " var canvas_div = $('
');\n", - "\n", - " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n", - "\n", - " function canvas_keyboard_event(event) {\n", - " return fig.key_event(event, event['data']);\n", - " }\n", - "\n", - " canvas_div.keydown('key_press', canvas_keyboard_event);\n", - " canvas_div.keyup('key_release', canvas_keyboard_event);\n", - " this.canvas_div = canvas_div\n", - " this._canvas_extra_style(canvas_div)\n", - " this.root.append(canvas_div);\n", - "\n", - " var canvas = $('');\n", - " canvas.addClass('mpl-canvas');\n", - " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n", - "\n", - " this.canvas = canvas[0];\n", - " this.context = canvas[0].getContext(\"2d\");\n", - "\n", - " var backingStore = this.context.backingStorePixelRatio ||\n", - "\tthis.context.webkitBackingStorePixelRatio ||\n", - "\tthis.context.mozBackingStorePixelRatio ||\n", - "\tthis.context.msBackingStorePixelRatio ||\n", - "\tthis.context.oBackingStorePixelRatio ||\n", - "\tthis.context.backingStorePixelRatio || 1;\n", - "\n", - " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n", - "\n", - " var rubberband = $('');\n", - " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n", - "\n", - " var pass_mouse_events = true;\n", - "\n", - " canvas_div.resizable({\n", - " start: function(event, ui) {\n", - " pass_mouse_events = false;\n", - " },\n", - " resize: function(event, ui) {\n", - " fig.request_resize(ui.size.width, ui.size.height);\n", - " },\n", - " stop: function(event, ui) {\n", - " pass_mouse_events = true;\n", - " fig.request_resize(ui.size.width, ui.size.height);\n", - " },\n", - " });\n", - "\n", - " function mouse_event_fn(event) {\n", - " if (pass_mouse_events)\n", - " return fig.mouse_event(event, event['data']);\n", - " }\n", - "\n", - " rubberband.mousedown('button_press', mouse_event_fn);\n", - " rubberband.mouseup('button_release', mouse_event_fn);\n", - " // Throttle sequential mouse events to 1 every 20ms.\n", - " rubberband.mousemove('motion_notify', mouse_event_fn);\n", - "\n", - " rubberband.mouseenter('figure_enter', mouse_event_fn);\n", - " rubberband.mouseleave('figure_leave', mouse_event_fn);\n", - "\n", - " canvas_div.on(\"wheel\", function (event) {\n", - " event = event.originalEvent;\n", - " event['data'] = 'scroll'\n", - " if (event.deltaY < 0) {\n", - " event.step = 1;\n", - " } else {\n", - " event.step = -1;\n", - " }\n", - " mouse_event_fn(event);\n", - " });\n", - "\n", - " canvas_div.append(canvas);\n", - " canvas_div.append(rubberband);\n", - "\n", - " this.rubberband = rubberband;\n", - " this.rubberband_canvas = rubberband[0];\n", - " this.rubberband_context = rubberband[0].getContext(\"2d\");\n", - " this.rubberband_context.strokeStyle = \"#000000\";\n", - "\n", - " this._resize_canvas = function(width, height) {\n", - " // Keep the size of the canvas, canvas container, and rubber band\n", - " // canvas in synch.\n", - " canvas_div.css('width', width)\n", - " canvas_div.css('height', height)\n", - "\n", - " canvas.attr('width', width * mpl.ratio);\n", - " canvas.attr('height', height * mpl.ratio);\n", - " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n", - "\n", - " rubberband.attr('width', width);\n", - " rubberband.attr('height', height);\n", - " }\n", - "\n", - " // Set the figure to an initial 600x600px, this will subsequently be updated\n", - " // upon first draw.\n", - " this._resize_canvas(600, 600);\n", - "\n", - " // Disable right mouse context menu.\n", - " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n", - " return false;\n", - " });\n", - "\n", - " function set_focus () {\n", - " canvas.focus();\n", - " canvas_div.focus();\n", - " }\n", - "\n", - " window.setTimeout(set_focus, 100);\n", - "}\n", - "\n", - "mpl.figure.prototype._init_toolbar = function() {\n", - " var fig = this;\n", - "\n", - " var nav_element = $('
')\n", - " nav_element.attr('style', 'width: 100%');\n", - " this.root.append(nav_element);\n", - "\n", - " // Define a callback function for later on.\n", - " function toolbar_event(event) {\n", - " return fig.toolbar_button_onclick(event['data']);\n", - " }\n", - " function toolbar_mouse_event(event) {\n", - " return fig.toolbar_button_onmouseover(event['data']);\n", - " }\n", - "\n", - " for(var toolbar_ind in mpl.toolbar_items) {\n", - " var name = mpl.toolbar_items[toolbar_ind][0];\n", - " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", - " var image = mpl.toolbar_items[toolbar_ind][2];\n", - " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", - "\n", - " if (!name) {\n", - " // put a spacer in here.\n", - " continue;\n", - " }\n", - " var button = $('');\n", - " button.click(method_name, toolbar_event);\n", - " button.mouseover(tooltip, toolbar_mouse_event);\n", - " nav_element.append(button);\n", - " }\n", - "\n", - " // Add the status bar.\n", - " var status_bar = $('');\n", - " nav_element.append(status_bar);\n", - " this.message = status_bar[0];\n", - "\n", - " // Add the close button to the window.\n", - " var buttongrp = $('
');\n", - " var button = $('');\n", - " button.click(function (evt) { fig.handle_close(fig, {}); } );\n", - " button.mouseover('Stop Interaction', toolbar_mouse_event);\n", - " buttongrp.append(button);\n", - " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n", - " titlebar.prepend(buttongrp);\n", - "}\n", - "\n", - "mpl.figure.prototype._root_extra_style = function(el){\n", - " var fig = this\n", - " el.on(\"remove\", function(){\n", - "\tfig.close_ws(fig, {});\n", - " });\n", - "}\n", - "\n", - "mpl.figure.prototype._canvas_extra_style = function(el){\n", - " // this is important to make the div 'focusable\n", - " el.attr('tabindex', 0)\n", - " // reach out to IPython and tell the keyboard manager to turn it's self\n", - " // off when our div gets focus\n", - "\n", - " // location in version 3\n", - " if (IPython.notebook.keyboard_manager) {\n", - " IPython.notebook.keyboard_manager.register_events(el);\n", - " }\n", - " else {\n", - " // location in version 2\n", - " IPython.keyboard_manager.register_events(el);\n", - " }\n", - "\n", - "}\n", - "\n", - "mpl.figure.prototype._key_event_extra = function(event, name) {\n", - " var manager = IPython.notebook.keyboard_manager;\n", - " if (!manager)\n", - " manager = IPython.keyboard_manager;\n", - "\n", - " // Check for shift+enter\n", - " if (event.shiftKey && event.which == 13) {\n", - " this.canvas_div.blur();\n", - " event.shiftKey = false;\n", - " // Send a \"J\" for go to next cell\n", - " event.which = 74;\n", - " event.keyCode = 74;\n", - " manager.command_mode();\n", - " manager.handle_keydown(event);\n", - " }\n", - "}\n", - "\n", - "mpl.figure.prototype.handle_save = function(fig, msg) {\n", - " fig.ondownload(fig, null);\n", - "}\n", - "\n", - "\n", - "mpl.find_output_cell = function(html_output) {\n", - " // Return the cell and output element which can be found *uniquely* in the notebook.\n", - " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n", - " // IPython event is triggered only after the cells have been serialised, which for\n", - " // our purposes (turning an active figure into a static one), is too late.\n", - " var cells = IPython.notebook.get_cells();\n", - " var ncells = cells.length;\n", - " for (var i=0; i= 3 moved mimebundle to data attribute of output\n", - " data = data.data;\n", - " }\n", - " if (data['text/html'] == html_output) {\n", - " return [cell, data, j];\n", - " }\n", - " }\n", - " }\n", - " }\n", - "}\n", - "\n", - "// Register the function which deals with the matplotlib target/channel.\n", - "// The kernel may be null if the page has been refreshed.\n", - "if (IPython.notebook.kernel != null) {\n", - " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n", - "}\n" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "example_values['N_fid_steps'] = 1\n", "example_values['N_repetitions'] = 1\n", @@ -1763,17 +169,9 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Successfully saved pulse and example parameters\n" - ] - } - ], + "outputs": [], "source": [ "import json\n", "from qupulse.serialization import FilesystemBackend, PulseStorage\n", @@ -1794,22 +192,8 @@ } ], "metadata": { - "kernelspec": { - "display_name": "Python [default]", - "language": "python", - "name": "python3" - }, "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.0" + "name": "python" } }, "nbformat": 4, diff --git a/doc/source/examples/03GateConfigurationExample.ipynb b/doc/source/examples/03GateConfigurationExample.ipynb index 95e2fe12a..f20bf30d7 100644 --- a/doc/source/examples/03GateConfigurationExample.ipynb +++ b/doc/source/examples/03GateConfigurationExample.ipynb @@ -16,13 +16,13 @@ "An example for a real use case of qupulse is the search for and evaluation of parameters for pulses that represent quantum gate operations on a toy example. To see an example closer to reality but less verbose in explanations, please see [Free Induction Decay - A Real Use Case](03FreeInductionDecayExample.ipynb).\n", "\n", "## Description of the Experiment\n", - "The experiment will typically involve a set of gate pulses $G_j, 0 \\leq j \\lt N_{Gates}$.\n", + "The experiment will typically involve a set of gate pulses $G_j, 0 \\leq j < N_{Gates}$.\n", "\n", - "The template for a gate pulse $G_j$ is a sequence of $\\epsilon_i, 0 \\leq i \\lt N_{G_j}$ voltage levels held for time $\\Delta t = 1$ ns as illustrated in the figure below (with $N_{G_j} = 7$).\n", + "The template for a gate pulse $G_j$ is a sequence of $\\epsilon_i, 0 \\leq i < N_{G_j}$ voltage levels held for time $\\Delta t = 1$ ns as illustrated in the figure below (with $N_{G_j} = 7$).\n", "\n", "![Template of a gate pulse](img/gate_pulse_scheme.png)\n", "\n", - "The experiment defines a number of sequences $S_k, 0 \\leq k \\lt N_{Sequences}$ of the $G_j$ as $$S_k = (G_{m_k(1)}, G_{m_k(2)}, \\dots, G_{m_k(N_{S_k})})$$ where $N_{S_k}$ is the length of sequence $k$ and $m_k(i): \\{0, \\dots, N_{S_k} - 1\\} \\rightarrow \\{0, \\dots, N_{Gates} - 1\\}$ is a function that maps an index $i$ to the $m_k(i)$-th gate of sequence $S_k$ and thus fully describes the sequence. (These sequences express the sequential application of the gates to the qubit. In terms of quantum mathematics they may rather be expressed as multiplication of the matrices describing the unitary transformations applied by the gates: $S_k = \\prod_{i=N_{S_k} - 1}^{0} G_{m_k(i)} = G_{(N_{S_k} - 1)} \\cdot \\dots \\cdot G_{1} \\cdot G_{0}$.)\n", + "The experiment defines a number of sequences $S_k, 0 \\leq k < N_{Sequences}$ of the $G_j$ as $$S_k = (G_{m_k(1)}, G_{m_k(2)}, \\dots, G_{m_k(N_{S_k})})$$ where $N_{S_k}$ is the length of sequence $k$ and $m_k(i): \\{0, \\dots, N_{S_k} - 1\\} \\rightarrow \\{0, \\dots, N_{Gates} - 1\\}$ is a function that maps an index $i$ to the $m_k(i)$-th gate of sequence $S_k$ and thus fully describes the sequence. (These sequences express the sequential application of the gates to the qubit. In terms of quantum mathematics they may rather be expressed as multiplication of the matrices describing the unitary transformations applied by the gates: $S_k = \\prod_{i=N_{S_k} - 1}^{0} G_{m_k(i)} = G_{(N_{S_k} - 1)} \\cdot \\dots \\cdot G_{1} \\cdot G_{0}$.)\n", "\n", "Measuring and analysing the effects of these sequences on the qubit's state to derive parameters $\\epsilon_i$ for gate pulses that achieve certain state transformations is the goal of the experiment.\n", "\n", @@ -126,797 +126,13 @@ { "cell_type": "code", "execution_count": 4, - "metadata": { - "scrolled": false - }, + "metadata": {}, "outputs": [ { "data": { - "application/javascript": [ - "/* Put everything inside the global mpl namespace */\n", - "window.mpl = {};\n", - "\n", - "\n", - "mpl.get_websocket_type = function() {\n", - " if (typeof(WebSocket) !== 'undefined') {\n", - " return WebSocket;\n", - " } else if (typeof(MozWebSocket) !== 'undefined') {\n", - " return MozWebSocket;\n", - " } else {\n", - " alert('Your browser does not have WebSocket support.' +\n", - " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", - " 'Firefox 4 and 5 are also supported but you ' +\n", - " 'have to enable WebSockets in about:config.');\n", - " };\n", - "}\n", - "\n", - "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n", - " this.id = figure_id;\n", - "\n", - " this.ws = websocket;\n", - "\n", - " this.supports_binary = (this.ws.binaryType != undefined);\n", - "\n", - " if (!this.supports_binary) {\n", - " var warnings = document.getElementById(\"mpl-warnings\");\n", - " if (warnings) {\n", - " warnings.style.display = 'block';\n", - " warnings.textContent = (\n", - " \"This browser does not support binary websocket messages. \" +\n", - " \"Performance may be slow.\");\n", - " }\n", - " }\n", - "\n", - " this.imageObj = new Image();\n", - "\n", - " this.context = undefined;\n", - " this.message = undefined;\n", - " this.canvas = undefined;\n", - " this.rubberband_canvas = undefined;\n", - " this.rubberband_context = undefined;\n", - " this.format_dropdown = undefined;\n", - "\n", - " this.image_mode = 'full';\n", - "\n", - " this.root = $('
');\n", - " this._root_extra_style(this.root)\n", - " this.root.attr('style', 'display: inline-block');\n", - "\n", - " $(parent_element).append(this.root);\n", - "\n", - " this._init_header(this);\n", - " this._init_canvas(this);\n", - " this._init_toolbar(this);\n", - "\n", - " var fig = this;\n", - "\n", - " this.waiting = false;\n", - "\n", - " this.ws.onopen = function () {\n", - " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n", - " fig.send_message(\"send_image_mode\", {});\n", - " if (mpl.ratio != 1) {\n", - " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n", - " }\n", - " fig.send_message(\"refresh\", {});\n", - " }\n", - "\n", - " this.imageObj.onload = function() {\n", - " if (fig.image_mode == 'full') {\n", - " // Full images could contain transparency (where diff images\n", - " // almost always do), so we need to clear the canvas so that\n", - " // there is no ghosting.\n", - " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", - " }\n", - " fig.context.drawImage(fig.imageObj, 0, 0);\n", - " };\n", - "\n", - " this.imageObj.onunload = function() {\n", - " fig.ws.close();\n", - " }\n", - "\n", - " this.ws.onmessage = this._make_on_message_function(this);\n", - "\n", - " this.ondownload = ondownload;\n", - "}\n", - "\n", - "mpl.figure.prototype._init_header = function() {\n", - " var titlebar = $(\n", - " '
');\n", - " var titletext = $(\n", - " '
');\n", - " titlebar.append(titletext)\n", - " this.root.append(titlebar);\n", - " this.header = titletext[0];\n", - "}\n", - "\n", - "\n", - "\n", - "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n", - "\n", - "}\n", - "\n", - "\n", - "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n", - "\n", - "}\n", - "\n", - "mpl.figure.prototype._init_canvas = function() {\n", - " var fig = this;\n", - "\n", - " var canvas_div = $('
');\n", - "\n", - " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n", - "\n", - " function canvas_keyboard_event(event) {\n", - " return fig.key_event(event, event['data']);\n", - " }\n", - "\n", - " canvas_div.keydown('key_press', canvas_keyboard_event);\n", - " canvas_div.keyup('key_release', canvas_keyboard_event);\n", - " this.canvas_div = canvas_div\n", - " this._canvas_extra_style(canvas_div)\n", - " this.root.append(canvas_div);\n", - "\n", - " var canvas = $('');\n", - " canvas.addClass('mpl-canvas');\n", - " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n", - "\n", - " this.canvas = canvas[0];\n", - " this.context = canvas[0].getContext(\"2d\");\n", - "\n", - " var backingStore = this.context.backingStorePixelRatio ||\n", - "\tthis.context.webkitBackingStorePixelRatio ||\n", - "\tthis.context.mozBackingStorePixelRatio ||\n", - "\tthis.context.msBackingStorePixelRatio ||\n", - "\tthis.context.oBackingStorePixelRatio ||\n", - "\tthis.context.backingStorePixelRatio || 1;\n", - "\n", - " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n", - "\n", - " var rubberband = $('');\n", - " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n", - "\n", - " var pass_mouse_events = true;\n", - "\n", - " canvas_div.resizable({\n", - " start: function(event, ui) {\n", - " pass_mouse_events = false;\n", - " },\n", - " resize: function(event, ui) {\n", - " fig.request_resize(ui.size.width, ui.size.height);\n", - " },\n", - " stop: function(event, ui) {\n", - " pass_mouse_events = true;\n", - " fig.request_resize(ui.size.width, ui.size.height);\n", - " },\n", - " });\n", - "\n", - " function mouse_event_fn(event) {\n", - " if (pass_mouse_events)\n", - " return fig.mouse_event(event, event['data']);\n", - " }\n", - "\n", - " rubberband.mousedown('button_press', mouse_event_fn);\n", - " rubberband.mouseup('button_release', mouse_event_fn);\n", - " // Throttle sequential mouse events to 1 every 20ms.\n", - " rubberband.mousemove('motion_notify', mouse_event_fn);\n", - "\n", - " rubberband.mouseenter('figure_enter', mouse_event_fn);\n", - " rubberband.mouseleave('figure_leave', mouse_event_fn);\n", - "\n", - " canvas_div.on(\"wheel\", function (event) {\n", - " event = event.originalEvent;\n", - " event['data'] = 'scroll'\n", - " if (event.deltaY < 0) {\n", - " event.step = 1;\n", - " } else {\n", - " event.step = -1;\n", - " }\n", - " mouse_event_fn(event);\n", - " });\n", - "\n", - " canvas_div.append(canvas);\n", - " canvas_div.append(rubberband);\n", - "\n", - " this.rubberband = rubberband;\n", - " this.rubberband_canvas = rubberband[0];\n", - " this.rubberband_context = rubberband[0].getContext(\"2d\");\n", - " this.rubberband_context.strokeStyle = \"#000000\";\n", - "\n", - " this._resize_canvas = function(width, height) {\n", - " // Keep the size of the canvas, canvas container, and rubber band\n", - " // canvas in synch.\n", - " canvas_div.css('width', width)\n", - " canvas_div.css('height', height)\n", - "\n", - " canvas.attr('width', width * mpl.ratio);\n", - " canvas.attr('height', height * mpl.ratio);\n", - " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n", - "\n", - " rubberband.attr('width', width);\n", - " rubberband.attr('height', height);\n", - " }\n", - "\n", - " // Set the figure to an initial 600x600px, this will subsequently be updated\n", - " // upon first draw.\n", - " this._resize_canvas(600, 600);\n", - "\n", - " // Disable right mouse context menu.\n", - " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n", - " return false;\n", - " });\n", - "\n", - " function set_focus () {\n", - " canvas.focus();\n", - " canvas_div.focus();\n", - " }\n", - "\n", - " window.setTimeout(set_focus, 100);\n", - "}\n", - "\n", - "mpl.figure.prototype._init_toolbar = function() {\n", - " var fig = this;\n", - "\n", - " var nav_element = $('
')\n", - " nav_element.attr('style', 'width: 100%');\n", - " this.root.append(nav_element);\n", - "\n", - " // Define a callback function for later on.\n", - " function toolbar_event(event) {\n", - " return fig.toolbar_button_onclick(event['data']);\n", - " }\n", - " function toolbar_mouse_event(event) {\n", - " return fig.toolbar_button_onmouseover(event['data']);\n", - " }\n", - "\n", - " for(var toolbar_ind in mpl.toolbar_items) {\n", - " var name = mpl.toolbar_items[toolbar_ind][0];\n", - " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", - " var image = mpl.toolbar_items[toolbar_ind][2];\n", - " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", - "\n", - " if (!name) {\n", - " // put a spacer in here.\n", - " continue;\n", - " }\n", - " var button = $('');\n", - " button.click(method_name, toolbar_event);\n", - " button.mouseover(tooltip, toolbar_mouse_event);\n", - " nav_element.append(button);\n", - " }\n", - "\n", - " // Add the status bar.\n", - " var status_bar = $('');\n", - " nav_element.append(status_bar);\n", - " this.message = status_bar[0];\n", - "\n", - " // Add the close button to the window.\n", - " var buttongrp = $('
');\n", - " var button = $('');\n", - " button.click(function (evt) { fig.handle_close(fig, {}); } );\n", - " button.mouseover('Stop Interaction', toolbar_mouse_event);\n", - " buttongrp.append(button);\n", - " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n", - " titlebar.prepend(buttongrp);\n", - "}\n", - "\n", - "mpl.figure.prototype._root_extra_style = function(el){\n", - " var fig = this\n", - " el.on(\"remove\", function(){\n", - "\tfig.close_ws(fig, {});\n", - " });\n", - "}\n", - "\n", - "mpl.figure.prototype._canvas_extra_style = function(el){\n", - " // this is important to make the div 'focusable\n", - " el.attr('tabindex', 0)\n", - " // reach out to IPython and tell the keyboard manager to turn it's self\n", - " // off when our div gets focus\n", - "\n", - " // location in version 3\n", - " if (IPython.notebook.keyboard_manager) {\n", - " IPython.notebook.keyboard_manager.register_events(el);\n", - " }\n", - " else {\n", - " // location in version 2\n", - " IPython.keyboard_manager.register_events(el);\n", - " }\n", - "\n", - "}\n", - "\n", - "mpl.figure.prototype._key_event_extra = function(event, name) {\n", - " var manager = IPython.notebook.keyboard_manager;\n", - " if (!manager)\n", - " manager = IPython.keyboard_manager;\n", - "\n", - " // Check for shift+enter\n", - " if (event.shiftKey && event.which == 13) {\n", - " this.canvas_div.blur();\n", - " event.shiftKey = false;\n", - " // Send a \"J\" for go to next cell\n", - " event.which = 74;\n", - " event.keyCode = 74;\n", - " manager.command_mode();\n", - " manager.handle_keydown(event);\n", - " }\n", - "}\n", - "\n", - "mpl.figure.prototype.handle_save = function(fig, msg) {\n", - " fig.ondownload(fig, null);\n", - "}\n", - "\n", - "\n", - "mpl.find_output_cell = function(html_output) {\n", - " // Return the cell and output element which can be found *uniquely* in the notebook.\n", - " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n", - " // IPython event is triggered only after the cells have been serialised, which for\n", - " // our purposes (turning an active figure into a static one), is too late.\n", - " var cells = IPython.notebook.get_cells();\n", - " var ncells = cells.length;\n", - " for (var i=0; i= 3 moved mimebundle to data attribute of output\n", - " data = data.data;\n", - " }\n", - " if (data['text/html'] == html_output) {\n", - " return [cell, data, j];\n", - " }\n", - " }\n", - " }\n", - " }\n", - "}\n", - "\n", - "// Register the function which deals with the matplotlib target/channel.\n", - "// The kernel may be null if the page has been refreshed.\n", - "if (IPython.notebook.kernel != null) {\n", - " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n", - "}\n" - ], + "image/png": "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", "text/plain": [ - "" + "
" ] }, "metadata": {}, @@ -1704,803 +150,9 @@ }, { "data": { - "text/html": [ - "" - ], + "image/png": "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", "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/javascript": [ - "/* Put everything inside the global mpl namespace */\n", - "window.mpl = {};\n", - "\n", - "\n", - "mpl.get_websocket_type = function() {\n", - " if (typeof(WebSocket) !== 'undefined') {\n", - " return WebSocket;\n", - " } else if (typeof(MozWebSocket) !== 'undefined') {\n", - " return MozWebSocket;\n", - " } else {\n", - " alert('Your browser does not have WebSocket support.' +\n", - " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", - " 'Firefox 4 and 5 are also supported but you ' +\n", - " 'have to enable WebSockets in about:config.');\n", - " };\n", - "}\n", - "\n", - "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n", - " this.id = figure_id;\n", - "\n", - " this.ws = websocket;\n", - "\n", - " this.supports_binary = (this.ws.binaryType != undefined);\n", - "\n", - " if (!this.supports_binary) {\n", - " var warnings = document.getElementById(\"mpl-warnings\");\n", - " if (warnings) {\n", - " warnings.style.display = 'block';\n", - " warnings.textContent = (\n", - " \"This browser does not support binary websocket messages. \" +\n", - " \"Performance may be slow.\");\n", - " }\n", - " }\n", - "\n", - " this.imageObj = new Image();\n", - "\n", - " this.context = undefined;\n", - " this.message = undefined;\n", - " this.canvas = undefined;\n", - " this.rubberband_canvas = undefined;\n", - " this.rubberband_context = undefined;\n", - " this.format_dropdown = undefined;\n", - "\n", - " this.image_mode = 'full';\n", - "\n", - " this.root = $('
');\n", - " this._root_extra_style(this.root)\n", - " this.root.attr('style', 'display: inline-block');\n", - "\n", - " $(parent_element).append(this.root);\n", - "\n", - " this._init_header(this);\n", - " this._init_canvas(this);\n", - " this._init_toolbar(this);\n", - "\n", - " var fig = this;\n", - "\n", - " this.waiting = false;\n", - "\n", - " this.ws.onopen = function () {\n", - " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n", - " fig.send_message(\"send_image_mode\", {});\n", - " if (mpl.ratio != 1) {\n", - " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n", - " }\n", - " fig.send_message(\"refresh\", {});\n", - " }\n", - "\n", - " this.imageObj.onload = function() {\n", - " if (fig.image_mode == 'full') {\n", - " // Full images could contain transparency (where diff images\n", - " // almost always do), so we need to clear the canvas so that\n", - " // there is no ghosting.\n", - " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", - " }\n", - " fig.context.drawImage(fig.imageObj, 0, 0);\n", - " };\n", - "\n", - " this.imageObj.onunload = function() {\n", - " fig.ws.close();\n", - " }\n", - "\n", - " this.ws.onmessage = this._make_on_message_function(this);\n", - "\n", - " this.ondownload = ondownload;\n", - "}\n", - "\n", - "mpl.figure.prototype._init_header = function() {\n", - " var titlebar = $(\n", - " '
');\n", - " var titletext = $(\n", - " '
');\n", - " titlebar.append(titletext)\n", - " this.root.append(titlebar);\n", - " this.header = titletext[0];\n", - "}\n", - "\n", - "\n", - "\n", - "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n", - "\n", - "}\n", - "\n", - "\n", - "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n", - "\n", - "}\n", - "\n", - "mpl.figure.prototype._init_canvas = function() {\n", - " var fig = this;\n", - "\n", - " var canvas_div = $('
');\n", - "\n", - " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n", - "\n", - " function canvas_keyboard_event(event) {\n", - " return fig.key_event(event, event['data']);\n", - " }\n", - "\n", - " canvas_div.keydown('key_press', canvas_keyboard_event);\n", - " canvas_div.keyup('key_release', canvas_keyboard_event);\n", - " this.canvas_div = canvas_div\n", - " this._canvas_extra_style(canvas_div)\n", - " this.root.append(canvas_div);\n", - "\n", - " var canvas = $('');\n", - " canvas.addClass('mpl-canvas');\n", - " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n", - "\n", - " this.canvas = canvas[0];\n", - " this.context = canvas[0].getContext(\"2d\");\n", - "\n", - " var backingStore = this.context.backingStorePixelRatio ||\n", - "\tthis.context.webkitBackingStorePixelRatio ||\n", - "\tthis.context.mozBackingStorePixelRatio ||\n", - "\tthis.context.msBackingStorePixelRatio ||\n", - "\tthis.context.oBackingStorePixelRatio ||\n", - "\tthis.context.backingStorePixelRatio || 1;\n", - "\n", - " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n", - "\n", - " var rubberband = $('');\n", - " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n", - "\n", - " var pass_mouse_events = true;\n", - "\n", - " canvas_div.resizable({\n", - " start: function(event, ui) {\n", - " pass_mouse_events = false;\n", - " },\n", - " resize: function(event, ui) {\n", - " fig.request_resize(ui.size.width, ui.size.height);\n", - " },\n", - " stop: function(event, ui) {\n", - " pass_mouse_events = true;\n", - " fig.request_resize(ui.size.width, ui.size.height);\n", - " },\n", - " });\n", - "\n", - " function mouse_event_fn(event) {\n", - " if (pass_mouse_events)\n", - " return fig.mouse_event(event, event['data']);\n", - " }\n", - "\n", - " rubberband.mousedown('button_press', mouse_event_fn);\n", - " rubberband.mouseup('button_release', mouse_event_fn);\n", - " // Throttle sequential mouse events to 1 every 20ms.\n", - " rubberband.mousemove('motion_notify', mouse_event_fn);\n", - "\n", - " rubberband.mouseenter('figure_enter', mouse_event_fn);\n", - " rubberband.mouseleave('figure_leave', mouse_event_fn);\n", - "\n", - " canvas_div.on(\"wheel\", function (event) {\n", - " event = event.originalEvent;\n", - " event['data'] = 'scroll'\n", - " if (event.deltaY < 0) {\n", - " event.step = 1;\n", - " } else {\n", - " event.step = -1;\n", - " }\n", - " mouse_event_fn(event);\n", - " });\n", - "\n", - " canvas_div.append(canvas);\n", - " canvas_div.append(rubberband);\n", - "\n", - " this.rubberband = rubberband;\n", - " this.rubberband_canvas = rubberband[0];\n", - " this.rubberband_context = rubberband[0].getContext(\"2d\");\n", - " this.rubberband_context.strokeStyle = \"#000000\";\n", - "\n", - " this._resize_canvas = function(width, height) {\n", - " // Keep the size of the canvas, canvas container, and rubber band\n", - " // canvas in synch.\n", - " canvas_div.css('width', width)\n", - " canvas_div.css('height', height)\n", - "\n", - " canvas.attr('width', width * mpl.ratio);\n", - " canvas.attr('height', height * mpl.ratio);\n", - " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n", - "\n", - " rubberband.attr('width', width);\n", - " rubberband.attr('height', height);\n", - " }\n", - "\n", - " // Set the figure to an initial 600x600px, this will subsequently be updated\n", - " // upon first draw.\n", - " this._resize_canvas(600, 600);\n", - "\n", - " // Disable right mouse context menu.\n", - " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n", - " return false;\n", - " });\n", - "\n", - " function set_focus () {\n", - " canvas.focus();\n", - " canvas_div.focus();\n", - " }\n", - "\n", - " window.setTimeout(set_focus, 100);\n", - "}\n", - "\n", - "mpl.figure.prototype._init_toolbar = function() {\n", - " var fig = this;\n", - "\n", - " var nav_element = $('
')\n", - " nav_element.attr('style', 'width: 100%');\n", - " this.root.append(nav_element);\n", - "\n", - " // Define a callback function for later on.\n", - " function toolbar_event(event) {\n", - " return fig.toolbar_button_onclick(event['data']);\n", - " }\n", - " function toolbar_mouse_event(event) {\n", - " return fig.toolbar_button_onmouseover(event['data']);\n", - " }\n", - "\n", - " for(var toolbar_ind in mpl.toolbar_items) {\n", - " var name = mpl.toolbar_items[toolbar_ind][0];\n", - " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", - " var image = mpl.toolbar_items[toolbar_ind][2];\n", - " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", - "\n", - " if (!name) {\n", - " // put a spacer in here.\n", - " continue;\n", - " }\n", - " var button = $('');\n", - " button.click(method_name, toolbar_event);\n", - " button.mouseover(tooltip, toolbar_mouse_event);\n", - " nav_element.append(button);\n", - " }\n", - "\n", - " // Add the status bar.\n", - " var status_bar = $('');\n", - " nav_element.append(status_bar);\n", - " this.message = status_bar[0];\n", - "\n", - " // Add the close button to the window.\n", - " var buttongrp = $('
');\n", - " var button = $('');\n", - " button.click(function (evt) { fig.handle_close(fig, {}); } );\n", - " button.mouseover('Stop Interaction', toolbar_mouse_event);\n", - " buttongrp.append(button);\n", - " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n", - " titlebar.prepend(buttongrp);\n", - "}\n", - "\n", - "mpl.figure.prototype._root_extra_style = function(el){\n", - " var fig = this\n", - " el.on(\"remove\", function(){\n", - "\tfig.close_ws(fig, {});\n", - " });\n", - "}\n", - "\n", - "mpl.figure.prototype._canvas_extra_style = function(el){\n", - " // this is important to make the div 'focusable\n", - " el.attr('tabindex', 0)\n", - " // reach out to IPython and tell the keyboard manager to turn it's self\n", - " // off when our div gets focus\n", - "\n", - " // location in version 3\n", - " if (IPython.notebook.keyboard_manager) {\n", - " IPython.notebook.keyboard_manager.register_events(el);\n", - " }\n", - " else {\n", - " // location in version 2\n", - " IPython.keyboard_manager.register_events(el);\n", - " }\n", - "\n", - "}\n", - "\n", - "mpl.figure.prototype._key_event_extra = function(event, name) {\n", - " var manager = IPython.notebook.keyboard_manager;\n", - " if (!manager)\n", - " manager = IPython.keyboard_manager;\n", - "\n", - " // Check for shift+enter\n", - " if (event.shiftKey && event.which == 13) {\n", - " this.canvas_div.blur();\n", - " event.shiftKey = false;\n", - " // Send a \"J\" for go to next cell\n", - " event.which = 74;\n", - " event.keyCode = 74;\n", - " manager.command_mode();\n", - " manager.handle_keydown(event);\n", - " }\n", - "}\n", - "\n", - "mpl.figure.prototype.handle_save = function(fig, msg) {\n", - " fig.ondownload(fig, null);\n", - "}\n", - "\n", - "\n", - "mpl.find_output_cell = function(html_output) {\n", - " // Return the cell and output element which can be found *uniquely* in the notebook.\n", - " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n", - " // IPython event is triggered only after the cells have been serialised, which for\n", - " // our purposes (turning an active figure into a static one), is too late.\n", - " var cells = IPython.notebook.get_cells();\n", - " var ncells = cells.length;\n", - " for (var i=0; i= 3 moved mimebundle to data attribute of output\n", - " data = data.data;\n", - " }\n", - " if (data['text/html'] == html_output) {\n", - " return [cell, data, j];\n", - " }\n", - " }\n", - " }\n", - " }\n", - "}\n", - "\n", - "// Register the function which deals with the matplotlib target/channel.\n", - "// The kernel may be null if the page has been refreshed.\n", - "if (IPython.notebook.kernel != null) {\n", - " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n", - "}\n" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "" - ], + "image/png": "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1dChQ7Vq1SqrS/J6f/7zn2Wz2Zp89ezZ0/n+oUOHNGXKFLVv316RkZG6/PLLtW/fPgsr9i5LlizRJZdcotTUVNlsNn344YdN3jcMQw888IBSUlIUFhamUaNGafv27U22KS4u1tVXX63o6GjFxsbqhhtuUEVFhYnfhXc41VxOmjTpmJ/VsWPHNtmGuWwwe/ZsnXHGGYqKilJiYqIuu+wybd26tck2rvzZzs7O1rhx4xQeHq7ExET94Q9/UF1dnZnfildwZT7PO++8Y34+J0+e3GQb5rP12kyomTt3rqZPn64HH3xQa9asUf/+/TVmzBgVFBRYXZrX6927t/Ly8pxfS5cudb43bdo0/fe//9W7776rxYsXa+/evfr1r39tYbXe5eDBg+rfv7+ef/75474/Z84cPfPMM3rxxRe1cuVKRUREaMyYMTp06JBzm6uvvlo//vij5s+fr08++URLlizRzTffbNa34DVONZeSNHbs2CY/q2+//XaT95nLBosXL9aUKVO0YsUKzZ8/X7W1tRo9erQOHjzo3OZUf7br6+s1btw41dTU6Ntvv9Ubb7yh119/XQ888IAV35KlXJlPSbrpppua/HzOmTPH+R7z6SZGGzFkyBBjypQpzuf19fVGamqqMXv2bAur8n4PPvig0b9//+O+V1JSYgQHBxvvvvuu87XNmzcbkozly5ebVKHvkGR88MEHzucOh8NITk42HnvsMedrJSUlht1uN95++23DMAxj06ZNhiRj9erVzm0+//xzw2azGbm5uabV7m2OnkvDMIyJEycav/zlL0/4GebyxAoKCgxJxuLFiw3DcO3P9meffWYEBAQY+fn5zm1eeOEFIzo62qiurjb3G/AyR8+nYRjGueeea/z+978/4WeYT/doE0dqampq9P3332vUqFHO1wICAjRq1CgtX77cwsp8w/bt25WamqouXbro6quvVnZ2tiTp+++/V21tbZN57dmzpzp16sS8uiArK0v5+flN5i8mJkZDhw51zt/y5csVGxurwYMHO7cZNWqUAgICtHLlStNr9naLFi1SYmKievTooVtvvVVFRUXO95jLEystLZX0801/XfmzvXz5cvXt21dJSUnObcaMGaOysjL9+OOPJlbvfY6ez0ZvvfWW4uPj1adPH82cOVOVlZXO95hP92gTN7QsLCxUfX19kx8WSUpKStKWLVss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", "text/plain": [ - "" + "
" ] }, "metadata": {}, @@ -3424,791 +293,9 @@ "outputs": [ { "data": { - "application/javascript": [ - "/* Put everything inside the global mpl namespace */\n", - "window.mpl = {};\n", - "\n", - "\n", - "mpl.get_websocket_type = function() {\n", - " if (typeof(WebSocket) !== 'undefined') {\n", - " return WebSocket;\n", - " } else if (typeof(MozWebSocket) !== 'undefined') {\n", - " return MozWebSocket;\n", - " } else {\n", - " alert('Your browser does not have WebSocket support.' +\n", - " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", - " 'Firefox 4 and 5 are also supported but you ' +\n", - " 'have to enable WebSockets in about:config.');\n", - " };\n", - "}\n", - "\n", - "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n", - " this.id = figure_id;\n", - "\n", - " this.ws = websocket;\n", - "\n", - " this.supports_binary = (this.ws.binaryType != undefined);\n", - "\n", - " if (!this.supports_binary) {\n", - " var warnings = document.getElementById(\"mpl-warnings\");\n", - " if (warnings) {\n", - " warnings.style.display = 'block';\n", - " warnings.textContent = (\n", - " \"This browser does not support binary websocket messages. \" +\n", - " \"Performance may be slow.\");\n", - " }\n", - " }\n", - "\n", - " this.imageObj = new Image();\n", - "\n", - " this.context = undefined;\n", - " this.message = undefined;\n", - " this.canvas = undefined;\n", - " this.rubberband_canvas = undefined;\n", - " this.rubberband_context = undefined;\n", - " this.format_dropdown = undefined;\n", - "\n", - " this.image_mode = 'full';\n", - "\n", - " this.root = $('
');\n", - " this._root_extra_style(this.root)\n", - " this.root.attr('style', 'display: inline-block');\n", - "\n", - " $(parent_element).append(this.root);\n", - "\n", - " this._init_header(this);\n", - " this._init_canvas(this);\n", - " this._init_toolbar(this);\n", - "\n", - " var fig = this;\n", - "\n", - " this.waiting = false;\n", - "\n", - " this.ws.onopen = function () {\n", - " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n", - " fig.send_message(\"send_image_mode\", {});\n", - " if (mpl.ratio != 1) {\n", - " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n", - " }\n", - " fig.send_message(\"refresh\", {});\n", - " }\n", - "\n", - " this.imageObj.onload = function() {\n", - " if (fig.image_mode == 'full') {\n", - " // Full images could contain transparency (where diff images\n", - " // almost always do), so we need to clear the canvas so that\n", - " // there is no ghosting.\n", - " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", - " }\n", - " fig.context.drawImage(fig.imageObj, 0, 0);\n", - " };\n", - "\n", - " this.imageObj.onunload = function() {\n", - " fig.ws.close();\n", - " }\n", - "\n", - " this.ws.onmessage = this._make_on_message_function(this);\n", - "\n", - " this.ondownload = ondownload;\n", - "}\n", - "\n", - "mpl.figure.prototype._init_header = function() {\n", - " var titlebar = $(\n", - " '
');\n", - " var titletext = $(\n", - " '
');\n", - " titlebar.append(titletext)\n", - " this.root.append(titlebar);\n", - " this.header = titletext[0];\n", - "}\n", - "\n", - "\n", - "\n", - "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n", - "\n", - "}\n", - "\n", - "\n", - "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n", - "\n", - "}\n", - "\n", - "mpl.figure.prototype._init_canvas = function() {\n", - " var fig = this;\n", - "\n", - " var canvas_div = $('
');\n", - "\n", - " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n", - "\n", - " function canvas_keyboard_event(event) {\n", - " return fig.key_event(event, event['data']);\n", - " }\n", - "\n", - " canvas_div.keydown('key_press', canvas_keyboard_event);\n", - " canvas_div.keyup('key_release', canvas_keyboard_event);\n", - " this.canvas_div = canvas_div\n", - " this._canvas_extra_style(canvas_div)\n", - " this.root.append(canvas_div);\n", - "\n", - " var canvas = $('');\n", - " canvas.addClass('mpl-canvas');\n", - " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n", - "\n", - " this.canvas = canvas[0];\n", - " this.context = canvas[0].getContext(\"2d\");\n", - "\n", - " var backingStore = this.context.backingStorePixelRatio ||\n", - "\tthis.context.webkitBackingStorePixelRatio ||\n", - "\tthis.context.mozBackingStorePixelRatio ||\n", - "\tthis.context.msBackingStorePixelRatio ||\n", - "\tthis.context.oBackingStorePixelRatio ||\n", - "\tthis.context.backingStorePixelRatio || 1;\n", - "\n", - " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n", - "\n", - " var rubberband = $('');\n", - " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n", - "\n", - " var pass_mouse_events = true;\n", - "\n", - " canvas_div.resizable({\n", - " start: function(event, ui) {\n", - " pass_mouse_events = false;\n", - " },\n", - " resize: function(event, ui) {\n", - " fig.request_resize(ui.size.width, ui.size.height);\n", - " },\n", - " stop: function(event, ui) {\n", - " pass_mouse_events = true;\n", - " fig.request_resize(ui.size.width, ui.size.height);\n", - " },\n", - " });\n", - "\n", - " function mouse_event_fn(event) {\n", - " if (pass_mouse_events)\n", - " return fig.mouse_event(event, event['data']);\n", - " }\n", - "\n", - " rubberband.mousedown('button_press', mouse_event_fn);\n", - " rubberband.mouseup('button_release', mouse_event_fn);\n", - " // Throttle sequential mouse events to 1 every 20ms.\n", - " rubberband.mousemove('motion_notify', mouse_event_fn);\n", - "\n", - " rubberband.mouseenter('figure_enter', mouse_event_fn);\n", - " rubberband.mouseleave('figure_leave', mouse_event_fn);\n", - "\n", - " canvas_div.on(\"wheel\", function (event) {\n", - " event = event.originalEvent;\n", - " event['data'] = 'scroll'\n", - " if (event.deltaY < 0) {\n", - " event.step = 1;\n", - " } else {\n", - " event.step = -1;\n", - " }\n", - " mouse_event_fn(event);\n", - " });\n", - "\n", - " canvas_div.append(canvas);\n", - " canvas_div.append(rubberband);\n", - "\n", - " this.rubberband = rubberband;\n", - " this.rubberband_canvas = rubberband[0];\n", - " this.rubberband_context = rubberband[0].getContext(\"2d\");\n", - " this.rubberband_context.strokeStyle = \"#000000\";\n", - "\n", - " this._resize_canvas = function(width, height) {\n", - " // Keep the size of the canvas, canvas container, and rubber band\n", - " // canvas in synch.\n", - " canvas_div.css('width', width)\n", - " canvas_div.css('height', height)\n", - "\n", - " canvas.attr('width', width * mpl.ratio);\n", - " canvas.attr('height', height * mpl.ratio);\n", - " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n", - "\n", - " rubberband.attr('width', width);\n", - " rubberband.attr('height', height);\n", - " }\n", - "\n", - " // Set the figure to an initial 600x600px, this will subsequently be updated\n", - " // upon first draw.\n", - " this._resize_canvas(600, 600);\n", - "\n", - " // Disable right mouse context menu.\n", - " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n", - " return false;\n", - " });\n", - "\n", - " function set_focus () {\n", - " canvas.focus();\n", - " canvas_div.focus();\n", - " }\n", - "\n", - " window.setTimeout(set_focus, 100);\n", - "}\n", - "\n", - "mpl.figure.prototype._init_toolbar = function() {\n", - " var fig = this;\n", - "\n", - " var nav_element = $('
')\n", - " nav_element.attr('style', 'width: 100%');\n", - " this.root.append(nav_element);\n", - "\n", - " // Define a callback function for later on.\n", - " function toolbar_event(event) {\n", - " return fig.toolbar_button_onclick(event['data']);\n", - " }\n", - " function toolbar_mouse_event(event) {\n", - " return fig.toolbar_button_onmouseover(event['data']);\n", - " }\n", - "\n", - " for(var toolbar_ind in mpl.toolbar_items) {\n", - " var name = mpl.toolbar_items[toolbar_ind][0];\n", - " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", - " var image = mpl.toolbar_items[toolbar_ind][2];\n", - " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", - "\n", - " if (!name) {\n", - " // put a spacer in here.\n", - " continue;\n", - " }\n", - " var button = $('