# Repository: https://gitlab.com/qblox/packages/software/qblox-scheduler
# Licensed according to the LICENSE file on the main branch
#
# Copyright 2025, Qblox B.V.
"""Module containing scheduler YAML utilities."""
from __future__ import annotations
from enum import Enum
from typing import TYPE_CHECKING
import numpy as np
import ruamel.yaml as ry
if TYPE_CHECKING:
from pydantic import BaseModel
from qcodes.instrument import Instrument
def represent_enum(representer: ry.Representer, data: Enum) -> ry.ScalarNode:
"""Provide a value-based representation for Enum instances in YAML."""
# An enum value can be any scalar type; if there's no matching representer, default to str.
repr_method = representer.yaml_representers.get(type(data.value), str)
return repr_method(representer, data.value)
def represent_ndarray(representer: ry.Representer, data: np.ndarray) -> ry.MappingNode:
"""Represent a NumPy array as a mapping, including its type and shape."""
node = representer.represent_mapping(
"!numpy.ndarray",
{
"dtype": str(data.dtype),
"shape": data.shape,
"data": data.tolist(),
},
)
# Set flow_style for "shape" and "data", so they aren't serialized one element per line.
for key in node.value:
if key[0].value in ("shape", "data"):
key[1].flow_style = True
return node
def construct_ndarray(constructor: ry.Constructor, node: ry.MappingNode) -> np.ndarray:
"""Restore a NumPy array from a mapping with its proper type and shape."""
if isinstance(constructor, ry.RoundTripConstructor):
data = ry.CommentedMap()
constructor.construct_mapping(node, maptyp=data, deep=True)
else:
data = constructor.construct_mapping(node, deep=True)
return np.array(data["data"], dtype=data["dtype"]).reshape(data["shape"])
def represent_instrument(representer: ry.Representer, data: Instrument) -> ry.ScalarNode:
"""Provide a name-based representation for QCoDeS Instrument instances in YAML."""
repr_method = representer.yaml_representers.get(str)
return repr_method(representer, data.name)
[docs]
def register_model(cls: type[BaseModel], yaml_obj: ry.YAML) -> None:
"""
Register a Pydantic model to be serialized by `ruamel.yaml`, with a YAML tag corresponding
to the name of the model class.
The implementation mirrors the original :meth:`~ruamel.yaml.YAML.register_class`,
but unlike that it doesn't use `to_yaml/from_yaml` on the target class,
instead relying solely on `__getstate__` and `__setstate__`.
"""
tag = "!" + cls.__name__
def represent_model(representer: ry.Representer, data: BaseModel) -> ry.MappingNode:
return representer.represent_yaml_object(
tag,
data,
cls,
flow_style=representer.default_flow_style,
)
def construct_model(constructor: ry.Constructor, node: ry.MappingNode) -> BaseModel:
if isinstance(constructor, ry.RoundTripConstructor):
data = ry.CommentedMap()
constructor.construct_mapping(node, maptyp=data, deep=True)
else:
data = constructor.construct_mapping(node, deep=True)
return cls.model_validate(data)
yaml_obj.representer.add_representer(cls, represent_model)
yaml_obj.constructor.add_constructor(tag, construct_model)
[docs]
def register_legacy_instruments(yaml_obj: ry.YAML) -> None:
"""
Register `MeasurementControl` and `InstrumentCoordinator` with the global YAML object
to be de/serialized correctly.
"""
from qblox_scheduler.instrument_coordinator.instrument_coordinator import InstrumentCoordinator
from quantify_core.measurement.control import MeasurementControl
# Support QCoDeS instruments (for `MeasurementControl` and `InstrumentCoordinator`)
yaml_obj.representer.add_representer(MeasurementControl, represent_instrument)
yaml_obj.representer.add_representer(InstrumentCoordinator, represent_instrument)
# The "rt" (round-trip) loader can be advantageous compared to the "safe" loader,
# particularly when working with complex, nested Python classes:
# - Comments: it retains comments present in the YAML file
# - Key Order: it preserves the order of keys in YAML mappings (dictionaries)
# - Formatting: it attempts to maintain the original indentation and whitespace
# - Anchors and Aliases: it preserves YAML anchors and aliases
# - Tags: it handles tags, including custom tags that are used to represent your custom classes
[docs]
yaml = ry.YAML(typ="rt")
# Support Enum and its subclasses
yaml.representer.add_multi_representer(Enum, represent_enum)
# Support NumPy arrays
yaml.representer.add_representer(np.ndarray, represent_ndarray)
yaml.constructor.add_constructor("!numpy.ndarray", construct_ndarray)
__all__ = ["register_legacy_instruments", "register_model", "yaml"]