class SequenceToWaveforms: """Compile a multi qubit sequence into waveforms. Parameters ---------- n_qubit : type The maximum number of qubits (the default is 5). Attributes ---------- dt : float Pulse spacing, in seconds. local_xy : bool If False, collate all waveforms into one. simultaneous_pulses : bool If False, seperate all pulses in time. sample_rate : float AWG Sample rate. n_pts : float Number of points in the waveforms. first_delay : float Delay between start of waveform and start of the first pulse. trim_to_sequence : bool If True, adjust `n_points` to just fit the sequence. align_to_end : bool Align the whole sequence to the end of the waveforms. Only relevant if `trim_to_sequence` is False. sequences : list of :obj:`Step` The qubit sequences. qubits : list of :obj:`Qubit` Parameters of each qubit. wave_xy_delays : list of float Indiviudal delays for the XY waveforms. wave_z_delays : list of float Indiviudal delays for the Z waveforms. n_qubit """ def __init__(self, n_qubit=5): self.n_qubit = n_qubit self.dt = 10E-9 self.local_xy = True self.simultaneous_pulses = True # waveform parameter self.sample_rate = 1.2E9 self.n_pts = 240E3 self.first_delay = 100E-9 self.trim_to_sequence = True self.align_to_end = False self.sequences = [] self.qubits = [Qubit() for n in range(MAX_QUBIT)] # waveforms self._wave_xy = [ np.zeros(0, dtype=np.complex) for n in range(MAX_QUBIT) ] self._wave_z = [np.zeros(0) for n in range(MAX_QUBIT)] self._wave_gate = [np.zeros(0) for n in range(MAX_QUBIT)] # waveform delays self.wave_xy_delays = np.zeros(MAX_QUBIT) self.wave_z_delays = np.zeros(MAX_QUBIT) # define pulses self.pulses_1qb_xy = [Pulse() for n in range(MAX_QUBIT)] self.pulses_1qb_z = [Pulse() for n in range(MAX_QUBIT)] self.pulses_2qb = [Pulse() for n in range(MAX_QUBIT - 1)] self.pulses_readout = [ Pulse(pulse_type=PulseType.READOUT) for n in range(MAX_QUBIT) ] # cross-talk self.compensate_crosstalk = False self._crosstalk = Crosstalk() # predistortion self.perform_predistortion = False self._predistortions = [Predistortion(n) for n in range(MAX_QUBIT)] self._predistortions_z = [ ExponentialPredistortion(n) for n in range(MAX_QUBIT) ] # gate switch waveform self.generate_gate_switch = False self.uniform_gate = False self.gate_delay = 0.0 self.gate_overlap = 20E-9 self.minimal_gate_time = 20E-9 # readout trig settings self.readout_trig_generate = False # readout wave object and settings self.readout = Readout(max_qubit=MAX_QUBIT) self.readout_trig = np.array([], dtype=float) self.readout_iq = np.array([], dtype=np.complex) def get_waveforms(self, sequences): """Compile the given sequence into waveforms. Parameters ---------- sequences : list of :obj:`Step` The qubit sequence to be compiled. Returns ------- type Description of returned object. """ self.sequences = sequences self._seperate_gates() self._add_timings() self._init_waveforms() if self.align_to_end: shift = self._round((self.n_pts - 2) / self.sample_rate - self.sequences[-1].t_end) for step in self.sequences: step.time_shift(shift) self._perform_virtual_z() self._generate_waveforms() # collapse all xy pulses to one waveform if no local XY control if not self.local_xy: # sum all waveforms to first one self._wave_xy[0] = np.sum(self._wave_xy[:self.n_qubit], 0) # clear other waveforms for n in range(1, self.n_qubit): self._wave_xy[n][:] = 0.0 self._perform_crosstalk_compensation() self._predistort_waveforms() self._add_readout_trig() self._add_microwave_gate() # Apply offsets self.readout_iq += self.readout_i_offset + 1j * self.readout_q_offset # create and return dictionary with waveforms waveforms = dict() waveforms['xy'] = self._wave_xy waveforms['z'] = self._wave_z waveforms['gate'] = self._wave_gate waveforms['readout_trig'] = self.readout_trig waveforms['readout_iq'] = self.readout_iq return waveforms def _seperate_gates(self): if not self.simultaneous_pulses: new_sequences = [] for step in self.sequences: if any( isinstance(gate, (ReadoutGate, IdentityGate)) for gate in step.gates): # Don't seperate I gates or readouts since we do # multiplexed readout new_sequences.append(step) continue for i, gate in enumerate(step.gates): if gate is not None: new_step = Step(n_qubit=step.n_qubit, t0=step.t0, dt=step.dt, align=step.align) new_step.add_gate(i, gate) new_sequences.append(new_step) self.sequences = new_sequences # Replace any missing gates with I for step in self.sequences: for i, gate in enumerate(step.gates): if gate is None: step.gates[i] = Gate.I.value def _add_timings(self): for step in self.sequences: if step.dt is None and step.t0 is None: # Use global pulse spacing step.dt = self.dt if self.sequences[0].t0 is None: t_start = self.first_delay - self.sequences[0].dt # Longest pulse in the step needed for correct timing for step in self.sequences: max_duration = -np.inf for q, g in enumerate(step.gates): if isinstance(g, IdentityGate) and g.width is not None: duration = g.width else: pulse = self._get_pulse_for_gate(q, g) if pulse is not None: duration = pulse.total_duration() if duration > max_duration: max_duration = duration if step.t0 is None: step.t_start = t_start + step.dt else: step.t_start = step.t0 - max_duration / 2 step.t_start = self._round(step.t_start) step.t_end = self._round(step.t_start + max_duration) step.t_middle = step.t_start + max_duration / 2 t_start = step.t_end # Make sure that the sequence is sorted chronologically. self.sequences.sort(key=lambda x: x.t_start) # Make sure that the sequnce start on first delay time_diff = self._round(self.first_delay - self.sequences[0].t_start) if np.abs(time_diff) > 1e-10: for step in self.sequences: step.time_shift(time_diff) def _get_pulse_for_gate(self, qubit, gate): # Virtual Z is special since it has no length if isinstance(gate, VirtualZGate): pulse = None # Get the corresponding pulse for other gates elif isinstance(gate, SingleQubitRotation): if gate.axis in ('X', 'Y'): pulse = self.pulses_1qb_xy[qubit] elif gate.axis == 'Z': pulse = self.pulses_1qb_z[qubit] elif isinstance(gate, IdentityGate): if gate.width is None: pulse = copy(self.pulses_1qb_xy[qubit]) else: pulse = copy(self.pulses_1qb_xy[qubit]) pulse.width = gate.width elif isinstance(gate, TwoQubitGate): pulse = self.pulses_2qb[qubit] elif isinstance(gate, ReadoutGate): pulse = self.pulses_readout[qubit] elif isinstance(gate, CustomGate): pulse = gate.pulse else: raise ValueError('Please provide a pulse for {}'.format(gate)) return pulse def _predistort_waveforms(self): """Pre-distort the waveforms.""" if self.perform_predistortion: # go through and predistort all waveforms n_wave = self.n_qubit if self.local_xy else 1 for n in range(n_wave): self._wave_xy[n] = self._predistortions[n].predistort( self._wave_xy[n]) if self.perform_predistortion_z: # go through and predistort all waveforms for n in range(self.n_qubit): self._wave_z[n] = self._predistortions_z[n].predistort( self._wave_z[n]) def _perform_crosstalk_compensation(self): """Compensate for Z-control crosstalk.""" if not self.compensate_crosstalk: return self._wave_z = self._crosstalk.compensate(self._wave_z) def _perform_virtual_z(self): """Shifts the phase of pulses subsequent to virutal z gates.""" for qubit in range(self.n_qubit): phase = 0 for m, step in enumerate(self.sequences): gate = step.gates[qubit] if isinstance(gate, VirtualZGate): phase += gate.angle continue if not isinstance(gate, ReadoutGate): step.gates[qubit] = gate.add_phase(phase) def _add_microwave_gate(self): """Create waveform for gating microwave switch.""" if not self.generate_gate_switch: return n_wave = self.n_qubit if self.local_xy else 1 # go through all waveforms for n, wave in enumerate(self._wave_xy[:n_wave]): if self.uniform_gate: # the uniform gate is all ones gate = np.ones_like(wave) # if creating readout trig, turn off gate during readout if self.readout_trig_generate: gate[-int((self.readout_trig_duration - self.gate_overlap - self.gate_delay) * self.sample_rate):] = 0.0 else: # non-uniform gate, find non-zero elements gate = np.array(np.abs(wave) > 0.0, dtype=float) # fix gate overlap n_overlap = int(np.round(self.gate_overlap * self.sample_rate)) diff_gate = np.diff(gate) indx_up = np.nonzero(diff_gate > 0.0)[0] indx_down = np.nonzero(diff_gate < 0.0)[0] # add extra elements to left and right for overlap for indx in indx_up: gate[max(0, indx - n_overlap):(indx + 1)] = 1.0 for indx in indx_down: gate[indx:(indx + n_overlap + 1)] = 1.0 # fix gaps in gate shorter than min (look for 1>0) diff_gate = np.diff(gate) indx_up = np.nonzero(diff_gate > 0.0)[0] indx_down = np.nonzero(diff_gate < 0.0)[0] # ignore first transition if starting in zero if gate[0] == 0: indx_up = indx_up[1:] n_down_up = min(len(indx_down), len(indx_up)) len_down = indx_up[:n_down_up] - indx_down[:n_down_up] # find short gaps short_gaps = np.nonzero( len_down < (self.minimal_gate_time * self.sample_rate))[0] for indx in short_gaps: gate[indx_down[indx]:(1 + indx_up[indx])] = 1.0 # shift gate in time n_shift = int(np.round(self.gate_delay * self.sample_rate)) if n_shift < 0: n_shift = abs(n_shift) gate = np.r_[gate[n_shift:], np.zeros((n_shift, ))] elif n_shift > 0: gate = np.r_[np.zeros((n_shift, )), gate[:(-n_shift)]] # make sure gate starts/ends in 0 gate[0] = 0.0 gate[-1] = 0.0 # store results self._wave_gate[n] = gate def _round(self, t, acc=1E-12): """Round the time `t` with a certain accuarcy `acc`. Parameters ---------- t : float The time to be rounded. acc : float The accuarcy (the default is 1E-12). Returns ------- float The rounded time. """ return int(np.round(t / acc)) * acc def _add_readout_trig(self): """Create waveform for readout trigger.""" if not self.readout_trig_generate: return trig = np.zeros_like(self.readout_iq) start = (np.abs(self.readout_iq) > 0.0).nonzero()[0][0] end = int( np.min((start + self.readout_trig_duration * self.sample_rate, self.n_pts_readout))) trig[start:end] = self.readout_trig_amplitude # make sure trig starts and ends in 0. trig[0] = 0.0 trig[-1] = 0.0 self.readout_trig = trig def _init_waveforms(self): """Initialize waveforms according to sequence settings.""" # To keep the first pulse delay, use the smallest delay as reference. min_delay = np.min([ self.wave_xy_delays[:self.n_qubit], self.wave_z_delays[:self.n_qubit] ]) self.wave_xy_delays -= min_delay self.wave_z_delays -= min_delay max_delay = np.max([ self.wave_xy_delays[:self.n_qubit], self.wave_z_delays[:self.n_qubit] ]) # find the end of the sequence # only include readout in size estimate if all waveforms have same size if self.readout_match_main_size: end = np.max([s.t_end for s in self.sequences]) + max_delay else: end = np.max([s.t_end for s in self.sequences[0:-1]]) + max_delay # create empty waveforms of the correct size if self.trim_to_sequence: self.n_pts = int(np.ceil(end * self.sample_rate)) + 1 if self.n_pts % 2 == 1: # Odd n_pts give spectral leakage in FFT self.n_pts += 1 for n in range(self.n_qubit): self._wave_xy[n] = np.zeros(self.n_pts, dtype=np.complex) self._wave_z[n] = np.zeros(self.n_pts, dtype=float) self._wave_gate[n] = np.zeros(self.n_pts, dtype=float) # Waveform time vector self.t = np.arange(self.n_pts) / self.sample_rate # readout trig and i/q waveforms if self.readout_match_main_size: # same number of points for readout and main waveform self.n_pts_readout = self.n_pts else: # different number of points for readout and main waveform self.n_pts_readout = 1 + int( np.ceil( self.sample_rate * (self.sequences[-1].t_end - self.sequences[-1].t_start))) if self.n_pts_readout % 2 == 1: # Odd n_pts give spectral leakage in FFT self.n_pts_readout += 1 self.readout_trig = np.zeros(self.n_pts_readout, dtype=float) self.readout_iq = np.zeros(self.n_pts_readout, dtype=np.complex) def _generate_waveforms(self): """Generate the waveforms corresponding to the sequence.""" # find out if CZ pulses are used, if so pre-calc envelope to save time pulses_cz = set() # find set of all CZ pulses in use for step in self.sequences: for qubit, gate in enumerate(step.gates): pulse = self._get_pulse_for_gate(qubit, gate) if pulse is not None and pulse.shape == PulseShape.CZ: pulses_cz.add(pulse) # once we've gone through all pulses, pre-calculate the waveforms for pulse in pulses_cz: pulse.calculate_cz_waveform() for step in self.sequences: for qubit, gate in enumerate(step.gates): pulse = self._get_pulse_for_gate(qubit, gate) if pulse is None: continue if pulse.pulse_type == PulseType.Z: waveform = self._wave_z[qubit] delay = self.wave_z_delays[qubit] elif pulse.pulse_type == PulseType.XY: waveform = self._wave_xy[qubit] delay = self.wave_xy_delays[qubit] elif pulse.pulse_type == PulseType.READOUT: waveform = self.readout_iq delay = 0 # get the range of indices in use if (pulse.pulse_type == PulseType.READOUT and not self.readout_match_main_size): # special case for readout if not matching main wave size start = 0.0 middle = self._round(step.t_middle - step.t_start) end = self._round(step.t_end - step.t_start) else: start = self._round(step.t_start + delay) middle = self._round(step.t_middle + delay) end = self._round(step.t_end + delay) indices = np.arange(max(np.floor(start * self.sample_rate), 0), min(np.ceil(end * self.sample_rate), len(waveform)), dtype=int) # return directly if no indices if len(indices) == 0: continue # calculate time values for the pulse indices t = indices / self.sample_rate max_duration = end - start if step.align == 'center': t0 = middle elif step.align == 'left': t0 = middle - (max_duration - pulse.total_duration()) / 2 elif step.align == 'right': t0 = middle + (max_duration - pulse.total_duration()) / 2 # calculate the pulse waveform for the selected indices waveform[indices] += gate.get_waveform(pulse, t0, t) def set_parameters(self, config={}): """Set base parameters using config from from Labber driver. Parameters ---------- config : dict Configuration as defined by Labber driver configuration window """ # sequence parameters d = dict(Zero=0, One=1, Two=2, Three=3, Four=4, Five=5, Six=6, Seven=7, Eight=8, Nine=9) self.n_qubit = d[config.get('Number of qubits')] self.dt = config.get('Pulse spacing') self.local_xy = config.get('Local XY control') self.simultaneous_pulses = config.get('Simultaneous pulses') # waveform parameters self.sample_rate = config.get('Sample rate') self.n_pts = int(config.get('Number of points', 0)) self.first_delay = config.get('First pulse delay') self.trim_to_sequence = config.get('Trim waveform to sequence') self.trim_start = config.get('Trim both start and end') self.align_to_end = config.get('Align pulses to end of waveform') # qubit spectra for n in range(self.n_qubit): m = n + 1 # pulses are indexed from 1 in Labber qubit = Transmon( config.get('f01 max #{}'.format(m)), config.get('f01 min #{}'.format(m)), config.get('Ec #{}'.format(m)), config.get('Vperiod #{}'.format(m)), config.get('Voffset #{}'.format(m)), config.get('V0 #{}'.format(m)), ) self.qubits[n] = qubit # single-qubit pulses XY for n, pulse in enumerate(self.pulses_1qb_xy): m = n + 1 # pulses are indexed from 1 in Labber # global parameters pulse.shape = PulseShape(config.get('Pulse type')) pulse.truncation_range = config.get('Truncation range') pulse.start_at_zero = config.get('Start at zero') pulse.use_drag = config.get('Use DRAG') pulse.pulse_type = PulseType.XY # pulse shape if config.get('Uniform pulse shape'): pulse.width = config.get('Width') pulse.plateau = config.get('Plateau') else: pulse.width = config.get('Width #%d' % m) pulse.plateau = config.get('Plateau #%d' % m) if config.get('Uniform amplitude'): pulse.amplitude = config.get('Amplitude') else: pulse.amplitude = config.get('Amplitude #%d' % m) # pulse-specific parameters pulse.frequency = config.get('Frequency #%d' % m) pulse.drag_coefficient = config.get('DRAG scaling #%d' % m) pulse.drag_detuning = config.get('DRAG frequency detuning #%d' % m) # single-qubit pulses Z for n, pulse in enumerate(self.pulses_1qb_z): # pulses are indexed from 1 in Labber m = n + 1 # global parameters pulse.shape = PulseShape(config.get('Pulse type, Z')) pulse.truncation_range = config.get('Truncation range, Z') pulse.start_at_zero = config.get('Start at zero, Z') pulse.pulse_type = PulseType.Z # pulse shape if config.get('Uniform pulse shape, Z'): pulse.width = config.get('Width, Z') pulse.plateau = config.get('Plateau, Z') else: pulse.width = config.get('Width #%d, Z' % m) pulse.plateau = config.get('Plateau #%d, Z' % m) if config.get('Uniform amplitude, Z'): pulse.amplitude = config.get('Amplitude, Z') else: pulse.amplitude = config.get('Amplitude #%d, Z' % m) # two-qubit pulses for n, pulse in enumerate(self.pulses_2qb): # pulses are indexed from 1 in Labber s = ' #%d%d' % (n + 1, n + 2) # global parameters pulse.shape = PulseShape(config.get('Pulse type, 2QB')) pulse.pulse_type = PulseType.Z if config.get('Pulse type, 2QB') == 'CZ': pulse.F_Terms = d[config.get('Fourier terms, 2QB')] if config.get('Uniform 2QB pulses'): pulse.width = config.get('Width, 2QB') pulse.plateau = config.get('Plateau, 2QB') else: pulse.width = config.get('Width, 2QB' + s) pulse.plateau = config.get('Plateau, 2QB') # spectra if config.get('Assume linear dependence' + s, True): pulse.qubit = None else: pulse.qubit = self.qubits[n] # Get Fourier values if d[config.get('Fourier terms, 2QB')] == 4: pulse.Lcoeff = np.array([ config.get('L1, 2QB' + s), config.get('L2, 2QB' + s), config.get('L3, 2QB' + s), config.get('L4, 2QB' + s) ]) elif d[config.get('Fourier terms, 2QB')] == 3: pulse.Lcoeff = np.array([ config.get('L1, 2QB' + s), config.get('L2, 2QB' + s), config.get('L3, 2QB' + s) ]) elif d[config.get('Fourier terms, 2QB')] == 2: pulse.Lcoeff = np.array( [config.get('L1, 2QB' + s), config.get('L2, 2QB' + s)]) elif d[config.get('Fourier terms, 2QB')] == 1: pulse.Lcoeff = np.array([config.get('L1, 2QB' + s)]) pulse.Coupling = config.get('Coupling, 2QB' + s) pulse.Offset = config.get('f11-f20 initial, 2QB' + s) pulse.amplitude = config.get('f11-f20 final, 2QB' + s) pulse.dfdV = config.get('df/dV, 2QB' + s) else: pulse.truncation_range = config.get('Truncation range, 2QB') pulse.start_at_zero = config.get('Start at zero, 2QB') # pulse shape if config.get('Uniform 2QB pulses'): pulse.width = config.get('Width, 2QB') pulse.plateau = config.get('Plateau, 2QB') else: pulse.width = config.get('Width, 2QB' + s) pulse.plateau = config.get('Plateau, 2QB' + s) # pulse-specific parameters pulse.amplitude = config.get('Amplitude, 2QB' + s) Gate.CZ.value.new_angles(config.get('QB1 Phi 2QB #12'), config.get('QB2 Phi 2QB #12')) # predistortion self.perform_predistortion = config.get('Predistort waveforms', False) # update all predistorting objects for p in self._predistortions: p.set_parameters(config) # Z predistortion self.perform_predistortion_z = config.get('Predistort Z') for p in self._predistortions_z: p.set_parameters(config) # crosstalk self.compensate_crosstalk = config.get('Compensate cross-talk', False) self._crosstalk.set_parameters(config) # gate switch waveform self.generate_gate_switch = config.get('Generate gate') self.uniform_gate = config.get('Uniform gate') self.gate_delay = config.get('Gate delay') self.gate_overlap = config.get('Gate overlap') self.minimal_gate_time = config.get('Minimal gate time') # readout self.readout_match_main_size = config.get( 'Match main sequence waveform size') self.readout_i_offset = config.get('Readout offset - I') self.readout_q_offset = config.get('Readout offset - Q') self.readout_trig_generate = config.get('Generate readout trig') self.readout_trig_amplitude = config.get('Readout trig amplitude') self.readout_trig_duration = config.get('Readout trig duration') self.readout_predistort = config.get('Predistort readout waveform') self.readout.set_parameters(config) # get readout pulse parameters phases = 2 * np.pi * np.array([ 0.8847060, 0.2043214, 0.9426104, 0.6947334, 0.8752361, 0.2246747, 0.6503154, 0.7305004, 0.1309068 ]) for n, pulse in enumerate(self.pulses_readout): # pulses are indexed from 1 in Labber m = n + 1 pulse.shape = PulseShape(config.get('Readout pulse type')) pulse.truncation_range = config.get('Readout truncation range') pulse.start_at_zero = config.get('Readout start at zero') pulse.iq_skew = config.get('Readout IQ skew') * np.pi / 180 pulse.iq_ratio = config.get('Readout I/Q ratio') if config.get('Distribute readout phases'): pulse.phase = phases[n] else: pulse.phase = 0 if config.get('Uniform readout pulse shape'): pulse.width = config.get('Readout width') pulse.plateau = config.get('Readout duration') else: pulse.width = config.get('Readout width #%d' % m) pulse.plateau = config.get('Readout duration #%d' % m) if config.get('Uniform readout amplitude') is True: pulse.amplitude = config.get('Readout amplitude') else: pulse.amplitude = config.get('Readout amplitude #%d' % (n + 1)) pulse.frequency = config.get('Readout frequency #%d' % m) # Delays self.wave_xy_delays = np.zeros(self.n_qubit) self.wave_z_delays = np.zeros(self.n_qubit) for n in range(self.n_qubit): m = n + 1 self.wave_xy_delays[n] = config.get('Qubit %d XY Delay' % m) self.wave_z_delays[n] = config.get('Qubit %d Z Delay' % m)
class Sequence(object): """This class represents a multi-qubit control sequence The class supports two ways of defining pulse sequences: (1) Use the functions `add_single_pulse` or `add_single_gate` to add pulses to individual qubit waveforms at arbitrary time positions, or, (2) Use the function `add_gates` to add a list of pulses to all qubits. The pulses will be separated by a fixed pulse period. Attributes ---------- n_qubit : int Number of qubits controlled by the sequence. period_1qb : float Period for single-qubit gates. period_2qb : float Period for two-qubit gates. local_xy : bool Define if qubits have local XY control lines. If False, all control pulses are added to a single output waveform. sample_rate : float Sample rate of output waveforms. n_pts : int Length of output waveforms. Note that the resulting waveform may be different if the waveforms are trimmed. first_delay : float Position of first pulse trim_to_sequence : bool If True, waveform is trimmed to fit sequence trim_start : bool If True, waveform is trimmed at both start and end (default is just end) perform_tomography : bool If True, tomography pulses will be added to the end of the qubit xy control waveforms. perform_predistortion : bool If True, the control waveforms will be pre-distorted. generate_gate_switch : bool If True, generate waveform for microwave gate switch uniform_gate : bool If True, the gate is open during the entire xy waveform gate_delay : float Delay of gate switch wave relative to the I/Q pulse gate_overlap : float Extra time before/after I/Q pulse during which the gate switch is open minimal_gate_time : float Shortest time the gate switch will stay open/closed. generate_readout_trig : bool If True, generate waveform with readout trig at the end of the waveform. readout_delay : float Readout trig delay. readout_amplitude : float Amplitude of readout trig pulse. readout_duration : float Duration of readout trig pulse. generate_readout_iq : bool If True, generate complex waveform with multi-qubit I/Q readout signals. compensate_crosstalk : bool If True, Z-control waveforms will be compensated for cross-talk. """ def __init__(self, n_qubit=5, period_1qb=30E-9, period_2qb=30E-9, sample_rate=1.2E9, n_pts=240E3, first_delay=100E-9, local_xy=True): # define parameters self.n_qubit = n_qubit self.period_1qb = period_1qb self.period_2qb = period_2qb self.local_xy = local_xy # waveform parameter self.sample_rate = sample_rate self.n_pts = n_pts self.first_delay = first_delay self.trim_to_sequence = True self.trim_start = False self.align_to_end = False # parameter for keeping track of current gate pulse time self.time_pulse = 0.0 # waveforms self.wave_xy = [np.zeros(0, dtype=np.complex) for n in range(MAX_QUBIT)] self.wave_z = [np.zeros(0) for n in range(MAX_QUBIT)] self.wave_gate = [np.zeros(0) for n in range(MAX_QUBIT)] # define pulses self.pulses_1qb = [Pulse() for n in range(MAX_QUBIT)] self.pulses_2qb = [Pulse() for n in range(MAX_QUBIT - 1)] # tomography self.perform_tomography = False self.tomography = Tomography() # cross-talk object self.compensate_crosstalk = False self.crosstalk = Crosstalk() # predistortion objects self.perform_predistortion = False self.predistortions = [Predistortion(n) for n in range(MAX_QUBIT)] # gate switch waveform self.generate_gate_switch = False self.uniform_gate = False self.gate_delay = 0.0 self.gate_overlap = 20E-9 self.minimal_gate_time = 20E-9 # readout trig settings self.generate_readout_trig = False self.readout_delay = 0.0 self.readout_amplitude = 1.0 self.readout_duration = 20E-9 # readout wave object and settings self.generate_readout_iq = False self.readout = Readout(max_qubit=MAX_QUBIT) self.readout_trig = np.array([], dtype=float) self.readout_iq = np.array([], dtype=np.complex) def init_waveforms(self): """Initialize waveforms according to sequence settings""" # clear waveforms for n in range(self.n_qubit): self.wave_xy[n] = np.zeros(self.n_pts, dtype=np.complex) self.wave_z[n] = np.zeros(self.n_pts, dtype=float) self.wave_gate[n] = np.zeros(self.n_pts, dtype=float) # readout trig pts = self.n_pts if self.generate_readout_trig else 0 self.readout_trig = np.zeros(pts, dtype=float) # readout i/q waveform pts = self.n_pts if self.generate_readout_iq else 0 self.readout_iq = np.zeros(pts, dtype=np.complex) # reset gate position counter self.time_pulse = self.first_delay def generate_sequence(self, config): """Generate sequence by adding gates/pulses to waveforms Parameters ---------- config : dict Configuration as defined by Labber driver configuration window """ # this function should be overloaded by specific sequence pass def calculate_waveforms(self, config): """Calculate waveforms for all qubits The function will initialize the waveforms, generate the qubit pulse sequence, create gates and readout pulses, perform pre-distortion, and finally return the qubit control waveforms. Parameters ---------- config : dict Configuration as defined by Labber driver configuration window Returns ------- waveforms : dict with numpy arrays Dictionary with qubit waveforms. Depending on the sequence configuration, the dictionary will have the following keys: wave_xy : list of complex numpy arrays Waveforms for qubit XY control. wave_z : list of numpy arrays Waveforms for qubit Z control. wave_gate : list of numpy arrays Waveforms for gating qubit XY pulses. readout_trig : numpy array Waveform for triggering/gating qubit readout readout_iq : complex numpy array Waveform for readout IQ control """ # start by initializing the waveforms self.init_waveforms() # generate sequence self.generate_sequence(config) # add tomography self.add_tomography_pulses() # collapse all xy pulses to one waveform if no local XY control if not self.local_xy: # sum all waveforms to first one self.wave_xy[0] = np.sum(self.wave_xy[:self.n_qubit], 0) # clear other waveforms for n in range(1, self.n_qubit): self.wave_xy[n][:] = 0.0 # cross-talk compensation self.perform_crosstalk_compensation() # read-out signals self.generate_readout() # trim waveforms, if wanted self.trim_waveforms() # microwave gate switch waveform self.add_microwave_gate(config) # I/Q waveform predistortion self.predistort_waveforms() # create and return dictionary with waveforms data = dict() data['wave_xy'] = self.wave_xy data['wave_z'] = self.wave_z data['wave_gate'] = self.wave_gate data['readout_trig'] = self.readout_trig data['readout_iq'] = self.readout_iq return data def add_single_pulse(self, qubit, pulse, t0, align_left=False): """Add single pulse to specified qubit waveform Parameters ---------- qubit : int or numpy array Qubit number, indexed from 0. If a numpy array is given, pulses will be added to the specified waveform instead of the qubit waveform. pulse : :obj:`Pulse` Definition of pulse to add. t0 : float Pulse position, referenced to center of pulse. align_left : bool, optional If True, the pulse position is referenced to the left edge of the pulse, otherwise to the center. Default is False. """ # find waveform to add pulse to if isinstance(qubit, np.ndarray): waveform = qubit else: if pulse.z_pulse: waveform = self.wave_z[qubit] else: waveform = self.wave_xy[qubit] # calculate total length of pulse duration = pulse.total_duration() # shift time to mid point if user gave start point if align_left: t0 = t0 + duration / 2 # get the range of indices in use indices = np.arange( max(np.floor((t0 - duration / 2) * self.sample_rate), 0), min(np.ceil((t0 + duration / 2) * self.sample_rate), self.n_pts), dtype=int ) # return directly if no indices if len(indices) == 0: return # calculate time values for the pulse indices t = indices / self.sample_rate # calculate the pulse envelope for the selected indices y = pulse.calculate_envelope(t0, t) # proceed depending on Z- or XY gate if pulse.z_pulse or waveform.dtype != np.complex: # Z pulse, add directly to Z waveform waveform[indices] += y else: # XY pulse, apply DRAG, if wanted if pulse.use_drag: beta = pulse.drag_coefficient * self.sample_rate y = y + 1j * beta * np.gradient(y) # single-sideband mixing, get frequency omega = 2 * np.pi * pulse.frequency # apply SSBM transform data_i = (y.real * np.cos(omega * t - pulse.phase) + -y.imag * np.cos(omega * t - pulse.phase + np.pi / 2)) data_q = (y.real * np.sin(omega * t - pulse.phase) + -y.imag * np.sin(omega * t - pulse.phase + np.pi / 2)) # # apply SSBM transform # data_i = (-y.real * np.sin(omega * t - pulse.phase) + # y.imag * np.sin(omega * t - pulse.phase + np.pi / 2)) # data_q = (y.real * np.cos(omega * t - pulse.phase) + # -y.imag * np.cos(omega * t - pulse.phase + np.pi / 2)) # store result waveform[indices] += (data_i + 1j * data_q) def add_single_gate(self, qubit, gate, t0, align_left=False): """Add single gate to specified qubit waveform Parameters ---------- qubit : int Qubit number, indexed from 0. gate : :enum:`Gate` Definition of gate to add. t0 : float Pulse position, referenced to center of pulse. align_left : bool, optional If True, the pulse position is referenced to the left edge of the pulse, otherwise to the center. Default is False. """ # check if one- or two-qubit gate if gate in ONE_QUBIT_GATES: # get copy of pulse to use pulse = copy(self.pulses_1qb[qubit]) # scale pulse by 0.5 if pi/2 if gate in (Gate.X2p, Gate.Y2p, Gate.X2m, Gate.Y2m): pulse.amplitude *= 0.5 # rotate by 90 deg if pulse is in Y if gate in (Gate.Yp, Gate.Y2p, Gate.Ym, Gate.Y2m): pulse.phase += np.pi / 2 # negate if negative pulse if gate in (Gate.Xm, Gate.X2m, Gate.Ym, Gate.Y2m): pulse.amplitude = -pulse.amplitude if gate is (Gate.I): pulse.amplitude = 0 # add pulse to waveform self.add_single_pulse(qubit, pulse, t0, align_left=align_left) else: # two-qubit gate, get pulse pulse = copy(self.pulses_2qb[qubit]) # add pulse to waveform self.add_single_pulse(qubit, pulse, t0, align_left=align_left) # TODO (simon): Update two-qubit pulse to include phase correction, # compensation pulses to neighboring qubits, etc. def add_gates(self, gates): """Add multiple gates to qubit waveforms Add multiple gates to the qubit waveform. Pulses are added to the end of the sequence, with gate period set by single- and two-qubit gate period parameters. Examples -------- Add three gates to a two-qubit sequence, first a positive pi-pulse around X to qubit 1, then a negative pi/2-pulse to qubit 2, finally simultaneous positive pi-pulses to qubits 1 and 2. >>> add_gates([[Gate.Xp, None ], [None, Gate.Y2m], [Gate.Xp, Gate.Xp]]) Parameters ---------- gates : list of list of :enum:`gate` List of lists defining gates to add. The innermost list should have the same length as number of qubits in the sequence. """ # make sure we have correct input if not isinstance(gates, (list, tuple)): raise Exception('The input must be a list of list with gates') if len(gates) == 0: return if not isinstance(gates[0], (list, tuple)): raise Exception('The input must be a list of list with gates') # add gates sequence to waveforms for gates_qubits in gates: # check if any two-qubit gates two_qubit = np.any([g in TWO_QUBIT_GATES for g in gates_qubits]) # pulse period may be different for two-qubi gates period = self.period_2qb if two_qubit else self.period_1qb # go through all qubits for n, g in enumerate(gates_qubits): # ignore if gate is None if g is None: continue # add gate to specific qubit waveform self.add_single_gate(n, g, t0=self.time_pulse + period / 2.0) # after adding all pulses, increment current gate time self.time_pulse += period def add_tomography_pulses(self): """Add tomography pulses to the end of the qubit xy waveforms """ if not self.perform_tomography: return # get time for adding tomograph pulse t = self.find_range_of_sequence()[1] # TODO(morten): add code to add tomography pulses self.tomography.add_pulses(self, t) def predistort_waveforms(self): """Add tomography pulses to the end of the qubit xy waveforms """ if not self.perform_predistortion: return # go through and predistort all waveforms n_wave = self.n_qubit if self.local_xy else 1 for n in range(n_wave): self.wave_xy[n] = self.predistortions[n].predistort(self.wave_xy[n]) def perform_crosstalk_compensation(self): """Compensate for Z-control crosstalk """ if not self.compensate_crosstalk: return self.wave_z = self.crosstalk.compensate(self.wave_z) def find_range_of_sequence(self): """Find and return time at start and end of gate sequence Returns ------- times : list List with two elements - Time at start and end of sequence. """ # disable check based on pulses, always check actual waveforms if False: # self.time_pulse > self.first_delay: # if pulses have been added with add_gates, get from pulse period t0 = self.first_delay - self.period_1qb t1 = self.time_pulse else: # find end by searching for last non-zero element sum_all = np.zeros_like(self.wave_xy[0]) for n in range(self.n_qubit): sum_all += np.abs(self.wave_xy[n]) sum_all += np.abs(self.wave_z[n]) non_zero = np.where(sum_all > self.readout_noise)[0] # if data is not all zero, add after last pulse if len(non_zero) > 0: t0 = max(0.0, (non_zero[0] - 1) / self.sample_rate) t1 = (non_zero[-1] + 1) / self.sample_rate else: t0 = 0.0 t1 = len(sum_all) / self.sample_rate return [t0, t1] def generate_readout(self): """Create read-out trig and waveform signals """ # get positon of readout if self.generate_readout_trig or self.generate_readout_iq: t = self.find_range_of_sequence()[1] + self.readout_delay i0 = int(round(t * self.sample_rate)) # start with readout trig signal if self.generate_readout_trig: # create trig waveform directly i1 = min(int(round((t + self.readout_duration) * self.sample_rate)), len(self.readout_trig) - 1) self.readout_trig[i0:i1] = self.readout_amplitude # # create pulse object and insert into trig waveform # trig = Pulse(amplitude=self.readout_amplitude, # width=0.0, # plateau=self.readout_duration, # shape=PulseShape.SQUARE) # self.add_single_pulse(self.readout_trig, trig, t, align_left=True) # readout I/Q waveform if self.generate_readout_iq: # ignore readout timestamp if pulses are aligned to end of waveform if self.align_to_end: wave = self.readout.create_waveform(t_start=0.0) else: wave = self.readout.create_waveform(t_start=t) # if not matching wave sizes, simply replace initialized waveform if not self.readout.match_main_size: self.readout_iq = wave else: i1 = min(len(self.readout_iq), i0 + len(wave)) self.readout_iq[i0:i1] = wave[:(i1 - i0)] # add IQ offsets self.readout_iq.real += self.i_offset self.readout_iq.imag += self.q_offset def add_microwave_gate(self, config): """Create waveform for gating microwave switch """ if not self.generate_gate_switch: return n_wave = self.n_qubit if self.local_xy else 1 # go through all waveforms for n, wave in enumerate(self.wave_xy[:n_wave]): if self.uniform_gate: # the uniform gate is all ones gate = np.ones_like(wave) # if creating readout trig, turn off gate during readout if self.generate_readout_trig: gate[-int((self.readout_duration - self.gate_overlap - self.gate_delay) * self.sample_rate):] = 0.0 else: # non-uniform gate, find non-zero elements gate = np.array(np.abs(wave) > 0.0, dtype=float) # fix gate overlap n_overlap = int(np.round(self.gate_overlap * self.sample_rate)) diff_gate = np.diff(gate) indx_up = np.nonzero(diff_gate > 0.0)[0] indx_down = np.nonzero(diff_gate < 0.0)[0] # add extra elements to left and right for overlap for indx in indx_up: gate[max(0, indx - n_overlap):(indx + 1)] = 1.0 for indx in indx_down: gate[indx:(indx + n_overlap + 1)] = 1.0 # fix gaps in gate shorter than min (look for 1>0) diff_gate = np.diff(gate) indx_up = np.nonzero(diff_gate > 0.0)[0] indx_down = np.nonzero(diff_gate < 0.0)[0] # ignore first transition if starting in zero if gate[0] == 0: indx_up = indx_up[1:] n_down_up = min(len(indx_down), len(indx_up)) len_down = indx_up[:n_down_up] - indx_down[:n_down_up] # find short gaps short_gaps = np.nonzero(len_down < (self.minimal_gate_time * self.sample_rate))[0] for indx in short_gaps: gate[indx_down[indx]:(1 + indx_up[indx])] = 1.0 # shift gate in time n_shift = int(np.round(self.gate_delay * self.sample_rate)) if n_shift < 0: n_shift = abs(n_shift) gate = np.r_[gate[n_shift:], np.zeros((n_shift,))] elif n_shift > 0: gate = np.r_[np.zeros((n_shift,)), gate[:(-n_shift)]] # make sure gate starts/ends in 0 gate[0] = 0.0 gate[-1] = 0.0 # store results self.wave_gate[n] = gate def trim_waveforms(self): """Trim waveforms to match length of sequence """ if not (self.trim_to_sequence or self.align_to_end): return # find range of sequence (t0, t1) = self.find_range_of_sequence() # don't trim past extent of microwave gate and readout trig, if in use dt_start = 0.0 dt_end = 0.0 if self.generate_gate_switch: dt_start = min(0.0, (self.gate_delay - self.gate_overlap)) dt_end = max(0.0, (self.gate_delay + self.gate_overlap)) if self.generate_readout_trig: # add a few extra points, to ensure read-out trig doesn't end high dt_end = max(dt_end, self.readout_delay + self.readout_duration + 1.0 / self.sample_rate) # same thing for I/Q readout if self.generate_readout_iq and self.readout.match_main_size: dt_end = max(dt_end, self.readout_delay + self.readout.duration + 1.0 / self.sample_rate) t0 += dt_start t1 += dt_end # get indices for start/end i0 = max(0, int(np.floor(t0 * self.sample_rate))) # check if don't trim beginning of waveform if not self.trim_start: i0 = 0 i1 = min(self.n_pts, int(np.ceil(t1 * self.sample_rate))) if self.align_to_end: # align pulses to end of waveform m = self.n_pts - (i1 - i0) for n in range(self.n_qubit): self.wave_xy[n] = np.r_[np.zeros(m), self.wave_xy[n][i0:i1]] self.wave_z[n] = np.r_[np.zeros(m), self.wave_z[n][i0:i1]] self.wave_gate[n] = np.r_[np.zeros(m), self.wave_gate[n][i0:i1]] if self.generate_readout_trig: self.readout_trig = np.r_[np.zeros(m), self.readout_trig[i0:i1]] # force readout trig to end in zero self.readout_trig[-1] = 0.0 if self.generate_readout_iq and self.readout.match_main_size: self.readout_iq = np.r_[np.zeros(m), self.readout_iq[i0:i1]] else: # trim waveforms for n in range(self.n_qubit): self.wave_xy[n] = self.wave_xy[n][i0:i1] self.wave_z[n] = self.wave_z[n][i0:i1] self.wave_gate[n] = self.wave_gate[n][i0:i1] if self.generate_readout_trig: self.readout_trig = self.readout_trig[i0:i1] # force readout trig to end in zero self.readout_trig[-1] = 0.0 if self.generate_readout_iq and self.readout.match_main_size: self.readout_iq = self.readout_iq[i0:i1] def set_parameters(self, config={}): """Set base parameters using config from from Labber driver Parameters ---------- config : dict Configuration as defined by Labber driver configuration window """ # sequence parameters d = dict(Zero=0, One=1, Two=2, Three=3, Four=4, Five=5, Six=6, Seven=7, Eight=8, Nine=9) self.n_qubit = d[config.get('Number of qubits')] self.period_1qb = config.get('Pulse period, 1-QB') self.period_2qb = config.get('Pulse period, 2-QB') self.local_xy = config.get('Local XY control') # waveform parameters self.sample_rate = config.get('Sample rate') self.readout_noise = 0.0 self.n_pts = int(config.get('Number of points')) self.first_delay = config.get('First pulse delay') self.trim_to_sequence = config.get('Trim waveform to sequence') self.trim_start = config.get('Trim both start and end') self.align_to_end = config.get('Align pulses to end of waveform') # single-qubit pulses for n, pulse in enumerate(self.pulses_1qb): # pulses are indexed from 1 in Labber m = n + 1 # global parameters pulse.shape = PulseShape(config.get('Pulse type')) pulse.truncation_range = config.get('Truncation range') pulse.start_at_zero = config.get('Start at zero') pulse.use_drag = config.get('Use DRAG') # pulse shape if config.get('Uniform pulse shape'): pulse.width = config.get('Width') pulse.plateau = config.get('Plateau') else: pulse.width = config.get('Width #%d' % m) pulse.plateau = config.get('Plateau #%d' % m) # pulse-specific parameters pulse.amplitude = config.get('Amplitude #%d' % m) pulse.frequency = config.get('Frequency #%d' % m) pulse.drag_coefficient = config.get('DRAG scaling #%d' % m) # two-qubit pulses for n, pulse in enumerate(self.pulses_2qb): # pulses are indexed from 1 in Labber s = ' #%d%d' % (n + 1, n + 2) # global parameters pulse.shape = PulseShape(config.get('Pulse type, 2QB')) pulse.z_pulse = True if config.get('Pulse type, 2QB') == 'CZ': pulse.F_Terms = d[config.get('Fourier terms, 2QB')] if config.get('Uniform 2QB pulses'): pulse.width = config.get('Width, 2QB') pulse.plateau = config.get('Plateau, 2QB') else: pulse.width = config.get('Width, 2QB' + s) pulse.plateau = config.get('Plateau, 2QB') # Get Fourier values if d[config.get('Fourier terms, 2QB')] == 4 : pulse.Lcoeff = np.array([config.get('L1, 2QB' + s),config.get('L2, 2QB' + s),config.get('L3, 2QB' + s),config.get('L4, 2QB' + s)]) elif d[config.get('Fourier terms, 2QB')] == 3 : pulse.Lcoeff = np.array([config.get('L1, 2QB' + s),config.get('L2, 2QB' + s),config.get('L3, 2QB' + s)]) elif d[config.get('Fourier terms, 2QB')] == 2 : pulse.Lcoeff = np.array([config.get('L1, 2QB' + s),config.get('L2, 2QB' + s)]) elif d[config.get('Fourier terms, 2QB')] == 1 : pulse.Lcoeff = np.array([config.get('L1, 2QB' + s)]) pulse.Coupling = config.get('Coupling, 2QB' + s) pulse.Offset = config.get('f11-f20 initial, 2QB' + s) pulse.amplitude = config.get('f11-f20 final, 2QB' + s) pulse.dfdV = config.get('df/dV, 2QB' + s) else: pulse.truncation_range = config.get('Truncation range, 2QB') pulse.start_at_zero = config.get('Start at zero, 2QB') # pulse shape if config.get('Uniform 2QB pulses'): pulse.width = config.get('Width, 2QB') pulse.plateau = config.get('Plateau, 2QB') else: pulse.width = config.get('Width, 2QB' + s) pulse.plateau = config.get('Plateau, 2QB' + s) # pulse-specific parameters pulse.amplitude = config.get('Amplitude, 2QB' + s) # tomography self.perform_tomography = config.get('Generate tomography pulse', False) self.tomography.set_parameters(config) # predistortion self.perform_predistortion = config.get('Predistort waveforms', False) # update all predistorting objects for p in self.predistortions: p.set_parameters(config) # crosstalk self.compensate_crosstalk = config.get('Compensate cross-talk', False) self.crosstalk.set_parameters(config) # gate switch waveform self.generate_gate_switch = config.get('Generate gate') self.uniform_gate = config.get('Uniform gate') self.gate_delay = config.get('Gate delay') self.gate_overlap = config.get('Gate overlap') self.minimal_gate_time = config.get('Minimal gate time') # readout, trig settings self.generate_readout_trig = config.get('Generate readout trig') self.readout_delay = config.get('Readout delay') self.readout_amplitude = config.get('Readout trig amplitude') self.readout_duration = config.get('Readout trig duration') self.iq_skew = config.get('Readout IQ skew') self.i_offset = config.get('Readout offset - I') self.q_offset = config.get('Readout offset - Q') # readout, wave settings self.generate_readout_iq = config.get('Generate readout waveform') self.readout.set_parameters(config)