"""QUAM machine building for gate-sweep trials on the OPX1000.
This module turns a plain description of the physical wiring — which LF-FEM
outputs drive which gates, and which output/input pair forms the readout
path — into a QUAM machine (:class:`NQCPMachine`) whose generated config can
be loaded onto a quantum machine.
The wiring is expressed with small frozen dataclasses (:class:`OPXOutputRef`,
:class:`OPXInputRef`, :class:`OPXWiring`) that address OPX terminals
explicitly. The longer-term goal is *gate-addressed* wiring: once station
calibration is trustworthy, an ``OPXWiring.from_station(...)`` factory can
route from a device gate through the station graph to the real OPX terminal
(see ``semi_cr.apps.spincontrol.calibration_demo`` for the prototype of that
routing).
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING
from quam import QuamRoot
from quam.components.channels import InOutSingleChannel, SingleChannel, StickyChannelAddon
from quam.components.ports import LFFEMAnalogInputPort, LFFEMAnalogOutputPort
from quam.components.pulses import SquareReadoutPulse
from quam.core import quam_dataclass
from quam_builder.architecture.quantum_dots.components import GateSet
if TYPE_CHECKING:
from collections.abc import Mapping
DEFAULT_SWEEP_GATES = ("P1", "P2")
PULSE_AMPLITUDE = 0
PULSE_LENGTH = 16
[docs]
@dataclass(frozen=True)
class OPXOutputRef:
"""Explicit address of an OPX1000 LF-FEM analog output."""
controller_id: str
fem_id: int
port_id: int
upsampling_mode: str = "pulse"
[docs]
@dataclass(frozen=True)
class OPXWiring:
"""How gates and the readout path map onto OPX terminals.
``sweep_gates`` names the two gates a 2D sweep varies; both must be keys
of ``gate_outputs``.
"""
gate_outputs: Mapping[str, OPXOutputRef]
readout_output: OPXOutputRef
readout_input: OPXInputRef
sweep_gates: tuple[str, str] = DEFAULT_SWEEP_GATES
[docs]
def output_port_from_ref(ref: OPXOutputRef) -> LFFEMAnalogOutputPort:
return LFFEMAnalogOutputPort(
upsampling_mode=ref.upsampling_mode,
controller_id=ref.controller_id,
fem_id=ref.fem_id,
port_id=ref.port_id,
)
[docs]
def build_gate_channels(
gate_ports: Mapping[str, LFFEMAnalogOutputPort],
) -> dict[str, SingleChannel]:
"""One sticky single channel per gate, so voltages hold between pulses."""
channels: dict[str, SingleChannel] = {}
for gate_id, gate_port in gate_ports.items():
channels[gate_id] = SingleChannel(
id=gate_id,
opx_output=gate_port,
sticky=StickyChannelAddon(duration=PULSE_LENGTH, analog=True, digital=False),
)
return channels
[docs]
@quam_dataclass
class NQCPMachine(QuamRoot):
gate_set: GateSet
readout_resonator: InOutSingleChannel
[docs]
def build_machine(
gate_ports: Mapping[str, LFFEMAnalogOutputPort],
readout_output_port: LFFEMAnalogOutputPort,
readout_input_port: LFFEMAnalogInputPort,
*,
readout_length: int,
) -> NQCPMachine:
"""Build the QUAM machine.
``readout_length`` (ns) is a calibration parameter, not a property of the
machine wiring, hence it is passed in rather than baked in here.
"""
channels = build_gate_channels(gate_ports)
resonator_channel = InOutSingleChannel(
id="R1",
opx_input=readout_input_port,
opx_output=readout_output_port,
intermediate_frequency=0,
operations={
"readout": SquareReadoutPulse(amplitude=PULSE_AMPLITUDE, length=readout_length),
},
)
return NQCPMachine(
gate_set=GateSet(id="dot_plungers", channels=channels),
readout_resonator=resonator_channel,
)
[docs]
def build_machine_from_wiring(wiring: OPXWiring, *, readout_length: int) -> NQCPMachine:
return build_machine(
gate_ports={gate: output_port_from_ref(ref) for gate, ref in wiring.gate_outputs.items()},
readout_output_port=output_port_from_ref(wiring.readout_output),
readout_input_port=input_port_from_ref(wiring.readout_input),
readout_length=readout_length,
)