Source code for semi_cr.core.trial.machine

"""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, Literal

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
OPXOutputMode = Literal["direct", "amplified"]
OPX_DIRECT_LIMIT_V = 0.5
OPX_AMPLIFIED_LIMIT_V = 2.5
OPX_OUTPUT_LIMITS_V: dict[OPXOutputMode, float] = {
    "direct": OPX_DIRECT_LIMIT_V,
    "amplified": OPX_AMPLIFIED_LIMIT_V,
}


[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" output_mode: OPXOutputMode = "direct"
[docs] @dataclass(frozen=True) class OPXInputRef: """Explicit address of an OPX1000 LF-FEM analog input.""" controller_id: str fem_id: int port_id: int
[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, output_mode=ref.output_mode, controller_id=ref.controller_id, fem_id=ref.fem_id, port_id=ref.port_id, )
[docs] def input_port_from_ref(ref: OPXInputRef) -> LFFEMAnalogInputPort: return LFFEMAnalogInputPort( 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, readout_amplitude: float = PULSE_AMPLITUDE, readout_frequency: float = 0.0, ) -> NQCPMachine: """Build the QUAM machine. ``readout_length`` (ns), ``readout_amplitude`` (V) and ``readout_frequency`` (Hz, the readout channel's IF) are calibration parameters, not properties of the machine wiring, hence they are 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=readout_frequency, operations={ "readout": SquareReadoutPulse(amplitude=readout_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, readout_amplitude: float = PULSE_AMPLITUDE, readout_frequency: float = 0.0, ) -> 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, readout_amplitude=readout_amplitude, readout_frequency=readout_frequency, )