Source code for silq.pulses.pulse_types

from typing import Union, Sequence, Callable
import numpy as np
import collections
import logging

from silq.tools.general_tools import get_truth, property_ignore_setter, \
    freq_to_str, is_between

from qcodes.instrument.parameter_node import ParameterNode, parameter
from qcodes.instrument.parameter import Parameter
from qcodes.utils import validators as vals

__all__ = ['Pulse', 'SteeredInitialization', 'SinePulse', 'FrequencyRampPulse',
           'DCPulse', 'DCRampPulse', 'TriggerPulse', 'MarkerPulse',
           'TriggerWaitPulse', 'MeasurementPulse', 'CombinationPulse',
           'AWGPulse', 'pulse_conditions']

# Set of valid connection conditions for satisfies_conditions. These are
# useful when multiple objects have distinct satisfies_conditions kwargs
pulse_conditions = ['name', 'id', 't', 't_start', 't_stop',
                    'duration', 'acquire', 'initialize', 'connection',
                    'amplitude', 'enabled', 'average', 'pulse_class']

logger = logging.getLogger(__name__)


[docs]class Pulse(ParameterNode): """ Representation of physical pulse, component in a `PulseSequence`. A Pulse is a representation of a physical pulse, usually one that is outputted by an instrument. All pulses have specific timings, defined by ``t_start``, ``t_stop``, and ``duration``. Additional attributes specify ancillary properties, such as if the acquisition instrument should ``acquire`` the pulse. Specific pulse types (subclasses of `Pulse`) have additional properties, such as a ``frequency``. Pulses can be added to a `PulseSequence`, which can in turn be targeted by the `Layout`. Here, each `Pulse` is targeted to a `Connection`, which can modify the pulse (e.g. applying an amplitude scale). Next the pulse is targeted by the output and input `InstrumentInterface` of the connection, which provide an instrument-specific implementation of the pulse. Pulses usually have a name, which is used to retrieve any default properties from the config. If the pulse name is an entry in ``silq.config.{environment}.pulses``, the properties in that entry are used by default. These default values can be overridden by either passing them explicitly during the pulse initialization, or afterwards. Example: If ``silq.config.{environment}.pulses`` contains: >>> {'read': {'amplitude': 0.5, 'duration': 100e-3}} Then creating the following pulse will partially use these properties: >>> DCPulse('read', duration=200e-3) DCPulse('read', amplitude=0.5, duration=200e-3) Here the default ``amplitude`` value is used, but the duration is overridden during initialization. Parameters: name: Pulse name. If corresponding name is registered in pulse config, its properties will be copied to the pulse. id: Unique pulse identifier, assigned when added to `PulseSequence` if it already has another pulse with same name. Pre-existing pulse will be assigned id 0, and will increase for each successive pulse added. full_name: Pulse name, including id if not None. If id is not None, full_name is '{name}[{id}]' environment: Config environment to use for pulse config. If not set, default environment (``silq.config.properties.default_environment``) is used. t_start: Pulse start time. If undefined and added to `PulseSequence`, it will be set to `Pulse`.t_stop of last pulse. If no pulses are present, it will be set to zero. t_stop: Pulse stop time. Is updated whenever ``t_start`` or ``duration`` is changed. Changing this modifies ``duration`` but not ``t_start``. duration: Pulse duration. acquire: Flag to acquire pulse. If True, pulse will be passed on to the acquisition `InstrumentInterface` by the `Layout` during targeting. initialize: Pulse is used for initialization. This signals that the pulse can exist before the pulse sequence starts. In this case, pulse duration should be zero. connection (Connection): Connection that pulse is targeted to. Is only set for targeted pulse. enabled: Pulse is enabled. If False, it still exists in a PulseSequence, but is not included in targeting. average: Pulse acquisition average mode. Allowed modes are: * **'none'**: No averaging (return ``samples x points_per_trace``). * **'trace'**: Average over time (return ``points_per_trace``). * **'point'**: Average over time and sample (return single point). * **'point_segment:{N}'** Segment trace into N segment, average each segment into a point. connection_label: `Connection` label that Pulse should be targeted to. These are defined in ``silq.config.{environment}.connections``. If unspecified, pulse can only be targeted if ``connection_requirements`` uniquely determine connection. connection_requirements: Requirements that a connection must satisfy for targeting. If ``connection_label`` is defined, these are ignored. pulse_config: Pulse config whose attributes to match. If it exists, equal to ``silq.config.{environment}.pulses.{pulse.name}``, otherwise equal to zero. properties_config: General properties config whose attributes to match. If it exists, equal to ``silq.config.{environment}.properties``, otherwise None. Only `Pulse`.properties_attrs are matched. properties_attrs (List[str]): Attributes in properties config to match. Should be defined in ``__init__`` before calling ``Pulse.__init__``. implementation (PulseImplementation): Pulse implementation for targeted pulse, see `PulseImplementation`. connect_to_config: Connect parameters to the config (default True) """ # base config link to use for connecting pulse parameters to the config # Changing this will only affect pulses instantiated after change config_link = 'environment:pulses' multiple_senders = False def __init__(self, name: str = None, id: int = None, t_start: float = None, t_stop: float = None, duration: float = None, acquire: bool = False, initialize: bool = False, connection = None, enabled: bool = True, average: str = 'none', connection_label: str = None, connection_requirements: dict = {}, connect_to_config: bool = True): super().__init__(use_as_attributes=True, log_changes=False, simplify_snapshot=True) self.name = Parameter(initial_value=name, vals=vals.Strings(), set_cmd=None) self.id = Parameter(initial_value=id, vals=vals.Ints(allow_none=True), set_cmd=None, wrap_get=False) self.full_name = Parameter() self['full_name'].get() # Update to latest value ### Set attributes # Set attributes that can also be retrieved from pulse_config self.t_start = Parameter(initial_value=t_start, unit='s', set_cmd=None, wrap_get=False) self.duration = Parameter(initial_value=duration, unit='s', set_cmd=None, wrap_get=False) self.t_stop = Parameter(unit='s', wrap_get=False) # We separately set and get t_stop to ensure duration is also updated self.t_stop = t_stop self['t_stop'].get() # Since t_stop get/set cmd depends on t_start and duration, we perform # another set to ensure that duration is also set if t_stop is not None if self['t_stop'].raw_value is not None: self.t_stop = self['t_stop'].raw_value self.connection_label = Parameter(initial_value=connection_label, set_cmd=None) # Set attributes that should not be retrieved from pulse_config self.acquire = Parameter(initial_value=acquire, vals=vals.Bool(), set_cmd=None) self.initialize = Parameter(initial_value=initialize, vals=vals.Bool(), set_cmd=None) self.enabled = Parameter(initial_value=enabled, vals=vals.Bool(), set_cmd=None) self.connection = Parameter(initial_value=connection, set_cmd=None) self.average = Parameter(initial_value=average, vals=vals.Strings(), set_cmd=None) # Pulses can have a PulseImplementation after targeting self.implementation = None # List of potential connection requirements. # These can be set so that the pulse can only be sent to connections # matching these requirements self.connection_requirements = connection_requirements self._connected_to_config = False if connect_to_config: # Sets _connected_to_config to True self._connect_parameters_to_config() @parameter def average_vals(self, parameter, value): if value in ['none', 'trace', 'point']: return True elif ('point_segment' in value or 'trace_segment' in value): return True else: return False @parameter def full_name_get(self, parameter): if self.id is None: return self.name else: return f'{self.name}[{self.id}]' @parameter def t_start_set_parser(self, parameter, t_start): if t_start is not None: t_start = round(t_start, 11) return t_start @parameter def t_start_set(self, parameter, t_start): # Emit a t_stop signal when t_start is set self['t_start']._latest['raw_value'] = t_start self['t_stop'].set(self.t_stop, evaluate=False) @parameter def duration_set_parser(self, parameter, duration): if duration is not None: duration = round(duration, 11) return duration @parameter def duration_set(self, parameter, duration): # Emit a t_stop signal when duration is set self['duration']._latest['raw_value'] = duration self['t_stop'].set(self.t_stop, evaluate=False) @parameter def t_stop_get(self, parameter): if self.t_start is not None and self.duration is not None: val = round(self.t_start + self.duration, 11) else: val = None parameter._save_val(val) # Explicit save_val since we don't wrap_get return val @parameter def t_stop_set(self, parameter, t_stop): if t_stop is not None: # Setting duration sends a signal for duration # do not evaluate as it otherwise sends a second t_stop signal self['duration'].set(round(t_stop - self.t_start, 11), evaluate=False) def __eq__(self, other): """Comparison when pulses are equal Pulses are equal if all of their parameters (excluding list below) are equal. Furthermore, their classes need to be identical, which further means that any non-pulse object will never be equal. Connections are handled slightly differently. For pulses to be the same, they must either have the same connection or connection_label. Alternatively, if one pulse has a connection and the other a connection label, the label of the first pulse's connection is compared instead. If one pulse has a connection/connection label, and the other has neither, the pulses are not equal. Excluded parameters: - id - connection_requirements Returns: True if all above conditions hold, False otherwise. """ exclude_parameters = ['connection', 'connection_label', 'id', 'full_name'] if not self.matches_parameter_node(other, exclude_parameters=exclude_parameters): return False # Perform additional checks based on connection (labels). # Pulses are equal if they have the same connection, or has a matching # label, or if both pulses don't have a connection and connection label. if self.connection is not None: if other.connection is not None: return self.connection == other.connection elif other.connection_label is not None: return self.connection.label == other.connection_label else: return False elif self.connection_label is not None: if other.connection is not None: return self.connection_label == other.connection.label elif other.connection_label is not None: return self.connection_label == other.connection_label else: return False else: return other.connection is None and other.connection_label is None def __ne__(self, other): return not self.__eq__(other) def __hash__(self): """Define custom hash, used for creating a set of unique elements""" return hash(tuple(sorted(self.parameters.items()))) def __bool__(self): """Pulse is always equal to True""" return True def __add__(self, other: 'Pulse') -> 'CombinationPulse': """ This method is called when adding two pulses: ``pulse1 + pulse2``. Args: other: The pulse instance to be added to self. Returns: A new pulse instance representing the combination of two pulses. """ name = f'CombinationPulse_{id(self)+id(other)}' return CombinationPulse(name, self, other, '+') def __radd__(self, other) -> 'Pulse': """ This method is called when reverse adding something to a pulse. The reason this method is implemented is so that the user can sum over multiple pulses by performing: >>> combination_pulse = sum([pulse1, pulse2, pulse3]) The sum method actually tries calling 0.__add__(pulse1), which doesn't exist, so it is converted into pulse1.__radd__(0). Args: other: an instance of unknown type that might be int(0) Returns: Either self (if other is zero) or self + other. """ if other == 0: return self else: return self.__add__(other) def __sub__(self, other: 'Pulse') -> 'CombinationPulse': """Called when subtracting two pulses: ``pulse1 - pulse2`` Args: other: The pulse instance to be subtracted from self. Returns: A new pulse instance representing the combination of two pulses. """ name = f'CombinationPulse_{id(self)+id(other)}' return CombinationPulse(name, self, other, '-') def __mul__(self, other: 'Pulse') -> 'CombinationPulse': """Called when multiplying two pulses: ``pulse1 * pulse2``. Args: other: The pulse instance to be multiplied with self. Returns: A new pulse instance representing the combination of two pulses. """ name = f'CombinationPulse_{id(self)+id(other)}' return CombinationPulse(name, self, other, '*') def __copy__(self): """Create a copy of the pulse. Aside from using the default copy feature of the ParameterNode, this also connects the copied parameters to the config if the original ones are also connected """ self_copy = super().__copy__() if self._connected_to_config: self_copy._connect_parameters_to_config() return self_copy def _get_repr(self, properties_str): """Get standard representation for pulse. Should be appended in each Pulse subclass.""" if self.connection: properties_str += f'\n\tconnection: {self.connection}' elif self.connection_label: properties_str += f'\n\tconnection_label: {self.connection_label}' if self.connection_requirements: properties_str += f'\n\trequirements: {self.connection_requirements}' if hasattr(self, 'additional_pulses') and self.additional_pulses: properties_str += '\n\tadditional_pulses:' for pulse in self.additional_pulses: pulse_repr = '\t'.join(repr(pulse).splitlines(True)) properties_str += f'\n\t{pulse_repr}' pulse_class = self.__class__.__name__ return f'{pulse_class}({self.full_name}, {properties_str})' def _connect_parameters_to_config(self, parameters=None): """Connect Pulse parameters to config using Pulse.config_link. By connecting a parameter, every time the corresponding config value changes, this in turn changes the parameter value. The config link is {Pulse.config_link}.{self.name}.{parameter.name} Args: parameters: Parameters to Connect. Can be - None: Connect all parameters in self.parameters - str list: Connect all parameter with given string names - Parameter list: Connect all parameters in list """ if isinstance(parameters, list): if isinstance(parameters[0], str): parameters = {parameter: self.parameters[parameter] for parameter in parameters} else: parameters = {parameter.name: parameter for parameter in parameters} elif parameters is None: parameters = self.parameters for parameter_name, parameter in parameters.items(): config_link = f'{self.config_link}.{self.name}.{parameter_name}' config_value = parameter.set_config_link(config_link=config_link) # Update parameter value if not yet set, and set in config if parameter.raw_value is None and config_value is not None: parameter(config_value) self._connected_to_config = True
[docs] def snapshot_base(self, update: bool=False, params_to_skip_update: Sequence[str]=None): snapshot = super().snapshot_base() if snapshot['connection']: snapshot['connection'] = repr(snapshot['connection']) return snapshot
[docs] def satisfies_conditions(self, pulse_class = None, name: str=None, **kwargs) -> bool: """Checks if pulse satisfies certain conditions. Each kwarg is a condition, and can be a value (equality testing) or it can be a tuple (relation, value), in which case the relation is tested. Possible relations: '>', '<', '>=', '<=', '==' Args: pulse_class: Pulse must have specific class. name: Pulse must have name, which may include id. **kwargs: Additional pulse attributes to be satisfied. Examples are ``t_start``, ``connection``, etc. Time ``t`` can also be passed, in which case the condition is satisfied if t is between `Pulse`.t_start and `Pulse`.t_stop (including limits). Returns: True if all conditions are satisfied. """ if pulse_class is not None and not isinstance(self, pulse_class): return False if name is not None: if name[-1] == ']': # Pulse id is part of name name, id = name[:-1].split('[') kwargs['id'] = int(id) kwargs['name'] = name for property, val in kwargs.items(): if val is None: continue elif property == 't': if val < self.t_start or val >= self.t_stop: return False elif property not in self.parameters: return False else: # If arg is a tuple, the first element specifies its relation if isinstance(val, (list, tuple)): relation, val = val if not get_truth(test_val=self.parameters[property].get_latest(), # test_val=getattr(self, property), target_val=val, relation=relation): return False elif self.parameters[property]._latest['raw_value'] != val: return False else: return True
[docs] def get_voltage(self, t: Union[float, Sequence]) -> Union[float, np.ndarray]: """Get voltage(s) at time(s) t. Raises: AssertionError: not all ``t`` between `Pulse`.t_start and `Pulse`.t_stop """ raise NotImplementedError('Pulse.get_voltage should be implemented in a subclass')
[docs]class SteeredInitialization(Pulse): """Initialization pulse to ensure a spin-down electron is loaded. This is performed by continuously measuring at the read stage until no blip has been measured for ``t_no_blip``, or until ``t_max_wait`` has elapsed. Parameters: name: Pulse name t_no_blip: Min duration without measuring blips. If condition is met, an event should be fired to the primary instrument to start the pulse sequence. t_max_wait: Maximum wait time for the no-blip condition. If ``t_max_wait`` has elapsed, an event should be fired to the primary instrument to start the pulse seqeuence. t_buffer: Duration of a single acquisition buffer. Shorter buffers mean that one can more closely approach ``t_no_blip``, but too short buffers may cause lagging. readout_threshold_voltage: Threshold voltage for a blip. **kwargs: Additional parameters of `Pulse`. """ def __init__(self, name: str = None, t_no_blip: float = None, t_max_wait: float = None, t_buffer: float = None, readout_threshold_voltage: float = None, **kwargs): super().__init__(name=name, t_start=0, duration=0, initialize=True, **kwargs) self.t_no_blip = Parameter(initial_value=t_no_blip, unit='s', set_cmd=None, vals=vals.Numbers()) self.t_max_wait = Parameter(initial_value=t_max_wait, unit='s', set_cmd=None, vals=vals.Numbers()) self.t_buffer = Parameter(initial_value=t_buffer, unit='s', set_cmd=None, vals=vals.Numbers()) self.readout_threshold_voltage = Parameter(initial_value=readout_threshold_voltage, unit='V', set_cmd=None, vals=vals.Numbers()) self._connect_parameters_to_config( ['t_no_blip', 't_max_wait', 't_buffer', 'readout_threshold_voltage']) def __repr__(self): try: properties_str = (f't_no_blip={self.t_no_blip} ms, ' + f't_max_wait={self.t_max_wait}, ' + f't_buffer={self.t_buffer}, ' + f'V_th={self.readout_threshold_voltage}') except: properties_str = '' return super()._get_repr(properties_str)
[docs]class SinePulse(Pulse): """Sinusoidal pulse Parameters: name: Pulse name frequency: Pulse frequency phase: Pulse phase amplitude: Pulse amplitude. If not set, power must be set. power: Pulse power. If not set, amplitude must be set. offset: amplitude offset, zero by default frequency_sideband: Mixer sideband frequency (off by default). sideband_mode: Sideband frequency to apply. This feature must be existent in interface. Not used if not set. phase_reference: What point in the the phase is with respect to. Can be two modes: - 'absolute': phase is with respect to `Pulse.t_start`. - 'relative': phase is with respect to t=0 (phase-coherent). **kwargs: Additional parameters of `Pulse`. Notes: Either amplitude or power must be set, depending on the instrument that should output the pulse. """ def __init__(self, name: str = None, frequency: float = None, phase: float = None, amplitude: float = None, power: float = None, offset: float = None, frequency_sideband: float = None, sideband_mode: float = None, phase_reference: str = None, **kwargs): super().__init__(name=name, **kwargs) self.frequency = Parameter(initial_value=frequency, unit='Hz', set_cmd=None, vals=vals.Numbers()) self.phase = Parameter(initial_value=phase, unit='deg', set_cmd=None, vals=vals.Numbers()) self.power = Parameter(initial_value=power, unit='dBm', set_cmd=None, vals=vals.Numbers()) self.amplitude = Parameter(initial_value=amplitude, unit='V', set_cmd=None, vals=vals.Numbers()) self.offset = Parameter(initial_value=offset, unit='V', set_cmd=None, vals=vals.Numbers()) self.frequency_sideband = Parameter(initial_value=frequency_sideband, unit='Hz', set_cmd=None, vals=vals.Numbers()) self.sideband_mode = Parameter(initial_value=sideband_mode, set_cmd=None, vals=vals.Enum('IQ', 'double')) self.phase_reference = Parameter(initial_value=phase_reference, set_cmd=None, vals=vals.Enum('relative', 'absolute')) self._connect_parameters_to_config( ['frequency', 'phase', 'power', 'amplitude', 'phase', 'offset', 'frequency_sideband', 'sideband_mode', 'phase_reference']) if self.sideband_mode is None: self.sideband_mode = 'IQ' if self.phase_reference is None: self.phase_reference = 'absolute' if self.phase is None: self.phase = 0 if self.offset is None: self.offset = 0 def __repr__(self): properties_str = '' try: properties_str = f'f={freq_to_str(self.frequency)}' properties_str += f', phase={self.phase} deg ' properties_str += '(rel)' if self.phase_reference == 'relative' else '(abs)' if self.power is not None: properties_str += f', power={self.power} dBm' if self.amplitude is not None: properties_str += f', A={self.amplitude} V' if self.offset: properties_str += f', offset={self.offset} V' if self.frequency_sideband is not None: properties_str += f'f_sb={freq_to_str(self.frequency_sideband)} ' \ f'{self.sideband_mode}' properties_str += f', t_start={self.t_start}' properties_str += f', duration={self.duration}' except: pass return super()._get_repr(properties_str)
[docs] def get_voltage(self, t: Union[float, Sequence]) -> Union[float, np.ndarray]: """Get voltage(s) at time(s) t. Raises: AssertionError: not all ``t`` between `Pulse`.t_start and `Pulse`.t_stop """ assert is_between(t, self.t_start, self.t_stop), \ f"voltage at {t} s is not in the time range " \ f"{self.t_start} s - {self.t_stop} s of pulse {self}" if self.phase_reference == 'relative': t = t - self.t_start amplitude = self.amplitude if amplitude is None: assert self.power is not None, f'Pulse {self.name} does not have a specified power or amplitude.' if self['power'].unit == 'dBm': # This formula assumes the source is 50 Ohm matched and power is in dBm # A factor of 2 comes from the conversion from amplitude to RMS. amplitude = np.sqrt(10**(self.power/10) * 1e-3 * 100) waveform = amplitude * np.sin(2 * np.pi * (self.frequency * t + self.phase / 360)) waveform += self.offset return waveform
[docs]class FrequencyRampPulse(Pulse): """Linearly increasing/decreasing frequency `Pulse`. Parameters: name: Pulse name frequency_start: Start frequency frequency_stop: Stop frequency. frequency: Center frequency, only used if ``frequency_start`` and ``frequency_stop`` not used. frequency_deviation: Frequency deviation, only used if ``frequency_start`` and ``frequency_stop`` not used. frequency_final: Can be either ``start`` or ``stop`` indicating the frequency when reaching ``frequency_stop`` should go back to the initial frequency or stay at current frequency. Useful if the pulse doesn't immediately stop at the end (this depends on how the corresponding instrument/interface is programmed). amplitude: Pulse amplitude. If not set, power must be set. power: Pulse power. If not set, amplitude must be set. offset: amplitude offset, zero by default frequency_sideband: Sideband frequency to apply. This feature must be existent in interface. Not used if not set. sideband_mode: Type of mixer sideband ('IQ' by default) **kwargs: Additional parameters of `Pulse`. Notes: Either amplitude or power must be set, depending on the instrument that should output the pulse. """ def __init__(self, name: str = None, frequency_start: float = None, frequency_stop: float = None, frequency: float = None, frequency_deviation: float = None, amplitude: float = None, power: float = None, offset: float = None, phase: float = None, frequency_sideband: float = None, sideband_mode=None, phase_reference: str = None, **kwargs): super().__init__(name=name, **kwargs) if frequency_start is not None and frequency_stop is not None: frequency = (frequency_start + frequency_stop) / 2 frequency_deviation = (frequency_stop - frequency_start) / 2 self.frequency = Parameter(initial_value=frequency, unit='Hz', set_cmd=None, vals=vals.Numbers()) self.frequency_deviation = Parameter(initial_value=frequency_deviation, unit='Hz', set_cmd=None, vals=vals.Numbers()) self.frequency_start = Parameter(unit='Hz', set_cmd=None, vals=vals.Numbers()) self.frequency_stop = Parameter(unit='Hz', set_cmd=None, vals=vals.Numbers()) self.frequency_sideband = Parameter(initial_value=frequency_sideband, unit='Hz', set_cmd=None, vals=vals.Numbers()) self.sideband_mode = Parameter(initial_value=sideband_mode, set_cmd=None, vals=vals.Enum('IQ', 'double')) self.amplitude = Parameter(initial_value=amplitude, unit='V', set_cmd=None, vals=vals.Numbers()) self.power = Parameter(initial_value=power, unit='dBm', set_cmd=None, vals=vals.Numbers()) self.phase = Parameter(initial_value=phase, unit='deg', set_cmd=None, vals=vals.Numbers()) self.offset = Parameter(initial_value=offset, unit='V', set_cmd=None, vals=vals.Numbers()) self.phase_reference = Parameter(initial_value=phase_reference, set_cmd=None, vals=vals.Enum('relative', 'absolute')) self._connect_parameters_to_config( ['frequency', 'frequency_deviation', 'frequency_start', 'frequency_stop', 'frequency_sideband', 'sideband_mode', 'amplitude', 'power', 'phase', 'offset', 'phase_reference']) # Set default value for sideband_mode after connecting parameters, # because its value may have been retrieved from config if self.sideband_mode is not None: self.sideband_mode = 'IQ' if self.phase is None: self.phase = 0 if self.offset is None: self.offset = 0 if self.phase_reference is None: self.phase_reference = 'relative' @parameter def frequency_start_get(self, parameter): return self.frequency - self.frequency_deviation @parameter def frequency_start_set(self, parameter, frequency_start): frequency_stop = self.frequency_stop self.frequency = (frequency_start + frequency_stop) / 2 self.frequency_deviation = (frequency_stop - frequency_start) / 2 @parameter def frequency_stop_get(self, parameter): return self.frequency + self.frequency_deviation @parameter def frequency_stop_set(self, parameter, frequency_stop): frequency_start = self.frequency_start self.frequency = (frequency_start + frequency_stop) / 2 self.frequency_deviation = (frequency_stop - frequency_start) / 2 def __repr__(self): properties_str = '' try: properties_str = f'f={freq_to_str(self.frequency)}' properties_str += f', f_dev={freq_to_str(self.frequency_deviation)}' if self.frequency_sideband is not None: properties_str += f', f_sb={freq_to_str(self.frequency_sideband)}' \ f'{self.sideband_mode}' if self.power is not None: properties_str += f', power={self.power} dBm' if self.amplitude is not None: properties_str += f', A={self.amplitude} V' if self.offset: properties_str += f', offset={self.offset} V' properties_str += f', t_start={self.t_start}' properties_str += f', duration={self.duration}' except: pass return super()._get_repr(properties_str)
[docs] def get_voltage(self, t): frequency_rate = self.frequency_deviation / self.duration frequency_start = self.frequency - self.frequency_deviation amplitude = self.amplitude if amplitude is None: assert self.power is not None, f'Pulse {self.name} does not have a specified power or amplitude.' if self['power'].unit == 'dBm': # This formula assumes the source is 50 Ohm matched and power is in dBm # A factor of 2 comes from the conversion from amplitude to RMS. amplitude = np.sqrt(10 ** (self.power / 10) * 1e-3 * 100) if self.phase_reference == 'relative': t = t - self.t_start return amplitude * np.sin(2 * np.pi * (frequency_start * t + frequency_rate * np.power(t,2) / 2))
[docs]class DCPulse(Pulse): """DC (fixed-voltage) `Pulse`. Parameters: name: Pulse name amplitue: Pulse amplitude **kwargs: Additional parameters of `Pulse`. """ def __init__(self, name: str = None, amplitude: float = None, **kwargs): super().__init__(name=name, **kwargs) self.amplitude = Parameter(initial_value=amplitude, unit='V', set_cmd=None) self._connect_parameters_to_config(['amplitude']) def __repr__(self): properties_str = '' try: properties_str += f'A={self.amplitude}' properties_str += f', t_start={self.t_start}' properties_str += f', duration={self.duration}' except: pass return super()._get_repr(properties_str)
[docs] def get_voltage(self, t: Union[float, Sequence]) -> Union[float, np.ndarray]: """Get voltage(s) at time(s) t. Raises: AssertionError: not all ``t`` between `Pulse`.t_start and `Pulse`.t_stop """ assert is_between(t, self.t_start, self.t_stop), \ f"voltage at {t} s is not in the time range " \ f"{self.t_start} s - {self.t_stop} s of pulse {self}" if isinstance(t, collections.Iterable): return np.ones(len(t)) * self.amplitude else: return self.amplitude
[docs]class DCRampPulse(Pulse): """Linearly ramping voltage `Pulse`. Parameters: name: Pulse name amplitude_start: Start amplitude of pulse. amplitude_stop: Final amplitude of pulse. **kwargs: Additional parameters of `Pulse`. """ def __init__(self, name: str = None, amplitude_start: float = None, amplitude_stop: float = None, **kwargs): super().__init__(name=name, **kwargs) self.amplitude_start = Parameter(initial_value=amplitude_start, unit='V', set_cmd=None, vals=vals.Numbers()) self.amplitude_stop = Parameter(initial_value=amplitude_stop, unit='V', set_cmd=None, vals=vals.Numbers()) self._connect_parameters_to_config(['amplitude_start', 'amplitude_stop']) def __repr__(self): properties_str = '' try: properties_str = f'A_start={self.amplitude_start}' properties_str += f', A_stop={self.amplitude_stop}' properties_str += f', t_start={self.t_start}' properties_str += f', duration={self.duration}' except: pass return super()._get_repr(properties_str)
[docs] def get_voltage(self, t: Union[float, Sequence]) -> Union[float, np.ndarray]: """Get voltage(s) at time(s) t. Raises: AssertionError: not all ``t`` between `Pulse`.t_start and `Pulse`.t_stop """ assert is_between(t, self.t_start, self.t_stop), \ f"voltage at {t} s is not in the time range {self.t_start} s " \ f"- {self.t_stop} s of pulse {self}" slope = (self.amplitude_stop - self.amplitude_start) / self.duration offset = self.amplitude_start - slope * self.t_start return offset + slope * t
[docs]class TriggerPulse(Pulse): """Triggering pulse. Parameters: name: Pulse name. duration: Pulse duration (default 100 ns). amplitude: Pulse amplitude (default 1V). **kwargs: Additional parameters of `Pulse`. """ default_duration = 100e-9 default_amplitude = 1.0 def __init__(self, name: str = 'trigger', duration: float = default_duration, amplitude: float = default_amplitude, **kwargs): super().__init__(name=name, duration=duration, **kwargs) self.amplitude = Parameter(initial_value=amplitude, unit='V', set_cmd=None, vals=vals.Numbers()) self._connect_parameters_to_config(['amplitude']) def __repr__(self): try: properties_str = f't_start={self.t_start}, duration={self.duration}' except: properties_str = '' return super()._get_repr(properties_str)
[docs] def get_voltage(self, t: Union[float, Sequence]) -> Union[float, np.ndarray]: """Get voltage(s) at time(s) t. Raises: AssertionError: not all ``t`` between `Pulse`.t_start and `Pulse`.t_stop """ assert is_between(t, self.t_start, self.t_stop), \ f"voltage at {t} s is not in the time range " \ f"{self.t_start} s - {self.t_stop} s of pulse {self}" # Amplitude can only be provided in an implementation. # This is dependent on input/output channel properties. if isinstance(t, collections.Iterable): return np.ones(len(t)) * self.amplitude else: return self.amplitude
[docs]class MarkerPulse(Pulse): """Marker pulse Parameters: name: Pulse name. amplitude: Pulse amplitude (default 1V). **kwargs: Additional parameters of `Pulse`. """ default_amplitude = 1.0 def __init__(self, name: str = None, amplitude: float = default_amplitude, **kwargs): super().__init__(name=name, **kwargs) self.amplitude = Parameter(initial_value=amplitude, unit='V', set_cmd=None, vals=vals.Numbers()) self._connect_parameters_to_config(['amplitude']) if self.amplitude is not None: self.amplitude = self.default_amplitude def __repr__(self): try: properties_str = f't_start={self.t_start}, duration={self.duration}' except: properties_str = '' return super()._get_repr(properties_str)
[docs] def get_voltage(self, t: Union[float, Sequence]) -> Union[float, np.ndarray]: """Get voltage(s) at time(s) t. Raises: AssertionError: not all ``t`` between `Pulse`.t_start and `Pulse`.t_stop """ assert is_between(t, self.t_start, self.t_stop), \ f"voltage at {t} s is not in the time range " \ f"{self.t_start} s - {self.t_stop} s of pulse {self}" # Amplitude can only be provided in an implementation. # This is dependent on input/output channel properties. if isinstance(t, collections.Iterable): return np.ones(len(t)) * self.amplitude else: return self.amplitude
[docs]class TriggerWaitPulse(Pulse): """Pulse that wait until condition is met and then applies trigger Parameters: name: Pulse name t_start: Pulse start time. **kwargs: Additional parameters of `Pulse`. Note: Duration is fixed at 0s. See Also: `SteeredInitialization` """ def __init__(self, name: str = None, t_start: float = None, **kwargs): super().__init__(name=name, t_start=t_start, duration=0, **kwargs) def __repr__(self): try: properties_str = 't_start={self.t_start}, duration={self.duration}' except: properties_str = '' return super()._get_repr(properties_str)
[docs]class MeasurementPulse(Pulse): """Pulse that is only used to signify an acquiition This pulse is not directed to any interface other than the acquisition interface. Parameters: name: Pulse name. acquire: Acquire pulse (default True) **kwargs: Additional parameters of `Pulse`. """ def __init__(self, name=None, acquire=True, **kwargs): super().__init__(name=name, acquire=acquire, **kwargs) def __repr__(self): try: properties_str = f't_start={self.t_start}, duration={self.duration}' except: properties_str = '' return super()._get_repr(properties_str)
[docs] def get_voltage(self, t: Union[float, Sequence]) -> Union[float, np.ndarray]: """Get voltage(s) at time(s) t. Raises: AssertionError: not all ``t`` between `Pulse`.t_start and `Pulse`.t_stop """ raise NotImplementedError('Measurement pulses do not have a voltage')
[docs]class CombinationPulse(Pulse): """Pulse that is a combination of multiple pulse types. Like any other pulse, a CombinationPulse has a name, t_start and t_stop. t_start and t_stop are calculated and updated from the pulses that make up the combination. A CombinationPulse is itself a child of the Pulse class, therefore a CombinationPulse can also be used in consecutive combinations like: >>> CombinationPulse1 = SinePulse1 + DCPulse >>>CombinationPulse2 = SinePulse2 + CombinationPulse1 Examples: >>> CombinationPulse = SinePulse + DCPulse >>> CombinationPulse = DCPulse * SinePulse Parameters: name: The name for this CombinationPulse. pulse1: The first pulse this combination is made up from. pulse2: The second pulse this combination is made up from. relation: The relation between pulse1 and pulse2. This must be one of the following: * '+' : pulse1 + pulse2 * '-' : pulse1 - pulse2 * '*' : pulse1 * pulse2 **kwargs: Additional kwargs of `Pulse`. """ def __init__(self, name: str = None, pulse1: Pulse = None, pulse2: Pulse = None, relation: str = None, **kwargs): super().__init__(name=name, **kwargs) self.pulse1 = pulse1 self.pulse2 = pulse2 self.relation = relation assert isinstance(pulse1, Pulse), 'pulse1 needs to be a Pulse' assert isinstance(pulse2, Pulse), 'pulse2 needs to be a Pulse' assert relation in ['+', '-', '*'], 'relation has a non-supported value' @property_ignore_setter def t_start(self): return min(self.pulse1.t_start, self.pulse2.t_start) @property_ignore_setter def t_stop(self): return max(self.pulse1.t_stop, self.pulse2.t_stop) @property def combination_string(self): if isinstance(self.pulse1, CombinationPulse): pulse1_string = self.pulse1.combination_string else: pulse1_string = self.pulse1.name if isinstance(self.pulse2, CombinationPulse): pulse2_string = self.pulse2.combination_string else: pulse2_string = self.pulse2.name return '({pulse1} {relation} {pulse2})'.format(pulse1=pulse1_string, relation=self.relation, pulse2=pulse2_string) @property def pulse_details(self): if isinstance(self.pulse1, CombinationPulse): pulse1_details = self.pulse1.pulse_details else: pulse1_details = f'\t {self.pulse1.name} : {repr(self.pulse1)}\n' if isinstance(self.pulse2, CombinationPulse): pulse2_details = self.pulse2.pulse_details else: pulse2_details = f'\t {self.pulse2.name} : {repr(self.pulse2)}\n' return pulse1_details + pulse2_details def __repr__(self): properties_str = '' try: properties_str = 'combination: {self.combination}' properties_str += ', {self.pulse_details}' except: pass return super()._get_repr(properties_str)
[docs] def get_voltage(self, t: Union[float, Sequence]) -> Union[float, np.ndarray]: """Get voltage(s) at time(s) t. Raises: AssertionError: not all ``t`` between `Pulse`.t_start and `Pulse`.t_stop """ assert is_between(t, self.t_start, self.t_stop), \ "voltage at {} s is not in the time range {} s - {} s of " \ "pulse {}".format(t, self.t_start, self.t_stop, self) result1 = np.zeros(t.shape[0]) result2 = np.zeros(t.shape[0]) pulse1_t = t[np.all([self.pulse1.t_start <= t, t <= self.pulse1.t_stop], axis=0)] pulse2_t = t[np.all([self.pulse2.t_start <= t, t <= self.pulse2.t_stop], axis=0)] voltage1 = self.pulse1.get_voltage(pulse1_t) voltage2 = self.pulse2.get_voltage(pulse2_t) result1[np.all([self.pulse1.t_start <= t, t <= self.pulse1.t_stop], axis=0)] = voltage1 result2[np.all([self.pulse2.t_start <= t, t <= self.pulse2.t_stop], axis=0)] = voltage2 if self.relation == '+': return result1 + result2 elif self.relation == '-': return result1 - result2 elif self.relation == '*': return result1 * result2
[docs]class AWGPulse(Pulse): """Arbitrary waveform pulses that can be implemented by AWGs. This class allows the user to create a truly arbitrary pulse by either: - providing a function that converts time-stamps to waveform points - an array of waveform points The resulting AWGPulse can be sampled at different sample rates, interpolating between waveform points if necessary. Parameters: name: Pulse name. fun: The function used for calculating waveform points based on time-stamps. wf_array: Numpy array of (float) with time-stamps and waveform points. interpolate: Use interpolation of the wf_array. """ def __init__(self, name: str = None, fun:Callable = None, wf_array: np.ndarray = None, interpolate: bool = True, **kwargs): super().__init__(name=name, **kwargs) if fun: if not callable(fun): raise TypeError('The argument `function` must be a callable function.') self.from_function = True self.function = fun elif wf_array is not None: if not type(wf_array) == np.ndarray: raise TypeError('The argument `array` must be of type `np.ndarray`.') if not len(wf_array) == 2: raise TypeError('The argument `array` must be of length 2.') if not len(wf_array[0]) == len(wf_array[1]): raise TypeError('The argument `array` must have equal time-stamps and waveform points') assert np.all(np.diff(wf_array[0]) > 0), 'the time-stamps must be increasing' self.t_start = wf_array[0][0] self.t_stop = wf_array[0][-1] self.from_function = False self.array = wf_array self.interpolate = interpolate else: raise TypeError('Provide either a function or an array.')
[docs] @classmethod def from_array(cls, array, **kwargs): return cls(wf_array=array, **kwargs)
[docs] @classmethod def from_function(cls, function, **kwargs): return cls(fun=function, **kwargs)
def __repr__(self): properties_str = '' try: if self.from_function: properties_str = f'function:{self.function}' else: properties_str = f'array:{self.array.shape}' properties_str += ', t_start={self.t_start}' properties_str += ', duration={self.duration}' except: pass return super()._get_repr(properties_str)
[docs] def get_voltage(self, t: Union[float, Sequence]) -> Union[float, np.ndarray]: """Get voltage(s) at time(s) t. Raises: AssertionError: not all ``t`` between `Pulse`.t_start and `Pulse`.t_stop """ assert is_between(t, self.t_start, self.t_stop), \ "voltage at {} s is not in the time range {} s - {} s of " \ "pulse {}".format(t, self.t_start, self.t_stop, self) if self.from_function: return self.function(t) else: if self.interpolate: return np.interp(t, self.array[0], self.array[1]) elif np.in1d(t, self.array[0]).all(): mask = np.in1d(self.array[0], t) return self.array[1][mask] else: raise IndexError('All requested t-values must be in wf_array since interpolation is disabled.')