Compilation¶
Native gates and costs¶
Each set of single-qubit gates in a sentence is a local layer, so a sentence of \(n\) native gates has \(n+1\) local layers. The cost of a sentence is
where \(c(g)\) is the cost of native gate \(g\) and
\(c_{\mathrm{local}}\) is local_layer_cost, which defaults to zero.
When costs are durations, set local_layer_cost to the duration of one
layer of single-qubit gates. It can also model a fixed cost per native
gate, such as pulse ramp times.
A local-layer charge can make a shorter sentence of more expensive native gates the cheapest. For this target, four \(\sqrt{\mathrm{CX}}\) gates cost 360 ns, and one \(\sqrt{\mathrm{CX}}\) with two \(\sqrt[3]{\mathrm{iSWAP}}\) gates costs 370 ns. A 20 ns charge for each local layer adds 100 ns to the first sentence and 80 ns to the second, so the three-gate sentence wins:
from qiskit.circuit.library import CSXGate, iSwapGate
from qiskit.quantum_info import Operator, random_unitary
from gulps.decomposition import GulpsDecomposer
gates = [CSXGate(), iSwapGate().power(1 / 3)]
target_unitary = random_unitary(4, seed=3)
for local_layer_cost in [0, 20]:
decomposer = GulpsDecomposer(
gates, costs=[90, 140], local_layer_cost=local_layer_cost
)
cost, sentence = decomposer.select(target_unitary)
print(f"local_layer_cost={local_layer_cost}: cost {cost:g} ns,",
[gate.name for gate in sentence])
local_layer_cost=0: cost 360 ns, ['csx', 'csx', 'csx', 'csx']
local_layer_cost=20: cost 450 ns, ['csx', 'xx_plus_yy', 'xx_plus_yy']
from qiskit.quantum_info import process_fidelity
compiled = decomposer(target_unitary)
print(f"Process fidelity: {process_fidelity(Operator(compiled), target_unitary):.12f}")
compiled.draw("mpl")
Process fidelity: 1.000000000000
Qiskit circuits¶
GulpsDecompositionPass groups the gates of a
circuit into two-qubit blocks and synthesizes each block with the
decomposer.
from qiskit.circuit.library import quantum_volume
from qiskit.transpiler import PassManager
from qiskit.transpiler.passes import (
HighLevelSynthesis, Unroll3qOrMore, Optimize1qGatesDecomposition,
)
from gulps.transpiler import GulpsDecompositionPass
decomposer = GulpsDecomposer(gates, costs=[90, 140], local_layer_cost=20)
circuit = quantum_volume(3, depth=3, seed=5)
pipeline = PassManager([
HighLevelSynthesis(),
Unroll3qOrMore(),
GulpsDecompositionPass(decomposer),
Optimize1qGatesDecomposition(basis=["u"]),
])
compiled = pipeline.run(circuit)
compiled.draw("mpl", fold=-1, scale=0.5)
With a backend Target, select GULPS as the translation method. The
plugin reads the native gates and their durations for each qubit pair from
the Target.
Build a Target
from itertools import permutations
from qiskit.circuit import Parameter
from qiskit.circuit.library import UGate
from qiskit.transpiler import InstructionProperties, Target
pairs = list(permutations(range(3), 2))
target = Target(num_qubits=3)
target.add_instruction(
CSXGate(), {p: InstructionProperties(duration=90e-9, error=3e-3) for p in pairs}
)
target.add_instruction(
iSwapGate().power(1 / 3),
{p: InstructionProperties(duration=140e-9, error=8e-3) for p in pairs},
)
target.add_instruction(
UGate(Parameter("a"), Parameter("b"), Parameter("c")),
{(q,): InstructionProperties(duration=20e-9, error=1e-4) for q in range(3)},
)
from qiskit import transpile
compiled = transpile(
circuit, target=target, translation_method="gulps", optimization_level=1
)
print(dict(compiled.count_ops()))
{'u': 17, 'csx': 6, 'xx_plus_yy': 1}
Warning
Optimization levels 2 and 3 can undo the least-cost sentences. Their later passes resynthesize two-qubit blocks with Qiskit’s own decomposers, which minimize the number of two-qubit gates instead of your costs.
A pass built from a Target minimizes the Target property named by cost,
"duration" (the default) or "error". With these error rates,
minimizing error uses only \(\sqrt{\mathrm{CX}}\):
pipeline = PassManager([
HighLevelSynthesis(),
Unroll3qOrMore(),
GulpsDecompositionPass(target, cost="error"),
Optimize1qGatesDecomposition(basis=["u"]),
])
print(dict(pipeline.run(circuit).count_ops()))
{'u': 19, 'csx': 8}
Synthesis costs in other passes¶
select returns the cost of a block without constructing its circuit.
Other transpiler passes can use it to compare choices by their compiled
cost instead of by gate counts. MIRAGE
introduced this idea for routing: a router can implement a block \(U\)
or its mirror \(\mathrm{SWAP}\,U\), which applies
\(U\) and then exchanges the two qubits. Using the mirror updates the
qubit mapping instead of inserting a SWAP gate.
from qiskit.circuit.library import quantum_volume, SwapGate
from gulps.analysis.coverage import two_qubit_blocks
routing_circuit = quantum_volume(4, depth=2, seed=1)
blocks = two_qubit_blocks(routing_circuit)
swap = Operator(SwapGate()).data
direct = decomposer.select(blocks)
mirrored = decomposer.select([swap @ Operator(block).data for block in blocks])
print("Block Original (ns) Mirrored (ns)")
for i, ((cost, _), (mirror_cost, _)) in enumerate(zip(direct, mirrored)):
print(f"{i:5} {cost:15g} {mirror_cost:15g}")
Block Original (ns) Mirrored (ns)
0 350 400
1 400 350
2 400 350
3 350 400
Because a mirror moves the logical qubits, the router weighs each saving against its effect on later gates.
Local equivalence¶
Two gates are locally equivalent when single-qubit gates before and after one of them give the other, up to global phase. For example, Hadamard gates on the target qubit before and after CZ give CX. GULPS searches over these equivalence classes, not over matrices, because the local layers of a sentence can absorb any difference within a class.
from qiskit.circuit.library import CXGate, CZGate
from gulps.invariants import LocalEquivalenceClass
cx, cz = LocalEquivalenceClass.from_unitaries([CXGate(), CZGate()])
print("Same class:", cx == cz)
print("Weyl coordinates:", cx.weyl.round(3) + 0.0)
Same class: True
Weyl coordinates: [0.5 0. 0. ]
Each class has one canonical gate, set by its three Weyl coordinates:
Here \(XX=X\otimes X\), and likewise for \(YY\) and \(ZZ\).
Reachable regions¶
The reachable region of a sentence is the set of classes that it implements as its local layers vary.
from gulps.analysis.region import ReachableRegion
from gulps.analysis.viz.polytope_viz import plot_region
sqrt_iswap = LocalEquivalenceClass.from_unitary(iSwapGate().power(1 / 2))
swap_class = LocalEquivalenceClass.from_unitary(SwapGate())
one = ReachableRegion.of([sqrt_iswap])
two = ReachableRegion.of([sqrt_iswap, sqrt_iswap])
print("One √iSWAP reaches CX:", one.reaches(cx))
print("Two √iSWAP reach CX:", two.reaches(cx))
print("Two √iSWAP reach SWAP:", two.reaches(swap_class))
One √iSWAP reaches CX: False
Two √iSWAP reach CX: True
Two √iSWAP reach SWAP: False
ax = plot_region(two, color="tab:blue")
ax.scatter(*cx.weyl, color="tab:orange", s=60, label="CX / CZ")
ax.scatter(*swap_class.weyl, color="tab:red", s=60, marker="x", label="SWAP")
ax.set_title("Reachable with two √iSWAP gates")
ax.legend();
The class of CX and CZ lies in the shaded region, so two \(\sqrt{\mathrm{iSWAP}}\) gates implement both. SWAP lies outside, so no choice of local layers makes two \(\sqrt{\mathrm{iSWAP}}\) gates implement SWAP. The outer tetrahedron is the Weyl chamber, the set of all Weyl coordinates, and the inner wireframe encloses the perfect entanglers. The shaded region includes both global-phase representatives of each class.
How synthesis works describes how GULPS searches these regions in order of cost.