Source code for quantify_scheduler.operations.operation
# Repository: https://gitlab.com/quantify-os/quantify-scheduler
# Licensed according to the LICENCE file on the main branch
"""Module containing the core concepts of the scheduler."""
from __future__ import annotations
import inspect
import logging
from collections import UserDict
from enum import Enum
from pydoc import locate
from quantify_scheduler.helpers.collections import make_hash
from quantify_scheduler.helpers.importers import export_python_object_to_path_string
from quantify_scheduler.json_utils import JSONSchemaValMixin, lru_cache
[docs]
cached_locate = lru_cache(locate)
[docs]
class Operation(JSONSchemaValMixin, UserDict):
"""
A representation of quantum circuit operations.
The :class:`~Operation` class is a JSON-compatible data structure that contains information
on how to represent the operation on the quantum-circuit and/or the quantum-device
layer. It also contains information on where the operation should be applied: the
:class:`~quantify_scheduler.resources.Resource` s used.
An operation always has the following attributes:
- duration (float): duration of the operation in seconds (can be 0).
- hash (str): an auto generated unique identifier.
- name (str): a readable identifier, does not have to be unique.
An Operation can contain information on several levels of abstraction.
This information is used when different representations are required. Note that when
initializing an operation not all of this information needs to be available
as operations are typically modified during the compilation steps.
.. tip::
:mod:`quantify_scheduler` comes with a
:mod:`~quantify_scheduler.operations.gate_library` and a
:mod:`~quantify_scheduler.operations.pulse_library` , both containing common
operations.
**JSON schema of a valid Operation**
.. jsonschema:: https://gitlab.com/quantify-os/quantify-scheduler/-/raw/main/quantify_scheduler/schemas/operation.json
.. note::
Two different Operations containing the same information generate the
same hash and are considered identical.
"""
[docs]
schema_filename = "operation.json"
[docs]
_class_signature = None
def __init__(self, name: str) -> None:
super().__init__()
# ensure keys exist
self.data["name"] = name
self.data["gate_info"] = {}
self.data["pulse_info"] = []
self.data["acquisition_info"] = []
self.data["logic_info"] = {}
[docs]
self._duration: float = 0
def __eq__(self, other: object) -> bool:
"""
Returns the equality of two instances based on its hash.
Parameters
----------
other
The other operation to compare to.
Returns
-------
:
"""
return hash(self) == hash(other)
def __str__(self) -> str:
"""
Returns a unique, evaluable string for unchanged data.
Returns a concise string representation which can be evaluated into a new
instance using :code:`eval(str(operation))` only when the data dictionary has
not been modified.
This representation is guaranteed to be unique.
"""
return f"{self.__class__.__name__}(name='{self.name}')"
def __getstate__(self) -> dict[str, object]:
return {
"deserialization_type": export_python_object_to_path_string(self.__class__),
"data": self.data,
}
def __setstate__(self, state: dict[str, dict]) -> None:
self.data = state["data"]
self._update()
def __hash__(self) -> int:
return make_hash(self.data)
[docs]
def _update(self) -> None:
"""Update the Operation's internals."""
def _get_operation_end(info: dict[str, float]) -> float:
"""Return the operation end in seconds."""
return info["t0"] + info["duration"]
# Iterate over the data and take the longest duration
self._duration = max(
map(
_get_operation_end,
self.data["pulse_info"] + self.data["acquisition_info"],
),
default=0,
)
@property
[docs]
def name(self) -> str:
"""Return the name of the operation."""
return self.data["name"]
@property
[docs]
def duration(self) -> float:
"""
Determine operation duration from pulse_info.
If the operation contains no pulse info, it is assumed to be ideal and
have zero duration.
"""
return self._duration
@property
[docs]
def hash(self) -> str:
"""
A hash based on the contents of the Operation.
Needs to be a str for easy compatibility with json.
"""
return str(hash(self))
@classmethod
[docs]
def _get_signature(cls, parameters: dict) -> str:
"""
Returns the constructor call signature of this instance for serialization.
The string constructor representation can be used to recreate the object
using eval(signature).
Parameters
----------
parameters : dict
The current data dictionary.
Returns
-------
:
"""
if cls._class_signature is None:
logging.info(f"Caching signature for class {cls.__name__}")
cls._class_signature = inspect.signature(cls)
signature = cls._class_signature
def to_kwarg(key: str) -> str:
"""
Returns a key-value pair in string format of a keyword argument.
Parameters
----------
key
The parameter key
Returns
-------
:
"""
value = parameters[key]
if isinstance(value, Enum):
enum_value = value.value
value = enum_value
value = f"'{value}'" if isinstance(value, str) else value
return f"{key}={value}"
required_params = list(signature.parameters.keys())
kwargs_list = map(to_kwarg, required_params)
return f'{cls.__name__}({",".join(kwargs_list)})'
[docs]
def add_gate_info(self, gate_operation: Operation) -> None:
"""
Updates self.data['gate_info'] with contents of gate_operation.
Parameters
----------
gate_operation
an operation containing gate_info.
"""
self.data["gate_info"].update(gate_operation.data["gate_info"])
[docs]
def add_device_representation(self, device_operation: Operation) -> None:
"""
Adds device-level representation details to the current operation.
Parameters
----------
device_operation
an operation containing the pulse_info and/or acquisition info describing
how to represent the current operation at the quantum-device layer.
"""
self.add_pulse(device_operation)
self.add_acquisition(device_operation)
[docs]
def add_pulse(self, pulse_operation: Operation) -> None:
"""
Adds pulse_info of pulse_operation Operation to this Operation.
Parameters
----------
pulse_operation
an operation containing pulse_info.
"""
self.data["pulse_info"] += pulse_operation.data["pulse_info"]
self._update()
[docs]
def add_acquisition(self, acquisition_operation: Operation) -> None:
"""
Adds acquisition_info of acquisition_operation Operation to this Operation.
Parameters
----------
acquisition_operation
an operation containing acquisition_info.
"""
self.data["acquisition_info"] += acquisition_operation.data["acquisition_info"]
self._update()
@classmethod
[docs]
def is_valid(cls, object_to_be_validated: Operation) -> bool:
"""
Validates the object's contents against the schema.
Additionally checks if the hash property of the object evaluates correctly.
"""
valid_operation = super().is_valid(object_to_be_validated)
if valid_operation:
_ = object_to_be_validated.hash # test that the hash property evaluates
return True
return False
@property
[docs]
def valid_gate(self) -> bool:
"""An operation is a valid gate if it has gate-level representation details."""
return len(self.data["gate_info"]) > 0
@property
[docs]
def valid_pulse(self) -> bool:
"""An operation is a valid pulse if it has pulse-level representation details."""
return len(self.data["pulse_info"]) > 0
@property
[docs]
def valid_acquisition(self) -> bool:
"""
An operation is a valid acquisition
if it has pulse-level acquisition representation details.
"""
return len(self.data["acquisition_info"]) > 0
@property
[docs]
def is_conditional_acquisition(self) -> bool:
"""
An operation is conditional if one of the following holds, ``self`` is an
an acquisition with a ``feedback_trigger_label`` assigned to it.
"""
if (acq_info := self.data.get("acquisition_info")) is not None:
return len(acq_info) > 0 and (acq_info[0].get("feedback_trigger_label") is not None)
return False
@property
[docs]
def is_control_flow(self) -> bool:
"""
Determine if operation is a control flow operation.
Returns
-------
bool
Whether the operation is a control flow operation.
"""
return self.data.get("control_flow_info") is not None
@property
[docs]
def has_voltage_offset(self) -> bool:
"""Checks if the operation contains information for a voltage offset."""
return any(
"offset_path_I" in pulse_info or "offset_path_Q" in pulse_info
for pulse_info in self.data["pulse_info"]
)