Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for QM OPX1000 in 0.2 #1068

Merged
merged 15 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions src/qibolab/_core/instruments/qm/components/configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,19 @@

from pydantic import Field

from qibolab._core.components import AcquisitionConfig, DcConfig
from qibolab._core.components import AcquisitionConfig, DcConfig, OscillatorConfig

__all__ = ["OpxOutputConfig", "QmAcquisitionConfig", "QmConfigs"]
__all__ = [
"OpxOutputConfig",
"QmAcquisitionConfig",
"QmConfigs",
"OctaveOscillatorConfig",
"OctaveOutputModes",
alecandido marked this conversation as resolved.
Show resolved Hide resolved
]

OctaveOutputModes = Literal[
"always_on", "always_off", "triggered", "triggered_reversed"
]


class OpxOutputConfig(DcConfig):
Expand All @@ -25,6 +35,15 @@ class OpxOutputConfig(DcConfig):
for more details.
Changing the filters affects the calibration of single shot discrimination (threshold and angle).
"""
output_mode: Literal["direct", "amplified"] = "direct"


class OctaveOscillatorConfig(OscillatorConfig):
"""Oscillator confing that allows switching the output mode."""

kind: Literal["octave-oscillator"] = "octave-oscillator"

output_mode: OctaveOutputModes = "triggered"


class QmAcquisitionConfig(AcquisitionConfig):
Expand All @@ -41,4 +60,4 @@ class QmAcquisitionConfig(AcquisitionConfig):
"""Constant voltage to be applied on the input."""


QmConfigs = Union[OpxOutputConfig, QmAcquisitionConfig]
QmConfigs = Union[OpxOutputConfig, OctaveOscillatorConfig, QmAcquisitionConfig]
1 change: 1 addition & 0 deletions src/qibolab/_core/instruments/qm/config/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from .config import Configuration
from .devices import ControllerId, ModuleTypes
from .pulses import SAMPLING_RATE, operation
31 changes: 24 additions & 7 deletions src/qibolab/_core/instruments/qm/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,17 @@
from qibolab._core.pulses import Pulse, Readout

from ..components import OpxOutputConfig, QmAcquisitionConfig
from .devices import AnalogOutput, Controller, Octave, OctaveInput, OctaveOutput
from .devices import (
AnalogOutput,
Controller,
ControllerId,
Controllers,
FemAnalogOutput,
ModuleTypes,
Octave,
OctaveInput,
OctaveOutput,
)
from .elements import AcquireOctaveElement, DcElement, Element, RfOctaveElement
from .pulses import (
QmAcquisition,
Expand Down Expand Up @@ -42,7 +52,7 @@ class Configuration:
"""

version: int = 1
controllers: dict[str, Controller] = field(default_factory=dict)
controllers: Controllers = field(default_factory=Controllers)
octaves: dict[str, Octave] = field(default_factory=dict)
elements: dict[str, Element] = field(default_factory=dict)
pulses: dict[str, Union[QmPulse, QmAcquisition]] = field(default_factory=dict)
Expand All @@ -53,20 +63,27 @@ class Configuration:
integration_weights: dict = field(default_factory=dict)
mixers: dict = field(default_factory=dict)

def add_controller(self, device: str):
def add_controller(self, device: ControllerId, modules: dict[str, ModuleTypes]):
if device not in self.controllers:
self.controllers[device] = Controller()
self.controllers[device] = Controller(type=modules[device])

def add_octave(self, device: str, connectivity: str):
def add_octave(
self, device: str, connectivity: ControllerId, modules: dict[str, ModuleTypes]
):
if device not in self.octaves:
self.add_controller(connectivity)
self.add_controller(connectivity, modules)
self.octaves[device] = Octave(connectivity)

def configure_dc_line(
self, id: ChannelId, channel: DcChannel, config: OpxOutputConfig
):
controller = self.controllers[channel.device]
controller.analog_outputs[channel.port] = AnalogOutput.from_config(config)
if controller.type == "opx1":
controller.analog_outputs[channel.port] = AnalogOutput.from_config(config)
else:
controller.analog_outputs[channel.port] = FemAnalogOutput.from_config(
config
)
self.elements[id] = DcElement.from_channel(channel)

def configure_iq_line(
Expand Down
124 changes: 108 additions & 16 deletions src/qibolab/_core/instruments/qm/config/devices.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
from dataclasses import dataclass, field
from typing import Any, Generic, TypeVar
from typing import Generic, Literal, TypeVar, Union

from qibolab._core.components import OscillatorConfig

from ..components import OpxOutputConfig, QmAcquisitionConfig

__all__ = ["AnalogOutput", "OctaveOutput", "OctaveInput", "Controller", "Octave"]
from ..components import (
OctaveOscillatorConfig,
OctaveOutputModes,
OpxOutputConfig,
QmAcquisitionConfig,
)

__all__ = [
"AnalogOutput",
"FemAnalogOutput",
"ModuleTypes",
"OctaveOutput",
"OctaveInput",
"Controller",
"Octave",
"ControllerId",
"Controllers",
]


DEFAULT_INPUTS = {"1": {}, "2": {}}
Expand All @@ -25,7 +40,7 @@ class PortDict(Generic[V], dict[str, V]):
in the QUA config.
"""

def __setitem__(self, key: Any, value: V):
def __setitem__(self, key: Union[str, int], value: V):
super().__setitem__(str(key), value)


Expand All @@ -39,6 +54,17 @@ def from_config(cls, config: OpxOutputConfig):
return cls(offset=config.offset, filter=config.filter)


@dataclass(frozen=True)
class FemAnalogOutput(AnalogOutput):
output_mode: Literal["direct", "amplified"] = "direct"

@classmethod
def from_config(cls, config: OpxOutputConfig):
return cls(
offset=config.offset, filter=config.filter, output_mode=config.output_mode
)


@dataclass(frozen=True)
class AnalogInput:
offset: float = 0.0
Expand All @@ -53,29 +79,37 @@ def from_config(cls, config: QmAcquisitionConfig):
class OctaveOutput:
LO_frequency: int
gain: int = 0
LO_source: str = "internal"
output_mode: str = "triggered"
LO_source: Literal["internal", "external"] = "internal"
output_mode: OctaveOutputModes = "triggered"

@classmethod
def from_config(cls, config: OscillatorConfig):
return cls(LO_frequency=config.frequency, gain=config.power)
def from_config(cls, config: Union[OscillatorConfig, OctaveOscillatorConfig]):
kwargs = dict(LO_frequency=config.frequency, gain=config.power)
if isinstance(config, OctaveOscillatorConfig):
kwargs["output_mode"] = config.output_mode
return cls(**kwargs)


@dataclass(frozen=True)
class OctaveInput:
LO_frequency: int
LO_source: str = "internal"
IF_mode_I: str = "direct"
IF_mode_Q: str = "direct"
LO_source: Literal["internal", "external"] = "internal"
IF_mode_I: Literal["direct", "envelop", "mixer"] = "direct"
IF_mode_Q: Literal["direct", "envelop", "mixer"] = "direct"


ModuleTypes = Literal["opx1", "LF", "MW"]


@dataclass
class Controller:
type: ModuleTypes = "opx1"
"""https://docs.quantum-machines.co/latest/docs/Introduction/config/?h=opx10#controllers"""
analog_outputs: PortDict[dict[str, AnalogOutput]] = field(default_factory=PortDict)
digital_outputs: PortDict[dict[str, dict]] = field(default_factory=PortDict)
analog_inputs: PortDict[dict[str, AnalogInput]] = field(
default_factory=lambda: PortDict(DEFAULT_INPUTS)
)
analog_inputs: PortDict[dict[str, AnalogInput]] = field(default_factory=PortDict)
# default_factory=lambda: PortDict(DEFAULT_INPUTS)
# )
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dead code?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching this. I decided to go with the original one (that is: not change in this PR), as I think the default value there was needed for something related to the mixer calibration. I just had to also add default offsets (see e4947d9) because qm-qua>1.2 fails without them.


def add_octave_output(self, port: int):
# TODO: Add offset here?
Expand All @@ -90,8 +124,66 @@ def add_octave_input(self, port: int, config: QmAcquisitionConfig):
)


@dataclass
class Opx1000:
type: Literal["opx1000"] = "opx1000"
fems: dict[str, Controller] = field(default_factory=PortDict)


@dataclass
class Octave:
connectivity: str
connectivity: Union[str, tuple[str, int]]
RF_outputs: PortDict[dict[str, OctaveOutput]] = field(default_factory=PortDict)
RF_inputs: PortDict[dict[str, OctaveInput]] = field(default_factory=PortDict)

def __post_init__(self):
if "/" in self.connectivity:
con, fem = self.connectivity.split("/")
self.connectivity = (con, int(fem))


ControllerId = Union[str, tuple[str, int]]


def process_controller_id(id: ControllerId):
"""Convert controller identifier depending on cluster type.

For OPX+ clusters ``id`` is just the controller name (eg. 'con1').
For OPX1000 clusters ``id`` has the format
'{controller_name}/{fem_number}' (eg. 'con1/4').
In that case ``id`` may also be a ``tuple``
`(controller_name, fem_number)`
"""
if isinstance(id, tuple):
con, fem = id
return con, str(fem)
if "/" in id:
return id.split("/")
return id, None


class Controllers(dict[str, Union[Controller, Opx1000]]):
"""Dictionary of controllers compatible with OPX+ and OPX1000."""

def __contains__(self, key: ControllerId) -> bool:
con, fem = process_controller_id(key)
contains = super().__contains__(con)
if fem is None:
return contains
return contains and fem in self[con].fems

def __getitem__(self, key: ControllerId) -> Controller:
con, fem = process_controller_id(key)
value = super().__getitem__(con)
if fem is None:
return value
return value.fems[fem]

def __setitem__(self, key: ControllerId, value: Controller):
con, fem = process_controller_id(key)
if fem is None:
super().__setitem__(key, value)
else:
if con not in self:
super().__setitem__(con, Opx1000())
self[con].fems[fem] = value
Loading