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. 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/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/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 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 diff --git a/tests/backward_compatibility/hardware_test_helper.py b/tests/backward_compatibility/hardware_test_helper.py index 1314283c2..2f2cef447 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 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/backward_compatibility/zhinst_charge_scan_test.py b/tests/backward_compatibility/zhinst_charge_scan_tests.py similarity index 98% rename from tests/backward_compatibility/zhinst_charge_scan_test.py rename to tests/backward_compatibility/zhinst_charge_scan_tests.py index 93c0c8038..de932f8b1 100644 --- a/tests/backward_compatibility/zhinst_charge_scan_test.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() 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 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() 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)