diff --git a/pulse_lib/base_pulse.py b/pulse_lib/base_pulse.py index 5cbce149a9b895aa789785ef60113eef0d29311c..bdd94b171cfffab97c85c05d9d2e52918b877f4c 100644 --- a/pulse_lib/base_pulse.py +++ b/pulse_lib/base_pulse.py @@ -534,12 +534,12 @@ class pulselib: self.digitizer_channels.values(), name=name, sample_rate=sample_rate, hres=hres) - def mk_sequence(self,seq): + def mk_sequence(self, segments): ''' - seq: list of segment_container. + segments: list of segment_container. ''' - seq_obj = sequencer(self.uploader, self.digitizer_channels) - seq_obj.add_sequence(seq) + seq_obj = sequencer(self.uploader, self.digitizer_channels, self.awg_channels) + seq_obj.add_sequence(segments) seq_obj.configure_digitizer = self.configure_digitizer return seq_obj diff --git a/pulse_lib/compiler/__init__.py b/pulse_lib/compiler/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/pulse_lib/compiler/condition_measurements.py b/pulse_lib/compiler/condition_measurements.py new file mode 100644 index 0000000000000000000000000000000000000000..58c97b93c290415651f126b9a261399e4fc8aa05 --- /dev/null +++ b/pulse_lib/compiler/condition_measurements.py @@ -0,0 +1,160 @@ +from collections.abc import Iterable +from collections import defaultdict +import logging +import copy + +import numpy as np + +from pulse_lib.segments.segment_measurements import measurement_acquisition +from pulse_lib.segments.conditional_segment import conditional_segment + + +logger = logging.getLogger(__name__) + +''' +ConditionSegment -> Acquisition +Acquisition: Segment#, end-time + +Feedback event: + * measurement: channel + * event set time: measurement end-time + * event clear time: condition end-time + * conditions + +* Qblox: + * latch-reset on segment start? latch idle time. + * if multiple measurements on sensor: latch-enable interval / latch-disable interval. +* Keysight: + * pxi-trigger assignment + * Add support for multiple pxi-triggers + * Add pxi-triggers assignment to configuration: single condition could use high bit. +''' + +class FeedbackEvent: + def __init__(self, measurement, end_times): + self.measurement = measurement + self._set_times = end_times + self._apply_times = np.full(1, np.nan) + self._reset_times = end_times + + @property + def set_times(self): + return self._set_times + + @property + def apply_times(self): + return self._apply_times + + @property + def reset_times(self): + return self._reset_times + + def add_condition(self, condition, start_times, end_times): + self._apply_times = np.fmin(self._apply_times, start_times) + self._reset_times = np.fmax(self._reset_times, end_times) + + + +class ConditionMeasurements: + def __init__(self, measurements_description, uploader, max_awg_to_dig_delay): + self._md = measurements_description + self._supports_conditionals = getattr(uploader, 'supports_conditionals', False) + if self._supports_conditionals: + self.min_feedback_time = uploader.get_roundtrip_latency() + max_awg_to_dig_delay + self._measurements = {} + self._feedback_events = {} + self._end_times = {} + self._n_segments = 0 + self._acquisition_count = defaultdict(int) + self._channel_measurements = defaultdict(list) + self._conditional_measurements = {} + + def add_segment(self, segment, seg_start_times): + self._n_segments += 1 + self._add_measurements(segment, seg_start_times) + + if not isinstance(segment, conditional_segment): + return + + if not self._supports_conditionals: + raise Exception(f'Backend does not support conditional segments') + + condition = segment.condition + seg_end = seg_start_times + segment.total_time + + # Lookup acquistions for condition + acquisition_names = self._get_acquisition_names(condition) + logger.info(f'segment {self._n_segments-1} conditional on: {acquisition_names}') + + # Add condition to feedback event + measurements = [] + for name in acquisition_names: + # NOTE: lastest measurement with this name before acquisition + try: + m = self._measurements[name] + except KeyError: + raise Exception(f'measurement {name} not found before condition') + measurements.append(m) + try: + fb = self._feedback_events[id(m)] + except KeyError: + fb = FeedbackEvent(m, self._end_times[id(m)]) + self._feedback_events[id(m)] = fb + fb.add_condition(condition, seg_start_times, seg_end) + + self._conditional_measurements[id(segment)] = measurements + + def _add_measurements(self, segment, seg_start_times): + if isinstance(segment, conditional_segment): + # Conditional branches must all have the same measurements. + # use 1st branch of conditional segment. + segment = segment.branches[0] + for measurement in segment.measurements: + if isinstance(measurement, measurement_acquisition): + m = copy.copy(measurement) + channel_acquisitions = self._channel_measurements[m.acquisition_channel] + m.index += len(channel_acquisitions) + channel_acquisitions.append(m) + + acq_channel = segment[m.acquisition_channel] + end_times = np.zeros(seg_start_times.shape) + for index in np.ndindex(seg_start_times.shape): + acq_data = acq_channel._get_data_all_at(index).data[measurement.index] + t_measure = acq_data.t_measure if acq_data.t_measure is not None else 0 + end_times[index] = seg_start_times[index] + acq_data.start + t_measure + self._end_times[id(m)] = end_times + else: + m = measurement + self._measurements[m.name] = m + + @property + def feedback_events(self): + return self._feedback_events + + @property + def measurement_acquisitions(self): + return self._channel_measurements + + def get_end_time(self, measurement, index): + return self._end_times[id(measurement)][tuple(index)] + + def get_measurements(self, conditional_segment): + return self._conditional_measurements[id(conditional_segment)] + + def check_feedback_timing(self): + + required_time = self.min_feedback_time + + for fb in self._feedback_events.values(): + margin = fb.apply_times - fb.set_times - required_time + if np.min(margin) < 0: + raise Exception(f'Insufficient time between measurement {fb.measurement.name} and condition') + + def _get_acquisition_names(self, condition): + if isinstance(condition, Iterable): + acquisition_names = set() + for ref in condition: + acquisition_names.update(ref.keys) + return list(acquisition_names) + return list(condition.keys) + diff --git a/pulse_lib/qblox/pulsar_sequencers.py b/pulse_lib/qblox/pulsar_sequencers.py index e6208cf743d688f3227610b6973cb6ae51198072..7d499731a1fc0b0017275558615c649e5335d48a 100644 --- a/pulse_lib/qblox/pulsar_sequencers.py +++ b/pulse_lib/qblox/pulsar_sequencers.py @@ -2,11 +2,20 @@ import logging import math from numbers import Number from copy import copy -from typing import Any, List, Dict, Callable +from typing import Any, List, Dict, Callable, Optional from dataclasses import dataclass +from contextlib import contextmanager import numpy as np +from packaging.version import Version +from q1pulse import __version__ as q1pulse_version + +if Version(q1pulse_version) < Version('0.9.0'): + raise Exception('Upgrade q1pulse to version 0.9+') + +from q1pulse.lang.conditions import CounterFlags + logger = logging.getLogger(__name__) def iround(value): @@ -31,6 +40,20 @@ class PulsarConfig: return math.floor(value / PulsarConfig.ALIGNMENT + 1e-8) * PulsarConfig.ALIGNMENT +@dataclass +class LatchEvent: + time: int + reset: bool = False + counters: Optional[List[str]] = None + +@dataclass +class MarkerEvent: + time: int + enabled_markers: int + ''' + every bit represents a physical marker output. + ''' + class SequenceBuilderBase: def __init__(self, name, sequencer): self.name = name @@ -73,20 +96,24 @@ class SequenceBuilderBase: def add_markers(self, markers): self.markers = markers self.imarker = -1 - self.set_next_marker() + self._set_next_marker() - def set_next_marker(self): + def _set_next_marker(self): self.imarker += 1 if len(self.markers) > self.imarker: - self.t_next_marker = self.markers[self.imarker][0] + self.t_next_marker = self.markers[self.imarker].time else: self.t_next_marker = None - def insert_markers(self, t): + def _insert_markers(self, t): while self.t_next_marker is not None and t >= self.t_next_marker: - marker = self.markers[self.imarker] - self._set_markers(marker[0], marker[1]) - self.set_next_marker() + self._add_next_marker() + + def _add_next_marker(self): + self.t_end = self.t_next_marker + marker = self.markers[self.imarker] + self._set_markers(marker.time, marker.enabled_markers) + self._set_next_marker() def _set_markers(self, t, value): self.seq.set_markers(value, t_offset=t) @@ -99,7 +126,7 @@ class SequenceBuilderBase: def _update_time_and_markers(self, t, duration): if t < self.t_end: raise Exception(f'Overlapping pulses {t} < {self.t_end} ({self.name})') - self.insert_markers(t) + self._insert_markers(t) self.t_end = t + duration def add_comment(self, comment): @@ -108,12 +135,13 @@ class SequenceBuilderBase: def wait_till(self, t): t += self.offset_ns self._update_time(t, 0) + self.seq.add_comment(f'wait {t}') self.seq.wait(t) def finalize(self): for i in range(self.imarker, len(self.markers)): marker = self.markers[i] - self._set_markers(marker[0], marker[1]) + self._set_markers(marker.time, marker.enabled_markers) # Range is -1.0 ... +1.0 with 16 bits => 2 / 2**16 @@ -440,8 +468,14 @@ class IQSequenceBuilder(SequenceBuilderBase): mixer_gain=None, mixer_phase_offset=None): super().__init__(name, sequencer) self.nco_frequency = nco_frequency - self._square_waves = [] self.add_comment(f'IQ: NCO={nco_frequency/1e6:7.2f} MHz') + self._square_waves = [] + self._trigger_counters = {} + self._uses_feedback = False + self._in_conditional = False + self._t_next_latch_event = None + self._ilatch_event = 0 + self._latch_events = [] if mixer_gain is not None: self.seq.mixer_gain_ratio = mixer_gain[1]/mixer_gain[0] @@ -491,13 +525,13 @@ class IQSequenceBuilder(SequenceBuilderBase): t_start_offset = t_start % 4 t_end_offset = t_end % 4 duration = t_end-t_start - if t_start_offset == 0 and t_end_offset == 0: - # Create aligned block pulse with offset - self.seq.block_pulse(duration, ampI, ampQ, t_offset=t_start) - elif duration < 200: + if duration < 200: # Create square waveform with offset wave_id = self._register_squarewave(t_start_offset, duration) self.seq.shaped_pulse(wave_id, ampI, wave_id, ampQ, t_offset=t_pulse) + elif t_start_offset == 0 and t_end_offset == 0: + # Create aligned block pulse with offset + self.seq.block_pulse(duration, ampI, ampQ, t_offset=t_start) else: # Create square waveform for start and end, and use offset in between. if t_start_offset: @@ -570,6 +604,116 @@ class IQSequenceBuilder(SequenceBuilderBase): raise Exception(f'{self.name}: NCO frequency {self.nco_frequency/1e6:5.1f} MHz out of range') self.seq.nco_frequency = self.nco_frequency + def add_trigger_counter(self, trigger): + self._uses_feedback = True + self._trigger_counters[trigger.sequencer_name] = self.seq.add_trigger_counter(trigger) + + @contextmanager + def conditional(self, channels, t_min, t_max): + t_min += self.offset_ns + t_max += self.offset_ns + t = max(self.t_end+4, t_min, t_max-40) + t = PulsarConfig.ceil(t) + if t > t_max-8: + raise Exception(f'Failed to schedule conditional pulse on {self.name}. ' + f' t:{t} > {t_max-8}') + # add markers before conditional + self._update_time_and_markers(t, 0) + self._in_conditional = True + counters = [self._trigger_counters[ch] for ch in channels] + flags = CounterFlags(self) + with self.seq.conditional(counters, t_offset=t): + yield flags + self._in_conditional = False + + @contextmanager + def condition(self, operator): + t_condition_start = self.t_end + self.seq.enter_condition(operator) + yield + self.seq.exit_condition(PulsarConfig.ceil(self.t_end)) + # reset end time + self.t_end = t_condition_start + + def _insert_markers(self, t): + ''' + Override of insert_marker taking care of conditional blocks and + counter latching. + ''' + if self._uses_feedback: + if self._in_conditional: + if self.t_next_marker is not None and t >= self.t_next_marker: + raise Exception(f'Cannot set marker in conditional segment {self.name}, t:{t}') + if self._t_next_latch_event is not None and t >= self._t_next_latch_event: + raise Exception(f'Cannot enable latches in conditional segment {self.name}, t:{t}') + return + else: + # add latch events, but not on same time as marker + loop = True + while loop: + loop = False + if (self._t_next_latch_event is not None + and self._t_next_latch_event < t + and (self.t_next_marker is None + or self._t_next_latch_event < self.t_next_marker)): + self._add_next_latch_event() + loop = True + elif self.t_next_marker is not None and t >= self.t_next_marker: + self._add_next_marker() + loop = True + else: + super()._insert_markers(t) + + def add_latch_events(self, latch_events): + self._latch_events = [] + for event in latch_events: + event = copy(event) + event.time = PulsarConfig.floor(event.time + self.offset_ns) + self._latch_events.append(event) + self._ilatch_event = -1 + self._set_next_latch_event() + + def _set_next_latch_event(self): + self._ilatch_event += 1 + if len(self._latch_events) > self._ilatch_event: + self._t_next_latch_event = self._latch_events[self._ilatch_event].time + else: + self._t_next_latch_event = None + + def _add_next_latch_event(self): + latch_event = self._latch_events[self._ilatch_event] + if latch_event.time + 20 < self.t_end: + raise Exception(f'Latch event {latch_event} on {self.name} scheduled too late t:{self.t_end}') + # Increment time. There could already be a phase shift, awg offset or marker on t_end + self.t_end += 4 + t = PulsarConfig.ceil(max(self.t_end, latch_event.time)) + logger.info(f'{latch_event} at t={self.t_end}') + if latch_event.reset: + self.seq.latch_reset(t_offset=t) + else: + counters = [self._trigger_counters[name] for name in latch_event.counters] + self.seq.latch_enable(counters, t_offset=t) + # Increment time with time used for latch instruction + self.t_end = t+4 + self._set_next_latch_event() + + def finalize(self): + if self._t_next_latch_event is not None: + for latch_event in self._latch_events[self._ilatch_event:]: + if latch_event.reset: + logger.info(f'latch reset at end {latch_event} ({self.t_end})') + if latch_event.time == np.inf: + t = PulsarConfig.ceil(self.t_end+4) + else: + t = latch_event.time + self.seq.latch_reset(t_offset=t) + self.t_end = max(self.t_end, t+4) + else: + logger.info(f'Skipping latch event at end: {latch_event}') + + super().finalize() + + @dataclass class _SeqCommand: time: int @@ -597,6 +741,30 @@ class AcquisitionSequenceBuilder(SequenceBuilderBase): self._rf_amplitude = rf_source.amplitude * scaling self._n_out_ch = 1 if isinstance(rf_source.output[1], int) else 2 + @property + def thresholded_acq_rotation(self): + return self.seq.thresholded_acq_rotation + + @thresholded_acq_rotation.setter + def thresholded_acq_rotation(self, rotation): + self.seq.thresholded_acq_rotation = rotation + + @property + def thresholded_acq_threshold(self): + return self.seq.thresholded_acq_threshold + + @thresholded_acq_threshold.setter + def thresholded_acq_threshold(self, threshold): + self.seq.thresholded_acq_threshold = threshold + + @property + def thresholded_acq_trigger_invert(self): + return self.seq.thresholded_acq_trigger_invert + + @thresholded_acq_trigger_invert.setter + def thresholded_acq_trigger_invert(self, invert): + self.seq.thresholded_acq_trigger_invert = invert + @property def integration_time(self): return self.seq.integration_length_acq diff --git a/pulse_lib/qblox/pulsar_uploader.py b/pulse_lib/qblox/pulsar_uploader.py index d427fead0eec2960cb103fc6b6a0218d244a0aaf..ee8360f42d75473d40d039ce2b8842796bdff239 100644 --- a/pulse_lib/qblox/pulsar_uploader.py +++ b/pulse_lib/qblox/pulsar_uploader.py @@ -1,4 +1,5 @@ import time +from collections import Iterable from uuid import UUID from datetime import datetime import numpy as np @@ -8,6 +9,8 @@ from dataclasses import dataclass, field from typing import Dict, Optional, List, Union from numbers import Number +from pulse_lib.segments.conditional_segment import conditional_segment + from .rendering import SineWaveform, get_modulation from .pulsar_sequencers import ( Voltage1nsSequenceBuilder, @@ -15,7 +18,10 @@ from .pulsar_sequencers import ( IQSequenceBuilder, AcquisitionSequenceBuilder, SequenceBuilderBase, - PulsarConfig) + PulsarConfig, + MarkerEvent, + LatchEvent) +from .qblox_conditional import get_conditional_channel from q1pulse import Q1Instrument @@ -66,7 +72,6 @@ class PulsarUploader: out_channels = [self.awg_channels[iq_out_ch.awg_channel_name] for iq_out_ch in iq_out_channels] module_name = out_channels[0].awg_name - # TODO @@@ check I and Q phase. q1.add_control(name, module_name, [out_ch.channel_number for out_ch in out_channels]) for name, dig_ch in self.digitizer_channels.items(): @@ -88,11 +93,18 @@ class PulsarUploader: if marker_ch.invert: raise Exception(f'Marker channel inversion not (yet) supported') - @staticmethod def set_output_dir(path): PulsarUploader.output_dir = path + @property + def supports_conditionals(self): + return True + + def get_roundtrip_latency(self): + # TODO @@@ put in configuration file. + return 260 + def _get_voltage_channels(self): iq_out_channels = [] @@ -145,11 +157,6 @@ class PulsarUploader: self.seq_markers = seq_markers self.marker_sequencers = marker_sequencers - - @property - def supports_conditionals(self): - return False - def get_effective_sample_rate(self, sample_rate): """ Returns the sample rate that will be used by the AWG. @@ -245,12 +252,12 @@ class PulsarUploader: total_seconds = job.playback_time * n_rep * 1e-9 timeout_minutes = int(total_seconds*1.1 / 60) + 1 - # update resonator frequency and amplitude + # update resonator frequency and phase for ch_name, dig_channel in self.digitizer_channels.items(): nco_freq = dig_channel.frequency - if nco_freq is None: - continue - job.program[ch_name].nco_frequency = nco_freq + if nco_freq is not None: + job.program[ch_name].nco_frequency = nco_freq + job.program[ch_name].thresholded_acq_rotation = dig_channel.phase self.q1instrument.start_program(job.program) self.q1instrument.wait_stopped(timeout_minutes=timeout_minutes) @@ -291,7 +298,6 @@ class PulsarUploader: if dig_ch.frequency or len(in_ch) == 2: if dig_ch.frequency or dig_ch.iq_input: - # @@@ if frequency, set phase in QRM.sequencer phase_rotation_acq raw_ch = (raw[0] + 1j * raw[1]) * np.exp(1j*dig_ch.phase) if not dig_ch.iq_out: raw_ch = raw_ch.real @@ -380,6 +386,7 @@ class Job(object): self.playback_time = 0 #total playtime of the waveform self.acquisition_conf = None self.acq_data_scaling = {} + self.feedback_channels = set() self.released = False @@ -399,6 +406,59 @@ class Job(object): def set_acquisition_conf(self, conf): self.acquisition_conf = conf + def set_feedback(self, condition_measurements): + self.condition_measurements = condition_measurements + # process feedback events + events = [] + for channel_name, measurements in condition_measurements.measurement_acquisitions.items(): + for m in measurements: + try: + fb = condition_measurements.feedback_events[id(m)] + events.append((self._at_index(fb.set_times), channel_name, 'latch-enable')) + events.append((self._at_index(fb.reset_times), channel_name, 'reset')) + except KeyError: + t = condition_measurements.get_end_time(m, self.index) + events.append((t, channel_name, 'latch-disable')) + + latch_events = [] + feedback_channels = set() + active_counters = set() + latching_counters = set() + pending_resets = set() + enabled_latches = set() + last_t = 0 + for t, channel_name, action in sorted(events): + if t != last_t and latching_counters != enabled_latches: + enabled_latches = latching_counters.copy() + latch_events.append(LatchEvent(last_t, counters=list(enabled_latches))) + feedback_channels.add(channel_name) + last_t = t + if action == 'latch-enable': + if channel_name in pending_resets: + raise Exception(f'Qblox feedback error: counter not reset before measurement ' + f'{channel_name}, t={t}') + active_counters.add(channel_name) + latching_counters.add(channel_name) + elif action == 'latch-disable': + latching_counters.discard(channel_name) + elif action == 'reset': + active_counters.remove(channel_name) + pending_resets.add(channel_name) + if not active_counters: + latch_events.append(LatchEvent(t, reset=True)) + pending_resets.clear() + if pending_resets: + latch_events.append(LatchEvent(np.inf, reset=True)) + self.latch_events = latch_events + self.feedback_channels = feedback_channels + logger.info(f'Feedback events {latch_events}') + + def _at_index(self, data): + try: + return data[tuple(self.index[-len(data.shape):])] + except AttributeError: + return data + def release(self): if self.released: logger.warning(f'job {self.seq_id}-{self.index} already released') @@ -515,7 +575,10 @@ class UploadAggregator: channel_info.integral = 0 if channel_info.dc_compensation: - seg_ch = seg[channel_name] + if isinstance(seg, conditional_segment): + seg_ch = get_conditional_channel(seg, channel_name) + else: + seg_ch = seg[channel_name] channel_info.integral += seg_ch.integrate(job.index, sample_rate) logger.debug(f'Integral seg:{iseg} {channel_name} integral:{channel_info.integral}') @@ -548,11 +611,16 @@ class UploadAggregator: def get_markers(self, job, marker_channel): # Marker on periods can overlap, also across segments. # Get all start/stop times and merge them. + channel_name = marker_channel.name start_stop = [] segments = self.segments for iseg,(seg,seg_render) in enumerate(zip(job.sequence,segments)): offset = seg_render.t_start + marker_channel.delay + self.max_pre_start_ns - seg_ch = seg[marker_channel.name] + if isinstance(seg, conditional_segment): + logger.debug(f'conditional for {channel_name}') + seg_ch = get_conditional_channel(seg, channel_name) + else: + seg_ch = seg[channel_name] ch_data = seg_ch._get_data_all_at(job.index) for pulse in ch_data.my_marker_data: @@ -561,7 +629,7 @@ class UploadAggregator: # merge markers marker_value = 1 << marker_channel.channel_number - return merge_markers(marker_channel.name, start_stop, marker_value, min_off_ns=20) + return merge_markers(channel_name, start_stop, marker_value, min_off_ns=20) def get_markers_seq(self, job, seq_name): marker_names = self.seq_markers.get(seq_name, []) @@ -581,9 +649,9 @@ class UploadAggregator: s += value if last is not None and t == last: # multiple markers on same time - seq_markers[-1] = (t,s) + seq_markers[-1] = MarkerEvent(t,s) else: - seq_markers.append((t,s)) + seq_markers.append(MarkerEvent(t,s)) last = t return seq_markers @@ -608,7 +676,11 @@ class UploadAggregator: for iseg,(seg,seg_render) in enumerate(zip(job.sequence,segments)): seg_start = seg_render.t_start - seg_ch = seg[channel_name] + if isinstance(seg, conditional_segment): + logger.debug(f'conditional for {channel_name}') + seg_ch = get_conditional_channel(seg, channel_name) + else: + seg_ch = seg[channel_name] data = seg_ch._get_data_all_at(job.index) entries = data.get_data_elements(break_ramps=True) for e in entries: @@ -675,17 +747,56 @@ class UploadAggregator: nco_freq, mixer_gain=qubit_channel.correction_gain, mixer_phase_offset=qubit_channel.correction_phase) - seq.set_time_offset(t_offset) - attenuation = 1.0 # TODO @@@ check if this is always true.. + + # lookup attenuation of AWG channels + iq_out_channels = qubit_channel.iq_channel.IQ_out_channels + att = [self.channels[output.awg_channel_name].attenuation for output in iq_out_channels] + if min(att) != max(att): + raise Exception('Attenuation for IQ output is not equal for channels ' + f'{[[output.awg_channel_name] for output in iq_out_channels]}') + attenuation = min(att) scaling = 1/(attenuation * seq.max_output_voltage*1000) + seq.set_time_offset(t_offset) + if self.feedback_triggers: + for trigger in self.feedback_triggers.values(): + seq.add_trigger_counter(trigger) + seq.add_latch_events(job.latch_events) + markers = self.get_markers_seq(job, channel_name) seq.add_markers(markers) for iseg,(seg,seg_render) in enumerate(zip(job.sequence,segments)): seg_start = seg_render.t_start + if not isinstance(seg, conditional_segment): + seg_ch = seg[channel_name] + self._add_iq_data(job, seg_ch, seg_start, scaling, seq, lo_freq) + else: + measurements = job.condition_measurements.get_measurements(seg) + logger.info(f'conditional segment {iseg}, cond:{[m.name for m in measurements]}') + if len(measurements) != 1: + raise Exception('Only 1 condition based on 1 measurement is supported.') + # TODO @@@ allow inversion of measurement! + m = measurements[0] + dig_channel = m.acquisition_channel + m_time = job.condition_measurements.get_end_time(m, job.index) + t_min = m_time+job.condition_measurements.min_feedback_time + with seq.conditional([dig_channel], t_min=t_min, t_max=seg_start) as cond: + branches = seg.branches + with cond.all_set(): + seg_ch = branches[1][channel_name] + self._add_iq_data(job, seg_ch, seg_start, scaling, seq, lo_freq) + with cond.none_set(): + seg_ch = branches[0][channel_name] + self._add_iq_data(job, seg_ch, seg_start, scaling, seq, lo_freq) + + + t_end = PulsarConfig.ceil(seg_render.t_end) + seq.wait_till(t_end) + # add final markers + seq.finalize() - seg_ch = seg[channel_name] + def _add_iq_data(self, job, seg_ch, seg_start, scaling, seq, lo_freq): data = seg_ch._get_data_all_at(job.index) entries = data.get_data_elements() @@ -715,12 +826,6 @@ class UploadAggregator: else: raise Exception('Unknown pulse element {type(e)}') - t_end = PulsarConfig.ceil(seg_render.t_end) - seq.wait_till(t_end) - # add final markers - seq.finalize() - - def add_acquisition_channel(self, job, digitizer_channel): for name in job.schedule_params: if name.startswith('dig_trigger_') or name.startswith('dig_wait'): @@ -741,6 +846,8 @@ class UploadAggregator: nco_frequency=nco_freq, rf_source=digitizer_channel.rf_source) seq.set_time_offset(t_offset) + seq.thresholded_acq_rotation = digitizer_channel.phase + if digitizer_channel.rf_source is not None: seq.offset_rf_ns = PulsarConfig.align(self.max_pre_start_ns + digitizer_channel.rf_source.delay) @@ -749,9 +856,16 @@ class UploadAggregator: if acq_conf.average_repetitions: seq.reset_bin_counter(t=0) + acq_threshold = None + acq_threshold_trigger_invert = False + use_feedback = channel_name in self.feedback_triggers for iseg, (seg, seg_render) in enumerate(zip(job.sequence, self.segments)): seg_start = seg_render.t_start - seg_ch = seg[channel_name] + if isinstance(seg, conditional_segment): + logger.debug(f'conditional for {channel_name}') + seg_ch = get_conditional_channel(seg, channel_name) + else: + seg_ch = seg[channel_name] acquisition_data = seg_ch._get_data_all_at(job.index).get_data() for acquisition in acquisition_data: @@ -776,6 +890,14 @@ class UploadAggregator: seq.repeated_acquire(t, trigger_period, n_cycles, trigger_period) else: seq.acquire(t, t_measure) + if acquisition.threshold is not None and use_feedback: + if acq_threshold is None: + acq_threshold = acquisition.threshold + acq_threshold_trigger_invert = acquisition.zero_on_high + else: + if (acq_threshold != acquisition.threshold + or acq_threshold_trigger_invert != acquisition.zero_on_high): + raise Exception(f'With feedback all thresholds on a channel must be equal') t_end = PulsarConfig.ceil(seg_render.t_end) try: @@ -783,6 +905,9 @@ class UploadAggregator: except: raise Exception(f"Acquisition doesn't fit in sequence. Add a wait to extend the sequence.") seq.finalize() + if acq_threshold is not None: + seq.thresholded_acq_threshold = acq_threshold + self.feedback_triggers[channel_name].invert = acq_threshold_trigger_invert job.acq_data_scaling[channel_name] = seq.get_data_scaling() def add_marker_seq(self, job, channel_name): @@ -798,11 +923,17 @@ class UploadAggregator: times.append(['start', time.perf_counter()]) name = datetime.now().strftime("%Y%m%d_%H%M%S_%f") - self.program = self.q1instrument.new_program(name) - job.program = self.program - self.program.repetitions = job.n_rep if job.n_rep else 1 + program = self.q1instrument.new_program(name) + self.program = program + job.program = program + program.repetitions = job.n_rep if job.n_rep else 1 - self.program._timeline.disable_update() # @@@ Yuk + self.feedback_triggers = {} + for channel in job.feedback_channels: + trigger = program.configure_trigger(channel) + self.feedback_triggers[channel] = trigger + + program._timeline.disable_update() # @@@ Yuk times.append(['init', time.perf_counter()]) @@ -834,23 +965,23 @@ class UploadAggregator: times.append(['marker', time.perf_counter()]) - self.program._timeline.enable_update() # @@@ Yuk + program._timeline.enable_update() # @@@ Yuk times.append(['done', time.perf_counter()]) # NOTE: compilation is 20...30% faster with listing=False, add_comments=False if UploadAggregator.verbose: - self.program.compile(listing=True, json=True) + program.compile(listing=True, json=True) else: retry = False try: - self.program.compile(add_comments=False, listing=False, json=False) + program.compile(add_comments=False, listing=False, json=False) except Exception as ex: retry = True print(f'Exception {ex} was raised during compilation. Compiling again with comments.') if retry: # retry with listing and comments. - self.program.compile(add_comments=True, listing=True, json=True) + program.compile(add_comments=True, listing=True, json=True) times.append(['compile', time.perf_counter()]) @@ -891,4 +1022,3 @@ class UploadAggregator: result = -channel_info.integral / channel_info.dc_compensation_min return result - diff --git a/pulse_lib/sequencer.py b/pulse_lib/sequencer.py index a7bafc6129764445adaeb4058142a10d9e8af3c3..4a56dc0d504fbabadb7606d608e1c1f380f0e901 100644 --- a/pulse_lib/sequencer.py +++ b/pulse_lib/sequencer.py @@ -10,16 +10,14 @@ from .segments.segment_measurements import measurement_acquisition from .segments.utility.data_handling_functions import find_common_dimension, update_dimension from .segments.utility.setpoint_mgr import setpoint_mgr, setpoint from .segments.utility.looping import loop_obj -from .segments.utility.measurement_ref import MeasurementRef from .measurements_description import measurements_description from .acquisition.acquisition_conf import AcquisitionConf from .acquisition.player import SequencePlayer from .acquisition.measurement_converter import MeasurementConverter, DataSelection, MeasurementParameter +from .compiler.condition_measurements import ConditionMeasurements from si_prefix import si_format -from typing import List -from collections.abc import Iterable from numbers import Number import numpy as np import uuid @@ -31,7 +29,7 @@ class sequencer(): """ Class to make sequences for segments. """ - def __init__(self, upload_module, digitizer_channels): + def __init__(self, upload_module, digitizer_channels, awg_channels): ''' make a new sequence object. Args: @@ -51,6 +49,7 @@ class sequencer(): self.sequence = list() self.uploader = upload_module self._digitizer_channels = digitizer_channels + self._awg_channels = awg_channels self._measurements_description = measurements_description(digitizer_channels) @@ -219,6 +218,11 @@ class sequencer(): self._HVI_variables = data_container(marker_HVI_variable()) self._HVI_variables = update_dimension(self._HVI_variables, self.shape) + dig_awg_delay = self._calculate_max_dig_delay() + self._condition_measurements = ConditionMeasurements(self._measurements_description, + self.uploader, + dig_awg_delay) + # enforce master clock for the current segments (affects the IQ channels (translated into a phase shift) and and the marker channels (time shifts)) t_tot = np.zeros(self.shape) @@ -229,16 +233,30 @@ class sequencer(): lp_time.add_data(t_tot, axis=list(range(self.ndim -1,-1,-1))) seg_container.add_master_clock(lp_time) self._HVI_variables += seg_container.software_markers.pulse_data_all - if isinstance(seg_container, conditional_segment): - self._check_conditional(seg_container, t_tot) + self._condition_measurements.add_segment(seg_container, t_tot) self._measurements_description.add_segment(seg_container, t_tot) - t_tot += seg_container.total_time self._measurements_description.add_HVI_variables(self._HVI_variables) self._total_time = t_tot + self._condition_measurements.check_feedback_timing() self._generate_sweep_params() self._create_metadata() + def _calculate_max_dig_delay(self): + ''' + Returns the maximum configured delay from AWG channel to digitizer channel. + ''' + awg_delays = [] + for channel in self._awg_channels.values(): + awg_delays.append(channel.delay) + + dig_delays = [] + for channel in self._digitizer_channels.values(): + dig_delays.append(channel.delay) + + return max(0, *dig_delays) - min(0, *awg_delays) + + def _generate_sweep_params(self): self.params =[] @@ -261,45 +279,6 @@ class sequencer(): LOdict[name] = iq.LO self.metadata['LOs'] = LOdict - - def _check_conditional(self, conditional:conditional_segment, total_time): - - if not getattr(self.uploader, 'supports_conditionals', False): - raise Exception(f'Backend does not support conditional segments') - - condition = conditional.condition - refs = condition if isinstance(condition, Iterable) else [condition] - - # Lookup acquistions for condition - acquisition_names = self._get_acquisition_names(refs) - logger.info(f'acquisitions: {acquisition_names}') - - # check start of conditional pulse - min_slack = self._get_min_slack(acquisition_names, total_time) - logger.info(f'min slack for conditional {min_slack} ns. (Must be < 0)') - if min_slack < 0: - raise Exception(f'condition triggered {-min_slack} ns too early') - - pass - - def _get_acquisition_names(self, refs:List[MeasurementRef]): - acquisition_names = set() - for ref in refs: - acquisition_names.update(ref.keys) - - return list(acquisition_names) - - def _get_min_slack(self, acquisition_names, seg_start_times): - # calculate slack for all sequence indices - slack = np.empty((len(acquisition_names), ) + seg_start_times.shape) - - for i, name in enumerate(acquisition_names): - slack[i] = seg_start_times - self._measurements_description.end_times[name] - - slack -= self.uploader.get_roundtrip_latency() - - return np.min(slack) - def voltage_compensation(self, compensate): ''' add a voltage compensation at the end of the sequence @@ -564,6 +543,8 @@ class sequencer(): if self.hw_schedule is not None: hvi_markers = self._HVI_variables.item(tuple(index)).HVI_markers upload_job.add_hw_schedule(self.hw_schedule, hvi_markers) + if self._condition_measurements.feedback_events: + upload_job.set_feedback(self._condition_measurements) self.uploader.add_upload_job(upload_job) return upload_job