Source code for quantify_scheduler.yaml_utils

# Repository: https://gitlab.com/quantify-os/quantify-scheduler
# Licensed according to the LICENCE file on the main branch
"""Module containing quantify YAML utilities."""

from __future__ import annotations

import os
from datetime import datetime, timezone
from enum import Enum
from typing import TYPE_CHECKING, Any
from typing_extensions import Self

import numpy as np
import ruamel.yaml as ry
from qcodes.instrument import Instrument
from qcodes.instrument.instrument_meta import InstrumentMeta

from quantify_core.data.handling import get_datadir

if TYPE_CHECKING:
    from pathlib import Path


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"])


# 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) class YAMLSerializableMeta(InstrumentMeta): """ Metaclass to register mixed in classes with the YAML parser. Needs to inherit from ``InstrumentMeta`` due to metaclass conflict. """ def __init__(cls, name, bases, dct) -> None: # noqa: ANN001, N805 """ Register the class with the YAML parser so that (de)serialization can happen automatically. """ super().__init__(name, bases, dct) yaml.register_class(cls)
[docs] class YAMLSerializable(metaclass=YAMLSerializableMeta): """ Mixin to allow (de)serialization of instruments from/to YAML. NOTE: Only works with ``Instrument`` subclasses, for others use `@yaml.register_class`. NOTE: `to_yaml` and `from_yaml` methods cannot be created because they would be found and used by the ``ruamel.yaml`` representers and constructors. """
[docs] def to_yaml_file( self, path: str | Path | None = None, add_timestamp: bool = True, ) -> str: """ Convert the object's data structure to a YAML string and store it in a file. Parameters ---------- path The path to the directory where the file is created. Default is `None`, in which case the file will be saved in the directory determined by :func:`~quantify_core.data.handling.get_datadir()`. add_timestamp Specify whether to append timestamp to the filename. Default is True. Returns ------- The name of the file containing the serialized object. """ if path is None: path = get_datadir() name = getattr(self, "name") # noqa: B009 This is to shut up the linter about self.name if add_timestamp: timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d_%H-%M-%S_%Z") filename = os.path.join(path, f"{name}_{timestamp}.yaml") else: filename = os.path.join(path, f"{name}.yaml") with open(filename, "w") as file: yaml.dump(self, file) return filename
@classmethod
[docs] def from_yaml_file(cls, filename: str | Path) -> Self: """ Read YAML data from a file and convert it to an instance of the attached class. Parameters ---------- filename The name of the file containing the serialized object. Returns ------- The deserialized object. """ with open(filename) as file: deserialized_obj = yaml.load(file) return deserialized_obj
def __setstate__(self, state: dict[str, Any]) -> None: """ When deserializing an ``Instrument``, add it to the qcodes global registry. Must be invoked by subclasses if :meth:`__setstate__` is overridden. """ if isinstance(self, Instrument): type(self).record_instance(self)
__all__ = ["YAMLSerializable", "yaml"]