# %% # # Copyright 2021 Quantum Benchmark Inc. # """ Phase Tracking with the Compiler ================================ """ # %% # Phase tracking is a technique that exploits the ability of quantum control hardware to # perform accurate and arbitrary phase rotations of pulse shapes in order to reduce the # number of pulse shapes required to form a device's native quantum gate set. Moreover, # it causes the native gate set to be continuously parametric which greatly reduces the # synthesis cost of arbitrary unitaries. For example, if an :math:`R_x(90)` gate is # tuned up, then any single qubit unitary can be synthesized using two of these pulses # and phase tracking. # # Below, we call these phase rotation gates *virtual gates* because they are not # directly used to update the quantum state. Instead, virtual gates are better thought # of as pulse compiler directives, where the pulse compiler is a program that prepares a # sequence of waveforms for the control electronics given a circuit of native gates. In # a multi-qubit device that employs phase tracking, the pulse compiler accumulates a # phase on each qubit in the circuit whenever a virtual gate is encountered. Any time a # non-virtual gate is encountered on a given subset of qubits, the corresponding # cummulative phases are used to modify the phases of the pulse shape. # # We illustrate how phase tracking works using the following single-qubit circuit # expressed as rotations in matrix multiplication order. This circuit is assumed to # be the output of a pre-compiler that has already converted some original circuit into # :math:`R_x(90)` and :math:`R_z(\phi)` rotations. # # .. math:: # # R_z(d)\cdot R_x(90) \cdot R_z(c)\cdot R_x(90) # \cdot R_z(b) \cdot R_x(90) \cdot R_z(a) # # This can be rewritten as # # .. math:: # # R_z(a+b+c+d) \cdot R_z(-(a+b+c)) \cdot R_x(90) \cdot R_z(a+b+c) # \\ \quad # \cdot R_z(-(a+b)) \cdot R_x(90) \cdot R_z(a+b) \cdot R_z(-a) # \cdot R_x(90) \cdot R_z(a) # # and further simplified to # # .. math:: # # R_z(a+b+c+d)R_{a+b+c}(90)R_{a+b}(90) R_a(90) # # where we have defined a nutation about some vector in the x-y plane as # # .. math :: # # R_\phi(\theta)=R_z(-\phi)R_x(90)R_z(\phi) # =\operatorname{exp}(-i \theta (\cos(\phi)X-\sin(\phi)Y)/2). # # Thus if a pulse shape for the :math:`R_x(90)` gate is tuned up, and our control # electronics are able to rotate it in quadrature by arbitrary angles thereby producing # operations :math:`R_\phi(90)`, then we have sufficient control to perform the # original circuit. Note further that any qubit unitary can be decomposed into an # alternating sequence :math:`R_z(c)\cdot R_x(90) \cdot R_z(b) \cdot R_x(90) \cdot # R_z(a)` where :math:`a`, :math:`b`, :math:`c` are its ZXZ Euler angles. It follows # that this is the *only* pulse shape that is required to perform single qubit gates. # # For most systems, the final accumulated phase in the z-axis, :math:`R_z(a+b+c+d)`, # does not actually need to be implemented on the qubit. This is because the final # operation will be a measurement of the qubit along the z-axis, whose outcome will not # be affected by a z-rotation. # # The process of phase tracking on multi-qubit gates is similar to the single qubit # case, which will be seen in the examples below. As previously mentioned, a different # phase needs to be tracked for each qubit, and pulse shapes may require a parameter for # every qubit they act on. import trueq as tq import numpy as np # %% # Using the Controlled-Z Gate # --------------------------- # A device whose only native two-qubit gate is the CZ gate is the simplest case because # this commutes with the virtual Z gate on both qubits. This means we only need to apply # accumulated phases to the single qubit gates. # # First, we define a config object that contains all of the necessary gate factories. # Since we will be doing phase tracking, we require not only the parametric native gate # factories and the virtual gate factory, but also a factory for each of the static # gates that correspond to the parametric gates at parameter value 0. # factories for our parametric gates. the first is a 90 degree rotation about an # axis in the x-y plane, equivalent to z(-a)*x(90)*z(a). the second is our cz gate # which happens to need no parameters because it commutes with virtual gates r90 = tq.config.GateFactory.from_hamiltonian( "r90", [["Z", "-theta"], ["X", "90"], ["Z", "theta"]] ) cz = tq.config.GateFactory.from_matrix("cz", np.diag([1, 1, 1, -1])) # factory for our virtual gate z = tq.config.GateFactory.from_hamiltonian("z", [["Z", "theta"]]) # factory for static gates, equal to our parametric gates at 0 x90 = tq.config.GateFactory.from_matrix("x90", r90(0)) factories = [x90, z, cz, r90] # %% # Next, we initialize our phase-tracking compiler pattern. Note that it chooses the # virtual gate factory by looking for a rotation about the Z-axis in units of degrees. # If this is not the case for you, specify any other single qubit rotation manually # using ``virtual=factory``. phase_tracker = tq.compilation.PhaseTrack(factories=factories) # %% # Next we set up our compiler and the rest of its passes. There are two essential steps: # # 1. Convert all abstract gates into the static gates and the virtual gate # (:py:class:`~trueq.compilation.Native1Q` and # :py:class:`~trueq.compilation.Native2Q`). # 2. Use the :py:class:`~trueq.compilation.PhaseTrack` pattern to accumulate phases and # compile them into the parametric gate parameters. # # There are additionally justification, merge, and identity removal book-keeping passes # that can be adjusted to one's needs. compiler = tq.Compiler( [ tq.compilation.Native2Q(factories), tq.compilation.Justify(), tq.compilation.Merge(), tq.compilation.RemoveId(), tq.compilation.Native1Q(factories), phase_tracker, tq.compilation.RemoveEmptyCycle(), ] ) # %% # We illustrate the output of this compiler using a circuit that generates the GHZ state # on :math:`n` qubits. Before compilation, our abstract circuit on 4 qubits is as # follows: def ghz(n): circuit = tq.Circuit([{0: tq.Gate.h}]) for idx in range(n - 1): circuit.append({(idx, idx + 1): tq.Gate.cnot}) circuit.measure_all() return circuit ghz(4) # %% # The output bitstring distribution is as expected: tq.Simulator().state(ghz(4)).to_results().plot() # %% # Following compilation, our circuit contains only gates that were generated using the # ``r90`` or ``cz`` factory. The parameters the factories used to construct the gates # are stored in the gate objects, along with the gates' matrix representations. Single # qubit gates, under this compiler, are always decomposed into two 90 degree nutations. circuit = compiler.compile(ghz(4)) circuit # %% # Finally, our output bitstring distribution is unchanged, as expected. tq.Simulator().state(circuit).probabilities().to_results().plot() # %% # Using the Cross-Resonance Gate # ------------------------------ # The maximally entangling cross-resonance gate is equal to # # .. math:: # CR=\begin{pmatrix} # 1 & -i & 0 & 0 \\ -i & 1 & 0 & 0 \\ 0 & 0 & 1 & i \\ 0 & 0 & i & 1 # \end{pmatrix}/\sqrt{2} # =\frac{I\otimes I-iZ\otimes X}{\sqrt{2}}. # # By adjusting the phase of the microwave control used to generate this gate, we can # rotate action of the second qubit about the z-axis, which results in the parametric # gate # # .. math:: # # CR(\phi) # = \frac{I\otimes I-i\cos(\phi)Z\otimes X+i\sin(\phi)Z\otimes Y}{\sqrt{2}}. # # # This parametric gate is defined below as a gate factory, along with other factories # necessary for this example. # define parametric gate factories r90 = tq.config.GateFactory.from_hamiltonian( "r90", [["Z", "-theta"], ["X", "90"], ["Z", "theta"]] ) cr = tq.config.GateFactory.from_hamiltonian( "cr", [["IZ", "-theta"], ["ZX", "90"], ["IZ", "theta"]] ) # factory for our virtual gate z = tq.config.GateFactory.from_hamiltonian("z", [["Z", "theta"]]) # factory for static gates, equal to our parametric gates at 0 x90 = tq.config.GateFactory.from_matrix("x90", r90(0)) cr0 = tq.config.GateFactory.from_matrix("cr0", cr(0)) # put everything into a factory list. static gates should go first factories = [x90, z, cr0, r90, cr] # %% # As in the CZ section, we define a compiler that performs phase-tracking using our # native gates, and test it on a circuit that generates a 4-qubit GHZ state. compiler = tq.Compiler( [ tq.compilation.Native2Q(factories), tq.compilation.Justify(), tq.compilation.Merge(), tq.compilation.RemoveId(), tq.compilation.Native1Q(factories), tq.compilation.PhaseTrack(factories), tq.compilation.Justify(), ] ) circuit = compiler.compile(ghz(4)) circuit # %% # Finally, we verify that our bitstring output is as expected. tq.Simulator().state(circuit).probabilities().to_results().plot() # %% # Using the Molmer-Sorensen Gate # ------------------------------ # A maximally entangling MS gate has the form # # .. math:: # # MS(90)=\begin{pmatrix} # 1 & 0 & 0 & -i \\ 0 & 1 & -i & 0 \\ 0 & -i & 1 & 0 \\ -i & 0 & 0 & 1 # \end{pmatrix}/\sqrt{2} # =\frac{I\otimes I-iX\otimes X}{/\sqrt{2}} # # which, for a trapped ion, is generated by lasing on both qubits simultaneously. # However, by changing the phase of the frequency sources sent to the optical modulators # on each qubit, the azimuthal angle of the MS gate can be changed independently on both # qubits qubits. This allows the implementation of the parametric gate # # .. math:: # # MS(90, \phi_1, \phi_2) = \frac{II-iR_{\phi_1}(90)\otimes R_{\phi_2}(90)}{\sqrt{2}} # # where :math:`R_\phi(\theta)` is defined above. # # This parametric gate is defined below as a gate factory, along with other factories # necessary for this example. # factories for our parametric gates. the first is a 90 degree rotation about an axis in # the x-y plane, equivalent to z(a)*x(90)*z(-a). the second is a molmer-sorensen gate # with phase rotation a on the first qubit and b on the second qubit, i.e. equivalent to # (z(a) & z(b))*xx(90)*(z(-a) & z(-b)) r90 = tq.config.GateFactory.from_hamiltonian( "r90", [["Z", "-theta"], ["X", "90"], ["Z", "theta"]] ) ms = tq.config.GateFactory.from_hamiltonian( "ms", [["IZ", "-b"], ["ZI", "-a"], ["XX", 90], ["ZI", "a"], ["IZ", "b"]] ) # factory for our virtual gate z = tq.config.GateFactory.from_hamiltonian("z", [["Z", "theta"]]) # factory for static gates, equal to our parametric gates at 0 x90 = tq.config.GateFactory.from_matrix("x90", r90(0)) ms0 = tq.config.GateFactory.from_matrix("ms0", ms(0, 0)) # put everything into a factory list factories = [x90, z, ms0, r90, ms] # %% phase_tracker = tq.compilation.PhaseTrack(factories) phase_tracker._rules # %% # As in the CZ section, we define a compiler that performs phase-tracking using our # native gates, and test it on a circuit that generates a 4-qubit GHZ state. compiler = tq.Compiler( [ tq.compilation.Native2Q(factories), tq.compilation.Justify(), tq.compilation.Merge(), tq.compilation.RemoveId(), tq.compilation.Native1Q(factories), tq.compilation.PhaseTrack(factories), tq.compilation.Justify(), ] ) circuit = compiler.compile(ghz(4)) circuit # %% # Finally, we verify that our bitstring output is as expected. tq.Simulator().state(circuit).probabilities().to_results().plot()