diff --git a/qualang_tools/wirer/README.md b/qualang_tools/wirer/README.md index 6859d7e5..9dac0ee9 100644 --- a/qualang_tools/wirer/README.md +++ b/qualang_tools/wirer/README.md @@ -113,6 +113,18 @@ instruments.add_octave(indices=1) instruments.add_lf_fem(controller=1, slots=[1]) instruments.add_mw_fem(controller=1, slots=[2]) ``` +#### Setups with External Mixers +Note: An **external mixer** is defined as abstractly as a combined, IQ-upconverter and IQ-downconverter instrument. +```python +# Single LF-FEM and 2x External Mixers +instruments.add_lf_fem(controller=1, slots=[1]) +instruments.add_external_mixer(indices=[1, 2]) +``` +```python +# Single OPX+ and 2x External Mixers +instruments.add_opx_plus(controllers=[1, 2]) +instruments.add_external_mixer(indices=[1, 2]) +```
Image Empty OPX1000 diff --git a/qualang_tools/wirer/instruments/instrument_channel.py b/qualang_tools/wirer/instruments/instrument_channel.py index 65fda5b0..71e23bac 100644 --- a/qualang_tools/wirer/instruments/instrument_channel.py +++ b/qualang_tools/wirer/instruments/instrument_channel.py @@ -39,24 +39,32 @@ class InstrumentChannelAnalog: signal_type = "analog" +InstrumentIdType = Literal["lf-fem", "mw-fem", "opx+", "octave", "external-mixer"] + + @dataclass(eq=False) class InstrumentChannelLfFem: - instrument_id: Literal["lf-fem", "mw-fem", "opx+", "octave"] = "lf-fem" + instrument_id: InstrumentIdType = "lf-fem" @dataclass(eq=False) class InstrumentChannelMwFem: - instrument_id: Literal["lf-fem", "mw-fem", "opx+", "octave"] = "mw-fem" + instrument_id: InstrumentIdType = "mw-fem" @dataclass(eq=False) class InstrumentChannelOpxPlus: - instrument_id: Literal["lf-fem", "mw-fem", "opx+", "octave"] = "opx+" + instrument_id: InstrumentIdType = "opx+" @dataclass(eq=False) class InstrumentChannelOctave: - instrument_id: Literal["lf-fem", "mw-fem", "opx+", "octave"] = "octave" + instrument_id: InstrumentIdType = "octave" + + +@dataclass(eq=False) +class InstrumentChannelExternalMixer: + instrument_id: InstrumentIdType = "external-mixer" @dataclass(eq=False) @@ -122,6 +130,20 @@ class InstrumentChannelOpxPlusDigitalOutput( pass +@dataclass(eq=False) +class InstrumentChannelExternalMixerInput( + InstrumentChannelAnalog, InstrumentChannelExternalMixer, InstrumentChannelInput, InstrumentChannel +): + pass + + +@dataclass(eq=False) +class InstrumentChannelExternalMixerOutput( + InstrumentChannelAnalog, InstrumentChannelExternalMixer, InstrumentChannelOutput, InstrumentChannel +): + pass + + @dataclass(eq=False) class InstrumentChannelOctaveInput( InstrumentChannelAnalog, InstrumentChannelOctave, InstrumentChannelInput, InstrumentChannel @@ -143,6 +165,13 @@ class InstrumentChannelOctaveDigitalInput( pass +@dataclass(eq=False) +class InstrumentChannelExternalMixerDigitalInput( + InstrumentChannelDigital, InstrumentChannelExternalMixer, InstrumentChannelInput, InstrumentChannel +): + pass + + AnyInstrumentChannel = Union[ InstrumentChannelLfFemInput, InstrumentChannelLfFemOutput, @@ -152,4 +181,6 @@ class InstrumentChannelOctaveDigitalInput( InstrumentChannelOpxPlusOutput, InstrumentChannelOctaveInput, InstrumentChannelOctaveOutput, + InstrumentChannelExternalMixerInput, + InstrumentChannelExternalMixerOutput, ] diff --git a/qualang_tools/wirer/instruments/instruments.py b/qualang_tools/wirer/instruments/instruments.py index a11bbb8e..45ff3890 100644 --- a/qualang_tools/wirer/instruments/instruments.py +++ b/qualang_tools/wirer/instruments/instruments.py @@ -4,6 +4,9 @@ InstrumentChannelOctaveInput, InstrumentChannelOctaveOutput, InstrumentChannelOctaveDigitalInput, + InstrumentChannelExternalMixerInput, + InstrumentChannelExternalMixerOutput, + InstrumentChannelExternalMixerDigitalInput, ) from .instrument_channels import * from .constants import * @@ -20,6 +23,26 @@ def __init__(self): self.used_channels = InstrumentChannels() self.available_channels = InstrumentChannels() + def add_external_mixer(self, indices: Union[List[int], int]): + """ + Add an external mixer, which is defined abstractly as a combined, IQ-upconverter and + IQ-downconverter. + + `indices` (List[int] | int): Can be one or more indices for one or more external mixers. + """ + if isinstance(indices, int): + indices = [indices] + + for index in indices: + channel = InstrumentChannelExternalMixerInput(con=index, port=1) + self.available_channels.add(channel) + + channel = InstrumentChannelExternalMixerOutput(con=index, port=1) + self.available_channels.add(channel) + + channel = InstrumentChannelExternalMixerDigitalInput(con=index, port=1) + self.available_channels.add(channel) + def add_octave(self, indices: Union[List[int], int]): if isinstance(indices, int): indices = [indices] diff --git a/qualang_tools/wirer/visualizer/instrument_figure_manager.py b/qualang_tools/wirer/visualizer/instrument_figure_manager.py index 75db24c2..473a51c2 100644 --- a/qualang_tools/wirer/visualizer/instrument_figure_manager.py +++ b/qualang_tools/wirer/visualizer/instrument_figure_manager.py @@ -23,17 +23,19 @@ def get_ax(self, con: int, slot: int, instrument_id: str) -> Axes: if instrument_id == "OPX1000": fig, axs = self._make_opx1000_figure() self.figures[key] = {i + 1: ax for i, ax in enumerate(axs)} - fig.suptitle(f"con{con} - {instrument_id} Wiring Diagram", fontweight="bold", fontsize=14) + fig.suptitle(f"con{con} - {instrument_id} Wiring", fontweight="bold", fontsize=14) elif instrument_id == "OPX+": fig = self._make_opx_plus_figure() self.figures[key] = fig.axes[0] - fig.suptitle(f"con{con} - {instrument_id} Wiring Diagram", fontweight="bold", fontsize=14) + fig.suptitle(f"con{con} - {instrument_id} Wiring", fontweight="bold", fontsize=14) elif instrument_id == "Octave": fig = self._make_octave_figure() self.figures[key] = fig.axes[0] - fig.suptitle(f"oct{con} - {instrument_id} Wiring Diagram", fontweight="bold", fontsize=14) + fig.suptitle(f"oct{con} - {instrument_id} Wiring", fontweight="bold", fontsize=14) else: - raise NotImplementedError() + fig = self._make_external_mixer_figure() + self.figures[key] = fig.axes[0] + fig.suptitle(f"Mixers {con} Wiring", fontweight="bold", fontsize=14) return self.figures[key][slot] if slot is not None else self.figures[key] @@ -84,3 +86,27 @@ def _make_opx_plus_figure() -> Figure: @classmethod def _make_octave_figure(cls) -> Figure: return cls._make_opx_plus_figure() + + @classmethod + def _make_external_mixer_figure(cls) -> Figure: + fig, ax = plt.subplots( + 1, + 1, + figsize=( + INSTRUMENT_FIGURE_DIMENSIONS["Mixers"]["width"] * 2, + INSTRUMENT_FIGURE_DIMENSIONS["Mixers"]["height"] * 2, + ), + ) + ax.text(0.25, 0.25, "Up. RF", ha="center", va="center") + ax.text(0.25, 0.65, "Up. Mkr", ha="center") + ax.text(0.75, 0.25, "Down. RF", ha="center", va="center") + ax.set_ylim([0.15, 1.15]) + # ax.set_xlim([0.15 / 8 * 3, 1.15 / 8 * 3]) + ax.set_facecolor("darkgrey") + ax.set_xticks([]) + ax.set_yticks([]) + ax.set_xticklabels([]) + ax.set_yticklabels([]) + ax.set_aspect("equal") + + return fig diff --git a/qualang_tools/wirer/visualizer/layout.py b/qualang_tools/wirer/visualizer/layout.py index ef1c886d..ac7dbb63 100644 --- a/qualang_tools/wirer/visualizer/layout.py +++ b/qualang_tools/wirer/visualizer/layout.py @@ -4,6 +4,7 @@ "mw-fem": "OPX1000", "opx+": "OPX+", "octave": "Octave", + "external-mixer": "Mixers", } # Define the chassis dimensions @@ -11,6 +12,7 @@ "OPX1000": {"width": 8, "height": 3}, "OPX+": {"width": 8, "height": 1}, "Octave": {"width": 3, "height": 1}, + "Mixers": {"width": 1, "height": 1}, } OPX_PLUS_ASPECT = INSTRUMENT_FIGURE_DIMENSIONS["OPX+"]["height"] / INSTRUMENT_FIGURE_DIMENSIONS["OPX+"]["width"] @@ -57,4 +59,13 @@ "input": [((0.3 + j * 0.06) * 3, 0.18) for j in range(5)], }, }, + "external-mixer": { + "analog": { + "input": [(0.75, 0.45)], + "output": [(0.25, 0.45)], + }, + "digital": { + "input": [(0.25, 0.85)], + }, + }, } diff --git a/qualang_tools/wirer/visualizer/port_annotation.py b/qualang_tools/wirer/visualizer/port_annotation.py index 1848c3c4..c2716e5e 100644 --- a/qualang_tools/wirer/visualizer/port_annotation.py +++ b/qualang_tools/wirer/visualizer/port_annotation.py @@ -32,7 +32,7 @@ def draw(self, ax: Axes): if self.signal_type == "digital" and self.instrument_id in ["lf-fem", "mw-fem"]: port_size = PORT_SIZE / 2.4 bbox = dict(facecolor=fill_color, alpha=0.8, edgecolor="none") - else: + elif self.instrument_id in ["opx+", "octave"]: port_size = PORT_SIZE port_label_distance = PORT_SIZE * 1.3 ax.text( @@ -46,6 +46,22 @@ def draw(self, ax: Axes): color=outline_colour, ) bbox = None + elif self.instrument_id in ["external-mixer"]: + port_size = PORT_SIZE * 2.4 + port_label_distance = PORT_SIZE * 3.2 + ax.text( + x - port_label_distance, + y, + str(self.port), + ha="center", + va="center", + fontsize=8, + fontweight="bold", + color=outline_colour, + ) + bbox = None + else: + raise NotImplementedError(f"No port-annotation drawing for {self.instrument_id}") ax.add_patch(patches.Circle((x, y), port_size, edgecolor=outline_colour, facecolor=fill_color)) labels = combine_labels_for_same_line_type(self.labels) diff --git a/qualang_tools/wirer/wirer/channel_specs.py b/qualang_tools/wirer/wirer/channel_specs.py index bf1bd686..a9216672 100644 --- a/qualang_tools/wirer/wirer/channel_specs.py +++ b/qualang_tools/wirer/wirer/channel_specs.py @@ -9,10 +9,13 @@ InstrumentChannelOpxPlusOutput, InstrumentChannelOctaveInput, InstrumentChannelOctaveOutput, + InstrumentChannelExternalMixerInput, + InstrumentChannelExternalMixerOutput, InstrumentChannelOpxPlusDigitalOutput, InstrumentChannelMwFemDigitalOutput, InstrumentChannelLfFemDigitalOutput, InstrumentChannelOctaveDigitalInput, + InstrumentChannelExternalMixerDigitalInput, ) # A channel template is a partially filled InstrumentChannel object @@ -99,6 +102,15 @@ def __init__(self, index: int = None, rf_in: int = None, rf_out: int = None): ] +class ChannelSpecExternalMixer(ChannelSpec): + def __init__(self, index: int = None, rf_in: int = None, rf_out: int = None): + super().__init__() + self.channel_templates = [ + InstrumentChannelExternalMixerInput(con=index, port=rf_in), + InstrumentChannelExternalMixerOutput(con=index, port=rf_out), + ] + + class ChannelSpecLfFemBasebandAndOctave(ChannelSpec): def __init__( self, @@ -123,6 +135,28 @@ def __init__( ] +class ChannelSpecLfFemBasebandAndExternalMixer(ChannelSpec): + def __init__( + self, + con: int = None, + slot: int = None, + in_port_i: int = None, + in_port_q: int = None, + out_port_i: int = None, + out_port_q: int = None, + mixer_index: int = None, + ): + super().__init__() + self.channel_templates = [ + InstrumentChannelLfFemInput(con=con, slot=slot, port=in_port_i), + InstrumentChannelLfFemInput(con=con, slot=slot, port=in_port_q), + InstrumentChannelLfFemOutput(con=con, slot=slot, port=out_port_i), + InstrumentChannelLfFemOutput(con=con, slot=slot, port=out_port_q), + InstrumentChannelExternalMixerInput(con=mixer_index, port=1), + InstrumentChannelExternalMixerOutput(con=mixer_index, port=1), + ] + + class ChannelSpecOpxPlusBasebandAndOctave(ChannelSpec): def __init__( self, @@ -146,6 +180,27 @@ def __init__( ] +class ChannelSpecOpxPlusBasebandAndExternalMixer(ChannelSpec): + def __init__( + self, + con: int = None, + in_port_i: int = None, + in_port_q: int = None, + out_port_i: int = None, + out_port_q: int = None, + mixer_index: int = None, + ): + super().__init__() + self.channel_templates = [ + InstrumentChannelOpxPlusInput(con=con, port=in_port_i), + InstrumentChannelOpxPlusInput(con=con, port=in_port_q), + InstrumentChannelOpxPlusOutput(con=con, port=out_port_i), + InstrumentChannelOpxPlusOutput(con=con, port=out_port_q), + InstrumentChannelExternalMixerInput(con=mixer_index, port=1), + InstrumentChannelExternalMixerOutput(con=mixer_index, port=1), + ] + + class ChannelSpecOpxPlusDigital(ChannelSpec): def __init__(self, con: int = None, out_port: int = None): super().__init__() @@ -170,12 +225,21 @@ def __init__(self, con: int = None, in_port: int = None): self.channel_templates = [InstrumentChannelOctaveDigitalInput(con=con, port=in_port)] +class ChannelSpecExternalMixerDigital(ChannelSpec): + def __init__(self, con: int = None, in_port: int = None): + super().__init__() + self.channel_templates = [InstrumentChannelExternalMixerDigitalInput(con=con, port=in_port)] + + mw_fem_spec = ChannelSpecMwFemSingle lf_fem_spec = ChannelSpecLfFemSingle lf_fem_iq_spec = ChannelSpecLfFemBaseband lf_fem_iq_octave_spec = ChannelSpecLfFemBasebandAndOctave +lf_fem_iq_ext_mixer_spec = ChannelSpecLfFemBasebandAndExternalMixer opx_spec = ChannelSpecOpxPlusSingle opx_iq_spec = ChannelSpecOpxPlusBaseband opx_iq_octave_spec = ChannelSpecOpxPlusBasebandAndOctave +opx_iq_ext_mixer_spec = ChannelSpecOpxPlusBasebandAndExternalMixer octave_spec = ChannelSpecOctave +ext_mixer_spec = ChannelSpecExternalMixer opx_dig_spec = ChannelSpecOpxPlusDigital diff --git a/qualang_tools/wirer/wirer/wirer.py b/qualang_tools/wirer/wirer/wirer.py index fef189b5..2a2e9be6 100644 --- a/qualang_tools/wirer/wirer/wirer.py +++ b/qualang_tools/wirer/wirer/wirer.py @@ -13,11 +13,13 @@ ChannelSpecMwFemSingle, ChannelSpecLfFemBaseband, ChannelSpecOctave, + ChannelSpecExternalMixer, ChannelSpecOpxPlusBaseband, ChannelSpecMwFemDigital, ChannelSpecLfFemDigital, ChannelSpecOctaveDigital, ChannelSpecOpxPlusDigital, + ChannelSpecExternalMixerDigital, ) from .wirer_assign_channels_to_spec import assign_channels_to_spec from .wirer_exceptions import ConstraintsTooStrictException, NotEnoughChannelsException @@ -98,9 +100,22 @@ def allocate_rf_channels(spec: WiringSpec, instruments: Instruments): channels. """ rf_specs = [ + # MW-FEM, Single RF output ChannelSpecMwFemSingle() & ChannelSpecMwFemDigital(), + # LF-FEM I/Q output with Octave for upconversion ChannelSpecLfFemBaseband() & ChannelSpecLfFemDigital() & ChannelSpecOctave() & ChannelSpecOctaveDigital(), + # LF-FEM I/Q output with External Mixer for upconversion + ChannelSpecLfFemBaseband() + & ChannelSpecLfFemDigital() + & ChannelSpecExternalMixer() + & ChannelSpecExternalMixerDigital(), + # OPX+ I/Q output with Octave for upconversion ChannelSpecOpxPlusBaseband() & ChannelSpecOpxPlusDigital() & ChannelSpecOctave() & ChannelSpecOctaveDigital(), + # OPX+ I/Q output with External Mixer for upconversion + ChannelSpecOpxPlusBaseband() + & ChannelSpecOpxPlusDigital() + & ChannelSpecExternalMixer() + & ChannelSpecExternalMixerDigital(), ] allocate_channels(spec, rf_specs, instruments, same_con=True, same_slot=True) diff --git a/qualang_tools/wirer/wirer/wirer_exceptions.py b/qualang_tools/wirer/wirer/wirer_exceptions.py index b5e65b54..b3d90497 100644 --- a/qualang_tools/wirer/wirer/wirer_exceptions.py +++ b/qualang_tools/wirer/wirer/wirer_exceptions.py @@ -11,7 +11,9 @@ def __init__(self, wiring_spec: WiringSpec, constraints: List[ChannelSpec]): f"{wiring_spec.io_type.value} channels on the " f"{wiring_spec.line_type.value} line for elements " f"{','.join([str(e.id) for e in wiring_spec.elements])} with the " - f"following constraints: {constraints}" + f"following constraints: {constraints}. If you are intentionally trying to " + f"allocate multiple lines to the same port, see documentation for the " + f"correct approach." ) super(ConstraintsTooStrictException, self).__init__(message) diff --git a/tests/wirer/conftest.py b/tests/wirer/conftest.py index d16c925f..e7932c5f 100644 --- a/tests/wirer/conftest.py +++ b/tests/wirer/conftest.py @@ -49,3 +49,11 @@ def instruments_2lf_2mw() -> Instruments: instruments.add_lf_fem(controller=1, slots=[1, 2]) instruments.add_mw_fem(controller=1, slots=[3, 7]) return instruments + + +@pytest.fixture() +def instruments_1opx_2external_mixer() -> Instruments: + instruments = Instruments() + instruments.add_opx_plus(controllers=[1]) + instruments.add_external_mixer(indices=[1, 2]) + return instruments diff --git a/tests/wirer/test_wirer_opxp_and_external_mixers.py b/tests/wirer/test_wirer_opxp_and_external_mixers.py new file mode 100644 index 00000000..55117ade --- /dev/null +++ b/tests/wirer/test_wirer_opxp_and_external_mixers.py @@ -0,0 +1,54 @@ +import pytest + +from qualang_tools.wirer import * +from qualang_tools.wirer.connectivity.element import QubitReference +from qualang_tools.wirer.connectivity.wiring_spec import WiringLineType +from qualang_tools.wirer.instruments.instrument_channel import ( + InstrumentChannelOpxPlusInput, + InstrumentChannelOpxPlusOutput, + InstrumentChannelExternalMixerInput, + InstrumentChannelExternalMixerOutput, +) + +visualize_flag = pytest.visualize_flag + + +def test_1q_allocation(instruments_1opx_2external_mixer): + qubits = [1] + + connectivity = Connectivity() + connectivity.add_resonator_line(qubits=qubits) + connectivity.add_qubit_drive_lines(qubits=qubits) + connectivity.add_qubit_flux_lines(qubits=qubits) + + allocate_wiring(connectivity, instruments_1opx_2external_mixer) + + if visualize_flag: + visualize(connectivity.elements, instruments_1opx_2external_mixer.available_channels) + + for qubit in qubits: + for i, channel in enumerate(connectivity.elements[QubitReference(qubit)].channels[WiringLineType.RESONATOR]): + assert pytest.channels_are_equal( + channel, + [ + InstrumentChannelOpxPlusInput(con=1, port=1), + InstrumentChannelOpxPlusInput(con=1, port=2), + InstrumentChannelOpxPlusOutput(con=1, port=1), + InstrumentChannelOpxPlusOutput(con=1, port=2), + InstrumentChannelExternalMixerInput(con=1, port=1), + InstrumentChannelExternalMixerOutput(con=1, port=1), + ][i], + ) + + for i, channel in enumerate(connectivity.elements[QubitReference(qubit)].channels[WiringLineType.DRIVE]): + assert pytest.channels_are_equal( + channel, + [ + InstrumentChannelOpxPlusOutput(con=1, port=3), + InstrumentChannelOpxPlusOutput(con=1, port=4), + InstrumentChannelExternalMixerOutput(con=2, port=1), + ][i], + ) + + for i, channel in enumerate(connectivity.elements[QubitReference(qubit)].channels[WiringLineType.FLUX]): + assert pytest.channels_are_equal(channel, [InstrumentChannelOpxPlusOutput(con=1, port=5)][i])