From 6c770310e8e071b857ce02f6e5144184e644971d Mon Sep 17 00:00:00 2001 From: Boxi Li Date: Wed, 18 Aug 2021 15:02:04 +0200 Subject: [PATCH 01/12] Add pulse shape Different pulse shapes are generated using window function from scipy.signal.windows. A generate_pulse_shape method is defined and can be used to generate pulse shape with the arbitrary area and required maximum. A default single qubit compiler is defined and simplifies the various compiler subclasses. --- doc/source/qip-processor.rst | 26 +++ src/qutip_qip/compiler/cavityqedcompiler.py | 155 +++++++++-------- src/qutip_qip/compiler/circuitqedcompiler.py | 73 +++++--- src/qutip_qip/compiler/gatecompiler.py | 169 ++++++++++++++++++- src/qutip_qip/compiler/spinchaincompiler.py | 109 +++++++----- tests/test_compiler.py | 19 +++ 6 files changed, 407 insertions(+), 144 deletions(-) diff --git a/doc/source/qip-processor.rst b/doc/source/qip-processor.rst index 934046ecc..2ff80563d 100644 --- a/doc/source/qip-processor.rst +++ b/doc/source/qip-processor.rst @@ -191,6 +191,9 @@ To let it find the optimal pulses, we need to give the parameters for :func:`~qu Compiler and scheduler ====================== +Compiler +-------- + In order to simulate quantum circuits at the level of time evolution. We need to first compile the circuit into the Hamiltonian model, i.e. the control pulses. @@ -236,6 +239,9 @@ The second one is turned on from ``t=1`` to ``t=2`` with the same strength. The compiled pulse here is different from what is shown in the plot in the previous subsection because the scheduler is turned off by default. +Scheduler +--------- + The scheduler is implemented in the class :class:`.compiler.Scheduler`, based on the idea of https://doi.org/10.1117/12.666419. It schedules the order of quantum gates and instructions for the @@ -287,6 +293,26 @@ one will need to wrap the :class:`.Gate` object with :class:`.compiler.instructi with a parameter `duration`. The result will then be the start time of each instruction. +Pulse shape +----------- +Apart from square pulses, compilers also support different pulse shapes. +All pulse shapes from `SciPy window functions `_ that does not require additional parameters are supported. +The method :obj:`.GateCompiler.generate_pulse_shape` allows one to generate pulse shapes that fulfil the given maximum intensity and the total integral area. + +.. plot:: + + from qutip_qip.compiler import GateCompiler + compiler = GateCompiler() + coeff, tlist = compiler.generate_pulse_shape( + "hann", 1000, maximum=2., area=1.) + fig, ax = plt.subplots(figsize=(4,2)) + ax.plot(tlist, coeff) + ax.set_xlabel("Time") + fig.show() + +For predefined compilers, the compiled pulse shape can also be configured by the key word ``"shape"`` and ``"num_samples"`` in the dictionary attribute :attr:`.GateCompiler.args` +or the ``args`` parameter of :obj:`.GateCompiler.compile`. + Noise Simulation ================ diff --git a/src/qutip_qip/compiler/cavityqedcompiler.py b/src/qutip_qip/compiler/cavityqedcompiler.py index ca593eb4b..11e7cec1b 100644 --- a/src/qutip_qip/compiler/cavityqedcompiler.py +++ b/src/qutip_qip/compiler/cavityqedcompiler.py @@ -1,4 +1,6 @@ +from functools import partial import numpy as np +import scipy from ..circuit import QubitCircuit from ..operations import Gate @@ -10,8 +12,22 @@ class CavityQEDCompiler(GateCompiler): """ - Decompose a :class:`.QubitCircuit` into - the pulse sequence for the processor. + Compiler for :obj:`.DispersiveCavityQED`. + Compiled pulse strength is in the unit of GHz. + + Supported native gates: "RX", "RY", "RZ", "ISWAP", "SQRTISWAP", + "GLOBALPHASE". + + Default configuration (see :obj:`.GateCompiler.args` and + :obj:`.GateCompiler.compile`): + + +-----------------+--------------------+ + | key | value | + +=================+====================+ + | ``shape`` | ``rectangular`` | + +-----------------+--------------------+ + |``params`` | Hardware Parameters| + +-----------------+--------------------+ Parameters ---------- @@ -39,52 +55,21 @@ def __init__( self.gate_compiler.update({ "ISWAP": self.iswap_compiler, "SQRTISWAP": self.sqrtiswap_compiler, - "RZ": self.rz_compiler, - "RX": self.rx_compiler, + "RZ": partial(self.default_single_qubit_compiler, scaling=0.5), + "RX": partial(self.default_single_qubit_compiler, scaling=0.5), "GLOBALPHASE": self.globalphase_compiler }) self.wq = np.sqrt(self.params["eps"]**2 + self.params["delta"]**2) self.Delta = self.wq - self.params["w0"] self.global_phase = global_phase - def rz_compiler(self, gate, args): - """ - Compiler for the RZ gate - """ - targets = gate.targets - g = self.params["sz"][targets[0]] - coeff = np.sign(gate.arg_value) * g - tlist = abs(gate.arg_value) / (2*g) / np.pi / 2 - pulse_info = [("sz" + str(targets[0]), coeff)] - return [Instruction(gate, tlist, pulse_info)] - - def rx_compiler(self, gate, args): - """ - Compiler for the RX gate - """ - targets = gate.targets - g = self.params["sx"][targets[0]] - coeff = np.sign(gate.arg_value) * g - tlist = abs(gate.arg_value) / (2*g) / np.pi / 2 - pulse_info = [("sx" + str(targets[0]), coeff)] - return [Instruction(gate, tlist, pulse_info)] - - def sqrtiswap_compiler(self, gate, args): - """ - Compiler for the SQRTISWAP gate - - Notes - ----- - This version of sqrtiswap_compiler has very low fidelity, please use - iswap - """ - # FIXME This decomposition has poor behaviour + def _two_qubit_compiler(self, gate_name, gate, args): q1, q2 = gate.targets pulse_info = [] pulse_name = "sz" + str(q1) coeff = self.wq[q1] - self.params["w0"] pulse_info += [(pulse_name, coeff)] - pulse_name = "sz" + str(q1) + pulse_name = "sz" + str(q2) coeff = self.wq[q2] - self.params["w0"] pulse_info += [(pulse_name, coeff)] pulse_name = "g" + str(q1) @@ -95,55 +80,75 @@ def sqrtiswap_compiler(self, gate, args): pulse_info += [(pulse_name, coeff)] J = self.params["g"][q1] * self.params["g"][q2] * ( - 1 / self.Delta[q1] + 1 / self.Delta[q2]) / 2 - tlist = (4 * np.pi / abs(J)) / 8 / np.pi / 2 + 1. / self.Delta[q1] + 1. / self.Delta[q2]) / 2. + if gate_name == "ISWAP": + area = 1. / 2. + correction_angle = -np.pi / 2. + elif gate_name == "SQRTISWAP": + area = 1. / 4. + correction_angle = -np.pi / 4. + else: + raise ValueError(f"Gate {gate.name} cannot not be compiled.") + coeff, tlist = self.generate_pulse_shape( + args["shape"], args["num_samples"], maximum=J, area=area) instruction_list = [Instruction(gate, tlist, pulse_info)] # corrections - gate1 = Gate("RZ", [q1], None, arg_value=-np.pi/4) - compiled_gate1 = self.rz_compiler(gate1, args) + gate1 = Gate("RZ", [q1], None, arg_value=correction_angle) + compiled_gate1 = self.gate_compiler["RZ"](gate1, args) instruction_list += compiled_gate1 - gate2 = Gate("RZ", [q2], None, arg_value=-np.pi/4) - compiled_gate2 = self.rz_compiler(gate2, args) + gate2 = Gate("RZ", [q2], None, arg_value=correction_angle) + compiled_gate2 = self.gate_compiler["RZ"](gate2, args) instruction_list += compiled_gate2 - gate3 = Gate("GLOBALPHASE", None, None, arg_value=-np.pi/4) + gate3 = Gate("GLOBALPHASE", None, None, arg_value=correction_angle) self.globalphase_compiler(gate3, args) return instruction_list - def iswap_compiler(self, gate, args): - """ - Compiler for the ISWAP gate + def sqrtiswap_compiler(self, gate, args): """ - q1, q2 = gate.targets - pulse_info = [] - pulse_name = "sz" + str(q1) - coeff = self.wq[q1] - self.params["w0"] - pulse_info += [(pulse_name, coeff)] - pulse_name = "sz" + str(q2) - coeff = self.wq[q2] - self.params["w0"] - pulse_info += [(pulse_name, coeff)] - pulse_name = "g" + str(q1) - coeff = self.params["g"][q1] - pulse_info += [(pulse_name, coeff)] - pulse_name = "g" + str(q2) - coeff = self.params["g"][q2] - pulse_info += [(pulse_name, coeff)] + Compiler for the SQRTISWAP gate. + + Parameters + ---------- + gate : :obj:`.Gate`: + The quantum gate to be compiled. + args : dict + The compilation configuration defined in the attributes + :obj:`.GateCompiler.args` or given as a parameter in + :obj:`.GateCompiler.compile`. + + Returns + ------- + A list of :obj:`.Instruction`, including the compiled pulse + information for this gate. - J = self.params["g"][q1] * self.params["g"][q2] * ( - 1 / self.Delta[q1] + 1 / self.Delta[q2]) / 2 - tlist = (4 * np.pi / abs(J)) / 4 / np.pi / 2 - instruction_list = [Instruction(gate, tlist, pulse_info)] + Notes + ----- + This version of sqrtiswap_compiler has very low fidelity, please use + iswap + """ + # FIXME This decomposition has poor behaviour. + return self._two_qubit_compiler("SQRTISWAP", gate, args) - # corrections - gate1 = Gate("RZ", [q1], None, arg_value=-np.pi/2.) - compiled_gate1 = self.rz_compiler(gate1, args) - instruction_list += compiled_gate1 - gate2 = Gate("RZ", [q2], None, arg_value=-np.pi/2) - compiled_gate2 = self.rz_compiler(gate2, args) - instruction_list += compiled_gate2 - gate3 = Gate("GLOBALPHASE", None, None, arg_value=-np.pi/2) - self.globalphase_compiler(gate3, args) - return instruction_list + def iswap_compiler(self, gate, args): + """ + Compiler for the ISWAP gate. + + Parameters + ---------- + gate : :obj:`.Gate`: + The quantum gate to be compiled. + args : dict + The compilation configuration defined in the attributes + :obj:`.GateCompiler.args` or given as a parameter in + :obj:`.GateCompiler.compile`. + + Returns + ------- + A list of :obj:`.Instruction`, including the compiled pulse + information for this gate. + """ + return self._two_qubit_compiler("ISWAP", gate, args) def globalphase_compiler(self, gate, args): """ diff --git a/src/qutip_qip/compiler/circuitqedcompiler.py b/src/qutip_qip/compiler/circuitqedcompiler.py index 32af4f5e3..f9f09b3ed 100644 --- a/src/qutip_qip/compiler/circuitqedcompiler.py +++ b/src/qutip_qip/compiler/circuitqedcompiler.py @@ -1,3 +1,4 @@ +from functools import partial import numpy as np from ..circuit import Gate @@ -11,14 +12,36 @@ class SCQubitsCompiler(GateCompiler): """ Compiler for :class:`.SCQubits`. Compiled pulse strength is in the unit of GHz. + + Supported native gates: "RX", "RY", "CNOT". + + Default configuration (see :obj:`.GateCompiler.args` and + :obj:`.GateCompiler.compile`): + + +-----------------+-----------------------+ + | key | value | + +=================+=======================+ + | ``shape`` | ``hann`` | + +-----------------+-----------------------+ + |``num_samples`` | 1000 | + +-----------------+-----------------------+ + |``params`` | Hardware Parameters | + +-----------------+-----------------------+ """ def __init__(self, num_qubits, params): super(SCQubitsCompiler, self).__init__(num_qubits, params=params) self.gate_compiler.update({ - "RX": self.single_qubit_compiler, - "RY": self.single_qubit_compiler, + "RX": partial( + self.default_single_qubit_compiler, param_name="omega_single"), + "RY": partial( + self.default_single_qubit_compiler, param_name="omega_single"), "CNOT": self.cnot_compiler, }) + self.args = { # Default configuration + "shape": "hann", + "num_samples": 1000, + "params": self.params, + } def _normalized_gauss_pulse(self): """ @@ -32,37 +55,41 @@ def _normalized_gauss_pulse(self): # td normalization so that the total integral area is 1 td = 2.4384880692912567 sigma = 1/6 * td # 3 sigma - tlist = np.linspace(0, td, 100) + tlist = np.linspace(0, td, 1000) max_pulse = 1 - np.exp(-(0-td/2)**2/2/sigma**2) - coeff = (np.exp(-(tlist-td/2)**2/2/sigma**2) - - np.exp(-(0-td/2)**2/2/sigma**2)) / max_pulse + coeff = ( + np.exp(-(tlist-td/2)**2/2/sigma**2) + - np.exp(-(0-td/2)**2/2/sigma**2) + ) / max_pulse return tlist, coeff def single_qubit_compiler(self, gate, args): """ Compiler for the RX and RY gate. """ - targets = gate.targets - omega_single = self.params["omega_single"][targets[0]] - tlist, coeff = self._normalized_gauss_pulse() - sign = np.sign(omega_single) * np.sign(gate.arg_value) - tlist = tlist / omega_single * gate.arg_value/np.pi/2 * sign - coeff = coeff * omega_single * sign - if gate.name == "RY": - pulse_prefix = "sy" - elif gate.name == "RX": - pulse_prefix = "sx" - else: - raise ValueError(f"Gate {gate.name} cnot not be compiled.") - pulse_info = [(pulse_prefix + str(targets[0]), coeff)] - return [Instruction(gate, tlist, pulse_info)] + return partial( + self.default_single_qubit_compiler, param_name="omega_single") def cnot_compiler(self, gate, args): """ Compiler for CNOT gate using the cross resonance iteraction. See https://journals.aps.org/prb/abstract/10.1103/PhysRevB.81.134507 - for reference + for reference. + + Parameters + ---------- + gate : :obj:`.Gate`: + The quantum gate to be compiled. + args : dict + The compilation configuration defined in the attributes + :obj:`.GateCompiler.args` or given as a parameter in + :obj:`.GateCompiler.compile`. + + Returns + ------- + A list of :obj:`.Instruction`, including the compiled pulse + information for this gate. """ result = [] q1 = gate.controls[0] @@ -78,6 +105,12 @@ def cnot_compiler(self, gate, args): sign = np.sign(amplitude) * np.sign(area) tlist = tlist / amplitude * area * sign coeff = coeff * amplitude * sign + from scipy.integrate import simps + print(amplitude) + print(simps(coeff, tlist)) + coeff, tlist = self.generate_pulse_shape( + args["shape"], args["num_samples"], maximum=zx_coeff, area=area) + print(simps(coeff, tlist)) pulse_info = [("zx" + str(q1) + str(q2), coeff)] result += [Instruction(gate, tlist, pulse_info)] diff --git a/src/qutip_qip/compiler/gatecompiler.py b/src/qutip_qip/compiler/gatecompiler.py index 5e0a5b5de..352b90228 100644 --- a/src/qutip_qip/compiler/gatecompiler.py +++ b/src/qutip_qip/compiler/gatecompiler.py @@ -1,9 +1,11 @@ +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 -import warnings __all__ = ['GateCompiler'] @@ -36,8 +38,18 @@ class GateCompiler(object): Note that for continuous pulse, the first coeff should always be 0. args: dict - Arguments for individual compiling routines. - It adds more flexibility in customizing compiler. + 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 + `_. + * ``num_samples``: + Number of samples in continuous pulse. + It has not effect in rectangular pulse. + * ``params``: Hardware parameters computed in the :obj:`Processor`. + """ def __init__(self, num_qubits=None, params=None, pulse_dict=None, N=None): self.gate_compiler = {} @@ -48,8 +60,11 @@ def __init__(self, num_qubits=None, params=None, pulse_dict=None, N=None): "GLOBALPHASE": self.globalphase_compiler, "IDLE": self.idle_compiler } - self.args = {} - self.args.update({"params": self.params}) + self.args = { # Default configuration + "shape": "rectangular", + "num_samples": None, + "params": self.params, + } self.global_phase = 0. if pulse_dict is not None: warnings.warn( @@ -287,3 +302,147 @@ def _process_idling_tlist( # idling until the start time idling_tlist.append([start_time]) return np.concatenate(idling_tlist) + + def default_single_qubit_compiler( + self, gate, args, param_name=None, op_name=None, scaling=1.): + """ + Default compiler for single qubits gates. + It compiles ``"RY"`` ``"RX"`` and ``"RZ"`` gate, + assuming that the corresponding + operator is named ``"sy"``, ``"sx"``, and ``"sz"``. + + Parameters + ---------- + gate : :obj:`.Gate`: + The quantum gate to be compiled. + args : dict + The compilation configuration defined in the attributes + :obj:`.GateCompiler.args` or given as a parameter in + :obj:`.GateCompiler.compile`. + param_name :str + The name of the coefficient saved in the attribute + :obj:`GateCompiler.params`. + The corresponding value sets the maximum of the pulse. + It should be calculated in the corresponding :class:`.Processor` + and passed to the compiler. + op_name : str + The name of the corresponding operator. + It will be one of the keys of the returned dictionary of + :obj:`.GateCompiler.compile`. + The name should match the label prefix of the control Hamiltonians + in :obj:`.Processor`. + E.g., if the label is "sx0" for the zeroth qubit, + set ``op_name="sx"``. + scaling : float + Scaling for the ``tlist``. + By default, the operator is assumed to be + ``2*pi*sigmax()/2`` (or y, z). + If operators with other factors are used, e.g., ``2*pi*sigmax()``, + choose ``scaling=0.5``. + It will be multiplied to the ``tlist`` to ensure that + the total area is desired. + + Returns + ------- + A list of :obj:`.Instruction`, including the compiled pulse + information for this gate. + """ + targets = gate.targets + if gate.name == "RY": + pulse_prefix = "sy" + elif gate.name == "RX": + pulse_prefix = "sx" + elif gate.name == "RZ": + pulse_prefix = "sz" + else: + raise ValueError(f"Gate {gate.name} cannot not be compiled.") + if param_name is None: + param_name = pulse_prefix + if op_name is None: + op_name = pulse_prefix + coeff, tlist = self.generate_pulse_shape( + args["shape"], args["num_samples"], + maximum=self.params[param_name][targets[0]], + area=gate.arg_value / 2. / np.pi) + pulse_info = [(op_name + str(targets[0]), coeff)] + return [Instruction(gate, tlist * scaling, pulse_info)] + + def generate_pulse_shape(self, window, num_samples, maximum=1., area=1.): + """ + Return a tuple consisting of a coeff list and a time sequence + according to a given window function. + + Parameters + ---------- + window : str + The name ``"rectangular"`` for constant pulse or + the name of a Scipy window function. + See + `the Scipy documentation + `_ + for detail. + num_samples : int + The number of the samples of the coefficients. + maximum : float + The maximum of the coefficients. + The absolute value will be used if negative. + area : float + The total area if one integrates coeff as a function of the time. + + 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"``. + Also, finite number of samples will also make + the total integral of the coefficients slightly deviate from ``area``. + """ + coeff, tlist = _normalized_window(window, num_samples) + sign = np.sign(area) + coeff *= np.abs(maximum) * sign + tlist *= abs(area) / np.abs(maximum) + return coeff, tlist + + +""" +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. +""" +_default_window_t_max = { + "boxcar": 1., + "triang": 2., + "blackman": 1./0.42, + "hamming": 1./0.54, + "hann": 2., + "bartlett": 2., + "flattop": 1./0.21557897160000217, + "parzen": 1./0.375, + "bohman": 1./0.4052847750978287, + "blackmanharris": 1./0.35875003586900384, + "nuttall": 1./0.36358193632191405, + "barthann": 2., + "cosine": np.pi/2., + } + + +def _normalized_window(window, num_samples): + """ + Return a normalized window functions. + """ + if window == "rectangular": + return 1., 1. + if window not in _default_window_t_max.keys(): + raise RuntimeError(f"Window function {window} is not supported.") + coeff = signal.windows.get_window( + window, num_samples + ) + tlist = np.linspace(0, _default_window_t_max[window], num_samples) + return coeff, tlist diff --git a/src/qutip_qip/compiler/spinchaincompiler.py b/src/qutip_qip/compiler/spinchaincompiler.py index d7f253277..d82fdac46 100644 --- a/src/qutip_qip/compiler/spinchaincompiler.py +++ b/src/qutip_qip/compiler/spinchaincompiler.py @@ -1,3 +1,4 @@ +from functools import partial import numpy as np from ..circuit import QubitCircuit @@ -10,8 +11,22 @@ class SpinChainCompiler(GateCompiler): """ - Compile a :class:`.QubitCircuit` into - the pulse sequence for the processor. + Compiler for :obj:`.SpinChain`. + Compiled pulse strength is in the unit of MHz. + + Supported native gates: "RX", "RY", "RZ", "ISWAP", "SQRTISWAP", + "GLOBALPHASE". + + Default configuration (see :obj:`.GateCompiler.args` and + :obj:`.GateCompiler.compile`): + + +-----------------+--------------------+ + | key | value | + +=================+====================+ + | ``shape`` | ``rectangular`` | + +-----------------+--------------------+ + |``params`` | Hardware Parameters| + +-----------------+--------------------+ Parameters ---------- @@ -64,43 +79,25 @@ def __init__( self.gate_compiler.update({ "ISWAP": self.iswap_compiler, "SQRTISWAP": self.sqrtiswap_compiler, - "RZ": self.rz_compiler, - "RX": self.rx_compiler, + "RZ": partial(self.default_single_qubit_compiler, scaling=0.5), + "RX": partial(self.default_single_qubit_compiler, scaling=0.5), "GLOBALPHASE": self.globalphase_compiler }) self.global_phase = global_phase - def rz_compiler(self, gate, args): - """ - Compiler for the RZ gate - """ - targets = gate.targets - g = self.params["sz"][targets[0]] - coeff = np.sign(gate.arg_value) * g - tlist = abs(gate.arg_value) / (2 * g) / np.pi / 2 - pulse_info = [("sz" + str(targets[0]), coeff)] - return [Instruction(gate, tlist, pulse_info)] - - def rx_compiler(self, gate, args): - """ - Compiler for the RX gate - """ - targets = gate.targets - g = self.params["sx"][targets[0]] - coeff = np.sign(gate.arg_value) * g - tlist = abs(gate.arg_value) / (2 * g) / np.pi / 2 - pulse_info = [("sx" + str(targets[0]), coeff)] - return [Instruction(gate, tlist, pulse_info)] - - def iswap_compiler(self, gate, args): - """ - Compiler for the ISWAP gate - """ + def _two_qubit_compiler(self, gate_name, gate, args): targets = gate.targets q1, q2 = min(targets), max(targets) g = self.params["sxsy"][q1] - coeff = -g - tlist = np.pi / (4 * g) / np.pi / 2 + maximum = g + if gate_name == "ISWAP": + area = -1 / 8 + elif gate_name == "SQRTISWAP": + area = -1 / 16 + else: + raise ValueError(f"Gate {gate.name} cannot not be compiled.") + coeff, tlist = self.generate_pulse_shape( + args["shape"], args["num_samples"], maximum, area) if self.N != 2 and q1 == 0 and q2 == self.N - 1: pulse_name = "g" + str(q2) else: @@ -108,21 +105,45 @@ def iswap_compiler(self, gate, args): pulse_info = [(pulse_name, coeff)] return [Instruction(gate, tlist, pulse_info)] + def iswap_compiler(self, gate, args): + """ + Compiler for the ISWAP gate. + + Parameters + ---------- + gate : :obj:`.Gate`: + The quantum gate to be compiled. + args : dict + The compilation configuration defined in the attributes + :obj:`.GateCompiler.args` or given as a parameter in + :obj:`.GateCompiler.compile`. + + Returns + ------- + A list of :obj:`.Instruction`, including the compiled pulse + information for this gate. + """ + return self._two_qubit_compiler("ISWAP", gate, args) + def sqrtiswap_compiler(self, gate, args): """ - Compiler for the SQRTISWAP gate + Compiler for the SQRTISWAP gate. + + Parameters + ---------- + gate : :obj:`Gate`: + The quantum gate to be compiled. + args : dict + The compilation configuration defined in the attributes + :obj:`.GateCompiler.args` or given as a parameter in + :obj:`.GateCompiler.compile`. + + Returns + ------- + A list of :obj:`.Instruction`, including the compiled pulse + information for this gate. """ - targets = gate.targets - q1, q2 = min(targets), max(targets) - g = self.params["sxsy"][q1] - coeff = -g - tlist = np.pi / (8 * g) / np.pi / 2 - if self.N != 2 and q1 == 0 and q2 == self.N - 1: - pulse_name = "g" + str(q2) - else: - pulse_name = "g" + str(q1) - pulse_info = [(pulse_name, coeff)] - return [Instruction(gate, tlist, pulse_info)] + return self._two_qubit_compiler("SQRTISWAP", gate, args) def globalphase_compiler(self, gate, args): """ diff --git a/tests/test_compiler.py b/tests/test_compiler.py index feff8b31e..0fdc5c47a 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -1,6 +1,7 @@ import pytest import numpy as np from numpy.testing import assert_array_equal +from qutip_qip.compiler.gatecompiler import _default_window_t_max from qutip_qip.device import ( DispersiveCavityQED, CircularSpinChain, LinearSpinChain) @@ -151,3 +152,21 @@ def test_compiler_result_format(): processor.set_all_tlist(tlist) assert_array_equal(processor.pulses[0].coeff, coeffs[0]) assert_array_equal(processor.pulses[0].tlist, tlist[0]) + + +@pytest.mark.parametrize("shape", _default_window_t_max.keys()) +def test_pulse_shape(shape): + """Test different pulse shape functions""" + num_qubits = 1 + circuit = QubitCircuit(num_qubits) + circuit.add_gate("X", 0) + processor = LinearSpinChain(num_qubits) + compiler = SpinChainCompiler(num_qubits, processor.params) + compiler.args.update({"shape": shape, "num_samples": 100}) + processor.load_circuit(circuit, compiler=compiler) + processor.pulse_mode = "continuous" + init_state = basis(2, 0) + num_result = processor.run_state(init_state).states[-1] + ideal_result = circuit.run(init_state) + ifid = 1 - fidelity(num_result, ideal_result) + assert(pytest.approx(ifid, abs=0.01) == 0) From a1fb410c45634eb13053c02f39a4d05e26c1d414 Mon Sep 17 00:00:00 2001 From: Boxi Li Date: Wed, 18 Aug 2021 17:06:28 +0200 Subject: [PATCH 02/12] fix a typo --- doc/source/qip-processor.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/qip-processor.rst b/doc/source/qip-processor.rst index 2ff80563d..fb8a13cb4 100644 --- a/doc/source/qip-processor.rst +++ b/doc/source/qip-processor.rst @@ -296,7 +296,7 @@ The result will then be the start time of each instruction. Pulse shape ----------- Apart from square pulses, compilers also support different pulse shapes. -All pulse shapes from `SciPy window functions `_ that does not require additional parameters are supported. +All pulse shapes from `SciPy window functions `_ that do not require additional parameters are supported. The method :obj:`.GateCompiler.generate_pulse_shape` allows one to generate pulse shapes that fulfil the given maximum intensity and the total integral area. .. plot:: From c6f4d683e20fa075b575a2a10a6feca68aff90a9 Mon Sep 17 00:00:00 2001 From: Boxi Li Date: Mon, 13 Sep 2021 13:46:11 +0200 Subject: [PATCH 03/12] Remove prints --- src/qutip_qip/compiler/circuitqedcompiler.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/qutip_qip/compiler/circuitqedcompiler.py b/src/qutip_qip/compiler/circuitqedcompiler.py index f9f09b3ed..c24e55f05 100644 --- a/src/qutip_qip/compiler/circuitqedcompiler.py +++ b/src/qutip_qip/compiler/circuitqedcompiler.py @@ -106,11 +106,8 @@ def cnot_compiler(self, gate, args): tlist = tlist / amplitude * area * sign coeff = coeff * amplitude * sign from scipy.integrate import simps - print(amplitude) - print(simps(coeff, tlist)) coeff, tlist = self.generate_pulse_shape( args["shape"], args["num_samples"], maximum=zx_coeff, area=area) - print(simps(coeff, tlist)) pulse_info = [("zx" + str(q1) + str(q2), coeff)] result += [Instruction(gate, tlist, pulse_info)] From 525231a56777b24bb451df030514381ad4ca623b Mon Sep 17 00:00:00 2001 From: Boxi Li Date: Mon, 13 Sep 2021 13:45:49 +0200 Subject: [PATCH 04/12] remove - Remove the default_single_qubit_compiler and define compilers in each subclass. They all make use of the generate_pulse_shape method. This is easier to understand compared to including or variants (name of operators, parameters etc) in one function default_single_qubit_compiler. - Make generate_pulse_shape a class method as it is independent from the instances created. Having it as a parameter of the class however, simplifies the importation. --- src/qutip_qip/compiler/cavityqedcompiler.py | 28 ++++++++- src/qutip_qip/compiler/circuitqedcompiler.py | 32 +++++++--- src/qutip_qip/compiler/gatecompiler.py | 65 +------------------- src/qutip_qip/compiler/spinchaincompiler.py | 28 ++++++++- 4 files changed, 76 insertions(+), 77 deletions(-) diff --git a/src/qutip_qip/compiler/cavityqedcompiler.py b/src/qutip_qip/compiler/cavityqedcompiler.py index 11e7cec1b..89828fcdd 100644 --- a/src/qutip_qip/compiler/cavityqedcompiler.py +++ b/src/qutip_qip/compiler/cavityqedcompiler.py @@ -55,14 +55,38 @@ def __init__( self.gate_compiler.update({ "ISWAP": self.iswap_compiler, "SQRTISWAP": self.sqrtiswap_compiler, - "RZ": partial(self.default_single_qubit_compiler, scaling=0.5), - "RX": partial(self.default_single_qubit_compiler, scaling=0.5), + "RZ": self.rz_compiler, + "RX": self.rx_compiler, "GLOBALPHASE": self.globalphase_compiler }) self.wq = np.sqrt(self.params["eps"]**2 + self.params["delta"]**2) self.Delta = self.wq - self.params["w0"] self.global_phase = global_phase + def rz_compiler(self, gate, args): + """ + Compiler for the RZ gate + """ + targets = gate.targets + coeff, tlist = self.generate_pulse_shape( + args["shape"], args["num_samples"], + maximum=self.params["sz"][targets[0]], + area=gate.arg_value / 2. / np.pi * 0.5) # operator is Z, not Z/2 + pulse_info = [("sz" + str(targets[0]), coeff)] + return [Instruction(gate, tlist, pulse_info)] + + def rx_compiler(self, gate, args): + """ + Compiler for the RX gate + """ + targets = gate.targets + coeff, tlist = self.generate_pulse_shape( + args["shape"], args["num_samples"], + maximum=self.params["sx"][targets[0]], + area=gate.arg_value / 2. / np.pi * 0.5) # operator is= X, not X/2 + pulse_info = [("sx" + str(targets[0]), coeff)] + return [Instruction(gate, tlist, pulse_info)] + def _two_qubit_compiler(self, gate_name, gate, args): q1, q2 = gate.targets pulse_info = [] diff --git a/src/qutip_qip/compiler/circuitqedcompiler.py b/src/qutip_qip/compiler/circuitqedcompiler.py index c24e55f05..0f61c5e3a 100644 --- a/src/qutip_qip/compiler/circuitqedcompiler.py +++ b/src/qutip_qip/compiler/circuitqedcompiler.py @@ -1,4 +1,3 @@ -from functools import partial import numpy as np from ..circuit import Gate @@ -31,10 +30,8 @@ class SCQubitsCompiler(GateCompiler): def __init__(self, num_qubits, params): super(SCQubitsCompiler, self).__init__(num_qubits, params=params) self.gate_compiler.update({ - "RX": partial( - self.default_single_qubit_compiler, param_name="omega_single"), - "RY": partial( - self.default_single_qubit_compiler, param_name="omega_single"), + "RY": self.ry_compiler, + "RX": self.rx_compiler, "CNOT": self.cnot_compiler, }) self.args = { # Default configuration @@ -63,12 +60,29 @@ def _normalized_gauss_pulse(self): ) / max_pulse return tlist, coeff - def single_qubit_compiler(self, gate, args): + def ry_compiler(self, gate, args): """ - Compiler for the RX and RY gate. + Compiler for the RZ gate """ - return partial( - self.default_single_qubit_compiler, param_name="omega_single") + targets = gate.targets + coeff, tlist = self.generate_pulse_shape( + args["shape"], args["num_samples"], + maximum=self.params["omega_single"][targets[0]], + area=gate.arg_value / 2. / np.pi) + pulse_info = [("sy" + str(targets[0]), coeff)] + return [Instruction(gate, tlist, pulse_info)] + + def rx_compiler(self, gate, args): + """ + Compiler for the RX gate + """ + targets = gate.targets + coeff, tlist = self.generate_pulse_shape( + args["shape"], args["num_samples"], + maximum=self.params["omega_single"][targets[0]], + area=gate.arg_value / 2. / np.pi) + pulse_info = [("sx" + str(targets[0]), coeff)] + return [Instruction(gate, tlist, pulse_info)] def cnot_compiler(self, gate, args): """ diff --git a/src/qutip_qip/compiler/gatecompiler.py b/src/qutip_qip/compiler/gatecompiler.py index 352b90228..04377ea0c 100644 --- a/src/qutip_qip/compiler/gatecompiler.py +++ b/src/qutip_qip/compiler/gatecompiler.py @@ -303,70 +303,7 @@ def _process_idling_tlist( idling_tlist.append([start_time]) return np.concatenate(idling_tlist) - def default_single_qubit_compiler( - self, gate, args, param_name=None, op_name=None, scaling=1.): - """ - Default compiler for single qubits gates. - It compiles ``"RY"`` ``"RX"`` and ``"RZ"`` gate, - assuming that the corresponding - operator is named ``"sy"``, ``"sx"``, and ``"sz"``. - - Parameters - ---------- - gate : :obj:`.Gate`: - The quantum gate to be compiled. - args : dict - The compilation configuration defined in the attributes - :obj:`.GateCompiler.args` or given as a parameter in - :obj:`.GateCompiler.compile`. - param_name :str - The name of the coefficient saved in the attribute - :obj:`GateCompiler.params`. - The corresponding value sets the maximum of the pulse. - It should be calculated in the corresponding :class:`.Processor` - and passed to the compiler. - op_name : str - The name of the corresponding operator. - It will be one of the keys of the returned dictionary of - :obj:`.GateCompiler.compile`. - The name should match the label prefix of the control Hamiltonians - in :obj:`.Processor`. - E.g., if the label is "sx0" for the zeroth qubit, - set ``op_name="sx"``. - scaling : float - Scaling for the ``tlist``. - By default, the operator is assumed to be - ``2*pi*sigmax()/2`` (or y, z). - If operators with other factors are used, e.g., ``2*pi*sigmax()``, - choose ``scaling=0.5``. - It will be multiplied to the ``tlist`` to ensure that - the total area is desired. - - Returns - ------- - A list of :obj:`.Instruction`, including the compiled pulse - information for this gate. - """ - targets = gate.targets - if gate.name == "RY": - pulse_prefix = "sy" - elif gate.name == "RX": - pulse_prefix = "sx" - elif gate.name == "RZ": - pulse_prefix = "sz" - else: - raise ValueError(f"Gate {gate.name} cannot not be compiled.") - if param_name is None: - param_name = pulse_prefix - if op_name is None: - op_name = pulse_prefix - coeff, tlist = self.generate_pulse_shape( - args["shape"], args["num_samples"], - maximum=self.params[param_name][targets[0]], - area=gate.arg_value / 2. / np.pi) - pulse_info = [(op_name + str(targets[0]), coeff)] - return [Instruction(gate, tlist * scaling, pulse_info)] - + @classmethod def generate_pulse_shape(self, window, num_samples, maximum=1., area=1.): """ Return a tuple consisting of a coeff list and a time sequence diff --git a/src/qutip_qip/compiler/spinchaincompiler.py b/src/qutip_qip/compiler/spinchaincompiler.py index d82fdac46..7d28df2de 100644 --- a/src/qutip_qip/compiler/spinchaincompiler.py +++ b/src/qutip_qip/compiler/spinchaincompiler.py @@ -79,12 +79,36 @@ def __init__( self.gate_compiler.update({ "ISWAP": self.iswap_compiler, "SQRTISWAP": self.sqrtiswap_compiler, - "RZ": partial(self.default_single_qubit_compiler, scaling=0.5), - "RX": partial(self.default_single_qubit_compiler, scaling=0.5), + "RZ": self.rz_compiler, + "RX": self.rx_compiler, "GLOBALPHASE": self.globalphase_compiler }) self.global_phase = global_phase + def rz_compiler(self, gate, args): + """ + Compiler for the RZ gate + """ + targets = gate.targets + coeff, tlist = self.generate_pulse_shape( + args["shape"], args["num_samples"], + maximum=self.params["sz"][targets[0]], + area=gate.arg_value / 2. / np.pi * 0.5) # operator is Z, not Z/2 + pulse_info = [("sz" + str(targets[0]), coeff)] + return [Instruction(gate, tlist, pulse_info)] + + def rx_compiler(self, gate, args): + """ + Compiler for the RX gate + """ + targets = gate.targets + coeff, tlist = self.generate_pulse_shape( + args["shape"], args["num_samples"], + maximum=self.params["sx"][targets[0]], + area=gate.arg_value / 2. / np.pi * 0.5) # operator is= X, not X/2 + pulse_info = [("sx" + str(targets[0]), coeff)] + return [Instruction(gate, tlist, pulse_info)] + def _two_qubit_compiler(self, gate_name, gate, args): targets = gate.targets q1, q2 = min(targets), max(targets) From 56f7ac126bae7d172067d996b19423cb361be9c9 Mon Sep 17 00:00:00 2001 From: Boxi Li Date: Mon, 13 Sep 2021 23:19:22 +0200 Subject: [PATCH 05/12] Minor improvement and typo correction Co-Authored-By: Simon Cross <165551+hodgestar@users.noreply.github.com> --- doc/source/qip-processor.rst | 1 + src/qutip_qip/compiler/cavityqedcompiler.py | 2 +- src/qutip_qip/compiler/gatecompiler.py | 14 ++++++++------ 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/doc/source/qip-processor.rst b/doc/source/qip-processor.rst index fb8a13cb4..19f5590dd 100644 --- a/doc/source/qip-processor.rst +++ b/doc/source/qip-processor.rst @@ -295,6 +295,7 @@ The result will then be the start time of each instruction. Pulse shape ----------- + Apart from square pulses, compilers also support different pulse shapes. All pulse shapes from `SciPy window functions `_ that do not require additional parameters are supported. The method :obj:`.GateCompiler.generate_pulse_shape` allows one to generate pulse shapes that fulfil the given maximum intensity and the total integral area. diff --git a/src/qutip_qip/compiler/cavityqedcompiler.py b/src/qutip_qip/compiler/cavityqedcompiler.py index 89828fcdd..7295b0b17 100644 --- a/src/qutip_qip/compiler/cavityqedcompiler.py +++ b/src/qutip_qip/compiler/cavityqedcompiler.py @@ -83,7 +83,7 @@ def rx_compiler(self, gate, args): coeff, tlist = self.generate_pulse_shape( args["shape"], args["num_samples"], maximum=self.params["sx"][targets[0]], - area=gate.arg_value / 2. / np.pi * 0.5) # operator is= X, not X/2 + area=gate.arg_value / 2. / np.pi * 0.5) # operator is X, not X/2 pulse_info = [("sx" + str(targets[0]), coeff)] return [Instruction(gate, tlist, pulse_info)] diff --git a/src/qutip_qip/compiler/gatecompiler.py b/src/qutip_qip/compiler/gatecompiler.py index 04377ea0c..0736b0661 100644 --- a/src/qutip_qip/compiler/gatecompiler.py +++ b/src/qutip_qip/compiler/gatecompiler.py @@ -46,8 +46,8 @@ class GateCompiler(object): one of the `SciPy window functions `_. * ``num_samples``: - Number of samples in continuous pulse. - It has not effect in rectangular pulse. + Number of samples for continuous pulses. + It has no effect for rectangular pulses. * ``params``: Hardware parameters computed in the :obj:`Processor`. """ @@ -304,7 +304,7 @@ def _process_idling_tlist( return np.concatenate(idling_tlist) @classmethod - def generate_pulse_shape(self, window, num_samples, maximum=1., area=1.): + def generate_pulse_shape(cls, window, num_samples, maximum=1., area=1.): """ Return a tuple consisting of a coeff list and a time sequence according to a given window function. @@ -325,6 +325,7 @@ def generate_pulse_shape(self, window, num_samples, maximum=1., area=1.): The absolute value will be used if negative. area : float 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 ------- @@ -376,10 +377,11 @@ def _normalized_window(window, num_samples): """ if window == "rectangular": return 1., 1. - if window not in _default_window_t_max.keys(): - raise RuntimeError(f"Window function {window} is not supported.") + t_max = _default_window_t_max.get(window, None) + if t_max is None: + raise ValueError(f"Window function {window} is not supported.") coeff = signal.windows.get_window( window, num_samples ) - tlist = np.linspace(0, _default_window_t_max[window], num_samples) + tlist = np.linspace(0, t_max, num_samples) return coeff, tlist From 2f0e15f1cb3368a8ae3a0ad1143be086cb2e264a Mon Sep 17 00:00:00 2001 From: Boxi Li Date: Mon, 13 Sep 2021 23:20:07 +0200 Subject: [PATCH 06/12] remove debug code --- src/qutip_qip/compiler/circuitqedcompiler.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/qutip_qip/compiler/circuitqedcompiler.py b/src/qutip_qip/compiler/circuitqedcompiler.py index 0f61c5e3a..d3f0c0142 100644 --- a/src/qutip_qip/compiler/circuitqedcompiler.py +++ b/src/qutip_qip/compiler/circuitqedcompiler.py @@ -119,7 +119,6 @@ def cnot_compiler(self, gate, args): sign = np.sign(amplitude) * np.sign(area) tlist = tlist / amplitude * area * sign coeff = coeff * amplitude * sign - from scipy.integrate import simps coeff, tlist = self.generate_pulse_shape( args["shape"], args["num_samples"], maximum=zx_coeff, area=area) pulse_info = [("zx" + str(q1) + str(q2), coeff)] From 2d512a6c0c5af83fb0ec34274df735ec7cbe1dc8 Mon Sep 17 00:00:00 2001 From: Boxi Li Date: Mon, 13 Sep 2021 23:27:58 +0200 Subject: [PATCH 07/12] code climate issue --- src/qutip_qip/compiler/gatecompiler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/qutip_qip/compiler/gatecompiler.py b/src/qutip_qip/compiler/gatecompiler.py index 0736b0661..85d81029c 100644 --- a/src/qutip_qip/compiler/gatecompiler.py +++ b/src/qutip_qip/compiler/gatecompiler.py @@ -325,7 +325,8 @@ def generate_pulse_shape(cls, window, num_samples, maximum=1., area=1.): The absolute value will be used if negative. area : float 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). + If the area is negative, the pulse is flipped vertically + (i.e. the pulse is multiplied by the sign of the area). Returns ------- From 0eb2b5a343bc6f2127339ee76a22f58501e456fc Mon Sep 17 00:00:00 2001 From: Boxi Li Date: Mon, 13 Sep 2021 23:34:58 +0200 Subject: [PATCH 08/12] Simplify the compilation of iswap-like gates --- src/qutip_qip/compiler/cavityqedcompiler.py | 16 +++++----------- src/qutip_qip/compiler/spinchaincompiler.py | 12 +++--------- 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/src/qutip_qip/compiler/cavityqedcompiler.py b/src/qutip_qip/compiler/cavityqedcompiler.py index 7295b0b17..75d812ce2 100644 --- a/src/qutip_qip/compiler/cavityqedcompiler.py +++ b/src/qutip_qip/compiler/cavityqedcompiler.py @@ -87,7 +87,7 @@ def rx_compiler(self, gate, args): pulse_info = [("sx" + str(targets[0]), coeff)] return [Instruction(gate, tlist, pulse_info)] - def _two_qubit_compiler(self, gate_name, gate, args): + def _swap_compiler(self, gate, area, correction_angle, args): q1, q2 = gate.targets pulse_info = [] pulse_name = "sz" + str(q1) @@ -105,14 +105,6 @@ def _two_qubit_compiler(self, gate_name, gate, args): J = self.params["g"][q1] * self.params["g"][q2] * ( 1. / self.Delta[q1] + 1. / self.Delta[q2]) / 2. - if gate_name == "ISWAP": - area = 1. / 2. - correction_angle = -np.pi / 2. - elif gate_name == "SQRTISWAP": - area = 1. / 4. - correction_angle = -np.pi / 4. - else: - raise ValueError(f"Gate {gate.name} cannot not be compiled.") coeff, tlist = self.generate_pulse_shape( args["shape"], args["num_samples"], maximum=J, area=area) instruction_list = [Instruction(gate, tlist, pulse_info)] @@ -152,7 +144,8 @@ def sqrtiswap_compiler(self, gate, args): iswap """ # FIXME This decomposition has poor behaviour. - return self._two_qubit_compiler("SQRTISWAP", gate, args) + return self._swap_compiler( + gate, area=1/4, correction_angle=-np.pi/4, args=args) def iswap_compiler(self, gate, args): """ @@ -172,7 +165,8 @@ def iswap_compiler(self, gate, args): A list of :obj:`.Instruction`, including the compiled pulse information for this gate. """ - return self._two_qubit_compiler("ISWAP", gate, args) + return self._swap_compiler( + gate, area=1/2, correction_angle=-np.pi/2, args=args) def globalphase_compiler(self, gate, args): """ diff --git a/src/qutip_qip/compiler/spinchaincompiler.py b/src/qutip_qip/compiler/spinchaincompiler.py index 7d28df2de..608b427a4 100644 --- a/src/qutip_qip/compiler/spinchaincompiler.py +++ b/src/qutip_qip/compiler/spinchaincompiler.py @@ -109,17 +109,11 @@ def rx_compiler(self, gate, args): pulse_info = [("sx" + str(targets[0]), coeff)] return [Instruction(gate, tlist, pulse_info)] - def _two_qubit_compiler(self, gate_name, gate, args): + def _swap_compiler(self, gate, area, args): targets = gate.targets q1, q2 = min(targets), max(targets) g = self.params["sxsy"][q1] maximum = g - if gate_name == "ISWAP": - area = -1 / 8 - elif gate_name == "SQRTISWAP": - area = -1 / 16 - else: - raise ValueError(f"Gate {gate.name} cannot not be compiled.") coeff, tlist = self.generate_pulse_shape( args["shape"], args["num_samples"], maximum, area) if self.N != 2 and q1 == 0 and q2 == self.N - 1: @@ -147,7 +141,7 @@ def iswap_compiler(self, gate, args): A list of :obj:`.Instruction`, including the compiled pulse information for this gate. """ - return self._two_qubit_compiler("ISWAP", gate, args) + return self._swap_compiler(gate, area=-1/8, args=args) def sqrtiswap_compiler(self, gate, args): """ @@ -167,7 +161,7 @@ def sqrtiswap_compiler(self, gate, args): A list of :obj:`.Instruction`, including the compiled pulse information for this gate. """ - return self._two_qubit_compiler("SQRTISWAP", gate, args) + return self._swap_compiler(gate, area=-1/16, args=args) def globalphase_compiler(self, gate, args): """ From 2fa7a8231b12342a6290e3788204379f3658c877 Mon Sep 17 00:00:00 2001 From: Boxi Li Date: Mon, 13 Sep 2021 23:51:14 +0200 Subject: [PATCH 09/12] Simplify the compilation of single-qubit gates --- src/qutip_qip/compiler/cavityqedcompiler.py | 72 ++++++++++++++++---- src/qutip_qip/compiler/circuitqedcompiler.py | 69 ++++++++++++++++--- src/qutip_qip/compiler/spinchaincompiler.py | 72 ++++++++++++++++---- 3 files changed, 178 insertions(+), 35 deletions(-) diff --git a/src/qutip_qip/compiler/cavityqedcompiler.py b/src/qutip_qip/compiler/cavityqedcompiler.py index 75d812ce2..eb597f9c9 100644 --- a/src/qutip_qip/compiler/cavityqedcompiler.py +++ b/src/qutip_qip/compiler/cavityqedcompiler.py @@ -63,29 +63,77 @@ def __init__( self.Delta = self.wq - self.params["w0"] self.global_phase = global_phase - def rz_compiler(self, gate, args): + def _rotation_compiler(self, gate, op_label, param_label, args): """ - Compiler for the RZ gate + Single qubit gate compiler. + + Parameters + ---------- + gate : :obj:`.Gate`: + The quantum gate to be compiled. + op_label : str + Label of the corresponding control Hamiltonian. + param_label : str + Label of the hardware parameters saved in + :obj:`GateCompiler.params`. + args : dict + The compilation configuration defined in the attributes + :obj:`.GateCompiler.args` or given as a parameter in + :obj:`.GateCompiler.compile`. + + Returns + ------- + A list of :obj:`.Instruction`, including the compiled pulse + information for this gate. """ targets = gate.targets coeff, tlist = self.generate_pulse_shape( args["shape"], args["num_samples"], - maximum=self.params["sz"][targets[0]], - area=gate.arg_value / 2. / np.pi * 0.5) # operator is Z, not Z/2 - pulse_info = [("sz" + str(targets[0]), coeff)] + maximum=self.params[param_label][targets[0]], + # The operator is Pauli Z/X/Y, without 1/2. + area=gate.arg_value / 2. / np.pi * 0.5) + pulse_info = [(op_label + str(targets[0]), coeff)] return [Instruction(gate, tlist, pulse_info)] + def rz_compiler(self, gate, args): + """ + Compiler for the RZ gate + + Parameters + ---------- + gate : :obj:`.Gate`: + The quantum gate to be compiled. + args : dict + The compilation configuration defined in the attributes + :obj:`.GateCompiler.args` or given as a parameter in + :obj:`.GateCompiler.compile`. + + Returns + ------- + A list of :obj:`.Instruction`, including the compiled pulse + information for this gate. + """ + return self._rotation_compiler(gate, "sz", "sz", args) + def rx_compiler(self, gate, args): """ Compiler for the RX gate + + Parameters + ---------- + gate : :obj:`.Gate`: + The quantum gate to be compiled. + args : dict + The compilation configuration defined in the attributes + :obj:`.GateCompiler.args` or given as a parameter in + :obj:`.GateCompiler.compile`. + + Returns + ------- + A list of :obj:`.Instruction`, including the compiled pulse + information for this gate. """ - targets = gate.targets - coeff, tlist = self.generate_pulse_shape( - args["shape"], args["num_samples"], - maximum=self.params["sx"][targets[0]], - area=gate.arg_value / 2. / np.pi * 0.5) # operator is X, not X/2 - pulse_info = [("sx" + str(targets[0]), coeff)] - return [Instruction(gate, tlist, pulse_info)] + return self._rotation_compiler(gate, "sx", "sx", args) def _swap_compiler(self, gate, area, correction_angle, args): q1, q2 = gate.targets diff --git a/src/qutip_qip/compiler/circuitqedcompiler.py b/src/qutip_qip/compiler/circuitqedcompiler.py index d3f0c0142..bcb0065b2 100644 --- a/src/qutip_qip/compiler/circuitqedcompiler.py +++ b/src/qutip_qip/compiler/circuitqedcompiler.py @@ -60,29 +60,76 @@ def _normalized_gauss_pulse(self): ) / max_pulse return tlist, coeff - def ry_compiler(self, gate, args): + def _rotation_compiler(self, gate, op_label, param_label, args): """ - Compiler for the RZ gate + Single qubit gate compiler. + + Parameters + ---------- + gate : :obj:`.Gate`: + The quantum gate to be compiled. + op_label : str + Label of the corresponding control Hamiltonian. + param_label : str + Label of the hardware parameters saved in + :obj:`GateCompiler.params`. + args : dict + The compilation configuration defined in the attributes + :obj:`.GateCompiler.args` or given as a parameter in + :obj:`.GateCompiler.compile`. + + Returns + ------- + A list of :obj:`.Instruction`, including the compiled pulse + information for this gate. """ targets = gate.targets coeff, tlist = self.generate_pulse_shape( args["shape"], args["num_samples"], - maximum=self.params["omega_single"][targets[0]], + maximum=self.params[param_label][targets[0]], area=gate.arg_value / 2. / np.pi) - pulse_info = [("sy" + str(targets[0]), coeff)] + pulse_info = [(op_label + str(targets[0]), coeff)] return [Instruction(gate, tlist, pulse_info)] + def ry_compiler(self, gate, args): + """ + Compiler for the RZ gate + + Parameters + ---------- + gate : :obj:`.Gate`: + The quantum gate to be compiled. + args : dict + The compilation configuration defined in the attributes + :obj:`.GateCompiler.args` or given as a parameter in + :obj:`.GateCompiler.compile`. + + Returns + ------- + A list of :obj:`.Instruction`, including the compiled pulse + information for this gate. + """ + return self._rotation_compiler(gate, "sy", "omega_single", args) + def rx_compiler(self, gate, args): """ Compiler for the RX gate + + Parameters + ---------- + gate : :obj:`.Gate`: + The quantum gate to be compiled. + args : dict + The compilation configuration defined in the attributes + :obj:`.GateCompiler.args` or given as a parameter in + :obj:`.GateCompiler.compile`. + + Returns + ------- + A list of :obj:`.Instruction`, including the compiled pulse + information for this gate. """ - targets = gate.targets - coeff, tlist = self.generate_pulse_shape( - args["shape"], args["num_samples"], - maximum=self.params["omega_single"][targets[0]], - area=gate.arg_value / 2. / np.pi) - pulse_info = [("sx" + str(targets[0]), coeff)] - return [Instruction(gate, tlist, pulse_info)] + return self._rotation_compiler(gate, "sx", "omega_single", args) def cnot_compiler(self, gate, args): """ diff --git a/src/qutip_qip/compiler/spinchaincompiler.py b/src/qutip_qip/compiler/spinchaincompiler.py index 608b427a4..63b9c8749 100644 --- a/src/qutip_qip/compiler/spinchaincompiler.py +++ b/src/qutip_qip/compiler/spinchaincompiler.py @@ -85,29 +85,77 @@ def __init__( }) self.global_phase = global_phase - def rz_compiler(self, gate, args): + def _rotation_compiler(self, gate, op_label, param_label, args): """ - Compiler for the RZ gate + Single qubit gate compiler. + + Parameters + ---------- + gate : :obj:`.Gate`: + The quantum gate to be compiled. + op_label : str + Label of the corresponding control Hamiltonian. + param_label : str + Label of the hardware parameters saved in + :obj:`GateCompiler.params`. + args : dict + The compilation configuration defined in the attributes + :obj:`.GateCompiler.args` or given as a parameter in + :obj:`.GateCompiler.compile`. + + Returns + ------- + A list of :obj:`.Instruction`, including the compiled pulse + information for this gate. """ targets = gate.targets coeff, tlist = self.generate_pulse_shape( args["shape"], args["num_samples"], - maximum=self.params["sz"][targets[0]], - area=gate.arg_value / 2. / np.pi * 0.5) # operator is Z, not Z/2 - pulse_info = [("sz" + str(targets[0]), coeff)] + maximum=self.params[param_label][targets[0]], + # The operator is Pauli Z/X/Y, without 1/2. + area=gate.arg_value / 2. / np.pi * 0.5) + pulse_info = [(op_label + str(targets[0]), coeff)] return [Instruction(gate, tlist, pulse_info)] + def rz_compiler(self, gate, args): + """ + Compiler for the RZ gate + + Parameters + ---------- + gate : :obj:`.Gate`: + The quantum gate to be compiled. + args : dict + The compilation configuration defined in the attributes + :obj:`.GateCompiler.args` or given as a parameter in + :obj:`.GateCompiler.compile`. + + Returns + ------- + A list of :obj:`.Instruction`, including the compiled pulse + information for this gate. + """ + return self._rotation_compiler(gate, "sz", "sz", args) + def rx_compiler(self, gate, args): """ Compiler for the RX gate + + Parameters + ---------- + gate : :obj:`.Gate`: + The quantum gate to be compiled. + args : dict + The compilation configuration defined in the attributes + :obj:`.GateCompiler.args` or given as a parameter in + :obj:`.GateCompiler.compile`. + + Returns + ------- + A list of :obj:`.Instruction`, including the compiled pulse + information for this gate. """ - targets = gate.targets - coeff, tlist = self.generate_pulse_shape( - args["shape"], args["num_samples"], - maximum=self.params["sx"][targets[0]], - area=gate.arg_value / 2. / np.pi * 0.5) # operator is= X, not X/2 - pulse_info = [("sx" + str(targets[0]), coeff)] - return [Instruction(gate, tlist, pulse_info)] + return self._rotation_compiler(gate, "sx", "sx", args) def _swap_compiler(self, gate, area, args): targets = gate.targets From d186ef1324c7f2c298d256ab4fe33e6924f4d84f Mon Sep 17 00:00:00 2001 From: Boxi Li Date: Mon, 13 Sep 2021 23:52:21 +0200 Subject: [PATCH 10/12] Remove a debug print --- src/qutip_qip/device/cavityqed.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/qutip_qip/device/cavityqed.py b/src/qutip_qip/device/cavityqed.py index 8c90cd24f..7b2a58903 100644 --- a/src/qutip_qip/device/cavityqed.py +++ b/src/qutip_qip/device/cavityqed.py @@ -157,7 +157,6 @@ def set_up_params(self): if any((w0 - self.wq)/(w0 + self.wq) > 0.05): warnings.warn( "The rotating-wave approximation might not be valid.") - print(self.params) @property def sx_ops(self): From 512a936114e002d1ead41e9ec55824a9975e4162 Mon Sep 17 00:00:00 2001 From: Boxi Li Date: Tue, 14 Sep 2021 23:18:05 +0200 Subject: [PATCH 11/12] Add a test for discrete rectangular pulse --- tests/test_compiler.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/test_compiler.py b/tests/test_compiler.py index 0fdc5c47a..3fb71bf7d 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -154,9 +154,10 @@ def test_compiler_result_format(): assert_array_equal(processor.pulses[0].tlist, tlist[0]) -@pytest.mark.parametrize("shape", _default_window_t_max.keys()) -def test_pulse_shape(shape): - """Test different pulse shape functions""" +@pytest.mark.parametrize( + "shape", list(_default_window_t_max.keys()) + ["rectangular"]) +def test_pulse_shape_scipy(shape): + """Test different pulse shape functions imported from scipy""" num_qubits = 1 circuit = QubitCircuit(num_qubits) circuit.add_gate("X", 0) @@ -164,7 +165,10 @@ def test_pulse_shape(shape): compiler = SpinChainCompiler(num_qubits, processor.params) compiler.args.update({"shape": shape, "num_samples": 100}) processor.load_circuit(circuit, compiler=compiler) - processor.pulse_mode = "continuous" + if shape == "rectangular": + processor.pulse_mode = "discrete" + else: + processor.pulse_mode = "continuous" init_state = basis(2, 0) num_result = processor.run_state(init_state).states[-1] ideal_result = circuit.run(init_state) From e441d6b555e6b2856dc913578bb463c900e6b97c Mon Sep 17 00:00:00 2001 From: Boxi Li Date: Tue, 14 Sep 2021 23:34:05 +0200 Subject: [PATCH 12/12] Rename window to shape The term pulse window and pulse shape describe the same thing. However, the former is more used in the context of signal processing, in particular FFT, to remove the boundary effect. The term shape here is more appropriate and straightforward to understand. --- src/qutip_qip/compiler/gatecompiler.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/qutip_qip/compiler/gatecompiler.py b/src/qutip_qip/compiler/gatecompiler.py index 85d81029c..308987267 100644 --- a/src/qutip_qip/compiler/gatecompiler.py +++ b/src/qutip_qip/compiler/gatecompiler.py @@ -304,14 +304,14 @@ def _process_idling_tlist( return np.concatenate(idling_tlist) @classmethod - def generate_pulse_shape(cls, window, num_samples, maximum=1., area=1.): + def generate_pulse_shape(cls, shape, num_samples, maximum=1., area=1.): """ Return a tuple consisting of a coeff list and a time sequence - according to a given window function. + according to a given pulse shape. Parameters ---------- - window : str + shape : str The name ``"rectangular"`` for constant pulse or the name of a Scipy window function. See @@ -320,10 +320,10 @@ def generate_pulse_shape(cls, window, num_samples, maximum=1., area=1.): for detail. num_samples : int The number of the samples of the coefficients. - maximum : float + maximum : float, optional The maximum of the coefficients. The absolute value will be used if negative. - area : float + 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). @@ -340,10 +340,10 @@ def generate_pulse_shape(cls, window, num_samples, maximum=1., area=1.): ----- If Scipy window functions are used, it is suggested to set ``Processor.pulse_mode`` to ``"continuous"``. - Also, finite number of samples will also make + Notice that finite number of sampling points will also make the total integral of the coefficients slightly deviate from ``area``. """ - coeff, tlist = _normalized_window(window, num_samples) + coeff, tlist = _normalized_window(shape, num_samples) sign = np.sign(area) coeff *= np.abs(maximum) * sign tlist *= abs(area) / np.abs(maximum) @@ -372,17 +372,17 @@ def generate_pulse_shape(cls, window, num_samples, maximum=1., area=1.): } -def _normalized_window(window, num_samples): +def _normalized_window(shape, num_samples): """ Return a normalized window functions. """ - if window == "rectangular": + if shape == "rectangular": return 1., 1. - t_max = _default_window_t_max.get(window, None) + t_max = _default_window_t_max.get(shape, None) if t_max is None: - raise ValueError(f"Window function {window} is not supported.") + raise ValueError(f"Window function {shape} is not supported.") coeff = signal.windows.get_window( - window, num_samples + shape, num_samples ) tlist = np.linspace(0, t_max, num_samples) return coeff, tlist