Download
Download this file as Jupyter notebook: phase_tracking.ipynb.
Example: 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 \(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 multiqubit 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 nonvirtual 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 singlequbit circuit expressed as rotations in matrix multiplication order. This circuit is assumed to be the output of a precompiler that has already converted some original circuit into \(R_x(90)\) and \(R_z(\phi)\) rotations.
This can be rewritten as
and further simplified to
where we have defined a nutation about some vector in the xy plane as
Thus if a pulse shape for the \(R_x(90)\) gate is tuned up, and our control electronics are able to rotate it in quadrature by arbitrary angles thereby producing operations \(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 \(R_z(c)\cdot R_x(90) \cdot R_z(b) \cdot R_x(90) \cdot R_z(a)\) where \(a\), \(b\), \(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 zaxis, \(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 zaxis, whose outcome will not be affected by a zrotation.
The process of phase tracking on multiqubit 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.
[2]:
import trueq as tq
import numpy as np
Using the ControlledZ Gate
A device whose only native twoqubit 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 xy 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
[3]:
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 phasetracking compiler pattern. Note that it chooses the
virtual gate factory by looking for a rotation about the Zaxis in units of degrees.
If this is not the case for you, specify any other single qubit rotation manually
using virtual=factory
.
[4]:
phase_tracker = tq.compilation.PhaseTrack(factories=factories)
Next we set up our compiler and the rest of its passes. There are two essential steps:
Convert all abstract gates into the static gates and the virtual gate (
Native1Q
andNative2Q
).Use the
PhaseTrack
pattern to accumulate phases and compile them into the parametric gate parameters.
There are additionally justification, merge, and identity removal bookkeeping passes that can be adjusted to one’s needs.
[5]:
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 \(n\) qubits. Before compilation, our abstract circuit on 4 qubits is as follows:
[6]:
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)
[6]:
Circuit



(0):
Gate.h



(0, 1):
Gate.cx



(1, 2):
Gate.cx



(2, 3):
Gate.cx


1

(0):
Meas()

(1):
Meas()

(2):
Meas()

(3):
Meas()

The output bitstring distribution is as expected:
[7]:
tq.Simulator().sample(ghz(4), n_shots=np.inf).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.
[8]:
circuit = compiler.compile(ghz(4))
circuit
[8]:
Circuit



(0):
r90(theta)

(1):
r90(theta)



(0):
r90(theta)

(1):
r90(theta)



(0, 1):
cz()



(1):
r90(theta)

(2):
r90(theta)



(1):
r90(theta)

(2):
r90(theta)



(1, 2):
cz()



(2):
r90(theta)

(3):
r90(theta)



(2):
r90(theta)

(3):
r90(theta)



(2, 3):
cz()



(0):
r90(theta)

(1):
r90(theta)

(2):
r90(theta)

(3):
r90(theta)


(0):
r90(theta)

(1):
r90(theta)

(2):
r90(theta)

(3):
r90(theta)

1

(0):
Meas()

(1):
Meas()

(2):
Meas()

(3):
Meas()

Finally, our output bitstring distribution is unchanged, as expected.
[9]:
tq.Simulator().sample(circuit, n_shots=np.inf).plot()
Using the CrossResonance Gate
The maximally entangling crossresonance gate is equal to
By adjusting the phase of the microwave control used to generate this gate, we can rotate action of the second qubit about the zaxis, which results in the parametric gate
This parametric gate is defined below as a gate factory, along with other factories necessary for this example.
[10]:
# 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 phasetracking using our native gates, and test it on a circuit that generates a 4qubit GHZ state.
[11]:
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
[11]:
Circuit



(0):
r90(theta)

(1):
r90(theta)



(0):
r90(theta)

(1):
r90(theta)



(0, 1):
cr(theta)



(1):
r90(theta)

(2):
r90(theta)



(1):
r90(theta)

(2):
r90(theta)



(1, 2):
cr(theta)



(2):
r90(theta)

(3):
r90(theta)



(2):
r90(theta)

(3):
r90(theta)



(2, 3):
cr(theta)



(3):
r90(theta)



(3):
r90(theta)


1

(0):
Meas()

(1):
Meas()

(2):
Meas()

(3):
Meas()

Finally, we verify that our bitstring output is as expected.
[12]:
tq.Simulator().sample(circuit, n_shots=np.inf).plot()
Using the MølmerSørensen Gate
A maximally entangling MS gate has the form
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
where \(R_\phi(\theta)\) is defined above.
This parametric gate is defined below as a gate factory, along with other factories necessary for this example.
[13]:
# factories for our parametric gates. the first is a 90 degree rotation about an axis in
# the xy plane, equivalent to z(a)*x(90)*z(a). the second is a MølmerSørensen 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
[13]:
{x90(): ({0: 'theta'},
GateFactory(name='r90', layers=[Rotation.from_pauli('Z', 'theta', 1.0), FixedRotation.from_pauli('X', 90.0), Rotation.from_pauli('Z', 'theta', 1.0)], parameters={'theta': None})),
ms0(): ({1: 'b', 0: 'a'},
GateFactory(name='ms', layers=[Rotation.from_pauli('IZ', 'b', 1.0), Rotation.from_pauli('ZI', 'a', 1.0), FixedRotation.from_pauli('XX', 90.0), Rotation.from_pauli('ZI', 'a', 1.0), Rotation.from_pauli('IZ', 'b', 1.0)], parameters={'b': None, 'a': None}))}
As in the CZ section, we define a compiler that performs phasetracking using our native gates, and test it on a circuit that generates a 4qubit GHZ state.
[14]:
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
[14]:
Circuit



(0):
r90(theta)



(0):
r90(theta)



(0, 1):
ms(b, a)



(1):
r90(theta)



(1):
r90(theta)



(1, 2):
ms(b, a)



(2):
r90(theta)



(2):
r90(theta)



(2, 3):
ms(b, a)



(0):
r90(theta)

(1):
r90(theta)

(2):
r90(theta)

(3):
r90(theta)


(0):
r90(theta)

(1):
r90(theta)

(2):
r90(theta)

(3):
r90(theta)

1

(0):
Meas()

(1):
Meas()

(2):
Meas()

(3):
Meas()

Finally, we verify that our bitstring output is as expected.
[15]:
tq.Simulator().sample(circuit, n_shots=np.inf).plot()
Download
Download this file as Jupyter notebook: phase_tracking.ipynb.