The wirer
module provides a way to auto-assigning channels for a collection of Quantum elements given a specific QM instrument setup.
The module decouples two parts of a quantum computing setup:
Instruments
: Defines all accesssible Quantum Machines instruments.Connectivity
: Defines all functional quantum elements to be controlled by Quantum Machines instruments.
Once fully defined, the quantum elements can be mapped to channels on the Quantum Machines instruments using the allocate_wiring
function:
graph TD
A(Instruments) --> D["allocate_wiring()"]
C(Connectivity) --> D["allocate_wiring()"]
subgraph Wirer
D["allocate_wiring()"]
end
The wirer tool supports the following features:
- Assignment of channels to any combination of MW-FEM, LF-FEM, Octave or OPX+.
- Any mapping of N resonator lines to M qubits.
- Any mapping of N FEMs to OPX1000 chassis slots.
- Constrained-scope allocation according to user preferences.
- Natural overflowing during assignment to multiple slots, chassis, modules, octaves, etc.
- Any combination of resonator, drive line, flux line, coupler line for each qubit.
- Total visualization of the final connectivity.
from qualang_tools.wirer import Instruments, Connectivity, allocate_wiring, visualize, lf_fem_spec
# Define instruments
instruments = Instruments()
instruments.add_lf_fem(controller=1, slots=[1, 2])
instruments.add_mw_fem(controller=1, slots=[3, 7])
qubits = [1, 2, 3, 4, 5, 6]
qubit_pairs = [(1, 2), (2, 3), (3, 4), (4, 5), (5, 6)]
# Define quantum elements
connectivity = Connectivity()
connectivity.add_resonator_line(qubits=qubits, triggered=True)
connectivity.add_qubit_drive_lines(qubits=qubits, triggered=True)
connectivity.add_qubit_flux_lines(qubits=qubits)
connectivity.add_qubit_pair_flux_lines(qubit_pairs=qubit_pairs, constraints=lf_fem_spec(out_slot=2))
# Allocate channels for each quantum element
allocate_wiring(connectivity, instruments)
# Visualize the result
visualize(connectivity.elements, instruments.available_channels)
Start with an empty instruments container:
from qualang_tools.wirer import Instruments
instruments = Instruments()
Using the "builder" pattern, you are able to define and add instruments to the container, line-by-line.
Below are examples of some instrument setups:
# Single OPX+
instruments.add_opx_plus(con=1)
# Single OPX+ and Octave
instruments.add_opx_plus(controllers=1)
instruments.add_octave(indices=1)
# Multiple OPXs and Octaves
instruments.add_opx_plus(controllers=[1, 2])
instruments.add_octave(indices=[1, 2])
# Single LF-FEM and Octave
instruments.add_lf_fem(controller=1, slots=[1])
instruments.add_octave(indices=1)
# Single LF-FEM and MW-FEM
instruments.add_lf_fem(controller=1, slots=[1])
instruments.add_mw_fem(controller=1, slots=[2])
Note: An external mixer is defined as abstractly as a combined, IQ-upconverter and IQ-downconverter instrument.
# Single LF-FEM and 2x External Mixers
instruments.add_lf_fem(controller=1, slots=[1])
instruments.add_external_mixer(indices=[1, 2])
# Single OPX+ and 2x External Mixers
instruments.add_opx_plus(controllers=[1, 2])
instruments.add_external_mixer(indices=[1, 2])
# Multiple LF-FEMs and MW-FEMs
instruments.add_lf_fem(controller=1, slots=[1,2,3,4,5])
instruments.add_mw_fem(controller=1, slots=[6,7,8])
The instruments container allows you to construct a "disjoint" setup, that is one where the indices are not logically ordered or belong only partially to a full cluster:
# Disjoint OPX+ and Octave addressing
instruments.add_opx_plus(controllers=[2, 5])
instruments.add_octave(indices=[1, 3, 8])
# Disjoint OPX+ and Octave addressing
instruments.add_lf_fem(controller=3, slots=[2, 4])
instruments.add_mw_fem(controller=3, slots=[1, 7])
The Connectivity
class is like a blueprint for what quantum elements exist in our system, along with some optional
information about how channels should be allocated for these elements.
To define your own Connectivity
object, start with an empty container:
from qualang_tools.wirer import Connectivity
connectivity = Connectivity()
Now you can add "wiring specifications" to the container. These indicate what channels you need for a particular type of quantum functionality.
Note: Channels aren't being allocated at this stage- instead, we are adding "templates" for channels which will be allocated later.
# Define arbitrary set of qubits and qubit pairs for convenience
qubits = [1, 2, 5]
qubit_pairs = [(1, 2)]
# Define a single resonator feedline for multiple qubits
connectivity.add_resonator_line(qubits=qubits)
# Define a RF drive-lines for controlling multiple qubits
connectivity.add_qubit_drive_lines(qubits=qubits)
# Define DC lines for controlling flux
connectivity.add_qubit_flux_lines(qubits=qubits)
# Define DC lines for controlling tunable coupler
connectivity.add_qubit_pair_flux_lines(qubit_pairs=qubits)
connectivity.add_resonator_line(qubits=qubits, triggered=True)
In order to be more specific about the connectivity, we can add constraints
. First, you need to select which
type of constraint you will apply, based on the instruments you wish the line to be allocated. The types you can
choose from include:
from qualang_tools.wirer import (
mw_fem_spec,
lf_fem_spec,
lf_fem_iq_spec,
lf_fem_iq_octave_spec,
opx_spec,
opx_iq_spec,
opx_iq_octave_spec,
octave_spec
)
Then, you can set almost any combination of the available attributes according to what you want constrained. Here are some examples of how you could constrain something to a particular set of I/Q channels between an OPX+ and an Octave.
# Must allocate to OPX con1
constraint = opx_iq_octave_spec(con=1)
# Must allocate to OPX con2
constraint = opx_iq_octave_spec(con=2)
# Must allocate to Octave RF3
constraint = opx_iq_octave_spec(rf_out=3)
# Must allocate to OPX con1, I/Q channels 7 & 8, Octave RF3
constraint = opx_iq_octave_spec(con=1, out_port_i=7, out_port_q=8, rf_out=3)
After defining some constraint
, you can apply it to a wiring specification:
connectivity.add_qubit_drive_lines(qubits=[1], constraints=constraint)
Once you have defined suitable Instruments
and Connectivity
objects, you can allocate channels using the
allocate_wiring
function:
from qualang_tools.wirer import allocate_wiring
allocate_wiring(connectivity, instruments)
At the time of allocation, all of the channel information is put inside the connectivity.elements
dictionary.
In order to double-up allocations to channels, you can keep them "free for allocation" even after doing a round of allocation. Here's an example of how you could allocate multiple qubits to the same channel, either if you don't have enough channels to measure every element, or if you'd like to multiplex the outputs:
connectivity = Connectivity()
connectivity.add_qubit_drive_lines(qubits=1)
allocate_wiring(connectivity, instruments, block_used_channels=False)
connectivity.add_qubit_drive_lines(qubits=2)
allocate_wiring(connectivity, instruments)
connectivity.add_qubit_drive_lines(qubits=3)
allocate_wiring(connectivity, instruments, block_used_channels=False)
connectivity.add_qubit_drive_lines(qubits=4)
allocate_wiring(connectivity, instruments)
To visualize an allocated connectivity
object, you can pass it into the visualization function:
visualize(connectivity.elements, available_channels=instruments.available_channels)
Note: Passing instruments.available_channels
is optional, but it makes the plotting nicer by plotting white
space on un-allocated channels!