# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
from braket.device_schema import DeviceActionProperties, DeviceActionType
from braket.device_schema.aqt.aqt_device_capabilities_v1 import AqtDeviceCapabilities
from braket.device_schema.device_capabilities import DeviceCapabilities
from braket.device_schema.ionq.ionq_device_capabilities_v1 import IonqDeviceCapabilities
from braket.device_schema.result_type import ResultType
from braket.device_schema.standardized_gate_model_qpu_device_properties_v1 import (
OneQubitProperties,
TwoQubitProperties,
)
from braket.ir.openqasm import Program as OpenQASMProgram
from braket.ir.openqasm import ProgramSet as OpenQASMProgramSet
from braket.schema_common.schema_base import BraketSchemaBase
from braket.ahs import AnalogHamiltonianSimulation
from braket.circuits import Circuit
from braket.circuits.translations import BRAKET_GATES
from braket.emulation._standardization import (
_standardize_aqt_device_properties,
_standardize_ionq_device_properties,
)
from braket.program_sets import ProgramSet
ACTION_TO_SPECIFICATION = {
"braket.ir.openqasm.program": [OpenQASMProgram, Circuit],
"braket.ir.openqasm.program_set": [OpenQASMProgramSet, ProgramSet],
"braket.ir.ahs.program": [AnalogHamiltonianSimulation],
"braket.ir.jaqcd.program": [],
}
[docs]
class DeviceEmulatorProperties:
"""Properties for device emulation.
Args:
qubitCount (int): Number of qubits in the device
nativeGateSet (list[str]): List of native gates supported by the device. Must be valid
Braket gates. Valid gates include: gphase, i, h, x, y, z, cv, cnot, cy, cz, ecr, s, si,
t, ti, v, vi, phaseshift, cphaseshift, cphaseshift00, cphaseshift01, cphaseshift10, rx,
ry, rz, U, swap, iswap, pswap, xy, xx, yy, zz, ccnot, cswap, gpi, gpi2, prx, ms, unitary
connectivityGraph (dict[str, list[str]]): Graph representing qubit connectivity. If it is an
empty dictionary, the device is treated as fully connected.
oneQubitProperties (dict[str, OneQubitProperties]): Properties of one-qubit calibration
details
twoQubitProperties (dict[str, TwoQubitProperties]): Properties of two-qubit calibration
details
supportedResultTypes (list[ResultType]): List of supported result types.
supportedActions (list[DeviceActionType, DeviceActionProperties]): List of device actions
"""
def __init__(
self,
qubitCount: int,
nativeGateSet: list[str],
connectivityGraph: dict[str, list[str]],
oneQubitProperties: dict[str, OneQubitProperties],
twoQubitProperties: dict[str, TwoQubitProperties],
supportedResultTypes: list[ResultType],
supportedActions: dict[DeviceActionType, DeviceActionProperties] | None = None,
):
"""Initialize a DeviceEmulatorProperties instance."""
# Validate inputs
self._validate_native_gate_set(nativeGateSet)
self._validate_one_qubit_properties(oneQubitProperties, qubitCount)
# Get qubit labels for further validation
indices = list(oneQubitProperties.keys())
qubit_labels = sorted(int(x) for x in indices)
self._validate_connectivity_graph(connectivityGraph, qubit_labels)
self._validate_two_qubit_properties(twoQubitProperties, qubit_labels)
# Store properties
self._qubit_count = qubitCount
self._native_gate_set = nativeGateSet
self._connectivity_graph = connectivityGraph
self._one_qubit_properties = oneQubitProperties
self._two_qubit_properties = twoQubitProperties
self._supported_result_types = supportedResultTypes
self._supported_actions = supportedActions
@staticmethod
def _validate_native_gate_set(nativeGateSet: list[str]) -> None:
"""Validate that all gates in nativeGateSet are valid Braket gates."""
valid_gates = ", ".join(BRAKET_GATES.keys())
for gate in nativeGateSet:
if gate not in BRAKET_GATES:
raise ValueError(
f"Gate '{gate}' is not a valid Braket gate. Valid gates are: {valid_gates}"
)
@staticmethod
def _validate_one_qubit_properties(
oneQubitProperties: dict[str, OneQubitProperties], qubitCount: int
) -> None:
"""Validate oneQubitProperties."""
if len(oneQubitProperties) != qubitCount:
raise ValueError("The length of oneQubitProperties should be the same as qubitCount")
@staticmethod
def _node_validator(node: str, qubit_labels: list[int], field_name: str) -> None:
"""Validate that a node represents a valid qubit index."""
if int(node) not in qubit_labels:
raise ValueError(
f"Node {node} in {field_name} must represent a valid qubit index in {qubit_labels}."
)
@classmethod
def _validate_connectivity_graph(
cls, connectivityGraph: dict[str, list[str]], qubit_labels: list[int]
) -> None:
"""Validate connectivityGraph."""
for node, neighbors in connectivityGraph.items():
cls._node_validator(node, qubit_labels, "connectivityGraph")
for neighbor in neighbors:
if int(neighbor) not in qubit_labels:
raise ValueError(
f"Neighbor {neighbor} for node {node} must represent a valid qubit index "
f"in `qubit_labels`."
)
@classmethod
def _validate_two_qubit_properties(
cls, twoQubitProperties: dict[str, TwoQubitProperties], qubit_labels: list[int]
) -> None:
"""Validate twoQubitProperties."""
for edge in twoQubitProperties:
node_1, node_2 = edge.split("-")
cls._node_validator(node_1, qubit_labels, "twoQubitProperties")
cls._node_validator(node_2, qubit_labels, "twoQubitProperties")
@property
def qubit_labels(self) -> list[int]:
"""Get the sorted list of qubit indices."""
indices = list(self.one_qubit_properties.keys())
return sorted(int(x) for x in indices)
@property
def fully_connected(self) -> bool:
"""Determine if the connectivity graph is fully connected.
Note: We treat the graph as undirected, and determine if it is
a complete graph by counting the number of distinct edges
"""
if not self.connectivity_graph:
return True
edges = set()
for node, neighbors in self.connectivity_graph.items():
edges_node = [(int(node), int(neighbor)) for neighbor in neighbors]
edges_node = [(min(edge), max(edge)) for edge in edges_node]
edges.update(edges_node)
return len(edges) == self.qubit_count * (self.qubit_count - 1) // 2
@property
def directed(self) -> bool:
"""Determine if the connectivity graph is a directed graph."""
for node, neighbors in self.connectivity_graph.items():
for neighbor in neighbors:
# If neighbor doesn't link back to node, it's directed
if node not in self.connectivity_graph.get(neighbor, []):
return True
return False
@property
def qubit_count(self) -> int:
return self._qubit_count
@property
def native_gate_set(self) -> list[str]:
return self._native_gate_set
@property
def connectivity_graph(self) -> list[str]:
return self._connectivity_graph
@property
def one_qubit_properties(self) -> dict[str, OneQubitProperties]:
return self._one_qubit_properties
@property
def two_qubit_properties(self) -> dict[str, TwoQubitProperties]:
return self._two_qubit_properties
@property
def supported_result_types(self) -> list[ResultType]:
return self._supported_result_types
@property
def supported_specifications(self) -> tuple[BraketSchemaBase] | BraketSchemaBase:
return (
tuple(sum((ACTION_TO_SPECIFICATION[action] for action in self._supported_actions), [])) # noqa: RUF017
if self._supported_actions
else Circuit
)
@property
def supported_actions(self) -> dict[str, str]:
return self._supported_actions
[docs]
@classmethod
def from_device_properties(
cls, device_properties: DeviceCapabilities
) -> "DeviceEmulatorProperties":
"""Create a DeviceEmulatorProperties instance from DeviceCapabilities."""
if not isinstance(device_properties, DeviceCapabilities):
raise TypeError("device_properties has to be an instance of DeviceCapabilities.")
if isinstance(device_properties, IonqDeviceCapabilities):
device_properties = _standardize_ionq_device_properties(device_properties)
if isinstance(device_properties, AqtDeviceCapabilities):
device_properties = _standardize_aqt_device_properties(device_properties)
properties_dict = device_properties.dict()
required_keys = ["paradigm", "standardized"]
for key in required_keys:
if (key not in properties_dict) or (properties_dict[key] is None):
raise ValueError(f"device_properties must have non-empty value for key {key}")
if "braket.ir.openqasm.program" not in properties_dict["action"]:
raise ValueError(
"The action in device_properties must have key `braket.ir.openqasm.program`."
)
supportedActions = device_properties.action
# Convert dictionary representations to OneQubitProperties and TwoQubitProperties objects
one_qubit_props = {}
for key, value in properties_dict["standardized"]["oneQubitProperties"].items():
one_qubit_props[key] = OneQubitProperties.parse_obj(value)
two_qubit_props = {}
for key, value in properties_dict["standardized"]["twoQubitProperties"].items():
two_qubit_props[key] = TwoQubitProperties.parse_obj(value)
# Convert dictionary representations to ResultType objects
result_types = [
ResultType.parse_obj(value)
for value in properties_dict["action"]["braket.ir.openqasm.program"][
"supportedResultTypes"
]
]
return DeviceEmulatorProperties(
qubitCount=properties_dict["paradigm"]["qubitCount"],
nativeGateSet=properties_dict["paradigm"]["nativeGateSet"],
connectivityGraph=properties_dict["paradigm"]["connectivity"]["connectivityGraph"],
oneQubitProperties=one_qubit_props,
twoQubitProperties=two_qubit_props,
supportedResultTypes=result_types,
supportedActions=supportedActions,
)
[docs]
@classmethod
def from_json(cls, device_properties_json: str) -> "DeviceEmulatorProperties":
"""Create a DeviceEmulatorProperties instance from a JSON string."""
return cls.from_device_properties(BraketSchemaBase.parse_raw_schema(device_properties_json))