From 95edfd58681634dcc299eb89d219892b099d2687 Mon Sep 17 00:00:00 2001 From: Nomos11 <82180697+Nomos11@users.noreply.github.com> Date: Fri, 8 Mar 2024 17:27:29 +0100 Subject: [PATCH 1/2] preliminary HDAWG update for current firmware utilizing CommandTable, limited to 1024 wfs. --- qupulse/_program/seqc.py | 534 +++++++++++++++++++++++-------- qupulse/hardware/awgs/zihdawg.py | 348 +++++++++++++++----- 2 files changed, 672 insertions(+), 210 deletions(-) diff --git a/qupulse/_program/seqc.py b/qupulse/_program/seqc.py index 989ffdc5..f6156553 100644 --- a/qupulse/_program/seqc.py +++ b/qupulse/_program/seqc.py @@ -33,9 +33,9 @@ from qupulse.utils.types import ChannelID, TimeType from qupulse.utils import replace_multiple, grouper -from qupulse.program.waveforms import Waveform -from qupulse.program.loop import Loop -from qupulse.program.volatile import VolatileRepetitionCount, VolatileProperty +from qupulse._program.waveforms import Waveform +from qupulse._program._loop import Loop +from qupulse._program.volatile import VolatileRepetitionCount, VolatileProperty from qupulse.hardware.awgs.base import ProgramEntry from qupulse.hardware.util import zhinst_voltage_to_uint16 @@ -48,6 +48,11 @@ zhinst = None +from zhinst.toolkit import CommandTable +import json +from dataclasses import dataclass +from zhinst.toolkit import Waveforms + __all__ = ["HDAWGProgramManager"] @@ -67,7 +72,7 @@ class BinaryWaveform: `to_csv_compatible_table` can be used to create a compatible compact csv file (with marker data included) """ - __slots__ = ('data',) + __slots__ = ('data') PLAYBACK_QUANTUM = 16 PLAYBACK_MIN_QUANTA = 2 @@ -87,7 +92,7 @@ def __init__(self, data: np.ndarray): self.data = data self.data.flags.writeable = False - + @property def ch1(self): return self.data[::3] @@ -265,22 +270,35 @@ 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_{group_index}' + CONCATENATED_WAVEFORM_TEMPLATE = '{program_name}_concatenated_waveform_{pos_index}_{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): + ('binary_waveform', BinaryWaveform), + ('pos_index', int), + ('group_index', int), + ('program_name', str), + ('sample_length',int), + ('sample_rate',int), + ]) + + def __init__(self,awg_obj): self.shared_waveforms = OrderedDict() # type: MutableMapping[BinaryWaveform, set] - self.concatenated_waveforms = OrderedDict() # type: MutableMapping[str, ConcatenatedWaveform] - + self.concatenated_waveforms_subdivided = {} # dict should now automatically be ordered #Dict[str,List[Tuple[BinaryWaveform]]] + self.concatenated_waveforms_subdivided_info = {} #Dict[str,Tuple[int,int]] + self.fsp_waveforms = {} + self._awg = awg_obj + self._zhinst_waveforms_tuple = tuple([Waveforms() for i in range(self._awg.num_channels//2)]) + def clear(self): self.shared_waveforms.clear() - self.concatenated_waveforms.clear() + self.concatenated_waveforms_subdivided.clear() + self.concatenated_waveforms_subdivided_info.clear() + self.fsp_waveforms.clear() + self._zhinst_waveforms_tuple = tuple([Waveforms() for i in range(self._awg.num_channels//2)]) def _shared_waveforms_iter(self) -> Iterator[Tuple[str, _WaveInfo]]: for wf, program_set in self.shared_waveforms.items(): @@ -290,53 +308,186 @@ def _shared_waveforms_iter(self) -> Iterator[Tuple[str, _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 wave_placeholder, self._WaveInfo(wave_name, file_name, wf) + yield wave_placeholder, self._WaveInfo(wave_name, file_name, wf, + 0,0,"",len(wf.data)//3,0 + ) def _concatenated_waveforms_iter(self) -> Iterator[Tuple[str, Tuple[_WaveInfo, ...]]]: - for program_name, concatenated_waveform in self.concatenated_waveforms.items(): + for program_name, concatenated_waveform_list in self.concatenated_waveforms_subdivided.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) - + for pos_index, binary_tuple in enumerate(concatenated_waveform_list): + if binary_tuple: + infos = [] + for group_index, binary in enumerate(binary_tuple): + wave_hash = binary.fingerprint() + wave_name = self.CONCATENATED_WAVEFORM_TEMPLATE.format(program_name=program_name, + pos_index=pos_index, + group_index=group_index) + file_name = self.FILE_NAME_TEMPLATE.format(hash=wave_hash) + + infos.append(self._WaveInfo(wave_name, file_name, binary, + pos_index, + group_index, + program_name, + self.concatenated_waveforms_subdivided_info[program_name][pos_index][0], + self.concatenated_waveforms_subdivided_info[program_name][pos_index][1], + )) + + wave_placeholder = self.WF_PLACEHOLDER_TEMPLATE.format(id=id(binary)) + yield wave_placeholder, tuple(infos) + + + def _fsp_waveforms_iter(self): + + @dataclass + class ShortInfo: + file_name: str + binary_waveform: BinaryWaveform + + for program_name, (declaration_func,name_iter) in self.fsp_waveforms.items(): + for wf_name, binary in name_iter(self._awg): + yield ShortInfo(self.FILE_NAME_TEMPLATE.format(hash=wf_name),binary) + + 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 + for info in self._fsp_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 pos_var_start_name_replacements(self) -> Dict[str, str]: + + translation = {} + + for program_name,concat_nested_list in self.concatenated_waveforms_subdivided.items(): + translation[program_name+'_ct_pos_init'] = str(self.program_pos_var_start[program_name]) + + for program_name, (declaration_func,name_iter) in self.fsp_waveforms.items(): + translation[program_name+'_ct_pos_init'] = str(self.program_pos_var_start[program_name]) + return translation + + + + def fill_ct_dict(self,ct_dict): - def waveform_declaration(self) -> str: + awg_standard_rate = self._awg.sample_rate_divider + + for i,ct_key in enumerate(ct_dict.keys()): + for (ct_idx,info_tuple) in self.ct_info_link.items(): + + #ct_dict[ct_key].table[ct_idx].waveform.index = int(info_tuple[0][i]) + #this shouldn't be explicitly necessary, but do nonetheless... + ct_dict[ct_key].table[ct_idx].amplitude0.value = 1.0 + ct_dict[ct_key].table[ct_idx].amplitude0.increment = False + ct_dict[ct_key].table[ct_idx].amplitude0.register = 0 + ct_dict[ct_key].table[ct_idx].amplitude1.value = 1.0 + ct_dict[ct_key].table[ct_idx].amplitude1.increment = False + ct_dict[ct_key].table[ct_idx].amplitude1.register = 0 + + #this is not addressing the underlying problem as also only ~1000 entries in the command table can be made. + #(currently one-to-one correspondence between table and waveforms, which is bad, but cannot be done otherwise? + # on the other hand, number of sequencer instructions are now possible up to ~16000) + #manual claims this could be faster than playWave nonetheless + ct_dict[ct_key].table[ct_idx].waveform.index = int(info_tuple[0][i]) + ct_dict[ct_key].table[ct_idx].waveform.length = int(info_tuple[1]) + total_rate_divider = int(info_tuple[2])+awg_standard_rate + assert total_rate_divider <= self._awg.MAX_SAMPLE_RATE_DIVIDER + ct_dict[ct_key].table[ct_idx].waveform.samplingRateDivider = total_rate_divider + + return ct_dict + + def waveform_declaration(self,ct_dict) -> 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(): + + self._zhinst_waveforms_tuple = tuple([Waveforms() for i in range(self._awg.num_channels//2)]) + + filename_list_list = [set() for i in range(self._awg.num_channels//2)] + #this defeats the purpose of having generator object as concatenated waveform iter, but seems like easiest way without changing too much... + self.original_waveform_declarations_list = [{} for i in range(self._awg.num_channels//2)] + self.ct_info_link = {} + self.program_pos_var_start = {} + + ct_index,wave_table_index = 0, 0 + + for wave_placeholder, wave_infos in self._concatenated_waveforms_iter(): + wft_idxs = [] + for group_index,wave_info in enumerate(wave_infos): + current_filename = wave_info.file_name.replace('.csv', '') + if current_filename not in filename_list_list[group_index]: # allegedly `in set` has O(1) complexity + self._zhinst_waveforms_tuple[group_index][wave_table_index] = (wave_info.binary_waveform.ch1, + wave_info.binary_waveform.ch2, + wave_info.binary_waveform.marker_data) + + wave_str = ','.join(['{i},placeholder({length},true,true)'.format(i=i,length=wave_info.sample_length) for i in range(2*group_index+1,2*group_index+3)]) #should be same for all + + declarations.append( + 'assignWaveIndex({wave_str},{index});'.format(index=wave_table_index, wave_str=wave_str) + ) + + + filename_list_list[group_index].add(current_filename) + wft_idxs += [wave_table_index] + self.original_waveform_declarations_list[group_index][current_filename] = wave_table_index + + else: + wft_idxs += [self.original_waveform_declarations_list[group_index][current_filename]] + + self.program_pos_var_start.setdefault(wave_info.program_name, ct_index) + self.ct_info_link[ct_index] = [wft_idxs,wave_info.sample_length,wave_info.sample_rate] + + if len(set(wft_idxs)) >= 128: + raise RuntimeError('The WF-cache may be insufficient (too many individual wfs used).\n'\ + +'There are more intricate mechanisms posssibly available to circumvent this, but for now brute force raise error.\n'\ + +'use e.g. to_single_waveform to circumvent long sequences of short waveforms' + ) + + if ct_index >= 1024: + raise RuntimeError('too many CT entries. Clear HardwareSetup for now. (HardwareSetup.clear_programs())') #needs to be handled otherwise then (somehow)... + + ct_index += 1 + #TODO: this can be more efficient. correspondent to ct is fine however, since way more waveforms than ct entries available. + wave_table_index += 1 + + for wave_placeholder,wave_info in self._shared_waveforms_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) + + + for program_name, (declaration_func,name_iter) in self.fsp_waveforms.items(): + self.program_pos_var_start[program_name] = ct_index + wf_decl_string, ct_index, wave_table_index, self._zhinst_waveforms_tuple = \ + declaration_func(self._awg,ct_dict,ct_start_index=ct_index,wf_start_index=wave_table_index, + waveforms_tuple = self._zhinst_waveforms_tuple + ) + + + if ct_index >= 1024: + raise RuntimeError('too many CT entries') + declarations.append(wf_decl_string) + + joined_str = '\n'.join(declarations) + + return joined_str def sync_to_file_system(self, file_system: WaveformFileSystem): to_save = {wave_info.file_name: wave_info.binary_waveform @@ -356,10 +507,13 @@ def __init__(self, name: str, memory: WaveformMemory): 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() + assert self._program_name not in self._memory.concatenated_waveforms_subdivided + assert all(self._program_name not in programs for programs in self._memory.shared_waveforms.values()) + + self._memory.concatenated_waveforms_subdivided[self._program_name] = [] + self._memory.concatenated_waveforms_subdivided_info[self._program_name] = [] + @property def program_name(self) -> str: return self._program_name @@ -368,11 +522,18 @@ def program_name(self) -> str: def main_waveform_name(self) -> str: self._waveform_name - def clear_requested(self): + 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() - + #this currently does not clear the respective entries in the _zhinst_waveforms_tuple object. but is it really relevant?... + # self._memory.concatenated_waveforms[self._waveform_name].clear() + #probably unnecessary and only causes errors. put in try except. + try: + self._memory.concatenated_waveforms_subdivided[self._waveform_name].clear() + self._memory.concatenated_waveforms_subdivided_info[self._waveform_name].clear() + except: + pass + def request_shared(self, binary_waveform: Tuple[BinaryWaveform, ...]) -> str: """Register waveform if not already registered and return a unique identifier placeholder. @@ -386,19 +547,27 @@ def request_shared(self, binary_waveform: Tuple[BinaryWaveform, ...]) -> str: 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 request_list_append(self, binary_waveform: Tuple[BinaryWaveform, ...], + sample_rate_divider: int) -> str: + """Append the waveform to the concatenated waveform""" + self._memory.concatenated_waveforms_subdivided[self._program_name].append(binary_waveform) + self._memory.concatenated_waveforms_subdivided_info[self._program_name].append((len(binary_waveform[0].ch1),sample_rate_divider)) + return + + def finalize(self): - self._memory.concatenated_waveforms[self._waveform_name].finalize() + # self._memory.concatenated_waveforms[self._waveform_name].finalize() + return 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] + try: + del self._memory.concatenated_waveforms_subdivided[self._waveform_name] + del self._memory.concatenated_waveforms_subdivided_info[self._waveform_name] + except: + pass class UserRegister: @@ -509,26 +678,38 @@ def __init__(self, loop: Loop, selection_index: int, waveform_memory: WaveformMe 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) - + sample_rate: TimeType, + command_tables: CommandTable, + append_seqc_snippet: str = None, + ): + if not 'FixedStructureProgram' in [t.__name__ for t in type(loop).__mro__]: + super().__init__(loop, channels=channels, markers=markers, + amplitudes=amplitudes, + offsets=offsets, + voltage_transformations=voltage_transformations, + sample_rate=sample_rate) + + self._waveforms_sampled_values = {} + 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._is_fsp = False + else: + self._waveforms = loop.waveform_dict + self._is_fsp = True + self._loop = loop + self._waveform_manager = ProgramWaveformManager(program_name, waveform_memory) self.selection_index = selection_index self._trigger_wait_code = None @@ -537,13 +718,17 @@ def __init__(self, loop: Loop, selection_index: int, waveform_memory: WaveformMe self._var_declarations = None self._user_registers = None self._user_register_source = None - + self._ct_dict = command_tables + self.append_seqc_snippet = append_seqc_snippet + 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]): + available_registers: Iterable[UserRegister], + max_rate_divider: int + ): """Compile the loop representation to an internal sequencing c one using `loop_to_seqc` Args: @@ -555,18 +740,21 @@ def compile(self, 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, + + if not self._is_fsp: + 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) + user_registers=user_registers, + max_rate_divider=max_rate_divider) self._user_register_source = '\n'.join( '{indentation}var {user_reg_name} = getUserReg({register});'.format(indentation=indentation, @@ -576,15 +764,28 @@ def compile(self, ) self._user_registers = user_registers - self._var_declarations = '{indentation}var {pos_var_name} = 0;'.format(pos_var_name=pos_var_name, - indentation=indentation) + self._var_declarations = '{indentation}var {pos_var_name} = {pos_var_init_placeholder};'.format(pos_var_name=pos_var_name, + indentation=indentation, + pos_var_init_placeholder=self.name+'_ct_pos_init' + ) + self._trigger_wait_code = indentation + trigger_wait_code - self._seqc_source = '\n'.join(self._seqc_node.to_source_code(self._waveform_manager, + if not self._is_fsp: + 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() - + else: + self._seqc_source = self._loop.get_seqc_program_body(pos_var_name) + self._waveform_manager._memory.fsp_waveforms[self._waveform_manager._waveform_name] = (self._loop.wf_declarations_and_ct,self._loop.wf_definitions_iter) + + if self.append_seqc_snippet is not None: + self._seqc_source += '\n'+self.append_seqc_snippet + + self._waveform_manager.finalize() #not necessary anymore + + return + @property def seqc_node(self) -> 'SEQCNode': assert self._seqc_node is not None, "compile not called" @@ -628,16 +829,22 @@ class HDAWGProgramManager: the required waveforms to the file system. It does not talk to the device.""" class Constants: + INTEGER_SIZE = 8 #somehow, despite the manual claiming 64 bits, the USERREG creates playback issues before that. so change previous 32 to 16 for safety + # HOWEVER, this still induces errors. Disable userreg more or less completely PROG_SEL_REGISTER = UserRegister(zero_based_value=0) TRIGGER_REGISTER = UserRegister(zero_based_value=1) - TRIGGER_RESET_MASK = bin(1 << 31) + PLAYBACK_FINISHED_REGISTER = UserRegister(zero_based_value=2) + TRIGGER_RESET_MASK = bin(1 << INTEGER_SIZE-1) 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)) + PLAYBACK_FINISHED_AT_LEAST_ONCE_VALUE = 1 + PLAYBACK_NOT_FINISHED_AT_LEAST_ONCE_VALUE = 0 + RESET_VALUE = 2 + # # if not set the register is set to PROG_SEL_NONE + # NO_RESET_MASK = bin(1 << INTEGER_SIZE-1) + # # set to one if playback finished + # PLAYBACK_FINISHED_MASK = bin(1 << INTEGER_SIZE-2) + # PROG_SEL_MASK = bin((1 << INTEGER_SIZE-2) - 1) + # INVERTED_PROG_SEL_MASK = bin(((1 << INTEGER_SIZE) - 1) ^ int(PROG_SEL_MASK, 2)) IDLE_WAIT_CYCLES = 300 @classmethod @@ -655,7 +862,7 @@ class GlobalVariables: '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 @@ -672,7 +879,14 @@ def get_init_block(cls) -> str: return '\n'.join(lines) _PROGRAM_FUNCTION_NAME_TEMPLATE = '{program_name}_function' + + #TODO: this can be altered in the future to implement possible inter-core-triggering + #for more flexbility in program definition. Currently (23.02) not working reliably, + #but future updates promised WAIT_FOR_SOFTWARE_TRIGGER = "waitForSoftwareTrigger();" + # WAIT_FOR_SOFTWARE_TRIGGER = "waitDIOTrigger();" + # WAIT_FOR_SOFTWARE_TRIGGER = "" + SOFTWARE_WAIT_FOR_TRIGGER_FUNCTION_DEFINITION = ( 'void waitForSoftwareTrigger() {\n' ' while (true) {\n' @@ -695,8 +909,11 @@ def get_program_function_name(cls, program_name: str): 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() + def __init__(self,awg_obj,schema_tuple_func): + self._awg = awg_obj + self._waveform_memory = WaveformMemory(self._awg) + self._ct_schema_tuple_func = schema_tuple_func + self._ct_dict = None self._programs = OrderedDict() # type: MutableMapping[str, HDAWGProgramEntry] self._compiler_settings = [ # default settings: None -> take cls value @@ -707,7 +924,7 @@ def __init__(self): 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'} + required_compiler_args = (set(arg_spec.args) | set(arg_spec.kwonlyargs)) - {'self', 'available_registers', 'max_rate_divider'} settings = {} for regex, settings_dict in self._compiler_settings: @@ -738,7 +955,9 @@ def add_program(self, name: str, loop: Loop, amplitudes: Tuple[float, ...], offsets: Tuple[float, ...], voltage_transformations: Tuple[Optional[Callable], ...], - sample_rate: TimeType): + sample_rate: TimeType, + append_seqc_snippet: str = None, + ): """Register the given program and translate it to seqc. TODO: Add an interface to change the trigger mode @@ -754,20 +973,29 @@ def add_program(self, name: str, loop: Loop, sample_rate: Used to sample the waveforms """ assert name not in self._programs - + + max_available_rate_divider = self._awg.MAX_SAMPLE_RATE_DIVIDER - self._awg.sample_rate_divider + + self._ct_dict = {i:CommandTable(s,active_validation=False) for i,s in enumerate(self._ct_schema_tuple_func(tuple(range(self._awg.num_channels//2))))} + 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)] + available_registers = [UserRegister.from_seqc(idx) for idx in range(3, 16)] #now another one reserved program_entry = HDAWGProgramEntry(loop, selection_index, self._waveform_memory, name, - channels, markers, amplitudes, offsets, voltage_transformations, sample_rate) + channels, markers, amplitudes, offsets, voltage_transformations, sample_rate, + self._ct_dict, + append_seqc_snippet + ) 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._ct_start_idx = program_entry.compile(**compiler_settings, + available_registers=available_registers, + max_rate_divider=max_available_rate_divider + ) self._programs[name] = program_entry @@ -800,6 +1028,8 @@ def programs(self) -> Mapping[str, HDAWGProgramEntry]: return MappingProxyType(self._programs) def remove(self, name: str) -> None: + #as unsure whether it happens elsewhere: put here. may be smarter to relocate to seqc-program-generation + self._ct_dict = {i:CommandTable(s,active_validation=False) for i,s in enumerate(self._ct_schema_tuple_func(tuple(range(self._awg.num_channels//2))))} self._programs.pop(name).prepare_delete() def clear(self) -> None: @@ -830,7 +1060,7 @@ def to_seqc_program(self, single_program: Optional[str] = None) -> str: 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. + 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 @@ -845,14 +1075,19 @@ def to_seqc_program(self, single_program: Optional[str] = None) -> str: 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()) - + + wf_lines = self._waveform_memory.waveform_declaration(self._ct_dict) + lines.append(wf_lines) + 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() - + + replacements_waveforms = self._waveform_memory.waveform_name_replacements() + replacements_pos_var = self._waveform_memory.pos_var_start_name_replacements() + + replacements = replacements_waveforms | replacements_pos_var + + #TODO: replace_multiple with empty-dict-check? lines.append('\n// program definitions') if single_program: lines.append( @@ -873,6 +1108,14 @@ def to_seqc_program(self, single_program: Optional[str] = None) -> str: return '\n'.join(lines) + def finalize_ct_dict(self) -> str: + + #TODO: might need reset before? + ct_t = self._ct_dict + ct_t = self._waveform_memory.fill_ct_dict(ct_t) + + return {i:json.dumps(ct.as_dict(),) for i,ct in enumerate(ct_t.values())} + def find_sharable_waveforms(node_cluster: Sequence['SEQCNode']) -> Optional[Sequence[bool]]: """Expects nodes to have a compatible stepping @@ -1058,24 +1301,27 @@ 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': + user_registers: UserRegisterManager, + max_rate_divider: int) -> '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)) + node = WaveformPlayback(waveform_to_bin(loop.waveform),max_rate_divider=max_rate_divider) 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) + waveform_to_bin=waveform_to_bin, user_registers=user_registers, + max_rate_divider=max_rate_divider) 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)) + user_registers=user_registers, + max_rate_divider=max_rate_divider)) seqc_nodes = [] @@ -1329,9 +1575,8 @@ def get_node_name(): 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()) + yield '{line_prefix}++{pos_var_name};'.format(line_prefix=line_prefix, + pos_var_name=pos_var_name) class SteppingRepeat(SEQCNode): @@ -1385,15 +1630,18 @@ def to_source_code(self, waveform_manager: ProgramWaveformManager, node_name_gen class WaveformPlayback(SEQCNode): ADVANCE_DISABLED_COMMENT = ' // advance disabled do to parent repetition' - ENABLE_DYNAMIC_RATE_REDUCTION = False + ENABLE_DYNAMIC_RATE_REDUCTION = True __slots__ = ('waveform', 'shared', 'rate') - def __init__(self, waveform: Tuple[BinaryWaveform, ...], shared: bool = False, rate: int = None): + def __init__(self, waveform: Tuple[BinaryWaveform, ...], shared: bool = False, + rate: int = None, max_rate_divider: int = 13): assert isinstance(waveform, tuple) + if rate is not None: + assert rate <= max_rate_divider 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) + rate = min([wf.dynamic_rate(max_rate_divider) for wf in waveform]) + self.waveform = waveform self.shared = shared self.rate = rate @@ -1438,7 +1686,7 @@ def iter_waveform_playbacks(self) -> Iterator['WaveformPlayback']: def _visit_nodes(self, waveform_manager: ProgramWaveformManager): if not self.shared: - waveform_manager.request_concatenated(self.rate_reduced_waveform()) + waveform_manager.request_list_append(self.rate_reduced_waveform(),self.rate if self.rate is not None else 0) def to_source_code(self, waveform_manager: ProgramWaveformManager, node_name_generator: Iterator[str], line_prefix: str, pos_var_name: str, @@ -1449,32 +1697,64 @@ def to_source_code(self, waveform_manager: ProgramWaveformManager, 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});' + waveform_manager.request_list_append(self.rate_reduced_waveform(),self.rate if self.rate is not None else 0) + play_cmd = f'{line_prefix}executeTableEntry({pos_var_name});' if advance_pos_var: - advance_cmd = f' {pos_var_name} = {pos_var_name} + {wf_len};' + advance_cmd = f' ++{pos_var_name};' #now always step one by one for index 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; + +# // The HDAWG is apparently not a Swiss clock after all and has trouble being on time (for USERREG operations). +# // Therefore, resort to extra waiting cycle. +# wait(IDLE_WAIT_CYCLES); + +# switch (prog_sel) {{ +# {program_cases} +# default: +# wait(IDLE_WAIT_CYCLES); +# }} +# }}""" + _PROGRAM_SELECTION_BLOCK = """\ while (true) {{ // read program selection value prog_sel = getUserReg(PROG_SEL_REGISTER); - + // playback_finished = getUserReg(PLAYBACK_FINISHED_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); + // new_prog_sel = prog_sel | playback_finished; + // if (!(prog_sel & NO_RESET_MASK)) new_prog_sel &= INVERTED_PROG_SEL_MASK; + + // if (playback_finished==PLAYBACK_FINISHED_AT_LEAST_ONCE_VALUE) prog_sel = PROG_SEL_NONE; // reset playback flag - playback_finished = 0; + // playback_finished = 0; // only use part of prog sel that does not mean other things to select the program. - prog_sel &= PROG_SEL_MASK; + // prog_sel &= PROG_SEL_MASK; + + // //The HDAWG is apparently not a Swiss clock after all and has trouble being on time (for USERREG operations). + // //Therefore, resort to extra waiting cycle. + // wait(IDLE_WAIT_CYCLES); switch (prog_sel) {{ {program_cases} @@ -1482,12 +1762,14 @@ def to_source_code(self, waveform_manager: ProgramWaveformManager, wait(IDLE_WAIT_CYCLES); }} }}""" - + + _PROGRAM_SELECTION_CASE = """\ case {selection_index}: {program_function_name}(); - waitWave(); - playback_finished = PLAYBACK_FINISHED_MASK;""" + waitWave();""" + # for performance reasons, don't even do this as not used... + # setUserReg(PLAYBACK_FINISHED_REGISTER, PLAYBACK_FINISHED_AT_LEAST_ONCE_VALUE);""" def _make_program_selection_block(programs: Iterable[Tuple[int, str]]): diff --git a/qupulse/hardware/awgs/zihdawg.py b/qupulse/hardware/awgs/zihdawg.py index 68643db7..d7f20622 100644 --- a/qupulse/hardware/awgs/zihdawg.py +++ b/qupulse/hardware/awgs/zihdawg.py @@ -30,11 +30,12 @@ import time from qupulse.utils.types import ChannelID, TimeType, time_from_float -from qupulse.program.loop import Loop, make_compatible +from qupulse._program._loop import Loop, make_compatible, roll_constant_waveforms from qupulse._program.seqc import HDAWGProgramManager, UserRegister, WaveformFileSystem from qupulse.hardware.awgs.base import AWG, ChannelNotFoundException, AWGAmplitudeOffsetHandling from qupulse.hardware.util import traced - +from qupulse.utils import replace_multiple +from zhinst.toolkit import Session logger = logging.getLogger('qupulse.hdawg') @@ -104,19 +105,29 @@ def __init__(self, device_serial: str = None, :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) + + self._zhinst_session = Session(data_server_addr, data_server_port) + + #api_level_number not in use anymore (?) + self._device = self._zhinst_session.connect_device(serial=device_serial, interface=device_interface) + + self._api_session = self._zhinst_session.daq_server 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,... + #TODO: this currently creates errors when the sequencer is running as waveforms are deleted. need to disable sequencer separately? + self.disable_all_sequencers() zhinst.utils.disable_everything(self.api_session, self.serial) self._initialize() - - waveform_path = pathlib.Path(self.api_session.awgModule().getString('directory'), 'awg', 'waves') + + self._base_path = pathlib.Path(self.api_session.awgModule().getString('directory')) + waveform_path = pathlib.Path(self._base_path,'awg', 'waves') + self._waveform_file_system = WaveformFileSystem.get_waveform_file_system(waveform_path) self._channel_groups: Dict[HDAWGChannelGrouping, Tuple[HDAWGChannelGroup, ...]] = {} @@ -141,8 +152,15 @@ def __init__(self, device_serial: str = None, if grouping is None: grouping = self.channel_grouping # activates channel groups + # TODO: sometimes weird behavior with MDS? does this code always do what it is intended for? self.channel_grouping = grouping - + + def disable_all_sequencers(self): + for i in range(4): + node_path = '/{}/awgs/{:d}/enable'.format(self.serial, i) + self.api_session.setInt(node_path, 0) + self.api_session.sync() # Global sync: Ensure settings have taken effect on the device. + @property def waveform_file_system(self) -> WaveformFileSystem: return self._waveform_file_system @@ -177,19 +195,22 @@ def serial(self) -> str: 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. + (f'/{self.serial}/awgs/*/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 + output = HDAWGTriggerOutSource.OUT_2_MARK_1.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) + #TODO: is this intended behavior that sequencer still running? add disable now. + self.disable_all_sequencers() + #!!! this is buggy: + # zhinst.utils.disable_everything(self.api_session, self.serial) self._initialize() for tuple in self.channel_tuples: tuple.clear() @@ -197,7 +218,7 @@ def reset(self) -> None: (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), + (f'/{self.serial}/awgs/*/outputs/*/modulation/mode', HDAWGModulationMode.OFF.value), ]) # marker outputs @@ -206,7 +227,7 @@ def reset(self) -> None: if ch % 2 == 0: output = HDAWGTriggerOutSource.OUT_1_MARK_1.value else: - output = HDAWGTriggerOutSource.OUT_1_MARK_2.value + output = HDAWGTriggerOutSource.OUT_2_MARK_1.value marker_settings.append([f'/{self.serial}/triggers/out/{ch}/source', output]) self.api_session.set(marker_settings) self.api_session.sync() @@ -226,6 +247,13 @@ def _get_groups(self, grouping: 'HDAWGChannelGrouping') -> Tuple['HDAWGChannelGr return group else: raise + + def set_sample_clock(self,sample_clock_Hz: float): + assert sample_clock_Hz >= 1e8, 'Must be >= 100MHz' + assert sample_clock_Hz <= 2.4e9, 'Must be <= 2.4GHz' + + node_path = '/{}/system/clocks/sampleclock/freq'.format(self.serial) + sample_clock_Hz = self.api_session.setDouble(node_path,sample_clock_Hz) @property def channel_grouping(self) -> 'HDAWGChannelGrouping': @@ -253,7 +281,7 @@ def channel_grouping(self, channel_grouping: 'HDAWGChannelGrouping'): 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.""" @@ -273,10 +301,16 @@ def output(self, channel: int, status: bool = None) -> bool: 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/*')) + # 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/*'), + self.api_session.get('/{}/awg/{:d}/*'.format(self.serial,0)), + self.api_session.get('/{}/awg/{:d}/*'.format(self.serial,1)), + self.api_session.get('/{}/awg/{:d}/*'.format(self.serial,2)), + self.api_session.get('/{}/awg/{:d}/*'.format(self.serial,3)), + + ) def _get_mds_group_idx(self) -> Optional[int]: idx = 0 @@ -367,9 +401,17 @@ class HDAWGModulationMode(Enum): @traced class HDAWGChannelGroup(AWG): - MIN_WAVEFORM_LEN = 192 + + # MIN_WAVEFORM_LEN = 32 #With the command table and discarding playWaveIndexed this is the minimum. + MIN_WAVEFORM_LEN = 64 #However, repeat structures may require longer pulses, so 64 for now. WAVEFORM_LEN_QUANTUM = 16 - + + MAX_SAMPLE_RATE_DIVIDER = 13 + + CT_IDLE_STR = '{"header": {"version": "1.2.1"}, "table": []}' + + SLEEP_PARANOIA = 0.02 + def __init__(self, identifier: str, timeout: float) -> None: @@ -377,23 +419,30 @@ def __init__(self, self.timeout = timeout self._awg_module = None - self._program_manager = HDAWGProgramManager() + self._program_manager = HDAWGProgramManager(self,self.get_ct_schemata) self._elf_manager = None self._required_seqc_source = self._program_manager.to_seqc_program() self._uploaded_seqc_source = None + self._current_ct_dict = {i:self.CT_IDLE_STR for i in range(self.num_channels//2)} self._current_program = None # Currently armed program. self._upload_generator = () self._master_device = None - + + #TODO: this was a first test to integrate own code snippets. may be deleted/altered + self.append_seqc_snippet = None + def _initialize_awg_module(self): """Only run once""" + #TODO: if at some point the offline compilation works with grouped mode, + # this can be reworked as awgModules no longer needed (in favor of zhinst_toolkit) 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(self.awg_module) self._elf_manager = ELFManager.DEFAULT_CLS(self._awg_module) self._upload_generator = () @@ -403,7 +452,7 @@ def master_device(self) -> HDAWGRepresentation: 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.""" @@ -426,7 +475,8 @@ def upload(self, name: str, channels: Tuple[Optional[ChannelID], ...], markers: Tuple[Optional[ChannelID], ...], voltage_transformation: Tuple[Callable, ...], - force: bool = False) -> None: + force: bool = False, + ) -> None: """Upload a program to the AWG. Physically uploads all waveforms required by the program - excluding those already present - @@ -452,6 +502,7 @@ def upload(self, name: str, 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: @@ -463,13 +514,16 @@ def upload(self, name: str, raise HDAWGValueError('{} is already known on {}'.format(name, self.identifier)) # Go to qupulse nanoseconds time base. + # pulse lengths of 1 and 2 times 16 samples are bad in 2.4Gs/s as they are inaccurate on floating point level + # leading to weird qupulse behavior when comparing program.duration (ExpressionScalar/float) with sample rate (TimeType) 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 not 'FixedStructureProgram' in [t.__name__ for t in type(program).__mro__]: + 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 @@ -490,10 +544,18 @@ def upload(self, name: str, voltage_transformations=voltage_transformation, sample_rate=q_sample_rate, amplitudes=amplitudes, - offsets=voltage_offsets) - - self._required_seqc_source = self._program_manager.to_seqc_program() + offsets=voltage_offsets, + append_seqc_snippet=self.append_seqc_snippet, + ) + + #USERREG BUG: only upload single program always? + self._required_seqc_source = self._program_manager.to_seqc_program(name) + + #TODO: may be omitted if placeholder wfs used, perhaps faster? self._program_manager.waveform_memory.sync_to_file_system(self.master_device.waveform_file_system) + + #needs to be uploaded only after everything else (elf-upload) done + self._current_ct_dict = self._program_manager.finalize_ct_dict() # start compiling the source (non-blocking) self._start_compile_and_upload() @@ -503,17 +565,81 @@ def _start_compile_and_upload(self): 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): + def _wait_for_compile_and_upload_elf(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 _wait_for_compile_and_upload(self): + + self._wait_for_compile_and_upload_elf() + if self.SLEEP_PARANOIA>0: time.sleep(self.SLEEP_PARANOIA) + + #TODO: this should be the most time-consuming here... + # with self._master_device._device.set_transaction(): #disable set_transaction for now as it sometimes seems buggy. + for i in range(self.num_channels//2): + self._master_device._device.awgs[self.awg_group_index+i].write_to_waveform_memory(self._program_manager._waveform_memory._zhinst_waveforms_tuple[i]) + if self.SLEEP_PARANOIA>0: time.sleep(self.SLEEP_PARANOIA) + + for i in range(self.num_channels//2): + self._master_device._device.awgs[self.awg_group_index+i].commandtable.upload_to_device(self._current_ct_dict[i]) + if self.SLEEP_PARANOIA>0: time.sleep(self.SLEEP_PARANOIA) + + #!!! does this mean everything uploaded? check others too, or not relevant if grouped / potentially harmful? + self._master_device._device.awgs[self.awg_group_index].ready.wait_for_state_change(1,timeout=self.timeout) + + sequencer_ready_status = self._master_device.api_session.getInt('/{}/AWGS/{}/READY'.format(self.master_device.serial, self.awg_group_index)) + + if not sequencer_ready_status == 1: + print('sequencer ready status: ' + str(sequencer_ready_status)) + raise RuntimeError('the Zurich package did not appropriately wait for state change') + + #only after everything done set correctly. + self._uploaded_seqc_source = self._required_seqc_source + + def set_sample_rate_num(self,sample_rate_num: int): + assert type(sample_rate_num)==int, 'Must be integer' + assert sample_rate_num >= 0, 'Must be >=0' + assert sample_rate_num <= 13, 'Must be <= 13' + + node_path = '/{}/awgs/{}/time'.format(self.master_device.serial, self.awg_group_index) + self.master_device.api_session.setInt(node_path,sample_rate_num) + + @property + def commandtable(self) -> str: + node_base_path = '/{}/awgs/{}'.format(self.master_device.serial, self.awg_group_index) + tree_dict = self.master_device.api_session.getString(node_base_path+'/commandtable/data') + return tree_dict[str(self.master_device.serial)]["awgs"][str(self.awg_group_index)]["commandtable"]["data"] + + @commandtable.setter + def commandtable(self, ct: str): + node_base_path = '/{}/awgs/{}'.format(self.master_device.serial, self.awg_group_index) + self.master_device.api_session.set(node_base_path+'/commandtable/data', ct) + + def get_commandtable_schema(self,awg_int=None) -> dict: + node_base_path = '/{}/awgs/{}'.format(self.master_device.serial, awg_int if awg_int is not None else self.awg_group_index) + #there seems to be no method to get "vector" entries directly? + tree_dict = self.master_device.api_session.get(node_base_path+'/commandtable/schema') + + return tree_dict[str(self.master_device.serial).lower()]["awgs"][str(awg_int if awg_int is not None else self.awg_group_index)]["commandtable"]["schema"][0]["vector"] + + def get_ct_schemata(self, idx: tuple=(0,1,2,3)) -> Tuple[str]: + return tuple([self.get_commandtable_schema(i) for i in idx]) + + def _upload_ct_dict(self, ct_dict: Dict[int,str]): + for i,ct in enumerate(ct_dict.values()): + node_base_path = '/{}/awgs/{}'.format(self.master_device.serial, self.awg_group_index+i) + self.master_device.api_session.set(node_base_path+'/commandtable/data', ct) + 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) + # playback_finished_mask = int(HDAWGProgramManager.Constants.PLAYBACK_FINISHED_MASK, 2) + # return bool(self.user_register(HDAWGProgramManager.Constants.PROG_SEL_REGISTER) & playback_finished_mask) + #TODO: currently disabled. + return 'Function currently disabled' + # return bool(self.user_register(HDAWGProgramManager.Constants.PROG_SEL_REGISTER)==HDAWGProgramManager.Constants.PLAYBACK_FINISHED_VALUE) 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.""" @@ -532,7 +658,12 @@ def remove(self, name: str) -> None: name: The name of the program to remove. """ self._program_manager.remove(name) - self._required_seqc_source = self._program_manager.to_seqc_program() + #USERREG BUG + # self._required_seqc_source = self._program_manager.to_seqc_program() + if name != self._current_program: + self._required_seqc_source = self._program_manager.to_seqc_program(self._current_program) + else: + self._required_seqc_source = self._program_manager.to_seqc_program() def clear(self) -> None: """Removes all programs and waveforms from the AWG. @@ -542,9 +673,37 @@ def clear(self) -> None: self._program_manager.clear() self._current_program = None self._required_seqc_source = self._program_manager.to_seqc_program() + self._current_ct_dict = {i:self.CT_IDLE_STR for i in range(self.num_channels//2)} self._start_compile_and_upload() self.arm(None) - + + #TODO: this only becomes relevant for self-triggering in the future + def _prepare_for_DIO(self): + + node_base = '/{}'.format(self.master_device.serial) + + self.master_device.api_session.setInt(node_base+'/DIOS/0/MODE',1) + self.master_device.api_session.setInt(node_base+'/DIOS/0/DRIVE',1) + for i in range(4): + self.master_device.api_session.setInt(node_base+f'/AWGS/{i}/DIO/VALID/POLARITY',2) + self.master_device.api_session.setInt(node_base+f'/AWGS/{i}/DIO/VALID/INDEX',0) + self.master_device.api_session.setInt(node_base+f'/AWGS/{i}/DIO/STROBE/SLOPE',0) + + + with self.master_device._device.set_transaction(): + # Settings in 'DIO' tab + self.master_device._device.dios[0].drive(0b0001) # Enable driving first byte + self.master_device._device.dios[0].output(0x00) # Reset DIO to clean state + self.master_device._device.dios[0].mode('awg_sequencer_commands') # Switch to "AWG Sequencer" mode + + # Settings in "AWG Sequencer" - "Trigger" sub-tab + self.master_device._device.awgs["*"].dio.valid.polarity('high') + self.master_device._device.awgs["*"].dio.valid.index(0) + self.master_device._device.awgs["*"].dio.strobe.slope('off') + + # pass + return + 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. @@ -552,18 +711,24 @@ def arm(self, name: Optional[str]) -> None: 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._prepare_for_DIO() + + #!!! this now is a workaround to avoid playback problems occuring when using the program selection via user reg. + if name is None: + # self._required_seqc_source = "" + self._required_seqc_source = self._program_manager.to_seqc_program() + else: + self._required_seqc_source = self._program_manager.to_seqc_program(name) + if self._required_seqc_source != self._uploaded_seqc_source: 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." - + "as expected." + self.user_register(self._program_manager.Constants.TRIGGER_REGISTER, 0) if name is None: @@ -578,14 +743,15 @@ 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.Constants.PROG_SEL_REGISTER, - self._program_manager.Constants.TRIGGER_REGISTER) + self._program_manager.Constants.TRIGGER_REGISTER, + # self._program_manager.Constants.PLAYBACK_FINISHED_AND_RESET_REGISTER, + ) self.user_register(register, value) - + + #TODO: probably obsolete. 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)) + self._program_manager.name_to_index(name)) - if name is not None: - self.enable(True) def run_current_program(self) -> None: """Run armed program.""" @@ -603,6 +769,22 @@ def run_current_program(self) -> None: 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_divider(self) -> int: + """The integer sample rate divider of the AWG channel group, [0,13], + sample rate = sample clock / 2**divider""" + node_path = '/{}/awgs/{}/time'.format(self.master_device.serial, self.awg_group_index) + sample_rate_num = self.master_device.api_session.getInt(node_path) + return sample_rate_num + + @sample_rate_divider.setter + def sample_rate_divider(self, divider: int) -> int: + """The integer sample rate divider of the AWG channel group, [0,13], + sample rate = sample clock / 2**divider""" + node_path = '/{}/awgs/{}/time'.format(self.master_device.serial, self.awg_group_index) + self.master_device.api_session.setInt(node_path,divider) + return self.sample_rate_divider @property def sample_rate(self) -> TimeType: @@ -667,15 +849,19 @@ class MDSChannelGroup(HDAWGChannelGroup): def __init__(self, identifier: str, timeout: float) -> None: - super().__init__(identifier, timeout) - + #change order self._master_device = None self._mds_devices = None - + + super().__init__(identifier, timeout) + @property def num_channels(self) -> int: """Number of channels""" - return len(self._mds_devices) * 8 + if self._mds_devices is not None: + return len(self._mds_devices) * 8 + else: + return 0 @property def awg_group_index(self): @@ -698,20 +884,14 @@ def connect_group(self, hdawg_device: HDAWGRepresentation): 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) + node_path = '/{}/awgs/{:d}/enable'.format(self.master_device.serial, self.awg_group_index) if status is not None: - self.awg_module.set('awg/enable', int(status)) + # self.awg_module.set('awg/enable', int(status)) + self.master_device.api_session.setInt(node_path, int(status)) else: - status = self.awg_module.get('awg/module') + # status = self.awg_module.get('awg/module') + self.master_device.api_session.get('/{}/awgs/{:d}/module'.format(self.master_device.serial, self.awg_group_index)) - #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, ...]: @@ -745,7 +925,8 @@ def __init__(self, group_size: int, identifier: str, timeout: float) -> None: - super().__init__(identifier, timeout) + + #change order self._device = None assert group_idx in range(4) @@ -753,6 +934,9 @@ def __init__(self, self._group_idx = group_idx self._group_size = group_size + + super().__init__(identifier, timeout) + @property def num_channels(self) -> int: @@ -801,11 +985,10 @@ def amplitudes(self) -> Tuple[float, ...]: 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""" @@ -882,6 +1065,8 @@ def elf_status(self) -> Tuple[int, float]: 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. @@ -927,25 +1112,21 @@ def _source_hash(source_string: str) -> str: 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): + def __init__(self, awg_module: zhinst_core.AwgModule): """This implementation does not attempt to do something clever like caching.""" super().__init__(awg_module) @@ -981,7 +1162,7 @@ def compile_and_upload(self, source_string: str) -> Generator[str, str, None]: class CachingELFManager(ELFManager): - def __init__(self, awg_module: zhinst.ziPython.AwgModule): + def __init__(self, awg_module: zhinst_core.AwgModule): """FAILS TO UPLOAD THE CORRECT ELF FOR SOME REASON""" super().__init__(awg_module) @@ -1075,10 +1256,8 @@ def _update_upload_job_status(self): 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) + + self._start_elf_upload(elf_file) while True: self._update_upload_job_status() @@ -1195,14 +1374,15 @@ def get_group_for_channels(hdawg: HDAWGRepresentation, channels: Set[int]) -> HD 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: + # if len(channel_range) > 4 or len(channel_range) == 4 and channel_range.start == 2: + if True: # !!! disable other channel grouping modes as not seem to be compatible... (?) 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) + # 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]] From f875d0e9536d578b6523176c3d57da83cace8a0c Mon Sep 17 00:00:00 2001 From: Nomos11 <82180697+Nomos11@users.noreply.github.com> Date: Fri, 8 Mar 2024 17:30:27 +0100 Subject: [PATCH 2/2] update for non-private .program --- qupulse/_program/seqc.py | 6 +++--- qupulse/hardware/awgs/zihdawg.py | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/qupulse/_program/seqc.py b/qupulse/_program/seqc.py index f6156553..9e9e0612 100644 --- a/qupulse/_program/seqc.py +++ b/qupulse/_program/seqc.py @@ -33,9 +33,9 @@ from qupulse.utils.types import ChannelID, TimeType from qupulse.utils import replace_multiple, grouper -from qupulse._program.waveforms import Waveform -from qupulse._program._loop import Loop -from qupulse._program.volatile import VolatileRepetitionCount, VolatileProperty +from qupulse.program.waveforms import Waveform +from qupulse.program.loop import Loop +from qupulse.program.volatile import VolatileRepetitionCount, VolatileProperty from qupulse.hardware.awgs.base import ProgramEntry from qupulse.hardware.util import zhinst_voltage_to_uint16 diff --git a/qupulse/hardware/awgs/zihdawg.py b/qupulse/hardware/awgs/zihdawg.py index d7f20622..06c2ad87 100644 --- a/qupulse/hardware/awgs/zihdawg.py +++ b/qupulse/hardware/awgs/zihdawg.py @@ -30,11 +30,10 @@ import time from qupulse.utils.types import ChannelID, TimeType, time_from_float -from qupulse._program._loop import Loop, make_compatible, roll_constant_waveforms -from qupulse._program.seqc import HDAWGProgramManager, UserRegister, WaveformFileSystem +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 -from qupulse.utils import replace_multiple from zhinst.toolkit import Session logger = logging.getLogger('qupulse.hdawg')