Transpiler

The generate_boxing_pass_manager() function is a flexible and convenient tool to build a qiskit.transpiler.PassManager able to group the circuit instructions into annotated boxes. Whether you want to automate your workflow, you are interested in trying different boxing strategies for your circuits, or you simply want a quick and easy alternative to grouping and annotating by hand, generate_boxing_pass_manager() can help you achieve your goals.

This guide illustrates how to use generate_boxing_pass_manager() and its arguments. To highlight the effects of each of the function’s arguments, in the sections that follow we will mainly target the circuit below.

from qiskit.circuit import Parameter, QuantumCircuit

circuit = QuantumCircuit(4, 7)
circuit.h(1)
circuit.h(2)
circuit.cz(1, 2)
circuit.h(1)
circuit.cx(1, 0)
circuit.cx(2, 3)
circuit.measure(range(1, 4), range(3))
circuit.cx(0, 1)
circuit.cx(1, 2)
circuit.cx(2, 3)
for qubit in range(4):
    circuit.rz(Parameter(f"th_{qubit}"), qubit)
    circuit.rx(Parameter(f"phi_{qubit}"), qubit)
    circuit.rz(Parameter(f"lam_{qubit}"), qubit)
circuit.measure(range(4), range(3, 7))

circuit.draw("mpl", scale=0.8)
../_images/dc95ae5bfb324571ccc285658959159283ded5c691014771baab33548d724130.png

Group operations into boxes

The argument enable_gates can be set to True or False to specify whether the two-qubit gates should be grouped into boxes. Similarly, enable_measures allows specifying whether or not measurements should be grouped. The following snippet shows an example where both gates and measurements are grouped.

from samplomatic.transpiler import generate_boxing_pass_manager

boxing_pass_manager = generate_boxing_pass_manager(
    enable_gates=True,
    enable_measures=True,
)
transpiled_circuit = boxing_pass_manager.run(circuit)
transpiled_circuit.draw("mpl", scale=0.8)
../_images/a8e6c239946c3a9e3387b19ce89b055f3390250fb40c1fc18a1f28431320fbfc.png

As can be seen in the figure, the pass manager creates boxes of two types: those that contain a single layer of two-qubit gates, and those that contain a single layer of measurements. This separation reflects standard practices in noise learning and mitigation protocols, which usually target layers of homogeneous operations. The two-qubit gates and measurements are placed in the leftmost box that can accommodate them, and every single-qubit gates is placed in the same box as the two-qubit gate or measurement they preceed.

The following snippet shows another example where enable_gates is set to False. As can be seen, the two-qubit gates are not grouped into boxes, nor are the single-qubit gates that preceed them.

boxing_pass_manager = generate_boxing_pass_manager(
    enable_gates=False,
    enable_measures=True,
)
transpiled_circuit = boxing_pass_manager.run(circuit)
transpiled_circuit.draw("mpl", scale=0.8)
../_images/76d44bc95349dfa29c001f512d8ee486f19980b710d8be0b12235f2184489e43.png

Choose how to annotate your boxes

All the two-qubit gates and measurement boxes in the returned circuit own left-dressed annotations. In particular, all the boxes that contain two-qubit gates are annotated with a Twirl, while for measurement boxes, users can choose between Twirl, BasisTranform (with mode aset to "measure"), or both. The following code generates a circuit where the all the boxes are twirled, and the measurement boxes are additionally annotated with BasisTranform.

boxing_pass_manager = generate_boxing_pass_manager(
    enable_gates=True,
    enable_measures=True,
    measure_annotations="all",
)
transpiled_circuit = boxing_pass_manager.run(circuit)

Prepare your circuit for noise injection

The inject_noise_targets allows specifying what boxes should receive an InjectNoise annotation. As an example, the following snippet generates a circuit where the two-qubit gates boxes own an InjectNoise annotation but the measurement boxes do not.

boxing_pass_manager = generate_boxing_pass_manager(
    enable_gates=True,
    enable_measures=True,
    inject_noise_targets="gates",
)
transpiled_circuit = boxing_pass_manager.run(circuit)

If a circuit contains two or more boxes that are equivalent up to single-qubit gates, all of them are annotated with an InjectNoise annotation with the same ref. Thus, the number of unique refs in the returned circuit is equal to the number of unique boxes, with uniqueness defined up to single-qubit gates.

By selecting the appropriate value for inject_noise_strategy, users can decide whether the InjectNoise annotations should have:

  • modifier_ref='', recommended when modifying the noise maps prior to sampling from them is not required,

  • modifier_ref=ref, recommended when all the noise maps need to be scaled uniformly by the same factor, or

  • a unique value of modifier_ref, recommended when every noise map needs to be scaled by a different factor.

The following code generates a circuit where the two-qubit gates boxes own an InjectNoise annotation with unique values of modifier_ref.

boxing_pass_manager = generate_boxing_pass_manager(
    enable_gates=True,
    enable_measures=True,
    inject_noise_targets="gates",
    inject_noise_strategy="individual_modification",
)
transpiled_circuit = boxing_pass_manager.run(circuit)

Select a twirling strategy

The boxing pass manager begins by choosing which entangling gates and measurement instructions should appear in the same boxes. Once this is accomplished, boxes will not, in general, be full-width—they will likely include only a subset of the qubits that are active in the circuit or present in the backend target. In typical configurations of the boxing pass manager, since all qubits a box instruction acts on are twirled (even those for which no instruction inside the box act on), having partial-width boxes affects the noise-tailoring effects of twirling; the idling qubits are not twirled and can, for example, build up coherent errors while the box is applied. The twirling_strategy provides options for extending the qubits owned by each box, thereby modifying the number of idling qubits:

  • "active" (default) does not extend the boxes to any idling qubits.

  • "active_accum" extends all boxes to those qubits that have already been acted on by some prior instruction in the circuit. This is useful if you want to leave qubits at the start of the circuit in their ground state for as long as possible by not twirling them until they become active.

  • "active_circuit" extends all boxes to those qubits that are acted on by any instruction in the entire circuit. This is useful if you have a structured circuit with repeated entangling layers and you want to minimize the number of unique boxes because each one will need to have its noise learned.

  • "all" extends all boxes to all qubits in all quantum registers of the circuit. This is useful as an extension to "active_circuit" where you want all qubits in the QPU to always be twirled.

To demonstrate, we apply each of the strategies to our base circuit and draw the resulting outcomes.

circuit = QuantumCircuit(5, 2)
circuit.h(range(4))
circuit.cx(0, 1)
circuit.cx(1, 2)
circuit.cx(2, 3)
circuit.cx(0, 1)
circuit.cx(1, 2)
circuit.cx(2, 3)
circuit.barrier()
circuit.h(range(4))
circuit.measure([1, 2], [0, 1])

circuit.draw("mpl", scale=0.6)
../_images/3006ac89cd34be51ab9dfa38770202be91c5d758f166f9d18eadb8910c449212.png
import matplotlib.pyplot as plt

plt.figure(figsize=(9, 6))
for idx, strategy in enumerate(["active", "active_accum", "active_circuit", "all"]):
    ax = plt.subplot(2, 2, idx + 1)
    pm = generate_boxing_pass_manager(twirling_strategy=strategy, remove_barriers="never")
    transpiled_circuit = pm.run(circuit)
    fig = transpiled_circuit.draw("mpl", scale=0.8, ax=ax)
    ax.set_title(f"twirling_strategy='{strategy}'")

plt.tight_layout()
../_images/04e5d533bcd43384a9c014931da57debc2cbfa8bde0355732a901e7bbeda0fb1.png

Specify how to treat barriers

By default, barriers are removed from the circuit after all entangling gates and measurement instructions have been boxed as instructed, including their extensions due to the twirling_strategy selection, but prior to single-qubit gates being added to the box immediately on their right, if possible.

For example, if we start with the following circuit that contains a barrier:

circuit_with_barrier = QuantumCircuit(4)
circuit_with_barrier.h(range(4))
circuit_with_barrier.cz(0, 1)
circuit_with_barrier.barrier()
circuit_with_barrier.cz(2, 3)
circuit_with_barrier.measure_all()

circuit_with_barrier.draw("mpl", scale=0.8)
../_images/91362b789afdf1cf401ddb22088a5104aa6024f6cc95bf8c581b64de97c257cc.png

Then the default behaviour will prevent the two entangling gates from ending up in the same box, though it will let the single-qubit gates be absorbed into the next available box despite the barrier.

boxing_pass_manager = generate_boxing_pass_manager()
transpiled_circuit = boxing_pass_manager.run(circuit_with_barrier)
transpiled_circuit.draw("mpl", scale=0.8)
../_images/25aeeeb7cae3710db7c42b43c33cbe803a2a039769c70dbbec14ee716ade269b.png

Setting remove_barriers to 'never' preserves the barriers so that single-qubit gates are constrained.

boxing_pass_manager = generate_boxing_pass_manager(remove_barriers="never")

transpiled_circuit = boxing_pass_manager.run(circuit_with_barrier)
transpiled_circuit.draw("mpl", scale=0.8)
../_images/2590e7315918b9f9e97c620a187d14dc0347421fb81e188b2d751574031d9a5c.png

And finally, if we elect to remove the barriers immediately, then the entanglers are no longer constrained to end up in separate boxes.

boxing_pass_manager = generate_boxing_pass_manager(remove_barriers="immediately")

transpiled_circuit = boxing_pass_manager.run(circuit_with_barrier)
transpiled_circuit.draw("mpl", scale=0.8)
../_images/382ecb21e2d7352537cbc1749eaa058b4d5944a0f36f1ad096e9d1eb62ce16c4.png

Incorporate Samplomatic’s pass manager into Qiskit’s preset pass managers

The code below shows how to incorporate generate_boxing_pass_manager() into one of Qiskit’s preset pass managers. The transpiled circuit is ISA and contains boxes.

from qiskit.transpiler import generate_preset_pass_manager

preset_pass_manager = generate_preset_pass_manager(
    basis_gates=["rz", "sx", "cx"],
    coupling_map=[[0, 1], [1, 2]],
    optimization_level=0,
)
boxing_pass_manager = generate_boxing_pass_manager()

# Run the boxing pass manager after the scheduling stage
preset_pass_manager.post_scheduling = boxing_pass_manager

circuit = QuantumCircuit(3)
circuit.h(0)
circuit.cx(0, 1)
circuit.cx(0, 2)
circuit.measure_all()

transpiled_circuit = preset_pass_manager.run(circuit)
transpiled_circuit.draw("mpl", scale=0.8)
../_images/7908bb9b3811a181670d87bf3fbd74c6a315a6e33714b4de0760d081637612b0.png

Build your circuit

Every pass manager produced by generate_boxing_pass_manager() returns circuits that can be successfully turned into a template/samplex pair by build(). As an example, the following code calls the build() function on a circuit produced by a boxing pass manager.

from samplomatic import build

boxing_pass_manager = generate_boxing_pass_manager(
    enable_gates=True,
    enable_measures=True,
)
transpiled_circuit = boxing_pass_manager.run(circuit)

template, samplex = build(transpiled_circuit)

In order to ensure that any transpiled circuit can be successfully built, the pass managers know how to include additional boxes when they are needed. As an example, consider the circuit below, which ends with an unmeasured qubit 0.

circuit_with_unmeasured_qubit = QuantumCircuit(4, 3)
circuit_with_unmeasured_qubit.cz(0, 1)
circuit_with_unmeasured_qubit.cz(2, 3)
for qubit in range(4):
    circuit_with_unmeasured_qubit.rz(Parameter(f"th_{qubit}"), qubit)
    circuit_with_unmeasured_qubit.rx(Parameter(f"phi_{qubit}"), qubit)
    circuit_with_unmeasured_qubit.rz(Parameter(f"lam_{qubit}"), qubit)
circuit_with_unmeasured_qubit.measure(range(1, 4), range(3))

circuit_with_unmeasured_qubit.draw("mpl", scale=0.8)
../_images/21d9f7131aac6ebdda57472589dcde990298a7cb606b23c9c46be2ea03535e06.png

Drawing left-dressed boxes around the gates and the measurements would result in a circuit that has uncollected virtual gates on qubit 0, and calling build() on this circuit would result in an error. To avoid this, the pass managers returned by generate_boxing_pass_manager() are allowed to add right-dressed boxes to act as collectors. As an example, in the following snippet qubit 0 is terminated by a right-dressed box that picks up the uncollected virtual gate. The single-qubit gates acting on qubit 0 are also placed inside the box, in order to minimise the depth of the resulting circuit.

boxing_pass_manager = generate_boxing_pass_manager(
    enable_gates=True,
    enable_measures=True,
)
transpiled_circuit = boxing_pass_manager.run(circuit_with_unmeasured_qubit)
transpiled_circuit.draw("mpl", scale=0.8)
../_images/0382fb2624b74356e5afa887dfed60f49680ae09c7fb9721cbcf7d7c7174c1b6.png

In another example, a right-dressed box is added to collect the virtual gates that would otherwise remain uncollected due to the unboxed measurements.

boxing_pass_manager = generate_boxing_pass_manager(
    enable_gates=True,
    enable_measures=False,
)
transpiled_circuit = boxing_pass_manager.run(circuit_with_unmeasured_qubit)
transpiled_circuit.draw("mpl", scale=0.8)
../_images/0f69ee49139111fb80dd1049c40a5a97c9f3bdb44941843b0e8bb0428c759289.png