Source code for qutip_qip.compiler.gatecompiler

import warnings
import numpy as np
from scipy import signal

from .instruction import Instruction
from .scheduler import Scheduler
from ..circuit import QubitCircuit
from ..operations import Gate


__all__ = ["GateCompiler"]


[docs]class GateCompiler(object): """ Base class of compilers, including the :meth:`GateCompiler.compile` method. It compiles a :class:`.QubitCircuit` into the pulse sequence for the processor. The core member function `compile` calls compiling method from the sub-class and concatenate the compiled pulses. Parameters ---------- num_qubits: int The number of the component systems. params: dict, optional A Python dictionary contains the name and the value of the parameters, such as laser frequency, detuning etc. It will be saved in the class attributes and can be used to calculate the control pulses. Attributes ---------- gate_compiler: dict The Python dictionary in the form of {gate_name: compiler_function}. It saves the compiling routine for each gate. See sub-classes for implementation. Note that for continuous pulse, the first coeff should always be 0. args: dict The compilation configurations. It will be passed to each compiling functions. Available arguments: * ``shape``: The compiled pulse shape. ``rectangular`` or one of the `SciPy window functions <https://docs.scipy.org/doc/scipy/reference/signal.windows.html>`_. * ``num_samples``: Number of samples for continuous pulses. It has no effect for rectangular pulses. * ``params``: Hardware parameters computed in the :obj:`Processor`. """ def __init__(self, num_qubits=None, params=None, pulse_dict=None, N=None): self.gate_compiler = {} self.num_qubits = num_qubits or N self.N = num_qubits # backward compatibility self.params = params if params is not None else {} self.gate_compiler = { "GLOBALPHASE": self.globalphase_compiler, "IDLE": self.idle_compiler, } self.args = { # Default configuration "shape": "rectangular", "num_samples": None, "params": self.params, } self.global_phase = 0.0 if pulse_dict is not None: warnings.warn( """ Giving pulse_dict to compiler is deprecated. The compiler now returns the compiled pulses as a dictionary between the pulse's label and the coefficients/tlist. It can be given to the processor directly. The parameter pulse_dict has no effect now, you can simply remove it. """, DeprecationWarning, )
[docs] def globalphase_compiler(self, gate, args): """ Compiler for the GLOBALPHASE gate """ pass
[docs] def idle_compiler(self, gate, args): """ Compiler for the GLOBALPHASE gate """ idle_time = gate.arg_value return [Instruction(gate, idle_time, [])]
[docs] def compile(self, circuit, schedule_mode=None, args=None): """ Compile the the native gates into control pulse sequence. It calls each compiling method and concatenates the compiled pulses. Parameters ---------- circuit: :class:`.QubitCircuit` or list of :class:`~.operations.Gate` A list of elementary gates that can be implemented in the corresponding hardware. The gate names have to be in `gate_compiler`. schedule_mode: str, optional ``"ASAP"`` for "as soon as possible" or ``"ALAP"`` for "as late as possible" or ``False`` or ``None`` for no schedule. Default is None. args: dict, optional A dictionary of arguments used in a specific gate compiler function. Returns ------- tlist, coeffs: array_like or dict Compiled ime sequence and pulse coefficients. if ``return_array`` is true, return A 2d NumPy array of the shape ``(len(ctrls), len(tlist))``. Each row corresponds to the control pulse sequence for one Hamiltonian. if ``return_array`` is false """ if isinstance(circuit, QubitCircuit): gates = circuit.gates else: gates = circuit if args is not None: self.args.update(args) instruction_list = [] # compile gates for gate in gates: if gate.name not in self.gate_compiler: raise ValueError("Unsupported gate %s" % gate.name) instruction = self.gate_compiler[gate.name](gate, self.args) if instruction is None: continue # neglecting global phase gate instruction_list += instruction if not instruction_list: return None, None # schedule # scheduled_start_time: # An ordered list of the start_time for each pulse, # corresponding to gates in the instruction_list. # instruction_list reordered according to the scheduled result instruction_list, scheduled_start_time = self._schedule( instruction_list, schedule_mode ) # An instruction can be composed from several different pulse elements. # We separate them an assign them to each pulse index. pulse_ind_map = {} next_pulse_ind = 0 pulse_instructions = [] for instruction, start_time in zip( instruction_list, scheduled_start_time ): for pulse_name, coeff in instruction.pulse_info: if pulse_name not in pulse_ind_map: pulse_instructions.append([]) pulse_ind_map[pulse_name] = next_pulse_ind next_pulse_ind += 1 pulse_instructions[pulse_ind_map[pulse_name]].append( (start_time, instruction.tlist, coeff) ) # concatenate pulses compiled_tlist, compiled_coeffs = self._concatenate_pulses( pulse_instructions, scheduled_start_time, len(pulse_instructions) ) compiled_tlist_map, compiled_coeffs_map = {}, {} for key, index in pulse_ind_map.items(): compiled_tlist_map[key] = compiled_tlist[index] compiled_coeffs_map[key] = compiled_coeffs[index] return compiled_tlist_map, compiled_coeffs_map
def _schedule(self, instruction_list, schedule_mode): """ Schedule the instructions if required and reorder instruction_list accordingly """ if schedule_mode: scheduler = Scheduler(schedule_mode) scheduled_start_time = scheduler.schedule(instruction_list) time_ordered_pos = np.argsort(scheduled_start_time) instruction_list = [instruction_list[i] for i in time_ordered_pos] scheduled_start_time.sort() else: # no scheduling scheduled_start_time = [0.0] for instruction in instruction_list[:-1]: scheduled_start_time.append( instruction.duration + scheduled_start_time[-1] ) return instruction_list, scheduled_start_time def _concatenate_pulses( self, pulse_instructions, scheduled_start_time, num_controls ): """ Concatenate compiled pulses coefficients and tlist for each pulse. If there is idling time, add zeros properly to prevent wrong spline. """ min_step_size = np.inf # Concatenate tlist and coeffs for each control pulses compiled_tlist = [[] for tmp in range(num_controls)] compiled_coeffs = [[] for tmp in range(num_controls)] for pulse_ind in range(num_controls): last_pulse_time = 0.0 for start_time, tlist, coeff in pulse_instructions[pulse_ind]: # compute the gate time, step size and coeffs # according to different pulse mode ( gate_tlist, coeffs, step_size, pulse_mode, ) = self._process_gate_pulse(start_time, tlist, coeff) min_step_size = min(step_size, min_step_size) if abs(last_pulse_time) < step_size * 1.0e-6: # if first pulse compiled_tlist[pulse_ind].append([0.0]) if pulse_mode == "continuous": compiled_coeffs[pulse_ind].append([0.0]) # for discrete pulse len(coeffs) = len(tlist) - 1 # If there is idling time between the last pulse and # the current one, we need to add zeros in between. if np.abs(start_time - last_pulse_time) > step_size * 1.0e-6: idling_tlist = self._process_idling_tlist( pulse_mode, start_time, last_pulse_time, step_size ) compiled_tlist[pulse_ind].append(idling_tlist) compiled_coeffs[pulse_ind].append( np.zeros(len(idling_tlist)) ) # Add the gate time and coeffs to the list. execution_time = gate_tlist + start_time last_pulse_time = execution_time[-1] compiled_tlist[pulse_ind].append(execution_time) compiled_coeffs[pulse_ind].append(coeffs) final_time = np.max([tlist[-1][-1] for tlist in compiled_tlist]) for pulse_ind in range(num_controls): if not compiled_tlist[pulse_ind]: continue last_pulse_time = compiled_tlist[pulse_ind][-1][-1] if np.abs(final_time - last_pulse_time) > min_step_size * 1.0e-6: idling_tlist = self._process_idling_tlist( pulse_mode, final_time, last_pulse_time, min_step_size ) compiled_tlist[pulse_ind].append(idling_tlist) compiled_coeffs[pulse_ind].append(np.zeros(len(idling_tlist))) for i in range(num_controls): if not compiled_coeffs[i]: compiled_tlist[i] = None compiled_coeffs[i] = None else: compiled_tlist[i] = np.concatenate(compiled_tlist[i]) compiled_coeffs[i] = np.concatenate(compiled_coeffs[i]) return compiled_tlist, compiled_coeffs def _process_gate_pulse(self, start_time, tlist, coeff): # compute the gate time, step size and coeffs # according to different pulse mode if np.isscalar(tlist): pulse_mode = "discrete" # a single constant rectanglar pulse, where # tlist and coeff are just float numbers step_size = tlist coeff = np.array([coeff]) gate_tlist = np.array([tlist]) elif len(tlist) - 1 == len(coeff): # discrete pulse pulse_mode = "discrete" step_size = tlist[1] - tlist[0] coeff = np.asarray(coeff) gate_tlist = np.asarray(tlist)[1:] # first t always 0 by def elif len(tlist) == len(coeff): # continuos pulse pulse_mode = "continuous" step_size = tlist[1] - tlist[0] coeff = np.asarray(coeff)[1:] gate_tlist = np.asarray(tlist)[1:] else: raise ValueError("The shape of the compiled pulse is not correct.") return gate_tlist, coeff, step_size, pulse_mode def _process_idling_tlist( self, pulse_mode, start_time, last_pulse_time, step_size ): idling_tlist = [] if pulse_mode == "continuous": # We add sufficient number of zeros at the beginning # and the end of the idling to prevent wrong cubic spline. if start_time - last_pulse_time > 3 * step_size: idling_tlist1 = np.linspace( last_pulse_time + step_size / 5, last_pulse_time + step_size, 10, ) idling_tlist2 = np.linspace( start_time - step_size, start_time, 10 ) idling_tlist.extend([idling_tlist1, idling_tlist2]) else: idling_tlist.append( np.arange( last_pulse_time + step_size, start_time, step_size ) ) elif pulse_mode == "discrete": # idling until the start time idling_tlist.append([start_time]) return np.concatenate(idling_tlist)
[docs] @classmethod def generate_pulse_shape(cls, shape, num_samples, maximum=1.0, area=1.0): """ Return a tuple consisting of a coeff list and a time sequence according to a given pulse shape. Parameters ---------- shape : str The name ``"rectangular"`` for constant pulse or the name of a Scipy window function. See `the Scipy documentation <https://docs.scipy.org/doc/scipy/reference/signal.windows.html>`_ for detail. num_samples : int The number of the samples of the coefficients. maximum : float, optional The maximum of the coefficients. The absolute value will be used if negative. area : float, optional The total area if one integrates coeff as a function of the time. If the area is negative, the pulse is flipped vertically (i.e. the pulse is multiplied by the sign of the area). Returns ------- coeff, tlist : If the default window ``"shape"="rectangular"`` is used, both are float numbers. If Scipy window functions are used, both are a 1-dimensional numpy array with the same size. Notes ----- If Scipy window functions are used, it is suggested to set ``Processor.pulse_mode`` to ``"continuous"``. Notice that finite number of sampling points will also make the total integral of the coefficients slightly deviate from ``area``. Examples -------- .. plot:: :context: reset from qutip_qip.compiler import GateCompiler import numpy as np compiler = GateCompiler() coeff, tlist= compiler.generate_pulse_shape( "hann", # Scipy Hann window 1000, # 100 sampling point maximum=3., # Notice that 2 pi is added to H by qutip solvers. area= 1., ) We can plot the generated pulse shape: .. plot:: :context: close-figs import matplotlib.pyplot as plt plt.plot(tlist, coeff) plt.show() The pulse is normalized to fit the area. Notice that due to the finite number of sampling points, it is not exactly 1. .. testsetup:: from qutip_qip.compiler import GateCompiler import numpy as np compiler = GateCompiler() coeff, tlist= compiler.generate_pulse_shape( "hann", # Scipy Hann window 1000, # 100 sampling point maximum=3., # Notice that 2 pi is added to H by qutip solvers. area= 1., ) .. doctest:: >>> round(np.trapz(coeff, tlist), 2) 1.0 """ coeff, tlist = _normalized_window(shape, num_samples) sign = np.sign(area) coeff *= np.abs(maximum) * sign tlist *= abs(area) / np.abs(maximum) return coeff, tlist
_default_window_t_max = { "boxcar": 1.0, "triang": 2.0, "blackman": 1.0 / 0.42, "hamming": 1.0 / 0.54, "hann": 2.0, "bartlett": 2.0, "flattop": 1.0 / 0.21557897160000217, "parzen": 1.0 / 0.375, "bohman": 1.0 / 0.4052847750978287, "blackmanharris": 1.0 / 0.35875003586900384, "nuttall": 1.0 / 0.36358193632191405, "barthann": 2.0, "cosine": np.pi / 2.0, } # Analytically implementing the pulse shape because the Scipy version # subjects more to the finite sampling error under interpolation. # More analytical shape can be added here. _analytical_window = { "hann": lambda t: 1 / 2 - 1 / 2 * np.cos(np.pi * t), "hamming": lambda t: 0.54 - 0.46 * np.cos(np.pi * t * 2 * 0.54), } def _normalized_window(shape, num_samples): """ Normalized SciPy window functions. The SciPy implementation only makes sure that it is maximum is 1. Here, we save a default t_max so that the integral is always 1. """ if shape == "rectangular": return 1.0, 1.0 t_max = _default_window_t_max.get(shape, None) if t_max is None: raise ValueError(f"Window function {shape} is not supported.") tlist = np.linspace(0, t_max, num_samples) if shape in _analytical_window: coeff = _analytical_window[shape](tlist) else: coeff = signal.windows.get_window(shape, num_samples) return coeff, tlist