# Copyright (C) 2024 qBraid
#
# This file is part of the qBraid-SDK
#
# The qBraid-SDK is free software released under the GNU General Public License v3
# or later. You can redistribute and/or modify it under the terms of the GPL v3.
# See the LICENSE file in the project root or <https://www.gnu.org/licenses/gpl-3.0.html>.
#
# THERE IS NO WARRANTY for the qBraid-SDK, as per Section 15 of the GPL v3.
"""
Module providing granular type checking for quantum programs
that use Python's built-in types.
"""
from abc import ABCMeta, abstractmethod
from typing import Any, Optional, Type, TypeVar
from pyqasm.analyzer import Qasm3Analyzer
from pyqasm.exceptions import QasmParsingError
from .exceptions import QasmError as QbraidQasmError
from .exceptions import ValidationError as ProgramValidationError
IonQDictType = TypeVar("IonQDictType", bound=dict)
class IonQDictInstanceMeta(QbraidMetaType):
"""Metaclass for IonQ JSON type checking based on dict content."""
@property
def __alias__(cls) -> str:
return "ionq"
@property
def __bound__(cls) -> Type[dict]:
return dict
@staticmethod
def _validate_field(single: Any, multiple: Any, field_name: str) -> None:
"""Helper method to validate single or multiple target/control fields."""
if single is not None and multiple is not None:
raise ProgramValidationError(
f"Both {field_name} and {field_name}s are set; only one should be provided."
)
if single is not None and not isinstance(single, int):
raise ProgramValidationError(f"Invalid {field_name}: {single}. Must be an integer.")
if multiple is not None and not (
isinstance(multiple, list) and all(isinstance(item, int) for item in multiple)
):
raise ProgramValidationError(
f"Invalid {field_name}s: {multiple}. Must be a list of integers."
)
def __instancecheck__(cls, instance: Any) -> bool:
"""Custom instance checks based on dict format."""
if not isinstance(instance, dict):
return False
qubits = instance.get("qubits")
circuit = instance.get("circuit")
gateset = instance.get("gateset")
ciruit_format = instance.get("format")
if ciruit_format is not None and not isinstance(ciruit_format, str):
return False
if gateset is not None and not isinstance(gateset, str):
return False
if not isinstance(qubits, int) or not isinstance(circuit, list):
return False
for op in circuit:
if not isinstance(op, dict):
return False
gate = op.get("gate")
rotation = op.get("rotation")
target, targets = op.get("target"), op.get("targets")
control, controls = op.get("control"), op.get("controls")
if not isinstance(gate, str):
return False
if rotation is not None and not isinstance(rotation, (int, float)):
return False
try:
cls._validate_field(target, targets, "target")
cls._validate_field(control, controls, "control")
except ProgramValidationError:
return False
return True
[docs]
class IonQDict(metaclass=IonQDictInstanceMeta):
"""Marker class for dict that are valid IonQ JSON formatted programs."""
[docs]
def get_qasm_type_alias(qasm: str) -> str:
"""
Determines the type alias for an OpenQASM program based on its version.
Args:
qasm (str): The OpenQASM program string.
Returns:
str: The QASM version alias ('qasm2' or 'qasm3').
Raises:
`QbraidQasmError`: If the string does not represent a valid OpenQASM program.
Note:
`QbraidQasmError` is an alias for :exc:`QasmError`.
"""
try:
version = Qasm3Analyzer.extract_qasm_version(qasm)
type_alias = f"qasm{version}"
return type_alias
except QasmParsingError as err: # catch the pyqasm exception
raise QbraidQasmError(
"Could not determine the type alias: the OpenQASM program may be invalid."
) from err
class BaseQasmInstanceMeta(QbraidMetaType):
"""Metaclass for OpenQASM type checking based on string content.
Attributes:
version (Optional[int]): The specific OpenQASM type to check for.
"""
version: Optional[int] = None
@property
def __alias__(cls) -> Optional[str]:
if isinstance(cls.version, int):
return f"qasm{cls.version}"
return None
@property
def __bound__(cls) -> Type[str]:
return str
def __instancecheck__(cls, instance: Any) -> bool:
"""Custom instance checks based on OpenQASM type.
Args:
instance: The object to check.
Returns:
bool: True if instance is a string matching the OpenQASM type, False otherwise.
"""
if not isinstance(instance, str):
return False
try:
return Qasm3Analyzer.extract_qasm_version(instance) == cls.version
except QasmParsingError:
return False
class Qasm2StringMeta(BaseQasmInstanceMeta):
"""Metaclass for instances representing OpenQASM 2 strings."""
version = 2
class Qasm3StringMeta(BaseQasmInstanceMeta):
"""Metaclass for instances representing OpenQASM 3 strings."""
version = 3
[docs]
class Qasm2String(metaclass=Qasm2StringMeta):
"""Marker class for strings that are valid OpenQASM 2 programs."""
[docs]
class Qasm3String(metaclass=Qasm3StringMeta):
"""Marker class for strings that are valid OpenQASM 3 programs."""
class QasmStringType(str):
"""Base class for OpenQASM string types, providing validation upon instantiation."""
version: Optional[int] = None
def __new__(cls, value):
if not isinstance(value, str):
raise TypeError("OpenQASM strings must be initialized with a string.")
if not Qasm3Analyzer.extract_qasm_version(value) == cls.version:
raise ValueError(f"String does not conform to OpenQASM {cls.version} format.")
return str.__new__(cls, value)
class Qasm2StringType(QasmStringType):
"""Specifically typed string for OpenQASM 2 formatted text."""
version = 2
class Qasm3StringType(QasmStringType):
"""Specifically typed string for OpenQASM 3 formatted text."""
version = 3
class QuboCoefficientsDictInstanceMeta(QbraidMetaType):
"""Metaclass for Qubo coefficients JSON type checking based on dict content."""
@property
def __alias__(cls) -> str:
return "qubo"
@property
def __bound__(cls) -> Type[dict]:
return dict
def __instancecheck__(cls, instance: Any) -> bool:
"""Custom instance checks based on dict format."""
if not instance or not isinstance(instance, dict):
return False
for key, value in instance.items():
if not (
isinstance(key, tuple) and len(key) == 2 and all(isinstance(k, str) for k in key)
):
return False
if not isinstance(value, (float, int)):
return False
return True
class QuboCoefficientsDict(metaclass=QuboCoefficientsDictInstanceMeta):
"""Marker class for dict that are valid Qubo coefficients format."""
QBRAID_META_TYPES = {IonQDict, QuboCoefficientsDict}
BOUND_QBRAID_META_TYPES = {Qasm2String, Qasm3String}