Skip to content

Commit

Permalink
Support different pulse shapes (#85)
Browse files Browse the repository at this point in the history
* Add pulse shape

Different pulse shapes are generated using window functions 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. It is a class method as it is independent of the instances created. Having it as a class method, however, simplifies the importation.

Single qubit compilers are still defined in each subclass, despite some code duplication. They all make use of the generate_pulse_shape method. This is easier to understand compared to including all variants (name of operators, parameters etc) in one function.

We choose to use the keyword *shape* rather than *window*, in contrast to SciPy. The terms *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.

Co-authored-by: Simon Cross <165551+hodgestar@users.noreply.github.com>
  • Loading branch information
BoxiLi and hodgestar committed Sep 20, 2021
1 parent 0cf9aba commit 77a3a48
Show file tree
Hide file tree
Showing 7 changed files with 513 additions and 120 deletions.
27 changes: 27 additions & 0 deletions doc/source/qip-processor.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -287,6 +293,27 @@ 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 <https://docs.scipy.org/doc/scipy/reference/signal.windows.html>`_ 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 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
================

Expand Down
195 changes: 133 additions & 62 deletions src/qutip_qip/compiler/cavityqedcompiler.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from functools import partial
import numpy as np
import scipy

from ..circuit import QubitCircuit
from ..operations import Gate
Expand All @@ -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
----------
Expand Down Expand Up @@ -47,44 +63,85 @@ 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 rotation 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
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)]
coeff, tlist = self.generate_pulse_shape(
args["shape"], args["num_samples"],
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 rx_compiler(self, gate, args):
def rz_compiler(self, gate, args):
"""
Compiler for the RX gate
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.
"""
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)]
return self._rotation_compiler(gate, "sz", "sz", args)

def sqrtiswap_compiler(self, gate, args):
def rx_compiler(self, gate, args):
"""
Compiler for the SQRTISWAP gate
Compiler for the RX gate
Notes
-----
This version of sqrtiswap_compiler has very low fidelity, please use
iswap
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.
"""
# FIXME This decomposition has poor behaviour
return self._rotation_compiler(gate, "sx", "sx", args)

def _swap_compiler(self, gate, area, correction_angle, 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)
Expand All @@ -95,55 +152,69 @@ 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.
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._swap_compiler(
gate, area=1/4, correction_angle=-np.pi/4, args=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._swap_compiler(
gate, area=1/2, correction_angle=-np.pi/2, args=args)

def globalphase_compiler(self, gate, args):
"""
Expand Down

0 comments on commit 77a3a48

Please sign in to comment.