Repository: qutip/qutip-qip (language: python)

Issue:
Minor fix for plots in doc
They were not caught because they are plotting code and plotting was not included in the doctest.


Discussion / hints:

Fix patch:
diff --git a/VERSION b/VERSION
index 8f91ec14..0ea3a944 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-0.2.0.dev
+0.2.0
diff --git a/doc/pulse-paper/README.md b/doc/pulse-paper/README.md
index a66ff71d..6245e0b4 100644
--- a/doc/pulse-paper/README.md
+++ b/doc/pulse-paper/README.md
@@ -8,8 +8,9 @@ The following table summarizes the sections in the paper and the corresponding c
 
 | Section | Code example |
 | ----------- | ----------- |
-| Appendix A | `pulse_simulation.py` |
-| Appendix B | `dj_algorithm.py` |
+| Section 4 | `main_example.py`|
+| Appendix A | `dj_algorithm.py` |
 | Fig.4 and Appendix C | `customize.py` |
-| Fig.5 in Section 4 | `decoherence.py` |
-| Appendix D | `deutsch_jozsa.qasm` and `deutsch_jozsa-qasm.py` |
+| Fig.5 | `decoherence.py` |
+| Section 5 | `deutsch_jozsa.qasm` and `deutsch_jozsa-qasm.py` |
+| Appendix B | `qft.py` |
\ No newline at end of file
diff --git a/doc/pulse-paper/customize.py b/doc/pulse-paper/customize.py
index 45a14a7a..dce51c41 100644
--- a/doc/pulse-paper/customize.py
+++ b/doc/pulse-paper/customize.py
@@ -10,49 +10,37 @@
     global_setup(fontsize=10)
 except:
     pass
+plt.rcParams.update({"text.usetex": False, "font.size": 10})
 from joblib import Parallel, delayed  # for parallel simulations
-
 import numpy as np
 from qutip import sigmax, sigmay, sigmaz, basis, qeye, tensor, Qobj, fock_dm
 from qutip_qip.circuit import QubitCircuit, Gate
-from qutip_qip.device import ModelProcessor
+from qutip_qip.device import ModelProcessor, Model
 from qutip_qip.compiler import GateCompiler, Instruction
 from qutip import Options
 from qutip_qip.noise import Noise
 
 
-class MyProcessor(ModelProcessor):
-    """Custom processor built using ModelProcessor as the base class.
-
-    This custom processor will inherit all the methods of the base class 
-    such as setting up of the T1 and T2 decoherence rates in the simulations.
-
-    In addition, it is possible to write your own functions to add control
-    pulses.
-
-    Args:
-        num_qubits (int): Number of qubits in the processor.
-        t1, t2 (float or list): The T1 and T2 decoherence rates for the
-                                qubit. If it is a list, then it is assumed that
-                                each element of the list corresponds to the rates
-                                for each of the qubits.
-    """
-
-    def __init__(self, num_qubits, t1=None, t2=None):
-        super().__init__(num_qubits, t1=t1, t2=t2)
-        self.pulse_mode = "discrete"  # set the control pulse as discrete or continuous.
-        self.set_up_ops()  # set up the available Hamiltonians.
-        self.dims = [2] * num_qubits  # dimension of the quantum system.
-        self.num_qubits = num_qubits
-        self.native_gates = ["RX", "RY"]
+class MyModel(Model):
+    """A custom Hamiltonian model with sigmax and sigmay control."""
+    def get_control(self, label):
+        """
+        Get an avaliable control Hamiltonian.
+        For instance, sigmax control on the zeroth qubits is labeld "sx0".
 
-    def set_up_ops(self):
-        """Sets up the single qubit control operators for each qubit."""
-        for m in range(self.num_qubits):
-            self.add_control(2 * np.pi * sigmax() / 2, m, label="sx" + str(m))
-        for m in range(self.num_qubits):
-            self.add_control(2 * np.pi * sigmay() / 2, m, label="sy" + str(m))
+        Args:
+            label (str): The label of the Hamiltonian
 
+        Returns:
+            The Hamiltonian and target qubits as a tuple (qutip.Qobj, list).
+        """
+        targets = int(label[2:])
+        if label[:2] == "sx":
+            return 2 * np.pi * sigmax() / 2, [targets]
+        elif label[:2] == "sy":
+            return 2 * np.pi * sigmax() / 2, [targets]
+        else:
+            raise NotImplementError("Unknown control.")
 
 class MyCompiler(GateCompiler):
     """Custom compiler for generating pulses from gates using the base class 
@@ -87,10 +75,11 @@ def generate_pulse(self, gate, tlist, coeff, phase=0.0):
             to implement a gate containing the control pulses.                                               
         """
         pulse_info = [
+            # (control label, coeff)
             ("sx" + str(gate.targets[0]), np.cos(phase) * coeff),
             ("sy" + str(gate.targets[0]), np.sin(phase) * coeff),
         ]
-        return [Instruction(gate, tlist, pulse_info)]
+        return [Instruction(gate, tlist=tlist, pulse_info=pulse_info)]
 
     def single_qubit_gate_compiler(self, gate, args):
         """Compiles single qubit gates to pulses.
@@ -133,7 +122,8 @@ def rotation_with_phase_compiler(self, gate, args):
 circuit.add_gate("RX", targets=0, arg_value=np.pi / 2)
 circuit.add_gate("Z", targets=0)
 
-myprocessor = MyProcessor(1)
+myprocessor = ModelProcessor(model=MyModel(num_qubits))
+myprocessor.native_gates = ["RX", "RY"]
 
 mycompiler = MyCompiler(num_qubits, {"pulse_amplitude": 0.02})
 
@@ -141,9 +131,10 @@ def rotation_with_phase_compiler(self, gate, args):
 result = myprocessor.run_state(basis(2, 0))
 
 fig, ax = myprocessor.plot_pulses(
-    figsize=(LINEWIDTH * 0.7, LINEWIDTH / 2 * 0.7), dpi=200
+    figsize=(LINEWIDTH * 0.7, LINEWIDTH / 2 * 0.7), dpi=200,
+    use_control_latex=False
 )
-ax[-1].set_xlabel("Time")
+ax[-1].set_xlabel("$t$")
 fig.tight_layout()
 fig.savefig("custom_compiler_pulse.pdf")
 fig.show()
@@ -166,8 +157,8 @@ def get_noisy_dynamics(self, dims=None, pulses=None, systematic_noise=None):
             systematic_noise: A Pulse object (not used in this example). 
         """
         for i, pulse in enumerate(pulses):
-            if "sx" or "sy" not in pulse.label:
-                pass  # filter out other pulses, e.g. drift
+            if "sx" not in pulse.label and "sy" not in pulse.label:
+                continue  # filter out other pulses, e.g. drift
             target = pulse.targets[0]
             if target != 0:  # add pulse to the left neighbour
                 pulses[i].add_control_noise(
@@ -197,7 +188,7 @@ def single_crosstalk_simulation(num_gates):
                                       solver methods such as mesolve.
     """
     num_qubits = 2  # Qubit-0 is the target qubit. Qubit-1 suffers from crosstalk.
-    myprocessor = MyProcessor(num_qubits)
+    myprocessor = ModelProcessor(model=MyModel(num_qubits))
     # Add qubit frequency detuning 1.852MHz for the second qubit.
     myprocessor.add_drift(2 * np.pi * (sigmaz() + 1) / 2 * 1.852, targets=1)
     myprocessor.native_gates = None  # Remove the native gates
@@ -228,7 +219,6 @@ def single_crosstalk_simulation(num_gates):
     return result
 
 
-num_qubits = 2
 num_sample = 2
 # num_sample = 1600
 fidelity = []
@@ -247,31 +237,32 @@ def single_crosstalk_simulation(num_gates):
 # Recorded result
 num_gates_list = [250, 500, 750, 1000, 1250, 1500]
 data_y = [
-    0.9577285560461476,
-    0.9384849070716464,
-    0.9230217713086177,
-    0.9062344084919285,
-    0.889009550855518,
-    0.8749290612064392,
+    0.9566768747558925,
+    0.9388905075892828,
+    0.9229470389282218,
+    0.9075513000339529,
+    0.8941659320508855,
+    0.8756519016627652
 ]
+
 data_y_error = [
-    0.000431399017208067,
-    0.0008622091914303468,
-    0.0012216267555118497,
-    0.001537120153687202,
-    0.0018528957172559654,
-    0.0020169257334183596,
+    0.00042992029265330223,
+    0.0008339882813741004,
+    0.0012606632769758602,
+    0.0014643550337816722,
+    0.0017695604671714809,
+    0.0020964978542167617
 ]
 
 
-def linear(x, a):
+def rb_curve(x, a):
     return (1 / 2 + np.exp(-2 * a * x) / 2) * 0.975
 
 
-pos, cov = curve_fit(linear, num_gates_list, data_y, p0=[0.001])
+pos, cov = curve_fit(rb_curve, num_gates_list, data_y, p0=[0.001])
 
 xline = np.linspace(0, 1700, 200)
-yline = linear(xline, *pos)
+yline = rb_curve(xline, *pos)
 
 fig, ax = plt.subplots(figsize=(LINEWIDTH, 0.65 * LINEWIDTH), dpi=200)
 ax.errorbar(
diff --git a/doc/pulse-paper/decoherence.py b/doc/pulse-paper/decoherence.py
index 6eb9c434..b30b7d42 100644
--- a/doc/pulse-paper/decoherence.py
+++ b/doc/pulse-paper/decoherence.py
@@ -1,51 +1,99 @@
 TEXTWIDTH = 7.1398920714
 LINEWIDTH = 3.48692403487
-import matplotlib as mpl
 import matplotlib.pyplot as plt
+import matplotlib.gridspec as gridspec
 try:
     from quantum_plots import global_setup
     global_setup(fontsize = 10)
 except:
     pass
-
-import matplotlib.pyplot as plt
-
+plt.rcParams.update({"text.usetex": False, "font.size": 10})
 import numpy as np
-from qutip import sigmaz, basis
+import scipy
+from qutip import sigmaz, basis, sigmax, fidelity
 from qutip_qip.operations import hadamard_transform
 from qutip_qip.pulse import Pulse
 from qutip_qip.device import LinearSpinChain
 from qutip_qip.circuit import QubitCircuit
-
-# define a circuit with only idling
-t = 30.
-circuit = QubitCircuit(1)
-circuit.add_gate("IDLE", 0, arg_value=t)
-# define a processor with t2 relaxation
+pi = np.pi
+num_samples = 500
+amp = 0.1
 f = 0.5
 t2 = 10 / f
-processor = LinearSpinChain(1, t2=t2)
-processor.add_drift(
-    2*np.pi*sigmaz()/2*f, targets=[0])
-processor.load_circuit(circuit)
-# Record the expectation value
-plus_state = \
-    (basis(2,1) + basis(2,0)).unit()
-result = processor.run_state(
-    init_state=plus_state,
-    tlist = np.linspace(0., t, 1000),
-    # observable
-    e_ops=[plus_state*plus_state.dag()])
-
-tlist = np.linspace(0., t, 1000)
-fig, ax = plt.subplots(figsize = (LINEWIDTH, LINEWIDTH*0.65), dpi=200)
-# detail about lenght of tlist needs to be fixed
-ax.plot(tlist[:-1], result.expect[0][:-1], '-', label="Simulation", color="slategray")
-ax.plot(tlist[:-1], np.exp(-1./t2 * tlist[:-1])*0.5 + 0.5, '--', label="Theory", color="slategray")
-ax.set_xlabel(r"Time [$\mu$s]")
-ax.set_ylabel("Ramsey signal")
-ax.legend()
+
+# Define a processor.
+proc = LinearSpinChain(
+    num_qubits=1, sx=amp/2, t2=t2)
+ham_idle = 2*pi * sigmaz()/2 * f
+resonant_sx = 2*pi * sigmax() - \
+    ham_idle / (amp/2)
+proc.add_drift(ham_idle, targets=0)
+proc.add_control(
+    resonant_sx, targets=0, label="sx0")
+
+# Define a Ramsey experiment.
+def ramsey(t, proc):
+    qc = QubitCircuit(1)
+    qc.add_gate("RX", 0, arg_value=pi/2)
+    qc.add_gate("IDLE", 0, arg_value=t)
+    qc.add_gate("RX", 0, arg_value=pi/2)
+    proc.load_circuit(qc)
+    result = proc.run_state(
+        init_state=basis(2, 0),
+        e_ops = sigmaz()
+    )
+    return result.expect[0][-1]
+
+idle_tlist = np.linspace(0., 30., num_samples)
+measurements = np.asarray([ramsey(t, proc) for t in idle_tlist])
+
+fig, ax = plt.subplots(figsize = (LINEWIDTH, LINEWIDTH*0.60), dpi=200)
+
+rx_gate_time = 1/4/amp # pi/2
+total_time = 2*rx_gate_time + idle_tlist[-1]
+
+tlist = np.linspace(0., total_time, num_samples)
+ax.plot(idle_tlist[:], measurements[:], '-', label="Simulation", color="slategray")
+
+peak_ind = scipy.signal.find_peaks(measurements)[0]
+decay_func = lambda t, t2, f0: f0 * np.exp(-1./t2 * t)
+(t2_fit, f0_fit), _ = scipy.optimize.curve_fit(decay_func, idle_tlist[peak_ind], measurements[peak_ind])
+print("T2:", t2)
+print("Fitted T2:", t2_fit)
+
+ax.plot(idle_tlist, decay_func(idle_tlist, t2_fit, f0_fit), '--', label="Theory", color="slategray")
+ax.set_xlabel(r"Idling time $t$ [$\mu$s]")
+ax.set_ylabel("Ramsey signal", labelpad=2)
+ax.set_ylim((ax.get_ylim()[0], ax.get_ylim()[1]))
+ax.set_position([0.18, 0.2, 0.75, 0.75])
 ax.grid()
-fig.tight_layout()
+
 fig.savefig("fig5_decoherence.pdf")
-fig.show()
\ No newline at end of file
+fig.show()
+
+
+circuit = QubitCircuit(1)
+circuit.add_gate("RX", 0, arg_value=pi/2)
+circuit.add_gate("IDLE", 0, arg_value=15.)
+circuit.add_gate("RX", 0, arg_value=pi/2)
+proc.load_circuit(circuit)
+fig2, axis = proc.plot_pulses(figsize=(LINEWIDTH, LINEWIDTH*0.15), use_control_latex=False, dpi=200)
+axis[0].set_ylim((0. - axis[0].get_ylim()[1] * 0.05, axis[0].get_ylim()[1]))
+axis[0].set_position([0.18, 0.39, 0.75, 0.60])
+axis[0].set_ylabel("sx0", labelpad=25)
+axis[0].yaxis.set_label_coords(-0.13, 0.25)
+axis[0].set_xlabel("Ramsey pulse")
+fig2.savefig("fig5_decoherence_pulse.pdf")
+fig2.show()
+
+# Test for time-dependent decoherence
+from qutip_qip.noise import DecoherenceNoise
+from qutip import sigmam
+tlist = np.linspace(0, 30., 100)
+coeff = tlist * 0.01
+proc.add_noise(
+    DecoherenceNoise(sigmam(), targets=0, coeff=coeff, tlist=tlist))
+result = proc.run_state(
+    init_state=basis(2, 0),
+    e_ops = sigmaz()
+)
diff --git a/doc/pulse-paper/deutsch_jozsa-qasm.py b/doc/pulse-paper/deutsch_jozsa-qasm.py
index cc435498..de4d2c76 100644
--- a/doc/pulse-paper/deutsch_jozsa-qasm.py
+++ b/doc/pulse-paper/deutsch_jozsa-qasm.py
@@ -1,7 +1,5 @@
 from qutip_qip.qasm import read_qasm
-
 qc = read_qasm("deutsch-jozsa.qasm")
 
 from qutip_qip.qasm import save_qasm
-
 save_qasm(qc, "deutsch-jozsa-qutip.qasm")
diff --git a/doc/pulse-paper/dj_algorithm.py b/doc/pulse-paper/dj_algorithm.py
index a7d3f7af..17810a68 100644
--- a/doc/pulse-paper/dj_algorithm.py
+++ b/doc/pulse-paper/dj_algorithm.py
@@ -7,30 +7,20 @@
     global_setup(fontsize = 10)
 except:
     pass
-plt.rcParams.update({"text.usetex": False})
+plt.rcParams.update({"text.usetex": False, "font.size": 10})
 
 num_qubits = 3
-def get_operators_labels():
-    """
-    Get the labels for each Hamiltonian.
-    It is used in the method method :meth:`.Processor.plot_pulses`.
-    It is a 2-d nested list, in the plot,
-    a different color will be used for each sublist.
-    """
-    return ([[r"$\Omega^z_%d$" % n for n in range(num_qubits)],
-            [r"$\Omega^x_%d$" % n for n in range(num_qubits)],
-            [r"$g_{%d}$" % (n) for n in range(num_qubits - 1)],
-            ])
                 
 import numpy as np
 import matplotlib.pyplot as plt
-np.random.seed(0)
-from qutip_qip.device import OptPulseProcessor, LinearSpinChain, SCQubits
+np.random.seed(2)
+from qutip_qip.device import (
+    OptPulseProcessor, LinearSpinChain, SCQubits, SpinChainModel)
 from qutip_qip.circuit import QubitCircuit
 from qutip import sigmaz, sigmax, identity, tensor, basis
 
 # Deutsch-Josza algorithm
-dj_circuit = QubitCircuit(3)
+dj_circuit = QubitCircuit(num_qubits)
 dj_circuit.add_gate("X", targets=2)
 dj_circuit.add_gate("SNOT", targets=0)
 dj_circuit.add_gate("SNOT", targets=1)
@@ -44,14 +34,14 @@ def get_operators_labels():
 dj_circuit.add_gate("SNOT", targets=1)
 
 # Spin chain model
-spinchain_processor = LinearSpinChain(3, t2=30)  # T2 = 30
+spinchain_processor = LinearSpinChain(num_qubits=num_qubits, t2=30)  # T2 = 30
 spinchain_processor.load_circuit(dj_circuit)
 initial_state = basis([2, 2, 2], [0, 0, 0])  # 3 qubits in the 000 state
 t_record = np.linspace(0, 20, 300)
 result1 = spinchain_processor.run_state(initial_state, tlist=t_record)
 
 # Superconducting qubits
-scqubits_processor = SCQubits(num_qubits)
+scqubits_processor = SCQubits(num_qubits=num_qubits)
 scqubits_processor.load_circuit(dj_circuit)
 initial_state = basis([3, 3, 3], [0, 0, 0])  #  3-level
 result2 = scqubits_processor.run_state(initial_state)
@@ -60,11 +50,8 @@ def get_operators_labels():
 setting_args = {"SNOT": {"num_tslots": 6, "evo_time": 2},
                 "X": {"num_tslots": 1, "evo_time": 0.5},
                 "CNOT": {"num_tslots": 12, "evo_time": 5}}
-opt_processor = OptPulseProcessor(N=3)
-opt_processor.add_control(sigmax(), cyclic_permutation=True)
-opt_processor.add_control(sigmaz(), cyclic_permutation=True)
-opt_processor.add_control(tensor([sigmax(), sigmax(), identity(2)]))
-opt_processor.add_control(tensor([identity(2), sigmax(), sigmax()]))
+opt_processor = OptPulseProcessor(
+    num_qubits=num_qubits, model=SpinChainModel(3, setup="linear"))
 opt_processor.load_circuit(  # Provide parameters for the algorithm
     dj_circuit, setting_args=setting_args, merge_gates=False,
     verbose=True, amp_ubound=5, amp_lbound=0)
@@ -72,7 +59,6 @@ def get_operators_labels():
 result3 = opt_processor.run_state(initial_state)
 
 # For plotting
-opt_processor.get_operators_labels = get_operators_labels
 width = TEXTWIDTH/3
 fig, ax = opt_processor.plot_pulses(figsize=(width, width*3/2.9), dpi=200);
 
@@ -84,7 +70,14 @@ def get_operators_labels():
 ax[5].set_ylabel(r"$\Omega^z_{2}$")
 ax[6].set_ylabel(r"$g_{0}$")
 ax[7].set_ylabel(r"$g_{1}$")
-# ax[7].set_xlabel("Time")
+ax[7].set_xlabel(r"$t$")
+
+full_tlist = opt_processor.get_full_tlist()
+two_qubit_gate_region = [[0, 13, -12]] * 8
+for i, (point1, point2, point3) in enumerate(two_qubit_gate_region):
+    vmin, vmax = ax[i].get_ylim()
+    ax[i].fill_between([full_tlist[point2], full_tlist[point3]], [vmin ,vmin],  [vmax, vmax], color="lightgray", alpha=0.5)
+    ax[i].vlines([full_tlist[point2], full_tlist[point3]], vmin, vmax, "gray", "--", linewidth=0.8, alpha=0.5)
 
 fig.tight_layout()
 fig.savefig("optimal_control_pulse.pdf")
@@ -101,7 +94,15 @@ def get_operators_labels():
 ax2[5].set_ylabel(r"$\Omega^z_{2}$")
 ax2[6].set_ylabel(r"$g_{0}$")
 ax2[7].set_ylabel(r"$g_{1}$")
-# ax2[7].set_xlabel("Time")
+ax2[7].set_xlabel(r"$t$")
+
+full_tlist = spinchain_processor.get_full_tlist()
+two_qubit_gate_region = [[0, 7, -15], [0, 7, -6], [0, 8, -2], [0, 7, -15], [0, 7, -6], [0, 8, -2], [0, 7, -6], [0, 7, -1]]
+for i, (point1, point2, point3) in enumerate(two_qubit_gate_region):
+    vmin, vmax = ax2[i].get_ylim()
+    ax2[i].fill_between([full_tlist[point2], full_tlist[point3]], [vmin ,vmin],  [vmax, vmax], color="lightgray", alpha=0.5)
+    ax2[i].vlines([full_tlist[point2], full_tlist[point3]], vmin, vmax, "gray", "--", linewidth=0.8, alpha=0.5)
+
 fig2.tight_layout()
 fig2.savefig("spin_chain_pulse.pdf")
 fig2.show()
@@ -119,7 +120,15 @@ def get_operators_labels():
 ax3[7].set_ylabel(r"$\Omega^{\rm{cr}2}_{0}$")
 ax3[8].set_ylabel(r"$\Omega^{\rm{cr}1}_{1}$")
 ax3[9].set_ylabel(r"$\Omega^{\rm{cr}2}_{1}$")
-# ax3[9].set_xlabel("Time")
+ax3[9].set_xlabel(r"$t$")
+
+full_tlist = scqubits_processor.get_full_tlist()
+two_qubit_gate_region = np.array([[0, 6, -18], [0, 6, -4], [0, 9, -4], [0, 6, -18], [0, 6, -4], [0, 11, -4], [0, 6, -18], [0, 6, -4], [0, 6, -4]]) * 500
+for i, (point1, point2, point3) in enumerate(two_qubit_gate_region):
+    vmin, vmax = ax3[i].get_ylim()
+    ax3[i].fill_between([full_tlist[point2], full_tlist[point3]], [vmin ,vmin],  [vmax, vmax], color="lightgray", alpha=0.5)
+    ax3[i].vlines([full_tlist[point2], full_tlist[point3]], vmin, vmax, "gray", "--", linewidth=0.8, alpha=0.5)
+
 fig3.tight_layout()
 fig3.savefig("transmon_pulse.pdf")
 fig3.show()
diff --git a/doc/pulse-paper/main_example.py b/doc/pulse-paper/main_example.py
new file mode 100644
index 00000000..a18d3666
--- /dev/null
+++ b/doc/pulse-paper/main_example.py
@@ -0,0 +1,63 @@
+import numpy as np
+from qutip import basis
+from qutip_qip.circuit import QubitCircuit
+from qutip_qip.device import LinearSpinChain, SpinChainModel, Processor, ModelProcessor
+from qutip_qip.compiler import SpinChainCompiler
+from qutip_qip.noise import RelaxationNoise
+qc = QubitCircuit(3)
+qc.add_gate("X", targets=2)
+qc.add_gate("SNOT", targets=0)
+qc.add_gate("SNOT", targets=1)
+qc.add_gate("SNOT", targets=2)
+
+# Oracle function f(x)
+qc.add_gate(
+    "CNOT", controls=0, targets=2)
+qc.add_gate(
+    "CNOT", controls=1, targets=2)
+
+qc.add_gate("SNOT", targets=0)
+qc.add_gate("SNOT", targets=1)
+
+init_state = basis([2,2,2], [0,0,0])
+final_state = qc.run(init_state)
+
+processor = LinearSpinChain(
+    num_qubits=3, sx=0.25)
+
+processor.load_circuit(qc)
+
+tlist = np.linspace(0, 20, 300)
+result = processor.run_state(
+    init_state, tlist=tlist)
+
+processor.add_noise(
+    RelaxationNoise(t2=30))
+    
+# Section 4.1 Model
+model = SpinChainModel(
+    num_qubits=3, setup="circular", g=1)
+processor = Processor(model=model)
+
+model.get_control(label="sx0")
+
+model.get_control_labels()
+
+# Section 4.3 Scheduler
+from qutip_qip.compiler import Scheduler, Instruction
+
+Scheduler("ASAP").schedule(qc)
+
+inst_list = []
+for gate in qc.gates:
+    if gate.name in ("SNOT", "X"):
+        inst_list.append(
+            Instruction(gate, duration=1
+            )
+        )
+    else:
+        inst_list.append(
+            Instruction(gate, duration=2
+            )
+        )
+Scheduler("ALAP").schedule(inst_list)
\ No newline at end of file
diff --git a/doc/pulse-paper/pulse_simulation.py b/doc/pulse-paper/pulse_simulation.py
deleted file mode 100644
index 7b38b104..00000000
--- a/doc/pulse-paper/pulse_simulation.py
+++ /dev/null
@@ -1,93 +0,0 @@
-TEXTWIDTH = 7.1398920714
-LINEWIDTH = 3.48692403487
-import matplotlib as mpl
-import matplotlib.pyplot as plt
-try:
-    from quantum_plots import global_setup
-    global_setup(fontsize = 10)
-except:
-    pass
-
-# discrete pulse
-import numpy as np
-from qutip_qip.device import Processor
-from qutip import sigmaz, sigmax, basis
-from qutip_qip.pulse import Pulse
-
-processor = Processor(1)
-coeff = np.array([1.])
-tlist = np.array([0., np.pi/2])
-pulse = Pulse(
-    sigmax(), targets=0, tlist=tlist,
-    coeff=coeff, label="sigmax")
-processor.add_pulse(pulse)
-plot_time = np.linspace(0, np.pi/2, 20)
-solver_result = processor.run_state(
-    init_state=basis(2, 0),
-    tlist=plot_time)
-
-mpl.rcParams['axes.prop_cycle'] = mpl.cycler(color=["#1f77b4"]) 
-fig1, ax1 = processor.plot_pulses(figsize=(LINEWIDTH/2, LINEWIDTH/4), use_control_latex=False)
-ax1[0].set_xlim(-0.5, np.pi/2 + 0.5)
-ax1[0].set_ylim(-0.1, 1.1)
-ax1[0].axhline(0)
-ax1[0].set_ylabel(None)
-fig1.tight_layout()
-fig1.savefig("discrete_pulse.pdf")
-fig1.show()
-
-# continuous pulse
-processor = Processor(1)
-processor.pulse_mode = "continuous"
-processor.add_control(sigmax(), label="sigmax")
-tlist = np.linspace(0., np.pi/2, 21)
-coeff = np.array(np.sin(2*tlist) * np.pi/2)
-processor.set_coeffs({"sigmax": coeff})
-processor.set_tlist({"sigmax": tlist})
-solver_result2 = processor.run_state(
-    init_state=basis(2, 0),
-    tlist=plot_time)
-
-mpl.rcParams['axes.prop_cycle'] = mpl.cycler(color=["#ff7f0e"]) 
-fig2, ax2 = processor.plot_pulses(figsize=(LINEWIDTH/2, LINEWIDTH/4))
-ax2[0].set_xlim(-0.5, np.pi/2 + 0.5)
-ax2[0].set_ylim(-0.1, 1.7)
-ax2[0].axhline(0)
-ax2[0].set_ylabel(None)
-fig2.tight_layout()
-fig2.savefig("continuous_pulse.pdf")
-fig2.show()
-
-# bloch sphere
-from qutip import Bloch
-fig3 = plt.figure(figsize = (LINEWIDTH, LINEWIDTH))
-b = Bloch(fig3)
-b.point_color = ["#1f77b4"]
-
-theta = 2 * np.arctan(np.asarray([np.abs(s[1,0]/s[0,0]) for s in solver_result.states]))
-phi = np.angle(np.asarray([s[1,0]/s[0,0] for s in solver_result.states]))
-xp = np.sin(theta) * np.cos(phi)
-yp = np.sin(theta) * np.sin(phi)
-zp = np.cos(theta)
-pnts = [xp, yp, zp]
-b.add_points(pnts)
-b.show()
-fig3.savefig("bloch_sphere1.pdf")
-fig3.show()
-
-from qutip import Bloch
-fig4 = plt.figure(figsize = (LINEWIDTH, LINEWIDTH))
-b = Bloch(fig4)
-b.point_color = ["#ff7f0e"]
-
-theta = 2 * np.arctan(np.asarray([np.abs(s[1,0]/s[0,0]) for s in solver_result2.states]))
-phi = np.angle(np.asarray([s[1,0]/s[0,0] for s in solver_result2.states]))
-xp = np.sin(theta) * np.cos(phi)
-yp = np.sin(theta) * np.sin(phi)
-zp = np.cos(theta)
-pnts = [xp, yp, zp]
-b.add_points(pnts)
-b.show()
-fig4.savefig("bloch_sphere2.pdf")
-fig4.show()
-
diff --git a/doc/pulse-paper/qft.py b/doc/pulse-paper/qft.py
new file mode 100644
index 00000000..e7ea0fa5
--- /dev/null
+++ b/doc/pulse-paper/qft.py
@@ -0,0 +1,109 @@
+TEXTWIDTH = 5.93
+LINEWIDTH = 3.22
+import matplotlib as mpl
+import matplotlib.pyplot as plt
+try:
+    from quantum_plots import global_setup
+    global_setup(fontsize = 10)
+except:
+    pass
+plt.rcParams.update({"text.usetex": False, "font.size": 10})
+
+
+import numpy as np
+from qutip import basis, fidelity
+from qutip_qip.device import LinearSpinChain
+from qutip_qip.algorithms import qft_gate_sequence
+
+num_qubits = 10
+# The QFT circuit
+qc = qft_gate_sequence(num_qubits, swapping=False, to_cnot=True)
+# Gate-level simulation
+state1 = qc.run(basis([2]*num_qubits, [0]*num_qubits))
+# Pulse-level simulation
+processor = LinearSpinChain(num_qubits)
+processor.load_circuit(qc)
+state2 = processor.run_state(basis([2]*num_qubits, [0]*num_qubits)).states[-1]
+
+assert(abs(1 - fidelity(state1, state2)) < 1.e-4)
+
+
+def get_control_latex(model):
+    """
+    Get the labels for each Hamiltonian.
+    It is used in the method method :meth:`.Processor.plot_pulses`.
+    It is a 2-d nested list, in the plot,
+    a different color will be used for each sublist.
+    """
+    num_qubits = model.num_qubits
+    num_coupling = model._get_num_coupling()
+    return [
+        {f"sx{m}": r"$\sigma_x^{}$".format(m) for m in range(num_qubits)},
+        {f"sz{m}": r"$\sigma_z^{}$".format(m) for m in range(num_qubits)},
+        {
+            f"g{m}": r"$g_{}$".format(m) for m in range(num_coupling)
+        },
+    ]
+
+fig, axes = processor.plot_pulses(
+    figsize=(TEXTWIDTH, TEXTWIDTH), dpi=200,
+    pulse_labels=get_control_latex(processor.model))
+axes[-1].set_xlabel("$t$")
+fig.tight_layout()
+fig.savefig("qft_pulse.pdf")
+
+
+import time
+compiling_time = []
+simulation_time = []
+for num_qubits in range(1, 3):  # Test up to 3 qubits
+# for num_qubits in range(1, 11):
+    compiling_time_sample = []
+    simulation_time_sample = []
+    for _ in range(20):
+        qc = qft_gate_sequence(num_qubits, swapping=False, to_cnot=True)
+        state1 = qc.run(basis([2]*num_qubits, [0]*num_qubits))
+        processor = LinearSpinChain(num_qubits)
+        start=time.time()
+        processor.load_circuit(qc)
+        end=time.time()
+        compiling_time_sample.append(end - start)
+        start=time.time()
+        state2 = processor.run_state(basis([2]*num_qubits, [0]*num_qubits)).states[-1]
+        end=time.time()
+        simulation_time_sample.append(end - start)
+    compiling_time.append(np.average(compiling_time_sample))
+    simulation_time.append(np.average(simulation_time_sample))
+
+# Recorded results
+compiling_time = [0.0012659549713134766,
+ 0.008200037479400634,
+ 0.028502225875854492,
+ 0.07625222206115723,
+ 0.14860657453536988,
+ 0.26267787218093874,
+ 0.4118484020233154,
+ 0.644087290763855,
+ 0.9500880241394043,
+ 1.287346065044403]
+simulation_time = [0.007144343852996826,
+ 0.019202685356140135,
+ 0.04985215663909912,
+ 0.11975035667419434,
+ 0.28220993280410767,
+ 0.6953113317489624,
+ 1.624202561378479,
+ 4.255168056488037,
+ 11.539598500728607,
+ 31.34040207862854]
+
+fig, ax = plt.subplots(figsize=(LINEWIDTH, LINEWIDTH/3*2), dpi=200)
+ax.plot(range(1, 11), compiling_time, "-s", markersize=4, label="solver")
+ax.plot(range(1, 11), simulation_time, "-D", markersize=4, label="compiling")
+ax.set_ylabel("Simulation time [s]")
+ax.set_xlabel("Number of qubits")
+ax.set_yscale("log")
+ax.legend()
+fig.tight_layout()
+fig.savefig("runtime.pdf")
+plt.show()
diff --git a/doc/source/apidoc.rst b/doc/source/apidoc.rst
index 3637c7fd..695f6639 100644
--- a/doc/source/apidoc.rst
+++ b/doc/source/apidoc.rst
@@ -17,7 +17,6 @@ Simulation based on operator-state multiplication.
    qutip_qip.circuit
    qutip_qip.operations
    qutip_qip.qubits
-   qutip_qip.decompose
    qutip_qip.qasm
 
 Pulse-level simulation
diff --git a/doc/source/apidoc/qutip_qip.decompose.rst b/doc/source/apidoc/qutip_qip.decompose.rst
deleted file mode 100644
index 96cf24ee..00000000
--- a/doc/source/apidoc/qutip_qip.decompose.rst
+++ /dev/null
@@ -1,15 +0,0 @@
-﻿:orphan:
-
-qutip\_qip.decompose
-====================
-
-.. automodule:: qutip_qip.decompose
-   :members:
-   :show-inheritance:
-   :imported-members:
-
-   .. rubric:: Functions
-
-   .. autosummary::
-
-      decompose_one_qubit_gate
diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst
index b7bad67f..f280f2ef 100644
--- a/doc/source/changelog.rst
+++ b/doc/source/changelog.rst
@@ -1,8 +1,43 @@
 .. _changelog:
 
 **********
-Change Log
+Changelog
 **********
+Version 0.2.0 (Nov 26, 2021)
+++++++++++++++++++++++++++++
+This release adds a few new features to the pulse-level simulator.
+
+PRs are collected `https://github.com/qutip/qutip-qip/milestone/3?closed=1 <https://github.com/qutip/qutip-qip/milestone/3?closed=1>`_.
+
+Improvements
+------------
+- **MAJOR** Add the :obj:`.Model` class that represents the physical model including hardware parameters, control and drift Hamiltonians and noise objects. (`#105 <https://github.com/qutip/qutip-qip/pull/105>`_)
+- Separate the gate definition from the QubitCircuit.propagators method (`#83 <https://github.com/qutip/qutip-qip/pull/83>`_)
+- Support different pulse shapes. (`#85 <https://github.com/qutip/qutip-qip/pull/85>`_)
+- Use autosummary to generate a summary of API docs. (`#103 <https://github.com/qutip/qutip-qip/pull/103>`_)
+- Improve the scheduling algorithm. (`#105 <https://github.com/qutip/qutip-qip/pull/105>`_)
+
+.. note:: 
+    Compatibility Note: The behaviour of ``Processor.pulses`` changes significantly from version 0.1 to version 0.2. In 0.1, if no control coefficients are added, `pulses` contains a list of partially initialized :obj:`.Pulse` objects. They include control Hamiltonians but have no coefficients or tlist. This behaviour has changed. From 0.2, the list only includes controls that have non-trivial dynamics. To inspect the available control Hamiltonians, please use :obj:`.Processor.get_control` and :obj:`.Processor.get_control_labels`.
+
+
+Version 0.1.2 (Nov 25, 2021)
+++++++++++++++++++++++++++++
+This micro release adds more thorough documentation for the project and fixes a few bugs in :obj:`.QubitCircuit` and :obj:`.Processor`.
+
+PRs are collected at `https://github.com/qutip/qutip-qip/milestone/4?closed=1 <https://github.com/qutip/qutip-qip/milestone/4?closed=1>`_.
+
+Improvements
+------------
+- Efficient Hadamard transform. (`#103 <https://github.com/qutip/qutip-qip/pull/103>`_)
+- Make circuit latex code accessible in `QubitCircuit`. (`#108 <https://github.com/qutip/qutip-qip/pull/108>`_)
+
+
+Bug Fixes
+----------
+- Fix the leaking noise objects in `Processor`. (`#89 <https://github.com/qutip/qutip-qip/pull/89>`_)
+- Fix a bug in time-dependent collapse operators in  `Processor`. (`#107 <https://github.com/qutip/qutip-qip/pull/107>`_)
+
 
 Version 0.1.1 (July 28, 2021)
 +++++++++++++++++++++++++++++
diff --git a/doc/source/index.rst b/doc/source/index.rst
index ba2cff26..f285691a 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -38,7 +38,7 @@ qutip-qip: QuTiP quantum information processing
 
 .. toctree::
     :maxdepth: 2
-    :caption: Change log
+    :caption: Changelog
 
     changelog.rst
 
diff --git a/doc/source/qip-processor.rst b/doc/source/qip-processor.rst
index 93eb4356..2a4ccfb5 100644
--- a/doc/source/qip-processor.rst
+++ b/doc/source/qip-processor.rst
@@ -395,12 +395,16 @@ The second example demonstrates a biased Gaussian noise on the pulse amplitude.
     processor.add_noise(gaussnoise)
 
     # Plot the ideal pulse
-    fig1, axis1 = processor.plot_pulses(title="Original control amplitude", figsize=(5,3))
+    fig1, axis1 = processor.plot_pulses(
+        title="Original control amplitude", figsize=(5,3),
+        use_control_latex=False)
 
     # Plot the noisy pulse
     qobjevo, _ = processor.get_qobjevo(noisy=True)
     noisy_coeff = qobjevo.to_list()[1][1] + qobjevo.to_list()[2][1]
-    fig2, axis2 = processor.plot_pulses(title="Noisy control amplitude", figsize=(5,3))
+    fig2, axis2 = processor.plot_pulses(
+        title="Noisy control amplitude", figsize=(5,3),
+        use_control_latex=False)
     axis2[0].step(qobjevo.tlist, noisy_coeff)
 
 
diff --git a/setup.cfg b/setup.cfg
index 06c52c40..40be6f8d 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -8,7 +8,7 @@ keywords = quantum, physics, dynamics
 license = BSD 3-Clause License
 license_files = LICENSE
 classifiers =
-    Development Status :: 2 - Pre-Alpha
+    Development Status :: 5 - Production/Stable
     Intended Audience :: Science/Research
     License :: OSI Approved :: BSD License
     Programming Language :: Python
@@ -21,7 +21,6 @@ classifiers =
 author = Alexander Pitchford, Paul D. Nation, Robert J. Johansson, Chris Granade, Arne Grimsmo, Nathan Shammah, Shahnawaz Ahmed, Neill Lambert, Eric Giguere, Boxi Li, Jake Lishman
 author_email = qutip-admin@googlegroups.com
 platforms = Linux, Mac OSX, Unix, Windows
-version = 0.2.0dev
 
 [options]
 package_dir=
diff --git a/src/qutip_qip/decompose/__init__.py b/src/qutip_qip/_decompose/__init__.py
similarity index 100%
rename from src/qutip_qip/decompose/__init__.py
rename to src/qutip_qip/_decompose/__init__.py
diff --git a/src/qutip_qip/decompose/_utility.py b/src/qutip_qip/_decompose/_utility.py
similarity index 100%
rename from src/qutip_qip/decompose/_utility.py
rename to src/qutip_qip/_decompose/_utility.py
diff --git a/src/qutip_qip/decompose/decompose_single_qubit_gate.py b/src/qutip_qip/_decompose/decompose_single_qubit_gate.py
similarity index 99%
rename from src/qutip_qip/decompose/decompose_single_qubit_gate.py
rename to src/qutip_qip/_decompose/decompose_single_qubit_gate.py
index a1f5ba2b..cdb0de1e 100644
--- a/src/qutip_qip/decompose/decompose_single_qubit_gate.py
+++ b/src/qutip_qip/_decompose/decompose_single_qubit_gate.py
@@ -2,7 +2,7 @@
 import cmath
 
 
-from qutip_qip.decompose._utility import (
+from qutip_qip._decompose._utility import (
     check_gate,
     MethodError,
 )
diff --git a/src/qutip_qip/algorithms/__init__.py b/src/qutip_qip/algorithms/__init__.py
index e69de29b..f1c4bc07 100644
--- a/src/qutip_qip/algorithms/__init__.py
+++ b/src/qutip_qip/algorithms/__init__.py
@@ -0,0 +1,1 @@
+from .qft import *
diff --git a/src/qutip_qip/algorithms/qft.py b/src/qutip_qip/algorithms/qft.py
index 36794034..493c1e70 100644
--- a/src/qutip_qip/algorithms/qft.py
+++ b/src/qutip_qip/algorithms/qft.py
@@ -4,9 +4,10 @@
 
 
 import numpy as np
-from ..operations import snot, cphase, swap
+from ..operations import Gate, snot, cphase, swap
 from ..circuit import QubitCircuit
 from qutip import Qobj
+from qutip_qip._decompose import decompose_one_qubit_gate
 
 
 __all__ = ["qft", "qft_steps", "qft_gate_sequence"]
@@ -78,7 +79,7 @@ def qft_steps(N=1, swapping=True):
     return U_step_list
 
 
-def qft_gate_sequence(N=1, swapping=True):
+def qft_gate_sequence(N=1, swapping=True, to_cnot=False):
     """
     Quantum Fourier Transform operator on N qubits returning the gate sequence.
 
@@ -105,16 +106,42 @@ def qft_gate_sequence(N=1, swapping=True):
     else:
         for i in range(N):
             for j in range(i):
-                qc.add_gate(
-                    "CPHASE",
-                    targets=[j],
-                    controls=[i],
-                    arg_label=r"{\pi/2^{%d}}" % (i - j),
-                    arg_value=np.pi / (2 ** (i - j)),
-                )
+                if not to_cnot:
+                    qc.add_gate(
+                        "CPHASE",
+                        targets=[j],
+                        controls=[i],
+                        arg_label=r"{\pi/2^{%d}}" % (i - j),
+                        arg_value=np.pi / (2 ** (i - j)),
+                    )
+                else:
+                    decomposed_gates = _cphase_to_cnot(
+                        [j], [i], np.pi / (2 ** (i - j))
+                    )
+                    qc.gates.extend(decomposed_gates)
             qc.add_gate("SNOT", targets=[i])
         if swapping:
             for i in range(N // 2):
                 qc.add_gate("SWAP", targets=[N - i - 1, i])
-
     return qc
+
+
+def _cphase_to_cnot(targets, controls, arg_value):
+    rotation = Qobj([[1.0, 0.0], [0.0, np.exp(1.0j * arg_value)]])
+    decomposed_gates = list(
+        decompose_one_qubit_gate(rotation, method="ZYZ_PauliX")
+    )
+    new_gates = []
+    gate = decomposed_gates[0]
+    gate.targets = targets
+    new_gates.append(gate)
+    new_gates.append(Gate("CNOT", targets=targets, controls=controls))
+    gate = decomposed_gates[4]
+    gate.targets = targets
+    new_gates.append(gate)
+    new_gates.append(Gate("CNOT", targets=targets, controls=controls))
+    new_gates.append(Gate("RZ", targets=controls, arg_value=arg_value / 2))
+    gate = decomposed_gates[7]
+    gate.arg_value += arg_value / 4
+    new_gates.append(gate)
+    return new_gates


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 01b5b5bb..8404390e 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -4,7 +4,6 @@ on:
   push:
     branches: [ master ]
   pull_request:
-    branches: [ master ]
 
 jobs:
   test:
diff --git a/tests/decomposition_functions/test_single_qubit_gate_decompositions.py b/tests/decomposition_functions/test_single_qubit_gate_decompositions.py
index 333938c2..b60a7edc 100644
--- a/tests/decomposition_functions/test_single_qubit_gate_decompositions.py
+++ b/tests/decomposition_functions/test_single_qubit_gate_decompositions.py
@@ -5,12 +5,12 @@
 from qutip import (
     Qobj, average_gate_fidelity, rand_unitary, sigmax, sigmay, sigmaz
 )
-from qutip_qip.decompose.decompose_single_qubit_gate import (
+from qutip_qip._decompose.decompose_single_qubit_gate import (
     _ZYZ_rotation,
     _ZXZ_rotation,
     _ZYZ_pauli_X,
 )
-from qutip_qip.decompose import decompose_one_qubit_gate
+from qutip_qip._decompose import decompose_one_qubit_gate
 from qutip_qip.circuit import QubitCircuit
 from qutip_qip.operations.gates import snot, sqrtnot
 
diff --git a/tests/decomposition_functions/test_utility.py b/tests/decomposition_functions/test_utility.py
index b4e97433..1a70fc0f 100644
--- a/tests/decomposition_functions/test_utility.py
+++ b/tests/decomposition_functions/test_utility.py
@@ -2,7 +2,7 @@
 import pytest
 
 from qutip import Qobj, qeye
-from qutip_qip.decompose._utility import (
+from qutip_qip._decompose._utility import (
     check_gate,
 )
 
diff --git a/tests/test_circuit.py b/tests/test_circuit.py
index 9c7a8971..a4010431 100644
--- a/tests/test_circuit.py
+++ b/tests/test_circuit.py
@@ -14,7 +14,7 @@
     _para_gates
 )
 
-from qutip_qip.decompose.decompose_single_qubit_gate import _ZYZ_rotation
+from qutip_qip._decompose.decompose_single_qubit_gate import _ZYZ_rotation
 
 import qutip as qp
 
diff --git a/tests/test_qft.py b/tests/test_qft.py
index cacc5f49..9aad86e9 100644
--- a/tests/test_qft.py
+++ b/tests/test_qft.py
@@ -50,5 +50,15 @@ def testQFTGateSequenceWithSwapping(self):
             for i in range(phases, phases + swaps):
                 assert_string_equal(circuit.gates[i].name, "SWAP")
 
+    def testQFTGateSequenceWithCNOT(self):
+        """
+        qft: Inspect swap gates added to gate sequences if
+        it is decomposed into cnot.
+        """
+        for N in range(1, 6):
+            circuit = qft_gate_sequence(N, swapping=False, to_cnot=True)
+
+        assert not any([gate.name == "CPHASE" for gate in circuit.gates])
+
 if __name__ == "__main__":
     run_module_suite()