diff --git a/MATLAB/+qc/daq_operations.m b/MATLAB/+qc/daq_operations.m index 0d9abd91b..cb1bc51b5 100644 --- a/MATLAB/+qc/daq_operations.m +++ b/MATLAB/+qc/daq_operations.m @@ -56,10 +56,12 @@ % --- get length -------------------------------------------------------- elseif strcmp(ctrl, 'get length') % output is length % Operations need to have been added beforehand + mask_maker = py.getattr(daq, '_make_mask'); masks = util.py.py2mat(py.getattr(daq, '_registered_programs')); masks = util.py.py2mat(masks.(a.program_name)); operations = masks.operations; - masks = util.py.py2mat(masks.masks); + masks = util.py.py2mat(masks.masks(mask_maker)); + maskIdsFromOperations = cellfun(@(x)(char(x.maskID)), util.py.py2mat(operations), 'UniformOutput', false); maskIdsFromMasks = cellfun(@(x)(char(x.identifier)), util.py.py2mat(masks), 'UniformOutput', false); diff --git a/MATLAB/+qc/load_pulse.m b/MATLAB/+qc/load_pulse.m index 6478eb705..4c651bb92 100644 --- a/MATLAB/+qc/load_pulse.m +++ b/MATLAB/+qc/load_pulse.m @@ -1,4 +1,4 @@ function pulse = load_pulse(pulse_name) global plsdata - pulse = plsdata.qc.serializer.deserialize(pulse_name); \ No newline at end of file + pulse = plsdata.qc.pulse_storage{pulse_name}; \ No newline at end of file diff --git a/MATLAB/+qc/save_pulse.m b/MATLAB/+qc/save_pulse.m index 6b856b91b..4cafdd4ef 100644 --- a/MATLAB/+qc/save_pulse.m +++ b/MATLAB/+qc/save_pulse.m @@ -8,17 +8,23 @@ file_written = false; - try - plsdata.qc.serializer.serialize(pyargs('serializable', pulse_template, 'overwrite', overwrite)); - file_written = true; + if py.operator.contains(plsdata.qc.pulse_storage, pulse_template.identifier) + if overwrite + py.operator.delitem(plsdata.qc.pulse_storage, pulse_template.identifier); + else + warning('Did not write file as it exists and overwrite == false'); + return; + end + end + + try + plsdata.qc.pulse_storage{pulse_template.identifier} = pulse_template; + file_written = true; % fprintf('File(s) written\n'); - catch err - if util.str_contains(err.message, 'FileExistsError') - warning('%s\n', strrep(err.message, 'Python Error: ', '')); - else - warning(err.getReport()); - end - end + catch err + warning(err.getReport()); + end +end diff --git a/qupulse/_program/seqc.py b/qupulse/_program/seqc.py index 99c040a08..a139710d9 100644 --- a/qupulse/_program/seqc.py +++ b/qupulse/_program/seqc.py @@ -13,25 +13,25 @@ classes that convert `Loop` objects""" from typing import Optional, Union, Sequence, Dict, Iterator, Tuple, Callable, NamedTuple, MutableMapping, Mapping,\ - Iterable, Any + Iterable, Any, List, Deque from types import MappingProxyType import abc import itertools import inspect import logging -import os.path import hashlib from collections import OrderedDict -import string -import warnings +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 +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 @@ -129,6 +129,10 @@ def from_sampled(cls, ch1: Optional[np.ndarray], ch2: Optional[np.ndarray], marker_data += np.uint16((marker > 0) * 2**idx) return cls(zhinst.utils.convert_awg_waveform(ch1, ch2, marker_data)) + @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 @@ -161,8 +165,8 @@ def to_csv_compatible_table(self): class ConcatenatedWaveform: def __init__(self): """Handle the concatenation of multiple binary waveforms to create a big indexable waveform.""" - self._concatenated = [] - self._as_binary = None + self._concatenated: Optional[List[Tuple[BinaryWaveform, ...]]] = [] + self._as_binary: Optional[Tuple[BinaryWaveform, ...]] = None def __bool__(self): return bool(self._concatenated) @@ -170,19 +174,24 @@ def __bool__(self): def is_finalized(self): return self._as_binary is not None or self._concatenated is None - def as_binary(self) -> Optional[BinaryWaveform]: + def as_binary(self) -> Optional[Tuple[BinaryWaveform, ...]]: assert self.is_finalized() return self._as_binary - def append(self, binary_waveform): + 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: - concatenated_data = np.concatenate([wf.data for wf in self._concatenated]) - self._as_binary = BinaryWaveform(concatenated_data) + 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 @@ -238,13 +247,12 @@ def _sync(self, delete=True, write_all=False): class WaveformMemory: """Global waveform "memory" representation (currently the file system)""" - CONCATENATED_WAVEFORM_TEMPLATE = '{program_name}_concatenated_waveform' + 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), - ('wave_placeholder', str), ('file_name', str), ('binary_waveform', BinaryWaveform)]) @@ -256,7 +264,7 @@ def clear(self): self.shared_waveforms.clear() self.concatenated_waveforms.clear() - def _shared_waveforms_iter(self) -> Iterator[_WaveInfo]: + 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() @@ -264,16 +272,28 @@ def _shared_waveforms_iter(self) -> Iterator[_WaveInfo]: 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 self._WaveInfo(wave_name, wave_placeholder, file_name, wf) + yield wave_placeholder, self._WaveInfo(wave_name, file_name, wf) - def _concatenated_waveforms_iter(self) -> Iterator[_WaveInfo]: + 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: - wave_hash = concatenated_waveform.as_binary().fingerprint() + 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)) - wave_name = self.CONCATENATED_WAVEFORM_TEMPLATE.format(program_name=program_name) - file_name = self.FILE_NAME_TEMPLATE.format(hash=wave_hash) - yield self._WaveInfo(wave_name, wave_placeholder, file_name, concatenated_waveform.as_binary()) + 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 @@ -282,24 +302,18 @@ def waveform_name_replacements(self) -> Dict[str, str]: >>> seqc_program = qupulse.utils.replace_multiple(seqc_program, waveform_name_translation) """ translation = {} - for wave_info in self._shared_waveforms_iter(): - translation[wave_info.wave_placeholder] = wave_info.wave_name + for wave_placeholder, wave_info in self._shared_waveforms_iter(): + translation[wave_placeholder] = wave_info.wave_name - for wave_info in self._concatenated_waveforms_iter(): - translation[wave_info.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._concatenated_waveforms_iter(): - declarations.append( - 'wave {wave_name} = "{file_name}";'.format(wave_name=wave_info.wave_name, - file_name=wave_info.file_name.replace('.csv', '')) - ) - - for wave_info in self._shared_waveforms_iter(): + 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', '')) @@ -307,10 +321,8 @@ def waveform_declaration(self) -> str: 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 itertools.chain(self._concatenated_waveforms_iter(), - self._shared_waveforms_iter())} + for wave_info in self._all_info_iter()} file_system.sync(self, to_save) @@ -343,17 +355,20 @@ def clear_requested(self): programs.discard(self._program_name) self._memory.concatenated_waveforms[self._waveform_name].clear() - def request_shared(self, binary_waveform: BinaryWaveform) -> str: + 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. """ - program_set = self._memory.shared_waveforms.setdefault(binary_waveform, set()) - program_set.add(self._program_name) - return self._memory.WF_PLACEHOLDER_TEMPLATE.format(id=id(program_set)) - - def request_concatenated(self, binary_waveform: BinaryWaveform) -> str: + 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) @@ -471,19 +486,30 @@ 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], Optional[ChannelID]], - markers: Tuple[Optional[ChannelID], Optional[ChannelID], Optional[ChannelID], Optional[ChannelID]], - amplitudes: Tuple[float, float], - offsets: Tuple[float, float], - voltage_transformations: Tuple[Optional[Callable], Optional[Callable]], + 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, (sampled_channels, sampled_markers) in self._waveforms.items(): - self._waveforms[waveform] = BinaryWaveform.from_sampled(*sampled_channels, sampled_markers) + 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 @@ -569,7 +595,7 @@ def name(self) -> str: def parse_to_seqc(self, waveform_memory): raise NotImplementedError() - def get_binary_waveform(self, waveform: Waveform) -> BinaryWaveform: + def get_binary_waveform(self, waveform: Waveform) -> Tuple[BinaryWaveform, ...]: return self._waveforms[waveform] def prepare_delete(self): @@ -583,15 +609,51 @@ 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.""" - GLOBAL_CONSTS = dict(PROG_SEL_REGISTER=UserRegister(zero_based_value=0), - TRIGGER_REGISTER=UserRegister(zero_based_value=1), - TRIGGER_RESET_MASK=bin(1 << 15), - PROG_SEL_NONE=0, - NO_RESET_MASK=bin(1 << 15), - PROG_SEL_MASK=bin((1 << 15) - 1), - IDLE_WAIT_CYCLES=300) + 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' - INIT_PROGRAM_SWITCH = '// INIT program switch.\nvar prog_sel = 0;' WAIT_FOR_SOFTWARE_TRIGGER = "waitForSoftwareTrigger();" SOFTWARE_WAIT_FOR_TRIGGER_FUNCTION_DEFINITION = ( 'void waitForSoftwareTrigger() {\n' @@ -602,6 +664,12 @@ class HDAWGProgramManager: ' }\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): @@ -612,6 +680,29 @@ def get_program_function_name(cls, program_name: str): 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): @@ -620,15 +711,15 @@ def waveform_memory(self): 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.GLOBAL_CONSTS['PROG_SEL_NONE']: + 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], Optional[ChannelID]], - markers: Tuple[Optional[ChannelID], Optional[ChannelID], Optional[ChannelID], Optional[ChannelID]], - amplitudes: Tuple[float, float], - offsets: Tuple[float, float], - voltage_transformations: Tuple[Optional[Callable], Optional[Callable]], + 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. @@ -654,8 +745,10 @@ def add_program(self, name: str, loop: Loop, program_entry = HDAWGProgramEntry(loop, selection_index, self._waveform_memory, name, channels, markers, amplitudes, offsets, voltage_transformations, sample_rate) - # TODO: de-hardcode these parameters and put compilation in seperate function - program_entry.compile(20, 1000, ' ', self.WAIT_FOR_SOFTWARE_TRIGGER, + 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 @@ -701,7 +794,7 @@ def name_to_index(self, name: str) -> int: def to_seqc_program(self) -> str: lines = [] - for const_name, const_val in self.GLOBAL_CONSTS.items(): + for const_name, const_val in self.Constants.as_dict().items(): if isinstance(const_val, (int, str)): const_repr = str(const_val) else: @@ -710,7 +803,7 @@ def to_seqc_program(self) -> str: lines.append(self._waveform_memory.waveform_declaration()) - lines.append('\n//function used by manually triggered programs') + 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() @@ -722,14 +815,23 @@ def to_seqc_program(self) -> str: lines.append(replace_multiple(program.seqc_source, replacements)) lines.append('}\n') - lines.append(self.INIT_PROGRAM_SWITCH) + lines.append(self.GlobalVariables.get_init_block()) - lines.append('\n//runtime block') + lines.append('\n// runtime block') lines.append('while (true) {') lines.append(' // read program selection value') lines.append(' prog_sel = getUserReg(PROG_SEL_REGISTER);') - lines.append(' if (!(prog_sel & NO_RESET_MASK)) setUserReg(PROG_SEL_REGISTER, 0);') - lines.append(' prog_sel = prog_sel & PROG_SEL_MASK;') + lines.append(' ') + lines.append(' // calculate value to write back to PROG_SEL_REGISTER') + lines.append(' new_prog_sel = prog_sel | playback_finished;') + lines.append(' if (!(prog_sel & NO_RESET_MASK)) new_prog_sel &= INVERTED_PROG_SEL_MASK;') + lines.append(' setUserReg(PROG_SEL_REGISTER, new_prog_sel);') + lines.append(' ') + lines.append(' // reset playback flag') + lines.append(' playback_finished = 0;') + lines.append(' ') + lines.append(' // only use part of prog sel that does not mean other things to select the program.') + lines.append(' prog_sel &= PROG_SEL_MASK;') lines.append(' ') lines.append(' switch (prog_sel) {') @@ -738,6 +840,7 @@ def to_seqc_program(self) -> str: lines.append(' case {selection_index}:'.format(selection_index=program_entry.selection_index)) lines.append(' {program_function_name}();'.format(program_function_name=program_function_name)) lines.append(' waitWave();') + lines.append(' playback_finished = PLAYBACK_FINISHED_MASK;') lines.append(' default:') lines.append(' wait(IDLE_WAIT_CYCLES);') @@ -776,33 +879,159 @@ def mark_sharable_waveforms(node_cluster: Sequence['SEQCNode'], sharable_wavefor 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 - node_clusters = [] + # complexity: O( len(loop) * MAX_SUB_CLUSTER * loop.depth() ) + # I hope... + MAX_SUB_CLUSTER = 4 + + node_clusters: List[List[SEQCNode]] = [] + + + last_period = [] + # 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) - current_nodes = [last_node] + 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) - if last_node.same_stepping(current_node): - current_nodes.append(current_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)]) + + 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: - node_clusters.append(current_nodes) - current_nodes = [current_node] + 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) + node_clusters.append(current_cluster) + node_clusters.extend([node] for node in current_period) + node_clusters.extend([node] for node in last_nodes) - last_node = current_node - node_clusters.append(current_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], BinaryWaveform], + 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 @@ -858,6 +1087,10 @@ class SEQCNode(metaclass=abc.ABCMeta): 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 @@ -925,6 +1158,9 @@ 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 @@ -944,6 +1180,12 @@ def to_source_code(self, waveform_manager: ProgramWaveformManager, node_name_gen 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 + class Repeat(SEQCNode): """""" @@ -979,6 +1221,9 @@ def same_stepping(self, other: 'Repeat'): 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() @@ -1078,6 +1323,9 @@ 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 @@ -1111,22 +1359,32 @@ class WaveformPlayback(SEQCNode): __slots__ = ('waveform', 'shared') - def __init__(self, waveform: BinaryWaveform, shared: bool = False): + def __init__(self, waveform: Tuple[BinaryWaveform, ...], shared: bool = False): + assert isinstance(waveform, tuple) self.waveform = waveform self.shared = shared - def samples(self): + def samples(self) -> int: if self.shared: return 0 else: - return len(self.waveform) + wf_lens = set(map(len, self.waveform)) + assert len(wf_lens) == 1 + wf_len, = wf_lens + return wf_len + + 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'): + 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.waveform == other.waveform else: - return same_type and len(self.waveform) == len(other.waveform) + return same_type and self.samples() == other.samples() def iter_waveform_playbacks(self) -> Iterator['WaveformPlayback']: yield self @@ -1143,7 +1401,7 @@ def to_source_code(self, waveform_manager: ProgramWaveformManager, line_prefix=line_prefix) else: wf_name = waveform_manager.request_concatenated(self.waveform) - wf_len = len(self.waveform) + wf_len = self.samples() play_cmd = '{line_prefix}playWaveIndexed({wf_name}, {pos_var_name}, {wf_len});' if advance_pos_var: diff --git a/qupulse/hardware/awgs/zihdawg.py b/qupulse/hardware/awgs/zihdawg.py index a1d42a214..990e52050 100644 --- a/qupulse/hardware/awgs/zihdawg.py +++ b/qupulse/hardware/awgs/zihdawg.py @@ -1,6 +1,6 @@ from pathlib import Path import functools -from typing import Tuple, Set, Callable, Optional, Mapping, Generator, Union +from typing import Tuple, Set, Callable, Optional, Mapping, Generator, Union, List, Dict from enum import Enum import weakref import logging @@ -53,7 +53,8 @@ def __init__(self, device_serial: str = None, data_server_port: int = 8004, api_level_number: int = 6, reset: bool = False, - timeout: float = 20) -> None: + 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' @@ -66,6 +67,7 @@ def __init__(self, device_serial: str = None, self._api_session = zhinst.ziPython.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: @@ -76,34 +78,48 @@ def __init__(self, device_serial: str = None, waveform_path = pathlib.Path(self.api_session.awgModule().getString('directory'), 'awg', 'waves') self._waveform_file_system = WaveformFileSystem(waveform_path) - self._channel_pair_AB = HDAWGChannelPair(self, (1, 2), str(self.serial) + '_AB', timeout) - self._channel_pair_CD = HDAWGChannelPair(self, (3, 4), str(self.serial) + '_CD', timeout) - self._channel_pair_EF = HDAWGChannelPair(self, (5, 6), str(self.serial) + '_EF', timeout) - self._channel_pair_GH = HDAWGChannelPair(self, (7, 8), str(self.serial) + '_GH', timeout) + 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() + groups = [] + for group_idx in range(n_channels // group_size): + groups.append(HDAWGChannelGroup(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['HDAWGChannelPair', ...]: - return self._channel_pair_AB, self._channel_pair_CD, self._channel_pair_EF, self._channel_pair_GH + def channel_tuples(self) -> Tuple['HDAWGChannelGroup', ...]: + return self._channel_groups[self.channel_grouping] @property - def channel_pair_AB(self) -> 'HDAWGChannelPair': - return self._channel_pair_AB + def channel_pair_AB(self) -> 'HDAWGChannelGroup': + return self._channel_groups[HDAWGChannelGrouping.CHAN_GROUP_4x2][0] @property - def channel_pair_CD(self) -> 'HDAWGChannelPair': - return self._channel_pair_CD + def channel_pair_CD(self) -> 'HDAWGChannelGroup': + return self._channel_groups[HDAWGChannelGrouping.CHAN_GROUP_4x2][1] @property - def channel_pair_EF(self) -> 'HDAWGChannelPair': - return self._channel_pair_EF + def channel_pair_EF(self) -> 'HDAWGChannelGroup': + return self._channel_groups[HDAWGChannelGrouping.CHAN_GROUP_4x2][2] @property - def channel_pair_GH(self) -> 'HDAWGChannelPair': - return self._channel_pair_GH + def channel_pair_GH(self) -> 'HDAWGChannelGroup': + return self._channel_groups[HDAWGChannelGrouping.CHAN_GROUP_4x2][3] @property def api_session(self) -> zhinst.ziPython.ziDAQServer: @@ -114,20 +130,13 @@ def serial(self) -> str: return self._dev_ser def _initialize(self) -> None: - settings = [] - settings.append(['/{}/system/awg/channelgrouping'.format(self.serial), - HDAWGChannelGrouping.CHAN_GROUP_4x2.value]) - settings.append(['/{}/awgs/*/time'.format(self.serial), 0]) # Maximum sampling rate. - settings.append(['/{}/sigouts/*/range'.format(self.serial), HDAWGVoltageRange.RNG_1V.value]) - settings.append(['/{}/awgs/*/outputs/*/amplitude'.format(self.serial), 1.0]) # Default amplitude factor 1.0 - settings.append(['/{}/awgs/*/outputs/*/modulation/mode'.format(self.serial), HDAWGModulationMode.OFF.value]) - settings.append(['/{}/awgs/*/userregs/*'.format(self.serial), 0]) # Reset all user registers to 0. - settings.append(['/{}/awgs/*/single'.format(self.serial), 1]) # Single execution mode of sequence. + 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_2_MARK_1.value + output = HDAWGTriggerOutSource.OUT_1_MARK_2.value settings.append(['/{}/triggers/out/{}/source'.format(self.serial, ch), output]) self.api_session.set(settings) @@ -136,10 +145,54 @@ def _initialize(self) -> None: def reset(self) -> None: zhinst.utils.disable_everything(self.api_session, self.serial) self._initialize() - self.channel_pair_AB.clear() - self.channel_pair_CD.clear() - self.channel_pair_EF.clear() - self.channel_pair_GH.clear() + 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: + return str(self.serial) + '_' + 'ABCDEFGH'[group_idx*group_size:][:group_size] + + def _get_groups(self, grouping: 'HDAWGChannelGrouping') -> Tuple['HDAWGChannelGroup', ...]: + return self._channel_groups[grouping] + + @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() + + 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: @@ -154,7 +207,6 @@ def offset(self, channel: int, voltage: float = None) -> float: 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""" - assert channel > 0 node_path = '/{}/sigouts/{:d}/range'.format(self.serial, channel-1) if voltage is not None: self.api_session.setDouble(node_path, voltage) @@ -207,6 +259,13 @@ class HDAWGChannelGrouping(Enum): 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 + }[self] + class HDAWGVoltageRange(Enum): """All available voltage ranges for the HDAWG wave outputs. Define maximum output voltage.""" @@ -231,7 +290,7 @@ class HDAWGModulationMode(Enum): ADVANCED = 5 # AWG output modulates corresponding sines from modulation carriers. -class HDAWGChannelPair(AWG): +class HDAWGChannelGroup(AWG): """Represents a channel pair of the Zurich Instruments HDAWG as an independent AWG entity. It represents a set of channels that have to have(hardware enforced) the same control flow and sample rate. @@ -241,47 +300,77 @@ class HDAWGChannelPair(AWG): MIN_WAVEFORM_LEN = 192 WAVEFORM_LEN_QUANTUM = 16 - def __init__(self, hdawg_device: HDAWGRepresentation, - channels: Tuple[int, int], + def __init__(self, + group_idx: int, + group_size: int, identifier: str, timeout: float) -> None: super().__init__(identifier) - self._device = weakref.proxy(hdawg_device) + self._device = None + + assert group_idx in range(4) + assert group_size in (2, 4, 8) - if channels not in ((1, 2), (3, 4), (5, 6), (7, 8)): - raise HDAWGValueError('Invalid channel pair: {}'.format(channels)) - self._channels = channels + self._group_idx = group_idx + self._group_size = group_size 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 = () + + def _initialize_awg_module(self): + """Only run once""" + if self._awg_module: + self._awg_module.clear() self._awg_module = self.device.api_session.awgModule() self.awg_module.set('awgModule/device', self.device.serial) self.awg_module.set('awgModule/index', self.awg_group_index) self.awg_module.execute() + self._elf_manager = ELFManager(self.awg_module) + + 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._device = None + + def connect_group(self, hdawg_device: HDAWGRepresentation): + """""" + self.disconnect_group() + self._device = weakref.proxy(hdawg_device) + assert self.device.channel_grouping.group_size() == self._group_size, f"{self.device.channel_grouping} != {self._group_size}" + self._initialize_awg_module() # Seems creating AWG module sets SINGLE (single execution mode of sequence) to 0 per default. self.device.api_session.setInt('/{}/awgs/{:d}/single'.format(self.device.serial, self.awg_group_index), 1) - self._program_manager = HDAWGProgramManager() - self._elf_manager = ELFManager(self.awg_module) - 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 = () + def is_connected(self) -> bool: + return self._device is not None @property def num_channels(self) -> int: """Number of channels""" - return 2 + 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._group_size)) @property def num_markers(self) -> int: """Number of marker channels""" - return 4 + return 2 * self.num_channels def upload(self, name: str, program: Loop, - channels: Tuple[Optional[ChannelID], Optional[ChannelID]], - markers: Tuple[Optional[ChannelID], Optional[ChannelID], Optional[ChannelID], Optional[ChannelID]], - voltage_transformation: Tuple[Callable, Callable], + channels: Tuple[Optional[ChannelID], ...], + markers: Tuple[Optional[ChannelID], ...], + voltage_transformation: Tuple[Callable, ...], force: bool = False) -> None: """Upload a program to the AWG. @@ -328,14 +417,13 @@ def upload(self, name: str, sample_rate=q_sample_rate) if self._amplitude_offset_handling == AWGAmplitudeOffsetHandling.IGNORE_OFFSET: - voltage_offsets = (0., 0.) + voltage_offsets = (0.,) * self.num_channels elif self._amplitude_offset_handling == AWGAmplitudeOffsetHandling.CONSIDER_OFFSET: - voltage_offsets = (self._device.offset(self._channels[0]), - self._device.offset(self._channels[1])) + voltage_offsets = self.offsets() else: raise ValueError('{} is invalid as AWGAmplitudeOffsetHandling'.format(self._amplitude_offset_handling)) - amplitudes = self.amplitude(1), self.amplitude(2) + amplitudes = self.amplitudes() if name in self._program_manager.programs: self._program_manager.remove(name) @@ -365,6 +453,11 @@ def _wait_for_compile_and_upload(self): 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, ConstantParameter]): """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, @@ -405,11 +498,11 @@ def arm(self, name: Optional[str]) -> None: if self._required_seqc_source != self._uploaded_seqc_source: self._wait_for_compile_and_upload() - self.user_register(self._program_manager.GLOBAL_CONSTS['TRIGGER_REGISTER'], 0) + self.user_register(self._program_manager.Constants.TRIGGER_REGISTER, 0) if name is None: - self.user_register(self._program_manager.GLOBAL_CONSTS['PROG_SEL_REGISTER'], - self._program_manager.GLOBAL_CONSTS['PROG_SEL_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: @@ -418,12 +511,12 @@ def arm(self, name: Optional[str]) -> None: # 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.GLOBAL_CONSTS['PROG_SEL_REGISTER'], - self._program_manager.GLOBAL_CONSTS['TRIGGER_REGISTER']) + 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.GLOBAL_CONSTS['PROG_SEL_REGISTER'], - self._program_manager.name_to_index(name) | int(self._program_manager.GLOBAL_CONSTS['NO_RESET_MASK'], 2)) + self.user_register(self._program_manager.Constants.PROG_SEL_REGISTER, + self._program_manager.name_to_index(name) | int(self._program_manager.Constants.NO_RESET_MASK, 2)) # this is a workaround for problems in the past and should be re-thought in case of a re-write for ch_pair in self.device.channel_tuples: @@ -437,7 +530,8 @@ def run_current_program(self) -> None: raise HDAWGValueError('{} is unknown on {}'.format(self._current_program, self.identifier)) if not self.enable(): self.enable(True) - self.user_register(self._program_manager.GLOBAL_CONSTS['TRIGGER_REGISTER'], int(self._program_manager.GLOBAL_CONSTS['TRIGGER_RESET_MASK'], 2)) + self.user_register(self._program_manager.Constants.TRIGGER_REGISTER, + int(self._program_manager.Constants.TRIGGER_RESET_MASK, 2)) else: raise HDAWGRuntimeError('No program active') @@ -462,16 +556,20 @@ def sample_rate(self) -> TimeType: @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._channels[0] // 2 + return self._group_idx @property def device(self) -> HDAWGRepresentation: """Reference to HDAWG representation.""" + if self._device is None: + raise HDAWGValueError('Channel group is currently not connected') return self._device @property def awg_module(self) -> zhinst.ziPython.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 @@ -512,11 +610,12 @@ def user_register(self, reg: UserRegister, value: int = None) -> int: self.device.api_session.sync() # Global sync: Ensure settings have taken effect on the device. return self.device.api_session.getInt(node_path) - def _amplitude_scale(self, channel: int) -> float: - assert channel in (1, 2) - return self.device.api_session.getDouble(f'/{self.device.serial}/awgs/{self.awg_group_index:d}/outputs/{channel-1:d}/amplitude') + def _amplitude_scales(self) -> Tuple[float, ...]: + """not affected by grouping""" + return tuple(self.device.api_session.getDouble(f'/{self.device.serial}/awgs/{ch // 2:d}/outputs/{ch % 2:d}/amplitude') + for ch in self._channels(index_start=0)) - def amplitude(self, channel: int) -> float: + def amplitudes(self) -> Tuple[float, ...]: """Query AWG channel amplitude value (not peak to peak). From manual: @@ -524,16 +623,15 @@ def amplitude(self, channel: int) -> float: 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(), self._amplitude_scales()): + zi_range = self.device.range(ch) + amplitudes.append(zi_amplitude * zi_range / 2) + return tuple(amplitudes) - if channel not in (1, 2): - raise HDAWGValueError('{} not a valid (1-2) channel.'.format(channel)) - - # scale - zi_amplitude = self._amplitude_scale(channel) - zi_range = self.device.range(self.awg_group_index * 2 + channel) - - return zi_amplitude * zi_range / 2 + def offsets(self) -> Tuple[float, ...]: + return tuple(map(self.device.offset, self._channels())) class ELFManager: @@ -622,8 +720,8 @@ def __init__(self, awg_module: zhinst.ziPython.AwgModule): def clear(self): """Deletes all files with a SHA512 hash name""" - src_regex = re.compile('[a-z0-9]{128}\.seqc') - elf_regex = re.compile('[a-z0-9]{128}\.elf') + 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): @@ -843,7 +941,25 @@ def __str__(self) -> str: return "Upload to the instrument failed." -def example_upload(hdawg_kwargs: dict, channels: Set[int], markers: Set[Tuple[int, int]]): +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" + + 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]] + + +def example_upload(hdawg_kwargs: dict, channels: Set[int], markers: Set[Tuple[int, int]]): # pragma: no cover from qupulse.pulses import TablePT, SequencePT, RepetitionPT if isinstance(hdawg_kwargs, dict): hdawg = HDAWGRepresentation(**hdawg_kwargs) @@ -853,13 +969,15 @@ def example_upload(hdawg_kwargs: dict, channels: Set[int], markers: Set[Tuple[in assert not set(channels) - set(range(8)), "Channels must be in 0..=7" channels = sorted(channels) - channel_tuples = [ct for i, ct in enumerate(hdawg.channel_tuples) if i*2 in channels or i*2+1 in channels] - assert channel_tuples + required_channels = {*channels, *(ch for ch, _ in markers)} + channel_group = get_group_for_channels(hdawg, required_channels) + channel_group_channels = range(channel_group.awg_group_index * channel_group.num_channels, + (channel_group.awg_group_index + 1) * channel_group.num_channels) # choose length based on minimal sample rate - min_sr = min(ct.sample_rate for ct in channel_tuples) / 10**9 - min_t = channel_tuples[0].MIN_WAVEFORM_LEN / min_sr - quant_t = channel_tuples[0].WAVEFORM_LEN_QUANTUM / min_sr + sample_rate = channel_group.sample_rate / 10**9 + min_t = channel_group.MIN_WAVEFORM_LEN / sample_rate + quant_t = channel_group.WAVEFORM_LEN_QUANTUM / sample_rate assert min_t > 4 * quant_t, "Example not updated" @@ -881,54 +999,48 @@ def example_upload(hdawg_kwargs: dict, channels: Set[int], markers: Set[Tuple[in spt2 = SequencePT(rpt2, tpt3) p = spt2.create_program() - # use HardwareSetup for this - for ct in channel_tuples: - i = hdawg.channel_tuples.index(ct) - ch = tuple(ch if ch in channels else None for ch in (2*i, 2*i+1)) - mk = (None, None, None, None) - vt = (lambda x: x, lambda x: x) - ct.upload('pulse_test1', p, ch, mk, vt) + upload_ch = tuple(ch if ch in channels else None + for ch in channel_group_channels) + upload_mk = (None,) * channel_group.num_markers + upload_vt = (lambda x: x,) * channel_group.num_channels - for ct in channel_tuples: - ct.arm('pulse_test1') - - # channel_tuples[0].run_current_program() + channel_group.upload('pulse_test1', p, upload_ch, upload_mk, upload_vt) if markers: markers = sorted(markers) assert len(markers) == len(set(markers)) - - channel_tuples = [] - for ch, m in markers: - assert ch in range(8) - assert m in (0, 1) - ct = hdawg.channel_tuples[ch//2] - if ct not in channel_tuples: - channel_tuples.append(ct) + channel_group_markers = tuple((ch, mk) + for ch in channel_group_channels + for mk in (0, 1)) full_on = [(0, 1), (min_t, 1)] two_3rd = [(0, 1), (min_t*2/3, 0), (min_t, 0)] one_3rd = [(0, 0), (min_t*2/3, 1), (min_t, 1)] - marker_start = TablePT({'m1': full_on, 'm2': full_on}) - marker_body = TablePT({'m1': two_3rd, 'm2': one_3rd}) + marker_start = TablePT({'m0': full_on, 'm1': full_on}) + marker_body = TablePT({'m0': two_3rd, 'm1': one_3rd}) marker_test_pulse = marker_start @ RepetitionPT(marker_body, 10000) marker_program = marker_test_pulse.create_program() - for ct in channel_tuples: - i = hdawg.channel_tuples.index(ct) - ch = (None, None) - mk = ('m1' if (i*2, 0) in markers else None, - 'm2' if (i*2, 1) in markers else None, - 'm1' if (i*2 + 1, 0) in markers else None, - 'm2' if (i*2 + 1, 1) in markers else None) - vt = (lambda x: x, lambda x: x) - ct.upload('marker_test', marker_program, ch, mk, vt) - ct.arm('marker_test') - - channel_tuples[0].run_current_program() + upload_ch = (None, ) * channel_group.num_channels + upload_mk = tuple(f"m{mk}" if (ch, mk) in markers else None + for (ch, mk) in channel_group_markers) + + channel_group.upload('marker_test', marker_program, upload_ch, upload_mk, upload_vt) + + try: + while True: + for program in channel_group.programs: + print(f'playing {program}') + channel_group.arm(program) + channel_group.run_current_program() + while not channel_group.was_current_program_finished(): + print(f'waiting for {program} to finish') + time.sleep(1e-2) + finally: + channel_group.enable(False) if __name__ == "__main__": diff --git a/qupulse/hardware/dacs/alazar.py b/qupulse/hardware/dacs/alazar.py index 772104e84..c5fee4c2c 100644 --- a/qupulse/hardware/dacs/alazar.py +++ b/qupulse/hardware/dacs/alazar.py @@ -294,7 +294,7 @@ def delete_program(self, program_name: str) -> None: # todo [2018-06-14]: what if program to delete is currently armed? def clear(self) -> None: - self._registered_programs = dict() + self._registered_programs.clear() self.__armed_program = None @property diff --git a/qupulse/pulses/multi_channel_pulse_template.py b/qupulse/pulses/multi_channel_pulse_template.py index 264b342c7..75784eb7a 100644 --- a/qupulse/pulses/multi_channel_pulse_template.py +++ b/qupulse/pulses/multi_channel_pulse_template.py @@ -229,22 +229,26 @@ def overwritten_channels(self) -> Mapping[str, ExpressionScalar]: return self._overwritten_channels def _get_overwritten_channels_values(self, - parameters: Mapping[str, Union[numbers.Real]] + parameters: Mapping[str, Union[numbers.Real]], + channel_mapping: Dict[ChannelID, Optional[ChannelID]] ) -> Dict[str, numbers.Real]: - return {name: value.evaluate_in_scope(parameters) - for name, value in self.overwritten_channels.items()} + return {channel_mapping[name]: value.evaluate_in_scope(parameters) + for name, value in self.overwritten_channels.items() + if channel_mapping[name] is not None} def _internal_create_program(self, *, scope: Scope, global_transformation: Optional[Transformation], + channel_mapping: Dict[ChannelID, Optional[ChannelID]], **kwargs): - overwritten_channels = self._get_overwritten_channels_values(parameters=scope) + overwritten_channels = self._get_overwritten_channels_values(parameters=scope, channel_mapping=channel_mapping) transformation = ParallelConstantChannelTransformation(overwritten_channels) if global_transformation is not None: transformation = chain_transformations(global_transformation, transformation) self._template._create_program(scope=scope, + channel_mapping=channel_mapping, global_transformation=transformation, **kwargs) @@ -253,7 +257,8 @@ def build_waveform(self, parameters: Dict[str, numbers.Real], inner_waveform = self._template.build_waveform(parameters, channel_mapping) if inner_waveform: - overwritten_channels = self._get_overwritten_channels_values(parameters=parameters) + overwritten_channels = self._get_overwritten_channels_values(parameters=parameters, + channel_mapping=channel_mapping) transformation = ParallelConstantChannelTransformation(overwritten_channels) return TransformingWaveform(inner_waveform, transformation) diff --git a/qupulse/utils/__init__.py b/qupulse/utils/__init__.py index 2358f1ce8..c0d71928c 100644 --- a/qupulse/utils/__init__.py +++ b/qupulse/utils/__init__.py @@ -60,6 +60,14 @@ def pairwise(iterable: Iterable[Any], return zip_function(a, b, **kwargs) +def grouper(iterable: Iterable[Any], n: int, fillvalue=None) -> Iterable[Tuple[Any, ...]]: + """Collect data into fixed-length chunks or blocks""" + # grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx" + # this is here instead of using more_itertools because there were problems with the old version's argument order + args = [iter(iterable)] * n + return itertools.zip_longest(fillvalue=fillvalue, *args) + + def replace_multiple(s: str, replacements: Mapping[str, str]) -> str: """Replace multiple strings at once. If multiple replacements overlap the precedence is given by the order in replacements. diff --git a/setup.py b/setup.py index 430c76a00..a07b7256c 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ def extract_version(version_file): package_dir={'qupulse': 'qupulse', 'qctoolkit': 'qctoolkit'}, packages=packages, python_requires='>=3.6', - install_requires=['sympy>=1.1.1', 'numpy', 'cached_property', 'more_itertools'], + install_requires=['sympy>=1.1.1', 'numpy', 'cached_property'], extras_require={ 'plotting': ['matplotlib'], 'VISA': ['pyvisa'], diff --git a/tests/_program/seqc_tests.py b/tests/_program/seqc_tests.py index 277024e41..7a2d9c7b4 100644 --- a/tests/_program/seqc_tests.py +++ b/tests/_program/seqc_tests.py @@ -1,8 +1,7 @@ import unittest from unittest import TestCase, mock import time -from more_itertools import take -from itertools import zip_longest +from itertools import zip_longest, islice import sys import tempfile import pathlib @@ -26,10 +25,15 @@ zhinst = None +def take(n, iterable): + "Return first n items of the iterable as a list" + return list(islice(iterable, n)) + + def make_binary_waveform(waveform): if waveform.duration == 0: data = np.asarray(3 * [1, 2, 3, 4, 5], dtype=np.uint16) - return BinaryWaveform(data) + return (BinaryWaveform(data),) else: chs = sorted(waveform.defined_channels) t = np.arange(0., waveform.duration, 1.) @@ -37,7 +41,7 @@ def make_binary_waveform(waveform): 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) + return (BinaryWaveform.from_sampled(ch1, ch2, markers),) def get_unique_wfs(n=10000, duration=32, defined_channels=frozenset(['A'])): @@ -371,9 +375,9 @@ def test_to_node_clusters(self): def dummy_loop_to_seqc(loop, **kwargs): return loop - loops = [wf1, wf2, wf1, wf1, wf3, wf1, wf1, wf1] + 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]] + 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) @@ -751,19 +755,23 @@ def test_full_run(self): 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 = 0b1000000000000000; +const TRIGGER_RESET_MASK = 0b10000000000000000000000000000000; const PROG_SEL_NONE = 0; -const NO_RESET_MASK = 0b1000000000000000; -const PROG_SEL_MASK = 0b111111111111111; +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 = "3e0090e8ffd002d1134ce38827c6a35fede89cf23d126a44057ef43f466ae4cd"; +wave test_concatenated_waveform_0 = "3e0090e8ffd002d1134ce38827c6a35fede89cf23d126a44057ef43f466ae4cd"; wave test_shared_waveform_121f5c6e8822793b3836fb3098fa4591b91d4c205cc2d8afd01ee1bf6956e518 = "121f5c6e8822793b3836fb3098fa4591b91d4c205cc2d8afd01ee1bf6956e518"; -//function used by manually triggered programs +// function used by manually triggered programs void waitForSoftwareTrigger() { while (true) { var trigger_register = getUserReg(TRIGGER_REGISTER); @@ -784,7 +792,7 @@ def test_full_run(self): repeat(1000) { // stepping repeat repeat(10) { repeat(42) { - playWaveIndexed(test_concatenated_waveform, pos, 32); // advance disabled do to parent repetition + playWaveIndexed(test_concatenated_waveform_0, pos, 32); // advance disabled do to parent repetition } repeat(98) { playWave(test_shared_waveform_121f5c6e8822793b3836fb3098fa4591b91d4c205cc2d8afd01ee1bf6956e518); @@ -793,35 +801,55 @@ def test_full_run(self): pos = pos + 32; } repeat(21) { - playWaveIndexed(test_concatenated_waveform, pos, 32); // advance disabled do to parent repetition + playWaveIndexed(test_concatenated_waveform_0, pos, 32); // advance disabled do to parent repetition } pos = pos + 32; repeat(23) { - playWaveIndexed(test_concatenated_waveform, pos, 48); // advance disabled do to parent repetition + 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, pos, 48); // advance disabled do to parent repetition + playWaveIndexed(test_concatenated_waveform_0, pos, 48); // advance disabled do to parent repetition } pos = pos + 48; } } -// INIT program switch. +// Declare and initialize global variables +// Selected program index (0 -> None) var prog_sel = 0; -//runtime block +// 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); - if (!(prog_sel & NO_RESET_MASK)) setUserReg(PROG_SEL_REGISTER, 0); - prog_sel = prog_sel & PROG_SEL_MASK; + + // 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); } diff --git a/tests/backward_compatibility/charge_scan_1/hdawg_preparation_commands.json b/tests/backward_compatibility/charge_scan_1/hdawg_preparation_commands.json new file mode 100644 index 000000000..ace9ec18c --- /dev/null +++ b/tests/backward_compatibility/charge_scan_1/hdawg_preparation_commands.json @@ -0,0 +1,5 @@ +{ + "/{device_serial}/system/clocks/sampleclock/freq": 2e9, + "/{device_serial}/awgs/*/time": 0, + "/{device_serial}/sigouts/*/on": 0 +} \ No newline at end of file diff --git a/tests/backward_compatibility/charge_scan_1/preparation_commands.json b/tests/backward_compatibility/charge_scan_1/tabor_preparation_commands.json similarity index 100% rename from tests/backward_compatibility/charge_scan_1/preparation_commands.json rename to tests/backward_compatibility/charge_scan_1/tabor_preparation_commands.json diff --git a/tests/backward_compatibility/hardware_test_helper.py b/tests/backward_compatibility/hardware_test_helper.py new file mode 100644 index 000000000..1314283c2 --- /dev/null +++ b/tests/backward_compatibility/hardware_test_helper.py @@ -0,0 +1,75 @@ +import unittest +import os +import json +import typing +import importlib.util +import sys + +from qupulse.serialization import Serializer, FilesystemBackend, PulseStorage +from qupulse.pulses.pulse_template import PulseTemplate + +class LoadingAndSequencingHelper: + def __init__(self, data_folder, pulse_name): + self.data_folder = data_folder + self.pulse_name = pulse_name + + self.parameters = self.load_json('parameters.json') + self.window_mapping = self.load_json('measurement_mapping.json') + self.channel_mapping = self.load_json('channel_mapping.json') + + self.validate_programs = self.load_function_from_file('binary_program_validation.py', 'validate_programs') + self.validation_data = self.load_json('binary_program_validation.json') + + self.pulse = None + self.program = None + + self.simulator_manager = None + + self.hardware_setup = None # type: HardwareSetup + self.dac = None # type: DummyDAC + + def load_json(self, file_name): + complete_file_name = os.path.join(self.data_folder, file_name) + if os.path.exists(complete_file_name): + with open(complete_file_name, 'r') as file_handle: + return json.load(file_handle) + else: + return None + + def load_function_from_file(self, file_name, function_name): + full_file_name = os.path.join(self.data_folder, file_name) + if not os.path.exists(full_file_name): + return None + module_name = os.path.normpath(os.path.splitext(full_file_name)[0]).replace(os.sep, '.') + + if module_name in sys.modules: + module = sys.modules[module_name] + else: + try: + spec = importlib.util.spec_from_file_location(module_name, full_file_name) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + except ImportError: + return None + return getattr(module, function_name, None) + + def deserialize_pulse(self): + serializer = Serializer(FilesystemBackend(os.path.join(self.data_folder, 'pulse_storage'))) + self.pulse = typing.cast(PulseTemplate, serializer.deserialize(self.pulse_name)) + + def deserialize_pulse_2018(self) -> None: + pulse_storage = PulseStorage(FilesystemBackend(os.path.join(self.data_folder, 'pulse_storage_converted_2018'))) + self.pulse = typing.cast(PulseTemplate, pulse_storage[self.pulse_name]) + + def sequence_pulse(self): + self.program = self.pulse.create_program( + parameters=self.parameters, + measurement_mapping=self.window_mapping, + channel_mapping=self.channel_mapping) + + def register_program(self): + self.hardware_setup.register_program(self.pulse_name, self.program) + + def arm_program(self): + self.hardware_setup.arm_program(self.pulse_name) + diff --git a/tests/backward_compatibility/tabor_backward_compatibility_tests.py b/tests/backward_compatibility/tabor_backward_compatibility_tests.py index 1aa00f00b..e98a48ce0 100644 --- a/tests/backward_compatibility/tabor_backward_compatibility_tests.py +++ b/tests/backward_compatibility/tabor_backward_compatibility_tests.py @@ -8,6 +8,7 @@ from tests.hardware.tabor_simulator_based_tests import TaborSimulatorManager from tests.hardware.dummy_devices import DummyDAC +from tests.backward_compatibility.hardware_test_helper import LoadingAndSequencingHelper from qupulse.serialization import Serializer, FilesystemBackend, PulseStorage from qupulse.pulses.pulse_template import PulseTemplate @@ -15,7 +16,6 @@ from qupulse.hardware.awgs.tabor import PlottableProgram - def do_not_skip(test_class): if hasattr(test_class, '__unittest_skip__'): test_class.__unittest_skip__ = False @@ -34,15 +34,11 @@ def test_dummy(self): self.assertTrue(True) -class PulseLoadingAndSequencingHelper: +class TaborLoadingAndSequencingHelper(LoadingAndSequencingHelper): def __init__(self, data_folder, pulse_name): - self.data_folder = data_folder - self.pulse_name = pulse_name + super().__init__(data_folder=data_folder, pulse_name=pulse_name) - self.parameters = self.load_json('parameters.json') - self.window_mapping = self.load_json('measurement_mapping.json') - self.channel_mapping = self.load_json('channel_mapping.json') - self.preparation_commands = self.load_json('preparation_commands.json') + self.preparation_commands = self.load_json('tabor_preparation_commands.json') expected_binary_programs = self.load_json('binary_programs.json') if expected_binary_programs: @@ -54,57 +50,9 @@ def __init__(self, data_folder, pulse_name): self.validate_programs = self.load_function_from_file('binary_program_validation.py', 'validate_programs') self.validation_data = self.load_json('binary_program_validation.json') - self.pulse = None - self.program = None - - self.simulator_manager = None - - self.hardware_setup = None # type: HardwareSetup - self.dac = None # type: DummyDAC - self.awg = None # type: TaborAWGRepresentation - self.program_AB = None self.program_CD = None - def load_json(self, file_name): - complete_file_name = os.path.join(self.data_folder, file_name) - if os.path.exists(complete_file_name): - with open(complete_file_name, 'r') as file_handle: - return json.load(file_handle) - else: - return None - - def load_function_from_file(self, file_name, function_name): - full_file_name = os.path.join(self.data_folder, file_name) - if not os.path.exists(full_file_name): - return None - module_name = os.path.normpath(os.path.splitext(full_file_name)[0]).replace(os.sep, '.') - - if module_name in sys.modules: - module = sys.modules[module_name] - else: - try: - spec = importlib.util.spec_from_file_location(module_name, full_file_name) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - except ImportError: - return None - return getattr(module, function_name, None) - - def deserialize_pulse(self): - serializer = Serializer(FilesystemBackend(os.path.join(self.data_folder, 'pulse_storage'))) - self.pulse = typing.cast(PulseTemplate, serializer.deserialize(self.pulse_name)) - - def deserialize_pulse_2018(self) -> None: - pulse_storage = PulseStorage(FilesystemBackend(os.path.join(self.data_folder, 'pulse_storage_converted_2018'))) - self.pulse = typing.cast(PulseTemplate, pulse_storage[self.pulse_name]) - - def sequence_pulse(self): - self.program = self.pulse.create_program( - parameters=self.parameters, - measurement_mapping=self.window_mapping, - channel_mapping=self.channel_mapping) - def initialize_hardware_setup(self): self.simulator_manager = TaborSimulatorManager() @@ -136,12 +84,6 @@ def initialize_hardware_setup(self): self.hardware_setup.set_measurement("MEAS_C", MeasurementMask(self.dac, "MASK_C")) self.hardware_setup.set_measurement("MEAS_D", MeasurementMask(self.dac, "MASK_D")) - def register_program(self): - self.hardware_setup.register_program(self.pulse_name, self.program) - - def arm_program(self): - self.hardware_setup.arm_program(self.pulse_name) - def read_program(self): self.program_AB = self.awg.channel_pair_AB.read_complete_program() self.program_CD = self.awg.channel_pair_CD.read_complete_program() @@ -156,7 +98,7 @@ class CompleteIntegrationTestHelper(unittest.TestCase): def setUpClass(cls): if cls.data_folder is None: raise unittest.SkipTest("Base class") - cls.test_state = PulseLoadingAndSequencingHelper(cls.data_folder, cls.pulse_name) + cls.test_state = TaborLoadingAndSequencingHelper(cls.data_folder, cls.pulse_name) def test_1_1_deserialization(self): with self.assertWarns(DeprecationWarning): diff --git a/tests/backward_compatibility/zhinst_charge_scan_test.py b/tests/backward_compatibility/zhinst_charge_scan_test.py new file mode 100644 index 000000000..93c0c8038 --- /dev/null +++ b/tests/backward_compatibility/zhinst_charge_scan_test.py @@ -0,0 +1,134 @@ +import unittest + +import unittest +import os +import json +import typing +import importlib.util +import sys + + +from tests.hardware.dummy_devices import DummyDAC + +from qupulse.serialization import Serializer, FilesystemBackend, PulseStorage +from qupulse.pulses.pulse_template import PulseTemplate +from qupulse.hardware.setup import HardwareSetup, PlaybackChannel, MarkerChannel, MeasurementMask + +from tests.backward_compatibility.hardware_test_helper import LoadingAndSequencingHelper + +try: + from qupulse.hardware.awgs import zihdawg +except ImportError: + zihdawg = None + + +def get_test_hdawg(): + assert zihdawg is not None + + serial_env_var = 'QUPULSE_HDAWG_TEST_DEV' + interface_env_var = 'QUPULSE_HDAWG_TEST_INTERFACE' + device_serial = os.environ.get(serial_env_var, None) + if device_serial is None: + raise unittest.SkipTest(f"No test HDAWG specified via environment variable {serial_env_var}") + kwargs = dict(device_serial=device_serial) + device_interface = os.environ.get(interface_env_var, None) + if device_interface is not None: + kwargs['device_interface'] = device_interface + + return zihdawg.HDAWGRepresentation(**kwargs) + + +class HDAWGLoadingAndSequencingHelper(LoadingAndSequencingHelper): + def __init__(self, data_folder, pulse_name): + if zihdawg is None: + raise unittest.SkipTest("zhinst import failed") + + super().__init__(data_folder=data_folder, pulse_name=pulse_name) + + self.preparation_commands = self.load_json('tabor_preparation_commands.json') + + self.awg: zihdawg.HDAWGRepresentation = None + self.channel_group: zihdawg.HDAWGChannelGroup = None + + def initialize_hardware_setup(self): + self.awg = get_test_hdawg() + + if self.preparation_commands: + preparation_commands = [(key.format(device_serial=self.awg.serial), value) + for key, value in self.preparation_commands.items() + ] + self.awg.api_session.set(preparation_commands) + + for idx in range(1, 9): + # switch off all outputs + self.awg.output(idx, False) + + self.awg.channel_grouping = zihdawg.HDAWGChannelGrouping.CHAN_GROUP_1x8 + + self.channel_group, = self.awg.channel_tuples + + self.dac = DummyDAC() + + hardware_setup = HardwareSetup() + + hardware_setup.set_channel('TABOR_A', PlaybackChannel(self.channel_group, 0)) + hardware_setup.set_channel('TABOR_B', PlaybackChannel(self.channel_group, 1)) + hardware_setup.set_channel('TABOR_A_MARKER', MarkerChannel(self.channel_group, 0)) + hardware_setup.set_channel('TABOR_B_MARKER', MarkerChannel(self.channel_group, 1)) + + hardware_setup.set_channel('TABOR_C', PlaybackChannel(self.channel_group, 2)) + hardware_setup.set_channel('TABOR_D', PlaybackChannel(self.channel_group, 3)) + hardware_setup.set_channel('TABOR_C_MARKER', MarkerChannel(self.channel_group, 3)) + hardware_setup.set_channel('TABOR_D_MARKER', MarkerChannel(self.channel_group, 4)) + + hardware_setup.set_measurement("MEAS_A", MeasurementMask(self.dac, "MASK_A")) + hardware_setup.set_measurement("MEAS_B", MeasurementMask(self.dac, "MASK_B")) + hardware_setup.set_measurement("MEAS_C", MeasurementMask(self.dac, "MASK_C")) + hardware_setup.set_measurement("MEAS_D", MeasurementMask(self.dac, "MASK_D")) + + self.hardware_setup = hardware_setup + + +class CompleteIntegrationTestHelper(unittest.TestCase): + data_folder = None + pulse_name = None + + @classmethod + def setUpClass(cls): + if cls.data_folder is None: + raise unittest.SkipTest("Base class") + cls.test_state = HDAWGLoadingAndSequencingHelper(cls.data_folder, cls.pulse_name) + + def test_1_1_deserialization(self): + self.test_state.deserialize_pulse() + + def test_1_2_deserialization_2018(self) -> None: + self.test_state.deserialize_pulse_2018() + + def test_2_1_sequencing(self): + if self.test_state.pulse is None: + self.skipTest("deserialization failed") + self.test_state.sequence_pulse() + + def test_3_1_initialize_hardware_setup(self): + self.test_state.initialize_hardware_setup() + + def test_4_1_register_program(self): + if self.test_state.hardware_setup is None: + self.skipTest("No hardware setup") + self.test_state.register_program() + self.assertIn(self.pulse_name, self.test_state.hardware_setup.registered_programs) + + def test_5_1_arm_program(self): + if self.test_state.hardware_setup is None: + self.skipTest("No hardware setup") + if self.pulse_name not in self.test_state.hardware_setup.registered_programs: + self.skipTest("Program is not registered") + self.test_state.hardware_setup.arm_program(self.pulse_name) + self.assertEqual(self.test_state.channel_group._current_program, self.pulse_name, + "Program not armed") + + +class ChargeScan1Tests(CompleteIntegrationTestHelper): + data_folder = os.path.join(os.path.dirname(__file__), 'charge_scan_1') + pulse_name = 'charge_scan' diff --git a/tests/hardware/zihdawg_tests.py b/tests/hardware/zihdawg_tests.py index 037e331df..471a88c69 100644 --- a/tests/hardware/zihdawg_tests.py +++ b/tests/hardware/zihdawg_tests.py @@ -7,8 +7,8 @@ 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 HDAWGChannelPair, HDAWGRepresentation, HDAWGValueError, UserRegister,\ - ConstantParameter, ELFManager +from qupulse.hardware.awgs.zihdawg import HDAWGChannelGroup, HDAWGRepresentation, HDAWGValueError, UserRegister,\ + ConstantParameter, ELFManager, HDAWGChannelGrouping class HDAWGRepresentationTests(unittest.TestCase): @@ -19,18 +19,21 @@ def test_init(self): 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('zhinst.ziPython.ziDAQServer') as mock_daq_server, \ mock.patch('qupulse.hardware.awgs.zihdawg.HDAWGRepresentation._initialize') as mock_init, \ - mock.patch('qupulse.hardware.awgs.zihdawg.HDAWGChannelPair') as mock_channel_pair,\ + mock.patch('qupulse.hardware.awgs.zihdawg.HDAWGRepresentation.channel_grouping', new_callable=mock.PropertyMock) as mock_grouping, \ + mock.patch('qupulse.hardware.awgs.zihdawg.HDAWGChannelGroup') 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) + 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 @@ -43,12 +46,19 @@ def test_init(self): 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() - pair_calls = [mock.call(representation, (2*i+1, 2*i+2), str(device_serial) + post_fix, 1.3) - for i, post_fix in enumerate(['_AB', '_CD', '_EF', '_GH'])] - for c1, c2 in zip(pair_calls, mock_channel_pair.call_args_list): + 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) @@ -76,9 +86,14 @@ def test_init(self): mock_reset.assert_called_once_with(representation.api_session, representation.serial) mock_init.assert_called_once_with() - pair_calls = [mock.call(representation, (2*i+1, 2*i+2), str(device_serial) + post_fix, 20) - for i, post_fix in enumerate(['_AB', '_CD', '_EF', '_GH'])] - self.assertEqual(pair_calls, mock_channel_pair.call_args_list) + 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) @@ -86,23 +101,31 @@ def test_init(self): self.assertIs(representation.channel_pair_GH, mock_channel_pair.return_value) -class HDAWGChannelPairTests(unittest.TestCase): +class HDAWGChannelGroupTests(unittest.TestCase): def test_init(self): with mock.patch('weakref.proxy') as proxy_mock: mock_device = mock.Mock() - channel_pair = HDAWGChannelPair(mock_device, (3, 4), 'foo', 3.4) + channels = (3, 4) + awg_group_idx = 1 + + channel_pair = HDAWGChannelGroup(awg_group_idx, 2, 'foo', 3.4) self.assertEqual(channel_pair.timeout, 3.4) - self.assertEqual(channel_pair._channels, (3, 4)) - self.assertEqual(channel_pair.awg_group_index, 1) + 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.device, proxy_mock.return_value) - self.assertIs(channel_pair.awg_module, channel_pair.device.api_session.awgModule.return_value) - self.assertEqual(channel_pair.num_channels, 2) - self.assertEqual(channel_pair.num_markers, 4) def test_set_volatile_parameters(self): mock_device = mock.Mock() @@ -112,7 +135,7 @@ def test_set_volatile_parameters(self): expected_user_reg_calls = [mock.call(*args) for args in requested_changes.items()] - channel_pair = HDAWGChannelPair(mock_device, (3, 4), 'foo', 3.4) + channel_pair = HDAWGChannelGroup(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', @@ -140,7 +163,7 @@ def test_upload(self): with mock.patch('weakref.proxy'),\ mock.patch('qupulse.hardware.awgs.zihdawg.make_compatible') as mock_make_compatible: - channel_pair = HDAWGChannelPair(mock.Mock(), (3, 4), 'foo', 3.4) + channel_pair = HDAWGChannelGroup(1, 2, 'foo', 3.4) with self.assertRaisesRegex(HDAWGValueError, 'Channel ID'): channel_pair.upload('bar', mock_loop, ('A'), (None, 'A', None, None), voltage_trafos) diff --git a/tests/pulses/multi_channel_pulse_template_tests.py b/tests/pulses/multi_channel_pulse_template_tests.py index 8e8be1318..4b1e51ad8 100644 --- a/tests/pulses/multi_channel_pulse_template_tests.py +++ b/tests/pulses/multi_channel_pulse_template_tests.py @@ -389,23 +389,24 @@ def test_integral(self): def test_get_overwritten_channels_values(self): template = DummyPulseTemplate(duration='t1', defined_channels={'X', 'Y'}, parameter_names={'a', 'b'}, measurement_names={'M'}) - overwritten_channels = {'Y': 'c', 'Z': 'a'} - - expected_overwritten_channel_values = {'Y': 1.2, 'Z': 3.4} + overwritten_channels = {'Y': 'c', 'Z': 'a', 'ToNone': 'foo'} + channel_mapping = {'X': 'X', 'Y': 'K', 'Z': 'Z', 'ToNone': None} + expected_overwritten_channel_values = {'K': 1.2, 'Z': 3.4} pccpt = ParallelConstantChannelPulseTemplate(template, overwritten_channels) real_parameters = {'c': 1.2, 'a': 3.4} - self.assertEqual(expected_overwritten_channel_values, pccpt._get_overwritten_channels_values(real_parameters)) + self.assertEqual(expected_overwritten_channel_values, pccpt._get_overwritten_channels_values(real_parameters, + channel_mapping=channel_mapping)) def test_internal_create_program(self): template = DummyPulseTemplate(duration='t1', defined_channels={'X', 'Y'}, parameter_names={'a', 'b'}, measurement_names={'M'}, waveform=DummyWaveform()) - overwritten_channels = {'Y': 'c', 'Z': 'a'} + overwritten_channels = {'Y': 'c', 'Z': 'a', 'ToNone': 'foo'} parent_loop = object() measurement_mapping = object() - channel_mapping = object() + channel_mapping = {'Y': 'O', 'Z': 'Z', 'X': 'X', 'ToNone': None} to_single_waveform = object() other_kwargs = dict(measurement_mapping=measurement_mapping, @@ -417,7 +418,7 @@ def test_internal_create_program(self): scope = DictScope.from_kwargs(c=1.2, a=3.4) kwargs = {**other_kwargs, 'scope': scope, 'global_transformation': None} - expected_overwritten_channels = {'Y': 1.2, 'Z': 3.4} + expected_overwritten_channels = {'O': 1.2, 'Z': 3.4} expected_transformation = ParallelConstantChannelTransformation(expected_overwritten_channels) expected_kwargs = {**kwargs, 'global_transformation': expected_transformation} @@ -438,11 +439,11 @@ def test_build_waveform(self): template = DummyPulseTemplate(duration='t1', defined_channels={'X', 'Y'}, parameter_names={'a', 'b'}, measurement_names={'M'}, waveform=DummyWaveform()) overwritten_channels = {'Y': 'c', 'Z': 'a'} - channel_mapping = {'X': 'X', 'Y': 'K'} + channel_mapping = {'X': 'X', 'Y': 'K', 'Z': 'Z'} pccpt = ParallelConstantChannelPulseTemplate(template, overwritten_channels) parameters = {'c': 1.2, 'a': 3.4} - expected_overwritten_channels = {'Y': 1.2, 'Z': 3.4} + expected_overwritten_channels = {'K': 1.2, 'Z': 3.4} expected_transformation = ParallelConstantChannelTransformation(expected_overwritten_channels) expected_waveform = TransformingWaveform(template.waveform, expected_transformation)