def _gen_rfcs_block(): optimize_pb = pb.PulseBlock() optimize_pb.insert(p_obj=po.PTrue(ch='aom', dur=1e-6)) optimize_pb.insert(p_obj=po.PTrue(ch='mw_gate', dur=1e-6)) optimize_pb.dflt_dict = dict(aom=po.DTrue(), mw_gate=po.DTrue()) return optimize_pb
def _gen_p_block(self, safety_window=200e-9): # ========== Prepare individual blocks ========= # ------- pre_init_pb -------- # Long optical pumping before first pulse pre_init_pb = pb.PulseBlock(p_obj_list=[po.PTrue(ch='aom', dur=3e-6)]) # ------- all_off_pb -------- # Switch all channels off after the full pulse sequence all_off_pb = pb.PulseBlock(p_obj_list=[ po.PFalse(ch='aom', dur=1e-6), po.PFalse(ch='mw_gate', dur=1e-6), po.PFalse(ch='ctr_gate', dur=1e-6) ]) # ========== Construct full rabi_pb ========= rabi_pb = pb.PulseBlock(name='RabiPB') # Pre-init block rabi_pb.insert_pb(pb_obj=pre_init_pb) # rabi_element ** n_pts for tau in self.tau_ar: rabi_pb.append_pb(pb_obj=self._gen_rabi_elem( tau=tau, safety_window=safety_window)) # All-off block rabi_pb.append_pb(pb_obj=all_off_pb) # Default values rabi_pb.dflt_dict = dict(aom=po.DFalse(), ctr_gate=po.DFalse(), mw_gate=po.DFalse()) # Correct for aom delay rabi_pb.add_offset(offset_dict=dict(aom=-self._aom_delay)) # Duration of full RabiPB # [for gated_ctr.get_count_ar() timeout] self._rabi_pb_dur = rabi_pb.dur return rabi_pb
def pb_expand_test(res_dict, indicate_bounds=True): """ Helper method to test pb_zip() This method takes direct output res_dict of pb_zip() call and reconstructs the original pulse block according to the sequence. To visualize boundaries between individual sequence elements, set optional indicate_bounds parameter to True. This will add zero-length divider pulses on one of the channels. :param res_dict: (dict) return of pb_zip() call to be reconstructed back into a plain waveform. :param indicate_bounds: (bool) if True, divider pulses will be added to one of the channels after each individual waveform. :return: (PulseBlock) reconstructed plain waveform """ import pulseblock.pulse as po new_pb = pb.PulseBlock() seq_list = res_dict['seq_list'] snip_list = res_dict['snip_list'] for snip_idx in range(len(seq_list)): snip_name, rep_num = seq_list[snip_idx] for rep_idx in range(rep_num): # Find wfm_snip corresponding to snip_name wfm_snip = None for wfm_snip in snip_list: if wfm_snip.name == snip_name: break new_pb.append_pb(pb_obj=wfm_snip) # Add a marker to indicate the block boundary if indicate_bounds: mrk_ch = list(wfm_snip.dflt_dict.keys())[0] new_pb.append(p_obj=po.PTrue(ch=mrk_ch, dur=0), cflct_er=False) return new_pb
def _gen_rabi_elem(tau=0, safety_window=200e-9): rabi_elem = pb.PulseBlock() # Init green_aom pulse rabi_elem.insert(p_obj=po.PTrue(ch='aom', dur=2e-6 + 2 * safety_window)) # Normalization pulse rabi_elem.insert(p_obj=po.PTrue(ch='ctr_gate', t0=1.5e-6, dur=0.5e-6)) # mw pulse rabi_elem.append( p_obj=po.PTrue(ch='mw_gate', dur=tau, t0=4 * safety_window)) tmp_dur = rabi_elem.dur + 4 * safety_window # Readout AOM pulse rabi_elem.insert(p_obj=po.PTrue(ch='aom', dur=1e-6, t0=tmp_dur)) # readout ctr_gate pulse rabi_elem.insert(p_obj=po.PTrue(ch='ctr_gate', dur=0.5e-6, t0=tmp_dur)) return rabi_elem
def _gen_p_block(self, debug=False): # Technical params # safety window between mw_src makes a step and # before counter starts accumulation of counts safety_window = 0.1 * self._accum_dur # Duration of sweep-step pulse step_pulse_dur = 0.1 * self._accum_dur # 200e-6 # ========== Prepare individual blocks ========= # ------- first_pb -------- # First point in the sweep: # mw_src should reset sweep position to start_f, # so there is no need to send step_trig pulse first_pb = pb.PulseBlock() # long safety window in the beginning first_pb.insert(p_obj=po.PFalse(ch='ctr_gate', dur=self._accum_dur)) # first ctr_gate pulse first_pb.append(p_obj=po.PTrue(ch='ctr_gate', dur=self._accum_dur)) # ------- step_pb -------- # This pb is repeated for every subsequent sweep step: # send ctr_trig pulse, wait for a safety window and # start accumulating pulses step_pb = pb.PulseBlock() # step pulse to mw_src step_pb.insert(p_obj=po.PTrue(ch='mw_step', dur=step_pulse_dur)) # ctr_gate pulse step_pb.append(p_obj=po.PTrue( ch='ctr_gate', dur=self._accum_dur, t0=safety_window)) # ------- off_pb -------- # Once sweep is complete, set all outputs to low off_pb = pb.PulseBlock() off_pb.insert(p_obj=po.PFalse(ch='aom', dur=safety_window)) off_pb.insert(p_obj=po.PFalse(ch='mw_gate', dur=safety_window)) off_pb.insert(p_obj=po.PFalse(ch='mw_step', dur=safety_window)) off_pb.insert(p_obj=po.PFalse(ch='ctr_gate', dur=safety_window)) # ========== Construct full odmr_pb ========= odmr_pb = pb.PulseBlock(name='ODMRPulseBlock') # Default values odmr_pb.dflt_dict = dict( aom=po.DFalse(), ctr_gate=po.DFalse(), mw_gate=po.DFalse(), mw_step=po.DFalse(), ) # first_bp odmr_pb.insert_pb(pb_obj=first_pb) # append step_bp ** (_n_pts-1) for _ in np.arange(start=1, stop=self._n_pts): odmr_pb.append_pb(pb_obj=step_pb) # aom and mw_gate are High during the whole block tmp_dur = odmr_pb.dur odmr_pb.insert(p_obj=po.PTrue(ch='aom', dur=tmp_dur)) odmr_pb.insert(p_obj=po.PTrue(ch='mw_gate', dur=tmp_dur)) # append off_pb odmr_pb.append_pb(pb_obj=off_pb) if debug: return dict(first_pb=first_pb, step_pb=step_pb, off_pb=off_pb, odmr_pb=odmr_pb) else: return odmr_pb
def compile_pulseblock(self): """ Compiles the list of pulse_specifiers and var dists into valid Pulseblock. """ pulseblock = pb.PulseBlock(name=self.name) for i, pb_spec in enumerate(self.pulse_specifiers): var_dict = pb_spec.pulsevar_dict arg_dict = {} # Extract parameters from the pulsevar dict offset = self.resolve_value(pb_spec.offset) * 1e-6 arg_dict["ch"] = pb_spec.channel arg_dict["dur"] = self.resolve_value(pb_spec.dur) * 1e-6 self.append_value_to_dict(var_dict, "val", arg_dict) self.append_value_to_dict(var_dict, "amp", arg_dict) self.append_value_to_dict(var_dict, "freq", arg_dict) self.append_value_to_dict(var_dict, "ph", arg_dict) self.append_value_to_dict(var_dict, "stdev", arg_dict, fn=lambda x: 1e-6 * x) self.append_value_to_dict(var_dict, "iq", arg_dict) self.append_value_to_dict(var_dict, "mod", arg_dict) self.append_value_to_dict(var_dict, "mod_freq", arg_dict) self.append_value_to_dict(var_dict, "mod_ph", arg_dict) supported_pulses = { "PTrue": po.PTrue, "PSin": po.PSin, "PGaussian": po.PGaussian, "PConst": po.PConst } # Handle IQ mixing case if "iq" in arg_dict and arg_dict["iq"]: iq_calibration = IQ_Calibration() iq_calibration.load_calibration(self.config["iq_cal_path"]) # Set arbitrarily so that neither channel will overflow 1 iq_calibration.IF_volt = 0.8 (if_freq, lo_freq, phase_opt, amp_i_opt, amp_q_opt, dc_i_opt, dc_q_opt) = iq_calibration.get_optimal_hdawg_and_LO_values( arg_dict["mod_freq"]) # Store the optimal IQ parameters as 2 separate dictionaries arg_dict_i = copy.deepcopy(arg_dict) arg_dict_q = copy.deepcopy(arg_dict) # Modify the channel names arg_dict_i["ch"] = arg_dict["ch"] + "_i" arg_dict_q["ch"] = arg_dict["ch"] + "_q" # Modulation frequency changed to IF arg_dict_i["mod_freq"] = if_freq arg_dict_q["mod_freq"] = if_freq # Relative phase arg_dict_i["mod_ph"] = arg_dict["mod_ph"] + phase_opt[0] # The amplitude is the amplitude of the Sin genarator and is # indepenent of ["amp"], the signal amplitude. arg_dict_i["iq_params"] = { "amp_iq": amp_i_opt[0], "dc_iq": dc_i_opt[0], "lo_freq": lo_freq } arg_dict_q["iq_params"] = { "amp_iq": amp_q_opt[0], "dc_iq": dc_q_opt[0], "lo_freq": lo_freq } arg_dict_list = [arg_dict_i, arg_dict_q] else: arg_dict_list = [arg_dict] # Construct a pulse and add it to the pulseblock # The iteration over arg_dict takes care of the IQ mixing case for idx, arg_dict in enumerate(arg_dict_list): # Construct single pulse. if pb_spec.pulsetype in supported_pulses: pulse = supported_pulses[pb_spec.pulsetype](**arg_dict) else: pulse = None self.log.warn( f"Found an unsupported pulse type {pb_spec.pulsetype}") # Store the duration of the first pulse (for IQ mixing) as the # pb duration is modified for the second pulse. if idx == 0: first_dur = pulse.dur pb_dur = pulseblock.dur # Insert pulse to correct position in pulseblock. if pb_spec.tref == "Absolute": pulseblock.append_po_as_pb(p_obj=pulse, offset=offset - pb_dur) elif pb_spec.tref == "After Last Pulse": if idx == 0: pulseblock.append_po_as_pb(p_obj=pulse, offset=offset) # Force the 2nd pulse to start at same time as the first # pulse in an IQ mix pulse. else: pulseblock.append_po_as_pb(p_obj=pulse, offset=-first_dur) elif pb_spec.tref == "After Last Pulse On Channel": # Get the end time of the last pulse on the ch ch = pb.Channel(name=arg_dict["ch"], is_analog=pulse.is_analog) if ch in pulseblock.p_dict.keys(): last_pulse = pulseblock.p_dict[ch][-1] last_pulsetime = last_pulse.t0 + last_pulse.dur else: last_pulsetime = 0 pulseblock.append_po_as_pb(p_obj=pulse, offset=last_pulsetime + offset - pb_dur) elif pb_spec.tref == "With Last Pulse": # Retrieve previous pulseblock: if i != 0: previous_pb_spec = self.pulse_specifiers[i - 1] else: raise ValueError( "Cannot chose timing reference 'With Last Pulse' for first pulse in pulse-sequence." ) # Retrieve duration of previous pulseblock. prev_dur = self.resolve_value(previous_pb_spec.dur) * 1e-6 if idx == 0: pulseblock.append_po_as_pb(p_obj=pulse, offset=-prev_dur + offset) # Force the 2nd pulse to start at same time as the first # pulse in an IQ mix pulse. else: pulseblock.append_po_as_pb(p_obj=pulse, offset=-first_dur) self.pulseblock = pulseblock
def pb_zip(pb_obj, dur_quant): """ Collapse default (wait) periods into repetitions of a single wait block. This method takes a plain PulseBlock and returns a sequence table and a list of individual waveforms (snippets). Corresponding pulse sequence reproduces the original waveform but with long wait periods replaced by multiple repetitions of the same short wait element for saving hardware memory (collapsing or zipping of a waveform). :param pb_obj: PulseObject instance - original pulse block to be collapsed :param dur_quant: (float) minimal waveform duration allowed by the hardware memory (in the same units as used in pb_obj). This value is normally determined as min_wfm_size / sampling_rate, where min_wfm_size is the minimal size of a sample array in hardware memory. The repeated wait waveform will have this size. :return: (dict) { 'seq_list': list of tuples ('wfm_name', repetition_number) - chronological sequence of (name of the wfm snippet, how many times it should be repeated) 'snip_list': list of PulseBlock objects - 'snippets' of the original pb_obj, which form the sequence according to seq_list Naming convention: pb_obj.name + '_wait' for the repeated wait wfm pb_obj.name + '_' + 'snip number {0, 1, ...}' for non-repeated snippets Order: wait is the first element, non-repeated snippets go next in chronological order. } """ # Non-default intervals --------------------------------------------------- # find all time intervals covered by non-default pulses p_interval_list = [] for ch in pb_obj.p_dict.keys(): for pulse in pb_obj.p_dict[ch]: p_interval_list.append((pulse.t0, pulse.t0 + pulse.dur)) # merge all overlapping pulse intervals p_interval_list = merge_intervals(i_list=p_interval_list) # Step usage dictionary --------------------------------------------------- # Now one knows all the wait periods which can be collapsed # (full duration of pb_obj minus all pulse intervals) # # But both the wait waveform and non-trivial pulse waveforms have to # meet the length constraint: dur_quant + n*dur_step. That is why it is # not trivial to split full duration into repetitions of wait element and # non-default waveforms. # # Here one uses the easiest (perhaps not the most efficient) way to meet # these restrictions: # - divide the entire duration of pb_obj into discrete steps of dur_quant # - construct dur_quant-long wait waveform # - if a given step does not contain any pulses (or any parts of pulses), # it will be replaced by a repetiotion of wait wfm # - if one or several adjacent steps contain non-default pulses, they are # merged and output as a single pulse block # - last integer dur_quant is merged with the reminder (shorter than dur_quant) # and is returned as a single pulse block (because the reminder is shorter # than minimal waveform length and cannot be sampled alone) # Number of whole dur_quant, fitting into pb_obj.dur quant_num = int(pb_obj.dur // dur_quant) # Use array: # False - this dur_quant is a wait period # True - this dur_quant is a part of pulse interval use_ar = np.full(shape=quant_num, fill_value=False) # Determine which steps contain non-default pulses for p_interval in p_interval_list: left_idx = int(p_interval[0] // dur_quant) right_idx = int(p_interval[1] // dur_quant) # Handle left-most interval # If p_interval[0] == 0.0, integer division by dur_quant # may accidentally evaluate to -1.0 if left_idx < 0: left_idx = 0 # Handle right-most intervals if left_idx == quant_num - 1 or left_idx == quant_num: # The entire interval fits into the 'default-True' area # [(quant_num - 1)-st element and reminder], so no change # has to be made in use_ar continue elif right_idx == quant_num: # The left edge of the interval is outside of the 'default-True' # area [(quant_num - 1)-st elem and reminder], but the right edge # is in the reminder. The pulse might introduce non-trivial change # into use_ar, but right_idx is currently out of range. # # Reduce it to the max available index to avoid out-of-range and # broadcasting errors: right_idx = quant_num - 1 use_ar[left_idx:right_idx + 1] = np.full(shape=right_idx - left_idx + 1, fill_value=True) # Last integer step of dur_quant must be considered as non-wait # (to be merged and sampled with the reminder, which is shorter # than the hardware minimum) use_ar[-1] = True # Find periods of pulses / repeating wait (perform 'run-length encoding'): # The return is a period dictionary: # { # 'val_ar': True or False - this run of steps is non-default or wait # 'len_ar': number of dur_quant steps within each run # 'start_ar': starting index of each run # } period_dict = run_len_encode(in_ar=use_ar) # Final sequence table and PulseBlock dictionary -------------------------- final_snip_list = [] final_seq_list = [] # Construct 'wait' PulseBlock wait_pb_name = pb_obj.name + '_wait' wait_pb = pb.PulseBlock(name=wait_pb_name) wait_pb.dur = dur_quant wait_pb.dflt_dict = copy.deepcopy(pb_obj.dflt_dict) final_snip_list.append(wait_pb) # Iterate through all pulse runs, take 'snippet pulse blocks', # and construct sequence table # The last run (must be non-wait) will be extended to include the reminder last_period_idx = len(period_dict['val_ar']) - 1 # This counter is used to keep track of PulseBlock names pb_name_idx = 0 for period_idx, period_val in enumerate(period_dict['val_ar']): # Pulse period if period_val: # Take a snippet of this pulse period tmp_pb_name = pb_obj.name + '_{}'.format(pb_name_idx) # Snippet edges start_t = period_dict['start_ar'][period_idx] * dur_quant if period_idx < last_period_idx: # pulse period in the bulk - take the snippet by the period edges stop_t = (period_dict['start_ar'][period_idx] + period_dict['len_ar'][period_idx]) * dur_quant else: # right-most pulse period must be extended to include the reminder stop_t = pb_obj.dur tmp_pb = pb_snip(pb_obj=pb_obj, start_t=start_t, stop_t=stop_t, snip_name=tmp_pb_name, use_centers=True) # Add the snippet to the PulseBlock list final_snip_list.append(tmp_pb) # Add corresponding entry to the sequence table: # output tmp_pb_name once final_seq_list.append((tmp_pb_name, 1)) # Increment name index for the next PulseBlock pb_name_idx += 1 # Wait period else: # Only add an entry to the sequence table: # output 'wait waveform n times' final_seq_list.append( (wait_pb_name, period_dict['len_ar'][period_idx])) return {'seq_list': final_seq_list, 'snip_list': final_snip_list}
def pb_snip(pb_obj, start_t, stop_t, snip_name=None, use_centers=False): """ PulseBlock snipping tool Copies all default values and all pulse objects between the snippet edges with preserved order and spacing between them. The start_t and stop_t edges are the origin and the end of the returned PulseBlock object. Note that snippet edge must not cross any pulse object (but can touch it). If any edge crosses any pulse object, ValueError exception is produced. Assumptions: - pulse objects are ascending-time-ordered within pb_obj.p_dict[ch_name] - all pulse objects have non-zero duration. The algorithm will still work for a zero-duration pulse, but it might be ether left out or included into the snippet unpredictably, if it touches the edges star_t or stop_t. :param pb_obj: original PulseBlock :param start_t: (float) left snippet edge :param stop_t: (float) right snippet edge :param snip_name: (str, optional) name for the new PulseBlock object If not given, pb_obj.name + '_snip' is used. :param use_centers: (bool) if False, belonging of a pulse to the snippet is determined by edges. If True - by position of the pulse central point :return: new PulseBlock object - snippet of pb_obj between start_t and stop_t Exception is produced in the case of error. """ # New PulseBlock to be filled and returned new_pb = pb.PulseBlock() new_pb.dur = stop_t - start_t if snip_name is not None: new_pb.name = snip_name else: new_pb.name = pb_obj.name + '_snip' # Copy all default pulses new_pb.dflt_dict = copy.deepcopy(pb_obj.dflt_dict) # Copy all pulses falling in between start_t and stop_t for ch in pb_obj.p_dict.keys(): for pulse in pb_obj.p_dict[ch]: # Assumption: pulses in pb_obj.p_dict[ch] are time-ordered # Assumption: all pulses have dur > 0. # The algorithm will also work for a pulse of dur = 0, # but such pulse might be left out or included unpredictably, # if it touches the window edges star_t or stop_t. # Two ways to determine belonging of a block to the snippet: # # 1) whether the center of a pulse falls into [start_t, stop_t] # interval. # Pros: robust against float-comparison errors if the pulse # touches snippet edge. # Cons: This method does not detect the cases when snippet # edges go across a pulse block. # # 2) whether both edges of a pulse fall into [start_t, stop_t] # Pros: checks for snippet edges crossing the pulse # Cons: may unpredictably produce exceptions if one of the pulses # touches snippet edge (due to unpredictable comparison result # for two nominally identical floats) # Method 1: use pulse center if use_centers: pulse_center = pulse.t0 + pulse.dur / 2 if pulse_center <= start_t: # pulse is fully to the left from snip window continue elif start_t <= pulse_center <= stop_t: # pulse is fully inside the snip window if ch not in new_pb.p_dict.keys(): # the first time the pulse is added to this channel new_pb.p_dict[ch] = [] pulse_copy = copy.deepcopy(pulse) pulse_copy.t0 -= start_t new_pb.p_dict[ch].append(pulse_copy) else: # This pulse and all subsequent ones lie to the right # from the snip window break # Method 2: use pulse edges else: if pulse.t0 + pulse.dur <= start_t: # pulse is fully to the left from snip window continue elif start_t <= pulse.t0 and pulse.t0 + pulse.dur <= stop_t: # pulse is fully inside the snip window if ch not in new_pb.p_dict.keys(): # the first time the pulse is added to this channel new_pb.p_dict[ch] = [] pulse_copy = copy.deepcopy(pulse) pulse_copy.t0 -= start_t new_pb.p_dict[ch].append(pulse_copy) elif stop_t <= pulse.t0: # This pulse and all subsequent ones lie to the right # from the snip window break # The window edge goes across one of the pulse objects. # Since it is not clear what to do with such a pulse, # one has to raise an exception. # Alternatively, it might be some unexpected comparison error. else: # Determine the conflicting edge if pulse.t0 < start_t < pulse.t0 + pulse.dur: edge_type = 'start_t' edge_t = start_t elif pulse.t0 < stop_t < pulse.t0 + pulse.dur: edge_type = 'stop_t' edge_t = stop_t # Something completely unexpected: # if it were just a crossing with an edge, # one of the above conditions should have been satisfied. # Hence, this part is reached only in the case of some # unexpected error. # # Just raise an exception and provide all the information else: raise ValueError( 'pb_snip(): condition check failed: \n' ' pulse_obj = {} \n' ' t0 = {} \n' ' dur = {} \n' ' start_t = {} \n' ' stop_t = {}' ''.format(pulse, pulse.t0, pulse.dur, start_t, stop_t)) raise ValueError( 'pb_snip(): snip edge goes across a pulse object \n' ' channel = {} \n' ' t0 = {} \n' ' t0+dur = {} \n' ' pulse object = {} \n' ' conflicting edge = {} \n' ' edge position = {}' ''.format(ch, pulse.t0, pulse.t0 + pulse.dur, str(pulse), edge_type, edge_t)) return new_pb
def compile_pulseblock(self): """ Compiles the list of pulse_specifiers and var dists into valid Pulseblock. """ pulseblock = pb.PulseBlock(name=self.name) for i, pb_spec in enumerate(self.pulse_specifiers): var_dict = pb_spec.pulsevar_dict arg_dict = {} # Extract parameters from the pulsevar dict offset = self.resolve_value(pb_spec.offset) * 1e-6 arg_dict["ch"] = pb_spec.channel arg_dict["dur"] = self.resolve_value(pb_spec.dur) * 1e-6 self.append_value_to_dict(var_dict, "val", arg_dict) self.append_value_to_dict(var_dict, "amp", arg_dict) self.append_value_to_dict(var_dict, "freq", arg_dict) self.append_value_to_dict(var_dict, "ph", arg_dict) self.append_value_to_dict(var_dict, "stdev", arg_dict, fn=lambda x: 1e-6 * x) self.append_value_to_dict(var_dict, "iq", arg_dict) self.append_value_to_dict(var_dict, "mod", arg_dict) self.append_value_to_dict(var_dict, "mod_freq", arg_dict) self.append_value_to_dict(var_dict, "mod_ph", arg_dict) supported_pulses = { "PTrue": po.PTrue, "PSin": po.PSin, "PGaussian": po.PGaussian, "PConst": po.PConst } # Handle IQ mixing case if "iq" in arg_dict and arg_dict["iq"]: (if_freq, lo_freq, phase_opt, amp_i_opt, amp_q_opt, dc_i_opt, dc_q_opt ) = self.iq_calibration.get_optimal_hdawg_and_LO_values( arg_dict["mod_freq"]) self.log.info(f"if={if_freq}, lo={lo_freq}, phase={phase_opt}") # Store the optimal IQ parameters as 2 separate dictionaries arg_dict_i = copy.deepcopy(arg_dict) arg_dict_q = copy.deepcopy(arg_dict) # Modify the channel names arg_dict_i["ch"] = arg_dict["ch"] + "_i" arg_dict_q["ch"] = arg_dict["ch"] + "_q" # Modulation frequency changed to IF arg_dict_i["mod_freq"] = if_freq arg_dict_q["mod_freq"] = if_freq # Relative phase arg_dict_i["mod_ph"] = arg_dict["mod_ph"] + phase_opt[0] # The amplitude is the amplitude of the Sin genarator and is # indepenent of ["amp"], the signal amplitude. arg_dict_i["iq_params"] = { "amp_iq": amp_i_opt[0], "dc_iq": dc_i_opt[0], "lo_freq": lo_freq } arg_dict_q["iq_params"] = { "amp_iq": amp_q_opt[0], "dc_iq": dc_q_opt[0], "lo_freq": lo_freq } arg_dict_list = [arg_dict_i, arg_dict_q] else: arg_dict_list = [arg_dict] # Construct a pulse and add it to the pulseblock # The iteration over arg_dict takes care of the IQ mixing case # idx = 0 is the I portion, idx = 1 is the Q portion. for idx, arg_dict in enumerate(arg_dict_list): # Construct single pulse. if pb_spec.pulsetype in supported_pulses: pulse = supported_pulses[pb_spec.pulsetype](**arg_dict) else: pulse = None self.log.warn( f"Found an unsupported pulse type {pb_spec.pulsetype}") pb_dur = pulseblock.dur prev_t0 = pulseblock.latest_t0 prev_dur = pulseblock.latest_dur # idx = 0 refers to the I pulse (or a normal non-IQ pulse) if idx == 0: # CASE 1 if pb_spec.tref == "Absolute": pulseblock.append_po_as_pb(p_obj=pulse, offset=-pb_dur + offset) # CASE 2 elif pb_spec.tref in ( "After Last Pulse", "At End of Sequence" ): # For compatbility with previous naming pulseblock.append_po_as_pb(p_obj=pulse, offset=offset) # CASE 3 elif pb_spec.tref in ( "With Last Pulse", "With Previous Pulse" ): # For compatbility with previous naming # Take timing reference based on the last pulse's t0 pulseblock.append_po_as_pb(p_obj=pulse, offset=-pb_dur + prev_t0 + offset) # CASE 4 elif pb_spec.tref == "After Previous Pulse": # Take timing reference based on the last pulse's t0 and duration pulseblock.append_po_as_pb(p_obj=pulse, offset=-pb_dur + prev_t0 + prev_dur + offset) # CASE 5 elif pb_spec.tref == "After Last Pulse On Channel": # Get the end time of the last pulse on the ch ch = pb.Channel(name=arg_dict["ch"], is_analog=pulse.is_analog) if ch in pulseblock.p_dict.keys(): last_pulse = pulseblock.p_dict[ch][-1] last_pulsetime = last_pulse.t0 + last_pulse.dur else: last_pulsetime = 0 pulseblock.append_po_as_pb(p_obj=pulse, offset=-pb_dur + last_pulsetime + offset) else: # idx = 1 here (Q pulse) # Force the 2nd pulse to start at same time as the first # pulse in an IQ mix pulse. Note that prev_t0 is the t0 of # the I pulse since this is executed right after the I pulse. pulseblock.append_po_as_pb(p_obj=pulse, offset=-pb_dur + prev_t0) self.pulseblock = pulseblock self.clean_pulseblock_timings()