from dataclasses import dataclass, field
from typing import Dict, Optional, Any, Type, TypeVar, Literal
from qeg_nmr_qua.config.integration import IntegrationWeightMapping
[docs]
@dataclass
class ControlPulse:
"""Configuration for a control (drive) pulse.
Defines a pulse used to drive/manipulate quantum states. Control pulses output
a waveform to an element and may optionally trigger a digital marker for
synchronization or external monitoring.
**Current Limitations:**
- Single waveform only (no waveform mixing)
- See :class:`MeasPulse` for measurement pulses with integration.
Attributes:
length (int): Pulse duration in nanoseconds (default: 0).
Must be a multiple of 4 ns (1 clock cycle).
waveform (str): Name of the waveform to play (default: "zero_wf").
Must be defined in the waveform configuration.
digital_marker (Literal["ON", "OFF"] | None): Whether to set a digital marker
during pulse execution (default: "OFF"). "ON" activates the marker,
"OFF" deactivates it. Use None for no marker.
"""
length: int = 0 # in nanoseconds
waveform: str = "zero_wf" # waveform name
digital_marker: Literal["ON", "OFF"] | None = (
"OFF" # set digital marker during pulse
)
[docs]
def to_opx_config(self) -> Dict[str, Any]:
return {
"operation": "control",
"length": self.length,
"waveforms": {"single": self.waveform},
"digital_marker": self.digital_marker,
}
[docs]
def to_dict(self) -> Dict[str, Any]:
return {
"type": "control",
"length": self.length,
"waveform": self.waveform,
"digital_marker": self.digital_marker,
}
[docs]
@classmethod
def from_dict(cls, d: Dict[str, Any]) -> "ControlPulse":
return cls(
length=d.get("length", 0),
waveform=d.get("waveform", "zero_wf"),
digital_marker=d.get("digital_marker", "OFF"),
)
def __repr__(self) -> str:
return f"<ControlPulse len={self.length} wf={self.waveform} marker={self.digital_marker}>"
[docs]
@dataclass
class MeasPulse:
"""Configuration for a measurement pulse.
Defines a pulse used for measurement/readout of quantum states. Measurement
pulses output a waveform, acquire data, and apply integration weights for
demodulation and signal extraction. Optionally triggers a digital marker.
**Current Limitations:**
- Single waveform only (no waveform mixing)
- See :class:`ControlPulse` for simple control pulses.
**Demodulation:**
Integration weights are applied to the acquired signal to extract in-phase (I)
and quadrature (Q) components. Common weight sets include cosine/sine,
rotated, and optimized variants.
Attributes:
length (int): Measurement duration in nanoseconds (default: 1000).
Must be a multiple of 4 ns (1 clock cycle).
waveform (str): Name of the readout waveform (default: "readout_wf").
Must be defined in the waveform configuration.
integration_weights (IntegrationWeightMapping): Mapping of demodulation weights
for extracting I/Q components (default: default mapping).
digital_marker (Literal["ON", "OFF"] | None): Whether to trigger a digital
marker during measurement (default: None). None means no marker action.
"""
length: int = 1000 # in nanoseconds
waveform: str = "readout_wf" # waveform name
integration_weights: IntegrationWeightMapping = field(
default_factory=IntegrationWeightMapping
)
digital_marker: Literal["ON", "OFF"] | None = (
None # set digital marker during pulse
)
[docs]
def to_opx_config(self) -> Dict[str, Any]:
dct = {
"operation": "measurement",
"length": self.length,
"waveforms": {"single": self.waveform},
"integration_weights": self.integration_weights.to_opx_config(),
}
if self.digital_marker is not None:
dct["digital_marker"] = self.digital_marker
return dct
[docs]
def to_dict(self) -> Dict[str, Any]:
return {
"type": "measurement",
"length": self.length,
"waveform": self.waveform,
"integration_weights": self.integration_weights.to_dict(),
"digital_marker": self.digital_marker,
}
[docs]
@classmethod
def from_dict(cls, d: Dict[str, Any]) -> "MeasPulse":
from qeg_nmr_qua.config.integration import IntegrationWeightMapping
iw = IntegrationWeightMapping()
if isinstance(d.get("integration_weights"), dict):
vals = d.get("integration_weights", {})
iw = IntegrationWeightMapping()
for k, v in vals.items():
if hasattr(iw, k):
setattr(iw, k, v)
return cls(
length=d.get("length", 1000),
waveform=d.get("waveform", "readout_wf"),
integration_weights=iw,
digital_marker=d.get("digital_marker"),
)
def __repr__(self) -> str:
# integration_weights may be an IntegrationWeights (has .weights) or a
# IntegrationWeightMapping (has .to_dict()). Be robust.
try:
n = len(self.integration_weights.weights)
except Exception:
try:
n = len(self.integration_weights.to_dict())
except Exception:
n = 0
iw_summary = f"weights={n}"
return f"<MeasPulse len={self.length} wf={self.waveforms} {iw_summary} marker={self.digital_marker}>"
[docs]
@dataclass
class PulseConfig:
"""
Configuration for a pulse, which can be either a control pulse or a measurement.
"""
pulses: dict[str, ControlPulse | MeasPulse] = field(default_factory=dict)
[docs]
def add_pulse(
self,
name: str,
pulse: ControlPulse | MeasPulse,
) -> None:
"""
Add a pulse configuration from one of the pulse types.
Args:
name: Name of the pulse.
pulse: ControlPulse or MeasPulse instance.
"""
self.pulses[name] = pulse
[docs]
def add_control_pulse(
self,
name: str,
length: int,
waveform: str = "const",
digital_marker: Literal["ON", "OFF"] | None = "OFF",
) -> None:
"""
Add a control pulse configuration.
Args:
name: Name of the control pulse.
length: Pulse length in nanoseconds.
waveform: Waveform name.
digital_marker: Whether to set the digital marker during the pulse.
"""
self.pulses[name] = ControlPulse(
length=length,
waveform=waveform,
digital_marker=digital_marker,
)
[docs]
def add_measurement_pulse(
self,
name: str,
length: int,
waveform: str = "readout_wf",
digital_marker: Literal["ON", "OFF"] | None = None,
integration_weights: IntegrationWeightMapping = IntegrationWeightMapping(),
) -> None:
"""
Add a measurement pulse configuration.
Args:
name: Name of the measurement pulse.
length: Pulse length in nanoseconds.
waveform: Waveform name.
integration_weights: Integration weight mapping.
"""
self.pulses[name] = MeasPulse(
length=length,
waveform=waveform,
integration_weights=integration_weights,
digital_marker=digital_marker,
)
[docs]
def to_dict(self) -> Dict[str, Any]:
return {name: pulse.to_dict() for name, pulse in self.pulses.items()}
[docs]
@classmethod
def from_dict(cls, d: Dict[str, Any]) -> "PulseConfig":
pc = cls()
for name, pd in d.items():
if not isinstance(pd, dict):
continue
ptype = pd.get("type")
if ptype == "control":
pulse = ControlPulse.from_dict(pd)
elif ptype == "measurement":
pulse = MeasPulse.from_dict(pd)
else:
pulse = ControlPulse.from_dict(pd)
pc.pulses[name] = pulse
return pc
def __repr__(self) -> str:
return f"<PulseConfig pulses={len(self.pulses)}>"
[docs]
def to_opx_config(self) -> Dict[str, Any]:
return {name: pulse.to_opx_config() for name, pulse in self.pulses.items()}