From ac3b90f9b69fefa0f24b15946ddfeacf6be36949 Mon Sep 17 00:00:00 2001 From: Sergei Mironov Date: Wed, 13 Mar 2024 23:33:46 +0400 Subject: [PATCH] Native quantum control toml (#554) By this PR we change the TOML file schema in a way that allows us to represent quantum control at per-gate level. [sc-57159] [sc-57172] --------- Co-authored-by: David Ittah Co-authored-by: Romain Moyard --- .gitignore | 1 + Makefile | 6 +- bin/toml-check.py | 94 ++++ doc/changelog.md | 8 +- doc/dev/custom_devices.rst | 196 ++++--- .../cuda/catalyst_to_cuda_interpreter.py | 10 +- frontend/catalyst/cuda/cuda_quantum.toml | 70 +-- frontend/catalyst/pennylane_extensions.py | 44 +- frontend/catalyst/qjit_device.py | 232 ++++---- frontend/catalyst/utils/runtime.py | 282 ++++++---- frontend/catalyst/utils/toml.py | 137 ++++- frontend/test/lit/test_quantum_control.py | 37 +- .../test/pytest/test_braket_local_devices.py | 3 +- frontend/test/pytest/test_config_functions.py | 512 +++++++++++++++--- frontend/test/pytest/test_custom_devices.py | 76 ++- frontend/test/pytest/test_device_api.py | 15 +- frontend/test/pytest/test_qnode.py | 2 +- requirements.txt | 1 + .../lightning/lightning.kokkos.schema2.toml | 109 ++++ .../lightning/lightning.qubit.schema2.toml | 114 ++++ .../backend/openqasm/braket_aws_qubit.toml | 124 ++--- .../backend/openqasm/braket_local_qubit.toml | 134 +++-- runtime/tests/third_party/dummy_device.toml | 193 ++++--- 23 files changed, 1607 insertions(+), 793 deletions(-) create mode 100755 bin/toml-check.py create mode 100644 runtime/lib/backend/lightning/lightning.kokkos.schema2.toml create mode 100644 runtime/lib/backend/lightning/lightning.qubit.schema2.toml diff --git a/.gitignore b/.gitignore index 0c442cb088..ed0f76c426 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ pennylane_catalyst.egg-info dist frontend/catalyst/bin frontend/catalyst/lib +frontend/catalyst/_revision.py frontend/mlir_quantum # IDEs diff --git a/Makefile b/Makefile index d64e0f1278..25117ba2e8 100644 --- a/Makefile +++ b/Makefile @@ -17,6 +17,7 @@ ENABLE_OPENQASM?=ON TEST_BACKEND ?= "lightning.qubit" TEST_BRAKET ?= NONE ENABLE_ASAN ?= OFF +TOML_SPECS ?= $(shell find ./runtime ./frontend -name '*.toml') PLATFORM := $(shell uname -s) ifeq ($(PLATFORM),Linux) @@ -108,7 +109,10 @@ dummy_device: $(MAKE) -C runtime dummy_device .PHONY: test test-runtime test-frontend lit pytest test-demos -test: test-runtime test-frontend test-demos +test: test-runtime test-frontend test-demos test-toml-spec + +test-toml-spec: + $(PYTHON) ./bin/toml-check.py $(TOML_SPECS) test-runtime: $(MAKE) -C runtime test diff --git a/bin/toml-check.py b/bin/toml-check.py new file mode 100755 index 0000000000..52009b4fb3 --- /dev/null +++ b/bin/toml-check.py @@ -0,0 +1,94 @@ +#!/usr/bin/python3 +# Copyright 2024 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License 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. + +""" +This program checks the syntax of quantum device configuration files. It is a strict parser of +TOML format, narrowed down to match our requirements. For the Lark's EBNF dialect syntax, see the +Lark grammar reference: + * https://lark-parser.readthedocs.io/en/latest/grammar.html +""" + +import sys +from argparse import ArgumentParser +from textwrap import dedent + +try: + from lark import Lark, LarkError, UnexpectedInput +except ImportError as e: + raise RuntimeError( + "toml-check.py requires `lark` library. Consider using `pip install lark`" + ) from e + +parser = Lark( + dedent( + """ + start: schema_body \ + gates_native_section \ + gates_decomp_section \ + gates_matrix_section \ + gates_observables_section \ + measurement_processes_section \ + compilation_section \ + options_section? + schema_body: schema_decl + gates_native_section: "[operators.gates.native]" gate_decls + gates_decomp_section: "[operators.gates.decomp]" gate_decls + gates_matrix_section: "[operators.gates.matrix]" gate_decls + gates_observables_section: "[operators.observables]" gate_decls + measurement_processes_section: "[measurement_processes]" gate_decls + compilation_section: "[compilation]" flag_decl* + options_section: "[options]" option_decl* + schema_decl: "schema" "=" "2" + gate_decls: (gate_decl)* + gate_decl: name "=" "{" (gate_trait ("," gate_trait)*)? "}" + gate_trait: gate_condition | gate_properties + gate_condition: "condition" "=" "[" ( "\\"finiteshots\\"" | "\\"analytic\\"" ) "]" + gate_properties: "properties" "=" "[" gate_property ("," gate_property)* "]" + gate_property: "\\"controllable\\"" | "\\"invertible\\"" | "\\"differentiable\\"" + flag_decl: ( "qjit_compatible" | "runtime_code_generation" | \ + "mid_circuit_measurement" | "dynamic_qubit_management" ) "=" boolean + option_decl: name "=" (name | "\\"" name "\\"") + name: /[a-zA-Z0-9_]+/ + boolean: "true" | "false" + COMMENT: "#" /./* + %import common.WS + %ignore WS + %ignore COMMENT + """ + ) +) + + +if __name__ == "__main__": + ap = ArgumentParser(prog="toml-check.py") + ap.add_argument( + "filenames", metavar="TOML", type=str, nargs="+", help="One or more *toml files to check" + ) + ap.add_argument("--verbose", action="store_true", help="Be verbose") + fname = None + try: + arguments = ap.parse_args(sys.argv[1:]) + for fname in arguments.filenames: + with open(fname, "r", encoding="utf-8") as f: + contents = f.read() + tree = parser.parse(contents) + if arguments.verbose: + print(tree.pretty()) + except UnexpectedInput as e: + print(f"toml-check: error in {fname}:{e.line}:{e.column}", file=sys.stderr) + raise e + except LarkError as e: + print(f"toml-check: error in {fname}", file=sys.stderr) + raise e diff --git a/doc/changelog.md b/doc/changelog.md index 5b23ed4eab..c4e7642f5e 100644 --- a/doc/changelog.md +++ b/doc/changelog.md @@ -9,6 +9,11 @@

Improvements

+* An updated quantum device specification format is now supported by Catalyst. The toml schema 2 + configs allow device autors to specify individual gate properties such as native quantum control + support, gate invertibility or differentiability. + [(#554)](https://github.com/PennyLaneAI/catalyst/pull/554) + * Catalyst now supports devices built from the [new PennyLane device API](https://docs.pennylane.ai/en/stable/code/api/pennylane.devices.Device.html). [(#565)](https://github.com/PennyLaneAI/catalyst/pull/565) @@ -57,8 +62,9 @@ This release contains contributions from (in alphabetical order): Ali Asadi, David Ittah, +Erick Ochoa Lopez, Romain Moyard, -Erick Ochoa Lopez. +Sergei Mironov. # Release 0.5.0 diff --git a/doc/dev/custom_devices.rst b/doc/dev/custom_devices.rst index 90bfc6bd0e..2e5f8f974e 100644 --- a/doc/dev/custom_devices.rst +++ b/doc/dev/custom_devices.rst @@ -230,130 +230,124 @@ headers and fields are generally required, unless stated otherwise. .. code-block:: toml # Which version of the specification format is being used. - schema = 1 - - [device] - name = "dummy.device.qubit" - - [operators] - # Observables supported by the device - observables = [ - "PauliX", - "PauliY", - "PauliZ", - "Hadamard", - "Hermitian", - "Identity", - "Projector", - "SparseHamiltonian", - "Hamiltonian", - "Sum", - "SProd", - "Prod", - "Exp", - ] + schema = 2 # The union of all gate types listed in this section must match what # the device considers "supported" through PennyLane's device API. - [[operators.gates]] - native = [ - # Operators that shouldn't be decomposed. - "QubitUnitary", - "PauliX", - "PauliY", - "PauliZ", - "MultiRZ", - "Hadamard", - "S", - "T", - "CNOT", - "SWAP", - "CSWAP", - "Toffoli", - "CY", - "CZ", - "PhaseShift", - "ControlledPhaseShift", - "RX", - "RY", - "RZ", - "Rot", - "CRX", - "CRY", - "CRZ", - "CRot", - "Identity", - "IsingXX", - "IsingYY", - "IsingZZ", - "IsingXY", - ] + # The gate definition has the following format: + # + # GATE = { properties = [ PROPS ], condition = [ COND ] } + # + # Where: + # + # PROPS: zero or more comma-separated quoted strings: + # "controllable", "invertible", "differentiable" + # COND: quoted string, on of: + # "analytic", "finiteshots" + # + [operators.gates.native] + + QubitUnitary = { properties = [ "controllable", "invertible"] } + PauliX = { properties = [ "controllable", "invertible"] } + PauliY = { properties = [ "controllable", "invertible"] } + PauliZ = { properties = [ "controllable", "invertible"] } + MultiRZ = { properties = [ "controllable", "invertible" ] } + Hadamard = { properties = [ "controllable", "invertible"] } + S = { properties = [ "controllable", "invertible" ] } + T = { properties = [ "controllable", "invertible" ] } + CNOT = { properties = [ "invertible" ] } + SWAP = { properties = [ "controllable", "invertible" ] } + CSWAP = { properties = [ "invertible" ] } + Toffoli = { properties = [ "controllable", "invertible" ] } + CY = { properties = [ "invertible" ] } + CZ = { properties = [ "invertible" ] } + PhaseShift = { properties = [ "controllable", "invertible" ] } + ControlledPhaseShift = { properties = [ "controllable", "invertible" ] } + RX = { properties = [ "controllable", "invertible" ] } + RY = { properties = [ "controllable", "invertible" ] } + RZ = { properties = [ "controllable", "invertible" ] } + Rot = { properties = [ "controllable", "invertible" ] } + CRX = { properties = [ "invertible" ] } + CRY = { properties = [ "invertible" ] } + CRZ = { properties = [ "invertible" ] } + CRot = { properties = [ "invertible" ] } + Identity = { properties = [ "controllable", "invertible" ] } + IsingXX = { properties = [ "controllable", "invertible" ] } + IsingYY = { properties = [ "controllable", "invertible" ] } + IsingZZ = { properties = [ "controllable", "invertible" ] } + IsingXY = { properties = [ "controllable", "invertible" ] } # Operators that should be decomposed according to the algorithm used # by PennyLane's device API. # Optional, since gates not listed in this list will typically be decomposed by # default, but can be useful to express a deviation from this device's regular # strategy in PennyLane. - decomp = [ - "SX", - "ISWAP", - "PSWAP", - "SISWAP", - "SQISW", - "CPhase", - "BasisState", - "QubitStateVector", - "StatePrep", - "ControlledQubitUnitary", - "DiagonalQubitUnitary", - "SingleExcitation", - "SingleExcitationPlus", - "SingleExcitationMinus", - "DoubleExcitation", - "DoubleExcitationPlus", - "DoubleExcitationMinus", - "QubitCarry", - "QubitSum", - "OrbitalRotation", - "QFT", - "ECR", - ] + [operators.gates.decomp] + + SX = {} + ISWAP = {} + PSWAP = {} + SISWAP = {} + SQISW = {} + CPhase = {} + BasisState = {} + QubitStateVector = {} + StatePrep = {} + ControlledQubitUnitary = {} + DiagonalQubitUnitary = {} + SingleExcitation = {} + SingleExcitationPlus = {} + SingleExcitationMinus = {} + DoubleExcitation = {} + DoubleExcitationPlus = {} + DoubleExcitationMinus = {} + QubitCarry = {} + QubitSum = {} + OrbitalRotation = {} + QFT = {} + ECR = {} # Gates which should be translated to QubitUnitary - matrix = [ - "MultiControlledX", - ] + [operators.gates.matrix] + + MultiControlledX = {} + + # Observables supported by the device + [operators.observables] + + PauliX = {} + PauliY = {} + PauliZ = {} + Hadamard = {} + Hermitian = {} + Identity = {} + Projector = {} + SparseHamiltonian = {} + Hamiltonian = {} + Sum = {} + SProd = {} + Prod = {} + Exp = {} [measurement_processes] - exactshots = [ - "Expval", - "Var", - "Probs", - "State", - ] - finiteshots = [ - "Expval", - "Var", - "Probs", - "Sample", - "Counts", - ] + + Expval = {} + Var = {} + Probs = {} + Sample = {} + Count = { condition = [ "finiteshots" ] } [compilation] + # If the device is compatible with qjit qjit_compatible = true # If the device requires run time generation of the quantum circuit. runtime_code_generation = false - # If the device supports adjoint - quantum_adjoint = true - # If the device supports quantum control instructions natively - quantum_control = false # If the device supports mid circuit measurements natively mid_circuit_measurement = true - # This field is currently unchecked but it is reserved for the purpose of # determining if the device supports dynamic qubit allocation/deallocation. - dynamic_qubit_management = false + dynamic_qubit_management = false [options] # Options is an optional field. diff --git a/frontend/catalyst/cuda/catalyst_to_cuda_interpreter.py b/frontend/catalyst/cuda/catalyst_to_cuda_interpreter.py index 5557eb5f21..0248d20e11 100644 --- a/frontend/catalyst/cuda/catalyst_to_cuda_interpreter.py +++ b/frontend/catalyst/cuda/catalyst_to_cuda_interpreter.py @@ -76,7 +76,7 @@ from catalyst.pennylane_extensions import QFunc from catalyst.utils.exceptions import CompileError from catalyst.utils.patching import Patcher -from catalyst.utils.toml import toml_load +from catalyst.utils.runtime import BackendInfo from .primitives import ( cuda_inst, @@ -821,14 +821,12 @@ def get_jaxpr(self, *args): an MLIR module """ - def cudaq_backend_info(device): + def cudaq_backend_info(device, _config) -> BackendInfo: """The extract_backend_info should not be run by the cuda compiler as it is catalyst-specific. We need to make this API a bit nicer for third-party compilers. """ - with open(device.config, "rb") as f: - config = toml_load(f) - - return config, device.name, None, None + device_name = device.short_name if isinstance(device, qml.Device) else device.name + return BackendInfo(device_name, device.name, "", {}) with Patcher( (catalyst.pennylane_extensions.QFunc, "extract_backend_info", cudaq_backend_info), diff --git a/frontend/catalyst/cuda/cuda_quantum.toml b/frontend/catalyst/cuda/cuda_quantum.toml index 5625ce21e2..889c7482e7 100644 --- a/frontend/catalyst/cuda/cuda_quantum.toml +++ b/frontend/catalyst/cuda/cuda_quantum.toml @@ -1,39 +1,26 @@ -schema = 1 - -[device] -name = "cuda_quantum" - -[operators] -# Observables supported by the device -observables = [ - "PauliX", - "PauliY", - "PauliZ", - "Hadamard", -] +schema = 2 # The union of all gate types listed in this section must match what # the device considers "supported" through PennyLane's device API. -[[operators.gates]] -native = [ - "CNOT", - "CY", - "CZ", - "CRX", - "CRY", - "CRZ", - "PauliX", - "PauliY", - "PauliZ", - "Hadamard", - "S", - "T", - "RX", - "RY", - "RZ", - "SWAP", - "CSWAP", -] +[operators.gates.native] + +CNOT = { properties = [ "invertible" ] } +CY = { properties = [ "invertible" ] } +CZ = { properties = [ "invertible" ] } +CRX = { properties = [ "invertible" ] } +CRY = { properties = [ "invertible" ] } +CRZ = { properties = [ "invertible" ] } +PauliX = { properties = [ "invertible" ] } +PauliY = { properties = [ "invertible" ] } +PauliZ = { properties = [ "invertible" ] } +Hadamard = { properties = [ "invertible" ] } +S = { properties = [ "invertible" ] } +T = { properties = [ "invertible" ] } +RX = { properties = [ "invertible" ] } +RY = { properties = [ "invertible" ] } +RZ = { properties = [ "invertible" ] } +SWAP = { properties = [ "invertible" ] } +CSWAP = { properties = [ "invertible" ] } # Operators that should be decomposed according to the algorithm used # by PennyLane's device API. @@ -41,24 +28,25 @@ native = [ # default, but can be useful to express a deviation from this device's regular # strategy in PennyLane. # Everything else should be decomposed. -decomp = [] +[operators.gates.decomp] # Gates which should be translated to QubitUnitary -matrix = [] +[operators.gates.matrix] + +# Observables supported by the device +[operators.observables] +PauliX = {} +PauliY = {} +PauliZ = {} +Hadamard = {} [measurement_processes] -exactshots = [] -finiteshots = [] [compilation] # If the device is compatible with qjit qjit_compatible = true # If the device requires run time generation of the quantum circuit. runtime_code_generation = false -# If the device supports adjoint -quantum_adjoint = true -# If the device supports quantum control instructions natively -quantum_control = false # Technically limited support mid_circuit_measurement = true diff --git a/frontend/catalyst/pennylane_extensions.py b/frontend/catalyst/pennylane_extensions.py index dd9b846e9a..034a1f2a24 100644 --- a/frontend/catalyst/pennylane_extensions.py +++ b/frontend/catalyst/pennylane_extensions.py @@ -20,7 +20,6 @@ import copy import numbers -import pathlib from collections.abc import Sequence, Sized from functools import update_wrapper from typing import Any, Callable, Iterable, List, Optional, Union @@ -94,7 +93,13 @@ JaxTracingContext, ) from catalyst.utils.exceptions import DifferentiableCompileError -from catalyst.utils.runtime import extract_backend_info, get_lib_path +from catalyst.utils.runtime import ( + BackendInfo, + device_get_toml_config, + extract_backend_info, + validate_config_with_device, +) +from catalyst.utils.toml import TOMLDocument def _check_no_measurements(tape: QuantumTape) -> None: @@ -130,41 +135,22 @@ def __init__(self, fn, device): # pragma: nocover update_wrapper(self, fn) @staticmethod - def _add_toml_file(device): - """Temporary function. This function adds the config field to devices. - TODO: Remove this function when `qml.Device`s are guaranteed to have their own - config file field.""" - if hasattr(device, "config"): # pragma: no cover - # Devices that already have a config field do not need it to be overwritten. - return - device_lpath = pathlib.Path(get_lib_path("runtime", "RUNTIME_LIB_DIR")) - name = device.name - if isinstance(device, qml.Device): - name = device.short_name - - # The toml files name convention we follow is to replace - # the dots with underscores in the device short name. - toml_file_name = name.replace(".", "_") + ".toml" - # And they are currently saved in the following directory. - toml_file = device_lpath.parent / "lib" / "backend" / toml_file_name - device.config = toml_file - - @staticmethod - def extract_backend_info(device): + def extract_backend_info(device: qml.QubitDevice, config: TOMLDocument) -> BackendInfo: """Wrapper around extract_backend_info in the runtime module.""" - return extract_backend_info(device) + return extract_backend_info(device, config) def __call__(self, *args, **kwargs): qnode = None if isinstance(self, qml.QNode): qnode = self - QFunc._add_toml_file(self.device) - dev_args = QFunc.extract_backend_info(self.device) - config, rest = dev_args[0], dev_args[1:] + config = device_get_toml_config(self.device) + validate_config_with_device(self.device, config) + backend_info = QFunc.extract_backend_info(self.device, config) + if isinstance(self.device, qml.devices.Device): - device = QJITDeviceNewAPI(self.device, config, *rest) + device = QJITDeviceNewAPI(self.device, config, backend_info) else: - device = QJITDevice(config, self.device.shots, self.device.wires, *rest) + device = QJITDevice(config, self.device.shots, self.device.wires, backend_info) else: # pragma: nocover # Allow QFunc to still be used by itself for internal testing. device = self.device diff --git a/frontend/catalyst/qjit_device.py b/frontend/catalyst/qjit_device.py index fa0b7b1c6e..54e44cf595 100644 --- a/frontend/catalyst/qjit_device.py +++ b/frontend/catalyst/qjit_device.py @@ -13,11 +13,24 @@ # limitations under the License. """This module contains the qjit device classes. """ +from typing import Optional, Set + import pennylane as qml from pennylane.measurements import MidMeasureMP from catalyst.utils.exceptions import CompileError from catalyst.utils.patching import Patcher +from catalyst.utils.runtime import ( + BackendInfo, + deduce_native_controlled_gates, + get_pennylane_observables, + get_pennylane_operations, +) +from catalyst.utils.toml import ( + TOMLDocument, + check_adjoint_flag, + check_mid_circuit_measurement_flag, +) RUNTIME_OPERATIONS = { "Identity", @@ -54,6 +67,30 @@ } +def get_qjit_pennylane_operations(config: TOMLDocument, shots_present, device_name) -> Set[str]: + """Get set of supported operations for the QJIT device in the PennyLane format. Take the target + device's config into account.""" + # Supported gates of the target PennyLane's device + native_gates = get_pennylane_operations(config, shots_present, device_name) + qir_gates = set.union( + QJITDeviceNewAPI.operations_supported_by_QIR_runtime, + deduce_native_controlled_gates(QJITDeviceNewAPI.operations_supported_by_QIR_runtime), + ) + supported_gates = list(set.intersection(native_gates, qir_gates)) + + # These are added unconditionally. + supported_gates += ["Cond", "WhileLoop", "ForLoop"] + + if check_mid_circuit_measurement_flag(config): # pragma: no branch + supported_gates += ["MidCircuitMeasure"] + + if check_adjoint_flag(config, shots_present): + supported_gates += ["Adjoint"] + + supported_gates += ["ControlledQubitUnitary"] + return set(supported_gates) + + class QJITDevice(qml.QubitDevice): """QJIT device. @@ -76,14 +113,10 @@ class QJITDevice(qml.QubitDevice): version = "0.0.1" author = "" - # These must be present even if empty. - operations = [] - observables = [] - operations_supported_by_QIR_runtime = RUNTIME_OPERATIONS @staticmethod - def _get_operations_to_convert_to_matrix(_config): + def _get_operations_to_convert_to_matrix(_config: TOMLDocument) -> Set[str]: # We currently override and only set a few gates to preserve existing behaviour. # We could choose to read from config and use the "matrix" gates. # However, that affects differentiability. @@ -91,79 +124,35 @@ def _get_operations_to_convert_to_matrix(_config): # TODO: https://github.com/PennyLaneAI/catalyst/issues/398 return {"MultiControlledX", "BlockEncode"} - @staticmethod - def _check_mid_circuit_measurement(config): - return config["compilation"]["mid_circuit_measurement"] - - @staticmethod - def _check_adjoint(config): - return config["compilation"]["quantum_adjoint"] - - @staticmethod - def _check_quantum_control(config): - return config["compilation"]["quantum_control"] - - @staticmethod - def _set_supported_operations(config): - """Override the set of supported operations.""" - native_gates = set(config["operators"]["gates"][0]["native"]) - qir_gates = QJITDevice.operations_supported_by_QIR_runtime - supported_native_gates = list(set.intersection(native_gates, qir_gates)) - QJITDevice.operations = supported_native_gates - - # These are added unconditionally. - QJITDevice.operations += ["Cond", "WhileLoop", "ForLoop"] - - if QJITDevice._check_mid_circuit_measurement(config): # pragma: no branch - QJITDevice.operations += ["MidCircuitMeasure"] - - if QJITDevice._check_adjoint(config): - QJITDevice.operations += ["Adjoint"] - - if QJITDevice._check_quantum_control(config): # pragma: nocover - # TODO: Once control is added on the frontend. - gates_to_be_decomposed_if_controlled = [ - "Identity", - "CNOT", - "CY", - "CZ", - "CSWAP", - "CRX", - "CRY", - "CRZ", - "CRot", - ] - native_controlled_gates = ["ControlledQubitUnitary"] + [ - f"C({gate})" - for gate in native_gates - if gate not in gates_to_be_decomposed_if_controlled - ] - QJITDevice.operations += native_controlled_gates - - @staticmethod - def _set_supported_observables(config): - """Override the set of supported observables.""" - QJITDevice.observables = config["operators"]["observables"] - - # pylint: disable=too-many-arguments def __init__( self, - config, + target_config: TOMLDocument, shots=None, wires=None, - backend_name=None, - backend_lib=None, - backend_kwargs=None, + backend: Optional[BackendInfo] = None, ): - QJITDevice._set_supported_operations(config) - QJITDevice._set_supported_observables(config) - - self.config = config - self.backend_name = backend_name if backend_name else "default" - self.backend_lib = backend_lib if backend_lib else "" - self.backend_kwargs = backend_kwargs if backend_kwargs else {} super().__init__(wires=wires, shots=shots) + self.target_config = target_config + self.backend_name = backend.c_interface_name if backend else "default" + self.backend_lib = backend.lpath if backend else "" + self.backend_kwargs = backend.kwargs if backend else {} + device_name = backend.device_name if backend else "default" + + shots_present = shots is not None + self._operations = get_qjit_pennylane_operations(target_config, shots_present, device_name) + self._observables = get_pennylane_observables(target_config, shots_present, device_name) + + @property + def operations(self) -> Set[str]: + """Get the device operations""" + return self._operations + + @property + def observables(self) -> Set[str]: + """Get the device observables""" + return self._observables + def apply(self, operations, **kwargs): """ Raises: RuntimeError @@ -191,7 +180,9 @@ def default_expand_fn(self, circuit, max_expansion=10): if any(isinstance(op, MidMeasureMP) for op in circuit.operations): raise CompileError("Must use 'measure' from Catalyst instead of PennyLane.") - decompose_to_qubit_unitary = QJITDevice._get_operations_to_convert_to_matrix(self.config) + decompose_to_qubit_unitary = QJITDevice._get_operations_to_convert_to_matrix( + self.target_config + ) def _decomp_to_unitary(self, *_args, **_kwargs): try: @@ -236,85 +227,48 @@ class QJITDeviceNewAPI(qml.devices.Device): backend_kwargs (Dict(str, AnyType)): An optional dictionary of the device specifications """ - # These must be present even if empty. - operations = [] - observables = [] - operations_supported_by_QIR_runtime = RUNTIME_OPERATIONS @staticmethod - def _check_mid_circuit_measurement(config): - return config["compilation"]["mid_circuit_measurement"] - - @staticmethod - def _check_adjoint(config): - return config["compilation"]["quantum_adjoint"] - - @staticmethod - def _check_quantum_control(config): - return config["compilation"]["quantum_control"] - - @staticmethod - def _set_supported_operations(config): - """Override the set of supported operations.""" - native_gates = set(config["operators"]["gates"][0]["native"]) - qir_gates = QJITDeviceNewAPI.operations_supported_by_QIR_runtime - QJITDeviceNewAPI.operations = list(native_gates.intersection(qir_gates)) - - # These are added unconditionally. - QJITDeviceNewAPI.operations += ["Cond", "WhileLoop", "ForLoop"] - - if QJITDeviceNewAPI._check_mid_circuit_measurement(config): # pragma: no branch - QJITDeviceNewAPI.operations += ["MidCircuitMeasure"] - - if QJITDeviceNewAPI._check_adjoint(config): - QJITDeviceNewAPI.operations += ["Adjoint"] - - if QJITDeviceNewAPI._check_quantum_control(config): # pragma: nocover - # TODO: Once control is added on the frontend. - gates_to_be_decomposed_if_controlled = [ - "Identity", - "CNOT", - "CY", - "CZ", - "CSWAP", - "CRX", - "CRY", - "CRZ", - "CRot", - ] - native_controlled_gates = ["ControlledQubitUnitary"] + [ - f"C({gate})" - for gate in native_gates - if gate not in gates_to_be_decomposed_if_controlled - ] - QJITDeviceNewAPI.operations += native_controlled_gates - - @staticmethod - def _set_supported_observables(config): - """Override the set of supported observables.""" - QJITDeviceNewAPI.observables = config["operators"]["observables"] + def _get_operations_to_convert_to_matrix(_config: TOMLDocument) -> Set[str]: # pragma: no cover + # We currently override and only set a few gates to preserve existing behaviour. + # We could choose to read from config and use the "matrix" gates. + # However, that affects differentiability. + # None of the "matrix" gates with more than 2 qubits parameters are differentiable. + # TODO: https://github.com/PennyLaneAI/catalyst/issues/398 + return {"MultiControlledX", "BlockEncode"} - # pylint: disable=too-many-arguments def __init__( self, original_device, - config, - backend_name=None, - backend_lib=None, - backend_kwargs=None, + target_config: TOMLDocument, + backend: Optional[BackendInfo] = None, ): self.original_device = original_device for key, value in original_device.__dict__.items(): self.__setattr__(key, value) - self.config = config - QJITDeviceNewAPI._set_supported_operations(self.config) - QJITDeviceNewAPI._set_supported_observables(self.config) - - self.backend_name = backend_name if backend_name else "default" - self.backend_lib = backend_lib if backend_lib else "" - self.backend_kwargs = backend_kwargs if backend_kwargs else {} + super().__init__(wires=original_device.wires, shots=original_device.shots) + + self.target_config = target_config + self.backend_name = backend.c_interface_name if backend else "default" + self.backend_lib = backend.lpath if backend else "" + self.backend_kwargs = backend.kwargs if backend else {} + device_name = backend.device_name if backend else "default" + + shots_present = original_device.shots is not None + self._operations = get_qjit_pennylane_operations(target_config, shots_present, device_name) + self._observables = get_pennylane_observables(target_config, shots_present, device_name) + + @property + def operations(self) -> Set[str]: + """Get the device operations""" + return self._operations + + @property + def observables(self) -> Set[str]: + """Get the device observables""" + return self._observables def preprocess( self, diff --git a/frontend/catalyst/utils/runtime.py b/frontend/catalyst/utils/runtime.py index e0ac75e237..6c20e446bf 100644 --- a/frontend/catalyst/utils/runtime.py +++ b/frontend/catalyst/utils/runtime.py @@ -21,12 +21,23 @@ import pathlib import platform import re +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Dict, Set import pennylane as qml from catalyst._configuration import INSTALLED from catalyst.utils.exceptions import CompileError -from catalyst.utils.toml import toml_load +from catalyst.utils.toml import ( + TOMLDocument, + check_quantum_control_flag, + get_decomposable_gates, + get_matrix_decomposable_gates, + get_native_gates, + get_observables, + toml_load, +) package_root = os.path.dirname(__file__) @@ -56,67 +67,89 @@ def get_lib_path(project, env_var): return os.getenv(env_var, DEFAULT_LIB_PATHS.get(project, "")) -def check_qjit_compatibility(device, config): - """Check that the device is qjit compatible. - - Args: - device (qml.Device): An instance of a quantum device. - config (Dict[Str, Any]): Configuration dictionary. - - Raises: - CompileError +def deduce_native_controlled_gates(native_gates: Set[str]) -> Set[str]: + """Return the set of controlled gates given the set of nativly supported gates. This function + is used with the toml config schema 1. Later schemas provide the required information directly """ - if config["compilation"]["qjit_compatible"]: - return - - name = device.name - msg = f"Attempting to compile program for incompatible device {name}." - raise CompileError(msg) - - -def check_device_config(device): - """Check that the device configuration exists. + gates_to_be_decomposed_if_controlled = [ + "Identity", + "CNOT", + "CY", + "CZ", + "CSWAP", + "CRX", + "CRY", + "CRZ", + "CRot", + "ControlledPhaseShift", + "QubitUnitary", + "Toffoli", + ] + native_controlled_gates = set( + [f"C({gate})" for gate in native_gates if gate not in gates_to_be_decomposed_if_controlled] + # TODO: remove after PR #642 is merged in lightning + + [f"Controlled{gate}" for gate in native_gates if gate in ["QubitUnitary"]] + ) + return native_controlled_gates + + +def get_pennylane_operations( + config: TOMLDocument, shots_present: bool, device_name: str +) -> Set[str]: + """Get gates that are natively supported by the device and therefore do not need to be + decomposed. Args: - device (qml.Device): An instance of a quantum device. + config (Dict[Str, Any]): Configuration dictionary + shots_present (bool): True is exact shots is specified in the current top-level program + device_name (str): Name of quantum device. Used for ad-hoc patching. - Raises: - CompileError + Returns: + Set[str]: List of gate names in the PennyLane format. """ - if hasattr(device, "config") and device.config.exists(): - return + gates_PL = set() + schema = int(config["schema"]) + + if schema == 1: + native_gates_attrs = get_native_gates(config, shots_present) + assert all(len(v) == 0 for v in native_gates_attrs.values()) + native_gates = set(native_gates_attrs) + supports_controlled = check_quantum_control_flag(config) + native_controlled_gates = ( + deduce_native_controlled_gates(native_gates) if supports_controlled else set() + ) - name = device.name - msg = f"Attempting to compile program for incompatible device {name}." - raise CompileError(msg) + # TODO: remove after PR #642 is merged in lightning + if device_name == "lightning.kokkos": # pragma: nocover + native_gates.update({"C(GlobalPhase)"}) + gates_PL = set.union(native_gates, native_controlled_gates) -def get_native_gates(config): - """Get gates that are natively supported by the device and therefore do not need to be - decomposed. + elif schema == 2: + native_gates = get_native_gates(config, shots_present) + for gate, attrs in native_gates.items(): + gates_PL.add(f"{gate}") + if "controllable" in attrs.get("properties", {}): + gates_PL.add(f"C({gate})") - Args: - config (Dict[Str, Any]): Configuration dictionary - """ - return config["operators"]["gates"][0]["native"] + else: + raise CompileError("Device configuration schema {schema} is not supported") + return gates_PL -def get_decomposable_gates(config): - """Get gates that will be decomposed according to PL's decomposition rules. - Args: - config (Dict[Str, Any]): Configuration dictionary - """ - return config["operators"]["gates"][0]["decomp"] +def get_pennylane_observables( + config: TOMLDocument, shots_present: bool, device_name: str +) -> Set[str]: + """Get observables in PennyLane format. Apply ad-hoc patching""" + observables = set(get_observables(config, shots_present)) -def get_matrix_decomposable_gates(config): - """Get gates that will be decomposed to QubitUnitary. + # TODO: remove after PR #642 is merged in lightning + if device_name == "lightning.kokkos": # pragma: nocover + observables.update({"Projector"}) - Args: - config (Dict[Str, Any]): Configuration dictionary - """ - return config["operators"]["gates"][0]["matrix"] + return observables def check_no_overlap(*args): @@ -134,12 +167,17 @@ def check_no_overlap(*args): if sum(len_of_sets) == len(union): return - msg = "Device has overlapping gates in native and decomposable sets." + overlaps = set() + for s in set_of_sets: + overlaps.update(s - union) + union = union - s + + msg = f"Device has overlapping gates: {overlaps}" raise CompileError(msg) -def filter_out_adjoint_and_control(operations): - """Remove Adjoint and C strings from operations. +def filter_out_adjoint(operations): + """Remove Adjoint from operations. Args: operations (List[Str]): List of strings with names of supported operations @@ -149,87 +187,126 @@ def filter_out_adjoint_and_control(operations): removed. """ adjoint = re.compile(r"^Adjoint\(.*\)$") - control = re.compile(r"^C\(.*\)$") def is_not_adj(op): return not re.match(adjoint, op) - def is_not_ctrl(op): - return not re.match(control, op) - operations_no_adj = filter(is_not_adj, operations) - operations_no_adj_no_ctrl = filter(is_not_ctrl, operations_no_adj) - return list(operations_no_adj_no_ctrl) + return set(operations_no_adj) -def check_full_overlap(device, *args): +def check_full_overlap(device_gates: Set[str], spec_gates: Set[str]) -> None: """Check that device.operations is equivalent to the union of *args Args: - device (qml.Device): An instance of a quantum device. - *args (List[Str]): List of strings. + device_gates (Set[str]): device gates + spec_gates (Set[str]): spec gates Raises: CompileError """ - operations = filter_out_adjoint_and_control(device.operations) - gates_in_device = set(operations) - set_of_sets = [set(arg) for arg in args] - union = set.union(*set_of_sets) - if gates_in_device == union: + if device_gates == spec_gates: return - msg = "Gates in qml.device.operations and specification file do not match" + msg = ( + "Gates in qml.device.operations and specification file do not match.\n" + f"Gates that present only in the device: {device_gates - spec_gates}\n" + f"Gates that present only in spec: {spec_gates - device_gates}\n" + ) raise CompileError(msg) -def check_gates_are_compatible_with_device(device, config): - """Validate configuration dictionary against device. +def validate_config_with_device(device: qml.QubitDevice, config: TOMLDocument) -> None: + """Validate configuration document against the device attributes. + Raise CompileError in case of mismatch: + * If device is not qjit-compatible. + * If configuration file does not exists. + * If decomposable, matrix, and native gates have some overlap. + * If decomposable, matrix, and native gates do not match gates in ``device.operations`` and + ``device.observables``. Args: device (qml.Device): An instance of a quantum device. - config (Dict[Str, Any]): Configuration dictionary + config (TOMLDocument): A TOML document representation. Raises: CompileError """ - native = get_native_gates(config) - decomposable = get_decomposable_gates(config) - matrix = get_matrix_decomposable_gates(config) + if not config["compilation"]["qjit_compatible"]: + raise CompileError( + f"Attempting to compile program for incompatible device '{device.name}': " + f"Config is not marked as qjit-compatible" + ) + + device_name = device.short_name if isinstance(device, qml.Device) else device.name + + shots_present = device.shots is not None + native = get_pennylane_operations(config, shots_present, device_name) + observables = get_pennylane_observables(config, shots_present, device_name) + decomposable = set(get_decomposable_gates(config, shots_present)) + matrix = set(get_matrix_decomposable_gates(config, shots_present)) + + # For toml schema 1 configs, the following condition is possible: (1) `QubitUnitary` gate is + # supported, (2) native quantum control flag is enabled and (3) `ControlledQubitUnitary` is + # listed in either matrix or decomposable sections. This is a contradiction, because condition + # (1) means that `ControlledQubitUnitary` is also in the native set. We solve it here by + # applying a fixup. + # TODO: Remove when the transition to the toml schema 2 is complete. + if "ControlledQubitUnitary" in native: + matrix = matrix - {"ControlledQubitUnitary"} + decomposable = decomposable - {"ControlledQubitUnitary"} + check_no_overlap(native, decomposable, matrix) - if not hasattr(device, "operations"): # pragma: nocover - # The new device API has no "operations" field - # so we cannot check that there's an overlap or not. - return - check_full_overlap(device, native, decomposable, matrix) + if hasattr(device, "operations") and hasattr(device, "observables"): + device_gates = set.union(set(device.operations), set(device.observables)) + device_gates = filter_out_adjoint(device_gates) + spec_gates = set.union(native, observables, matrix, decomposable) + spec_gates = filter_out_adjoint(spec_gates) + check_full_overlap(device_gates, spec_gates) -def validate_config_with_device(device): - """Validate configuration file against device. - Will raise a CompileError - * if device does not contain ``config`` attribute - * if configuration file does not exists - * if decomposable, matrix, and native gates have some overlap - * if decomposable, matrix, and native gates do not match gates in ``device.operations`` +def device_get_toml_config(device) -> Path: + """Get the path of the device config file.""" + if hasattr(device, "config"): + # The expected case: device specifies its own config. + toml_file = device.config + else: + # TODO: Remove this section when `qml.Device`s are guaranteed to have their own config file + # field. + device_lpath = pathlib.Path(get_lib_path("runtime", "RUNTIME_LIB_DIR")) - Args: - device (qml.Device): An instance of a quantum device. + name = device.short_name if isinstance(device, qml.Device) else device.name + # The toml files name convention we follow is to replace + # the dots with underscores in the device short name. + toml_file_name = name.replace(".", "_") + ".toml" + # And they are currently saved in the following directory. + toml_file = device_lpath.parent / "lib" / "backend" / toml_file_name - Raises: CompileError - """ - check_device_config(device) + try: + with open(toml_file, "rb") as f: + config = toml_load(f) + except FileNotFoundError as e: + raise CompileError( + "Attempting to compile program for incompatible device: " + f"Config file ({toml_file}) does not exist" + ) from e + + return config - with open(device.config, "rb") as f: - config = toml_load(f) - check_qjit_compatibility(device, config) - check_gates_are_compatible_with_device(device, config) +@dataclass +class BackendInfo: + """Backend information""" + device_name: str + c_interface_name: str + lpath: str + kwargs: Dict[str, Any] -def extract_backend_info(device): - """Extract the backend info as a tuple of (name, lib, kwargs).""" - validate_config_with_device(device) +def extract_backend_info(device: qml.QubitDevice, config: TOMLDocument) -> BackendInfo: + """Extract the backend info from a quantum device. The device is expected to carry a reference + to a valid TOML config file.""" dname = device.name if isinstance(device, qml.Device): @@ -255,7 +332,7 @@ def extract_backend_info(device): # Support third party devices with `get_c_interface` device_name, device_lpath = device.get_c_interface() else: - raise CompileError(f"The {dname} device is not supported for compilation at the moment.") + raise CompileError(f"The {dname} device does not provide C interface for compilation.") if not pathlib.Path(device_lpath).is_file(): raise CompileError(f"Device at {device_lpath} cannot be found!") @@ -281,12 +358,9 @@ def extract_backend_info(device): device._s3_folder # pylint: disable=protected-access ) - with open(device.config, "rb") as f: - config = toml_load(f) - - if "options" in config.keys(): - for k, v in config["options"].items(): - if hasattr(device, v): - device_kwargs[k] = getattr(device, v) + options = config.get("options", {}) + for k, v in options.items(): + if hasattr(device, v): + device_kwargs[k] = getattr(device, v) - return config, device_name, device_lpath, device_kwargs + return BackendInfo(dname, device_name, device_lpath, device_kwargs) diff --git a/frontend/catalyst/utils/toml.py b/frontend/catalyst/utils/toml.py index ff8e561e80..6e2bf2d468 100644 --- a/frontend/catalyst/utils/toml.py +++ b/frontend/catalyst/utils/toml.py @@ -16,6 +16,11 @@ """ import importlib.util +from functools import reduce +from itertools import repeat +from typing import Any, Dict, List + +from catalyst.utils.exceptions import CompileError # TODO: # Once Python version 3.11 is the oldest supported Python version, we can remove tomlkit @@ -31,9 +36,131 @@ raise ImportError(msg) # Give preference to tomllib -if tomllib: - from tomllib import load as toml_load # pragma: nocover -else: - from tomlkit import load as toml_load # pragma: nocover +if tomllib: # pragma: nocover + from tomllib import load as toml_load + + TOMLDocument = Any +else: # pragma: nocover + from tomlkit import TOMLDocument + from tomlkit import load as toml_load + +__all__ = ["toml_load", "TOMLDocument"] + + +def check_mid_circuit_measurement_flag(config: TOMLDocument) -> bool: + """Check the global mid-circuit measurement flag""" + return bool(config.get("compilation", {}).get("mid_circuit_measurement", False)) + + +def check_adjoint_flag(config: TOMLDocument, shots_present: bool) -> bool: + """Check the global adjoint flag for toml schema 1. For newer schemas the adjoint flag is + defined to be set if all native gates are inverible""" + schema = int(config["schema"]) + if schema == 1: + return bool(config.get("compilation", {}).get("quantum_adjoint", False)) + + elif schema == 2: + return all( + "invertible" in v.get("properties", {}) + for g, v in get_native_gates(config, shots_present).items() + ) + + raise CompileError("quantum_adjoint flag is not supported in TOMLs schema >= 3") + + +def check_quantum_control_flag(config: TOMLDocument) -> bool: + """Check the control flag. Only exists in toml config schema 1""" + schema = int(config["schema"]) + if schema == 1: + return bool(config.get("compilation", {}).get("quantum_control", False)) + + raise CompileError("quantum_control flag is not supported in TOMLs schema >= 2") + + +def get_gates(config: TOMLDocument, path: List[str], shots_present: bool) -> Dict[str, dict]: + """Read the toml config section specified by `path`. Filters-out gates which don't match + condition. For now the only condition we support is `shots_present`.""" + gates = {} + analytic = "analytic" + finiteshots = "finiteshots" + iterable = reduce(lambda x, y: x[y], path, config) + gen = iterable.items() if hasattr(iterable, "items") else zip(iterable, repeat({})) + for g, values in gen: + unknown_attrs = set(values) - {"condition", "properties"} + if len(unknown_attrs) > 0: + raise CompileError( + f"Configuration for gate '{str(g)}' has unknown attributes: {list(unknown_attrs)}" + ) + properties = values.get("properties", {}) + unknown_props = set(properties) - {"invertible", "controllable", "differentiable"} + if len(unknown_props) > 0: + raise CompileError( + f"Configuration for gate '{str(g)}' has unknown properties: {list(unknown_props)}" + ) + if "condition" in values: + conditions = values["condition"] + unknown_conditions = set(conditions) - {analytic, finiteshots} + if len(unknown_conditions) > 0: + raise CompileError( + f"Configuration for gate '{str(g)}' has unknown conditions: " + f"{list(unknown_conditions)}" + ) + if all(c in conditions for c in [analytic, finiteshots]): + raise CompileError( + f"Configuration for gate '{g}' can not contain both " + f"`{finiteshots}` and `{analytic}` conditions simultaniosly" + ) + if analytic in conditions and not shots_present: + gates[g] = values + elif finiteshots in conditions and shots_present: + gates[g] = values + else: + gates[g] = values + return gates + + +def get_observables(config: TOMLDocument, shots_present: bool) -> Dict[str, dict]: + """Override the set of supported observables.""" + return get_gates(config, ["operators", "observables"], shots_present) + + +def get_native_gates(config: TOMLDocument, shots_present: bool) -> Dict[str, dict]: + """Get the gates from the `native` section of the config.""" + + schema = int(config["schema"]) + if schema == 1: + return get_gates(config, ["operators", "gates", 0, "native"], shots_present) + elif schema == 2: + return get_gates(config, ["operators", "gates", "native"], shots_present) + + raise CompileError(f"Unsupported config schema {schema}") + + +def get_decomposable_gates(config: TOMLDocument, shots_present: bool) -> Dict[str, dict]: + """Get gates that will be decomposed according to PL's decomposition rules. + + Args: + config (TOMLDocument): Configuration dictionary + """ + schema = int(config["schema"]) + if schema == 1: + return get_gates(config, ["operators", "gates", 0, "decomp"], shots_present) + elif schema == 2: + return get_gates(config, ["operators", "gates", "decomp"], shots_present) + + raise CompileError(f"Unsupported config schema {schema}") + + +def get_matrix_decomposable_gates(config: TOMLDocument, shots_present: bool) -> Dict[str, dict]: + """Get gates that will be decomposed to QubitUnitary. + + Args: + config (TOMLDocument): Configuration dictionary + """ + schema = int(config["schema"]) + if schema == 1: + return get_gates(config, ["operators", "gates", 0, "matrix"], shots_present) + elif schema == 2: + return get_gates(config, ["operators", "gates", "matrix"], shots_present) -__all__ = ["toml_load"] + raise CompileError(f"Unsupported config schema {schema}") diff --git a/frontend/test/lit/test_quantum_control.py b/frontend/test/lit/test_quantum_control.py index eafabcf08b..5f91b383d4 100644 --- a/frontend/test/lit/test_quantum_control.py +++ b/frontend/test/lit/test_quantum_control.py @@ -19,18 +19,18 @@ import pennylane as qml from catalyst import qjit -from catalyst.compiler import get_lib_path # This is used just for internal testing from catalyst.pennylane_extensions import qfunc from catalyst.qjit_device import QJITDevice -from catalyst.utils.toml import toml_load +from catalyst.utils.runtime import device_get_toml_config -def get_custom_device(num_wires, discarded_operations=None, added_operations=None): +def get_custom_qjit_device(num_wires, discarded_operations=None, added_operations=None): """Generate a custom device with the modified set of supported gates.""" lightning = qml.device("lightning.qubit", wires=3) + config = device_get_toml_config(lightning) operations_copy = lightning.operations.copy() observables_copy = lightning.observables.copy() for op in discarded_operations or []: @@ -38,7 +38,7 @@ def get_custom_device(num_wires, discarded_operations=None, added_operations=Non for op in added_operations or []: operations_copy.add(op) - class CustomDevice(QJITDevice): + class CustomQJITDevice(QJITDevice): """Custom Device""" name = "Device without some operations" @@ -51,31 +51,18 @@ class CustomDevice(QJITDevice): observables = observables_copy # pylint: disable=too-many-arguments - def __init__( - self, shots=None, wires=None, backend_name=None, backend_lib=None, backend_kwargs=None - ): - self.backend_name = backend_name if backend_name else "default" - self.backend_lib = backend_lib if backend_lib else "default" - self.backend_kwargs = backend_kwargs if backend_kwargs else "" - - with open(lightning.config, "rb") as f: - config = toml_load(f) - super().__init__(config, wires=wires, shots=shots) + def __init__(self, shots=None, wires=None, backend=None): + super().__init__(config, wires=wires, shots=shots, backend=backend) def apply(self, operations, **kwargs): # pylint: disable=missing-function-docstring pass - @staticmethod - def get_c_interface(): - """Location to shared object with C/C++ implementation""" - return get_lib_path("runtime", "RUNTIME_LIB_DIR") + "/libdummy_device.so" - - return CustomDevice(wires=num_wires) + return CustomQJITDevice(wires=num_wires) def test_named_controlled(): """Test that named-controlled operations are passed as-is.""" - dev = get_custom_device(2, set(), set()) + dev = get_custom_qjit_device(2, set(), set()) @qjit(target="mlir") @qfunc(device=dev) @@ -97,7 +84,9 @@ def named_controlled(): def test_native_controlled_custom(): """Test native control of a custom operation.""" - dev = get_custom_device(3, discarded_operations={"CRot"}, added_operations={"Rot", "C(Rot)"}) + dev = get_custom_qjit_device( + 3, discarded_operations={"CRot"}, added_operations={"Rot", "C(Rot)"} + ) @qjit(target="mlir") @qfunc(device=dev) @@ -117,7 +106,7 @@ def native_controlled(): def test_native_controlled_unitary(): """Test native control of the unitary operation.""" - dev = get_custom_device(4, set(), added_operations={"C(QubitUnitary)"}) + dev = get_custom_qjit_device(4, set(), added_operations={"C(QubitUnitary)"}) @qjit(target="mlir") @qfunc(device=dev) @@ -149,7 +138,7 @@ def native_controlled_unitary(): def test_native_controlled_multirz(): """Test native control of the multirz operation.""" - dev = get_custom_device(3, set(), {"C(MultiRZ)"}) + dev = get_custom_qjit_device(3, set(), {"C(MultiRZ)"}) @qjit(target="mlir") @qfunc(device=dev) diff --git a/frontend/test/pytest/test_braket_local_devices.py b/frontend/test/pytest/test_braket_local_devices.py index e19d336b46..7844a9ff7a 100644 --- a/frontend/test/pytest/test_braket_local_devices.py +++ b/frontend/test/pytest/test_braket_local_devices.py @@ -17,6 +17,7 @@ import numpy as np import pennylane as qml import pytest +from numpy.testing import assert_allclose from catalyst import grad, qjit @@ -799,7 +800,7 @@ def interpretted_grad_default(x): i = qml.grad(h, argnum=0) return i(x) - assert np.allclose(compiled_grad_default(inp), interpretted_grad_default(inp), rtol=0.1) + assert_allclose(compiled_grad_default(inp), interpretted_grad_default(inp), rtol=0.1) if __name__ == "__main__": diff --git a/frontend/test/pytest/test_config_functions.py b/frontend/test/pytest/test_config_functions.py index 3c915165e3..0df918790c 100644 --- a/frontend/test/pytest/test_config_functions.py +++ b/frontend/test/pytest/test_config_functions.py @@ -14,124 +14,338 @@ """Unit tests for functions to check config validity.""" -import tempfile -from pathlib import Path +from os.path import join +from tempfile import TemporaryDirectory +from textwrap import dedent import pennylane as qml import pytest from catalyst.utils.exceptions import CompileError from catalyst.utils.runtime import ( - check_device_config, check_full_overlap, check_no_overlap, - check_qjit_compatibility, + check_quantum_control_flag, get_decomposable_gates, get_matrix_decomposable_gates, get_native_gates, + get_pennylane_observables, + get_pennylane_operations, + validate_config_with_device, ) -from catalyst.utils.toml import toml_load +from catalyst.utils.toml import check_adjoint_flag, toml_load class DummyDevice(qml.QubitDevice): """Test device""" - name = "Test Device" - short_name = "test.device" + name = "Dummy Device" + short_name = "dummy.device" + pennylane_requires = "0.33.0" + version = "0.0.1" + author = "Dummy" + operations = [] + observables = [] -def test_toml_file(): + def apply(self, operations, **kwargs): + """Unused""" + raise RuntimeError("Only C/C++ interface is defined") + + +ALL_SCHEMAS = [1, 2] + + +@pytest.mark.parametrize("schema", ALL_SCHEMAS) +def test_validate_config_with_device(schema): """Test error is raised if checking for qjit compatibility and field is false in toml file.""" - with tempfile.NamedTemporaryFile(mode="w+b") as f: - f.write( - b""" -[compilation] -qjit_compatible = false - """ - ) - f.flush() - f.seek(0) - config = toml_load(f) - f.close() - - name = DummyDevice.name + with TemporaryDirectory() as d: + toml_file = join(d, "test.toml") + with open(toml_file, "w", encoding="utf-8") as f: + f.write( + dedent( + f""" + schema = {schema} + [compilation] + qjit_compatible = false + """ + ) + ) + with open(toml_file, encoding="utf-8") as f: + config = toml_load(f) + + device = DummyDevice() with pytest.raises( - CompileError, match=f"Attempting to compile program for incompatible device {name}." + CompileError, + match=f"Attempting to compile program for incompatible device '{device.name}'", ): - check_qjit_compatibility(DummyDevice, config) + validate_config_with_device(device, config) -def test_device_has_config_attr(): - """Test error is raised when device has no config attr.""" - name = DummyDevice.name - msg = f"Attempting to compile program for incompatible device {name}." - with pytest.raises(CompileError, match=msg): - check_device_config(DummyDevice) +def test_get_observables_schema1(): + """Test observables are properly obtained from the toml schema 1.""" + with TemporaryDirectory() as d: + test_deduced_gates = {"TestNativeGate"} + + toml_file = join(d, "test.toml") + with open(toml_file, "w", encoding="utf-8") as f: + f.write( + dedent( + r""" + schema = 1 + [operators] + observables = [ "TestNativeGate" ] + """ + ) + ) + with open(toml_file, encoding="utf-8") as f: + config = toml_load(f) + assert test_deduced_gates == get_pennylane_observables(config, False, "device_name") + + +def test_get_observables_schema2(): + """Test observables are properly obtained from the toml schema 2.""" + with TemporaryDirectory() as d: + test_deduced_gates = {"TestNativeGate1"} + + toml_file = join(d, "test.toml") + with open(toml_file, "w", encoding="utf-8") as f: + f.write( + dedent( + r""" + schema = 2 + [operators.observables] + TestNativeGate1 = { } + """ + ) + ) + with open(toml_file, encoding="utf-8") as f: + config = toml_load(f) + assert test_deduced_gates == get_pennylane_observables(config, False, "device_name") + + +def test_get_native_gates_schema1_no_qcontrol(): + """Test native gates are properly obtained from the toml.""" + with TemporaryDirectory() as d: + test_deduced_gates = {"TestNativeGate"} + + toml_file = join(d, "test.toml") + with open(toml_file, "w", encoding="utf-8") as f: + f.write( + dedent( + r""" + schema = 1 + [[operators.gates]] + native = [ "TestNativeGate" ] + [compilation] + quantum_control = false + """ + ) + ) + with open(toml_file, encoding="utf-8") as f: + config = toml_load(f) + assert test_deduced_gates == get_pennylane_operations(config, False, "device_name") + + +def test_get_native_gates_schema1_qcontrol(): + """Test native gates are properly obtained from the toml.""" + with TemporaryDirectory() as d: + test_deduced_gates = {"C(TestNativeGate)", "TestNativeGate"} + + toml_file = join(d, "test.toml") + with open(toml_file, "w", encoding="utf-8") as f: + f.write( + dedent( + r""" + schema = 1 + [[operators.gates]] + native = [ "TestNativeGate" ] + [compilation] + quantum_control = true + """ + ) + ) + with open(toml_file, encoding="utf-8") as f: + config = toml_load(f) + assert test_deduced_gates == get_pennylane_operations(config, False, "device_name") + +def test_get_adjoint_schema2(): + """Test native gates are properly obtained from the toml.""" + with TemporaryDirectory() as d: + + toml_file = join(d, "test.toml") + with open(toml_file, "w", encoding="utf-8") as f: + f.write( + dedent( + r""" + schema = 2 + [operators.gates.native] + TestNativeGate1 = { properties = [ 'invertible' ] } + TestNativeGate2 = { properties = [ 'invertible' ] } + """ + ) + ) + with open(toml_file, encoding="utf-8") as f: + config = toml_load(f) + assert check_adjoint_flag(config, False) + + +def test_get_native_gates_schema2(): + """Test native gates are properly obtained from the toml.""" + with TemporaryDirectory() as d: + test_deduced_gates = {"C(TestNativeGate1)", "TestNativeGate1", "TestNativeGate2"} -def test_device_with_invalid_config_attr(): - """Test error is raised when device has invalid config attr.""" - name = DummyDevice.name - with tempfile.NamedTemporaryFile(mode="w+b") as f: - f.close() - setattr(DummyDevice, "config", Path(f.name)) - msg = f"Attempting to compile program for incompatible device {name}." - with pytest.raises(CompileError, match=msg): - check_device_config(DummyDevice) - delattr(DummyDevice, "config") + toml_file = join(d, "test.toml") + with open(toml_file, "w", encoding="utf-8") as f: + f.write( + dedent( + r""" + schema = 2 + [operators.gates.native] + TestNativeGate1 = { properties = [ 'controllable' ] } + TestNativeGate2 = { } + """ + ) + ) + with open(toml_file, encoding="utf-8") as f: + config = toml_load(f) + assert test_deduced_gates == get_pennylane_operations(config, False, "device_name") -def test_get_native_gates(): +def test_get_native_gates_schema2_optional_shots(): """Test native gates are properly obtained from the toml.""" - with tempfile.NamedTemporaryFile(mode="w+b") as f: - test_gates = ["TestNativeGate"] - payload = f""" -[[operators.gates]] -native = {str(test_gates)} - """ - f.write(str.encode(payload)) - f.flush() - f.seek(0) - config = toml_load(f) - f.close() - assert test_gates == get_native_gates(config) - - -def test_get_decomp_gates(): + with TemporaryDirectory() as d: + test_deduced_gates = {"TestNativeGate1"} + + toml_file = join(d, "test.toml") + with open(toml_file, "w", encoding="utf-8") as f: + f.write( + dedent( + r""" + schema = 2 + [operators.gates.native] + TestNativeGate1 = { condition = ['finiteshots'] } + TestNativeGate2 = { condition = ['analytic'] } + """ + ) + ) + with open(toml_file, encoding="utf-8") as f: + config = toml_load(f) + assert test_deduced_gates == get_pennylane_operations(config, True, "device_name") + + +def test_get_native_gates_schema2_optional_noshots(): + """Test native gates are properly obtained from the toml.""" + with TemporaryDirectory() as d: + test_deduced_gates = {"TestNativeGate2"} + toml_file = join(d, "test.toml") + with open(toml_file, "w", encoding="utf-8") as f: + f.write( + dedent( + r""" + schema = 2 + [operators.gates.native] + TestNativeGate1 = { condition = ['finiteshots'] } + TestNativeGate2 = { condition = ['analytic'] } + """ + ) + ) + with open(toml_file, encoding="utf-8") as f: + config = toml_load(f) + assert test_deduced_gates == get_pennylane_operations(config, False, "device") + + +def test_get_decomp_gates_schema1(): """Test native decomposition gates are properly obtained from the toml.""" - with tempfile.NamedTemporaryFile(mode="w+b") as f: - test_gates = ["TestDecompGate"] - payload = f""" -[[operators.gates]] -decomp = {str(test_gates)} - """ - f.write(str.encode(payload)) - f.flush() - f.seek(0) - config = toml_load(f) - f.close() - assert test_gates == get_decomposable_gates(config) - - -def test_get_matrix_decomposable_gates(): + with TemporaryDirectory() as d: + test_gates = {"TestDecompGate": {}} + toml_file = join(d, "test.toml") + with open(toml_file, "w", encoding="utf-8") as f: + f.write( + dedent( + f""" + schema = 1 + [[operators.gates]] + decomp = {str(list(test_gates.keys()))} + """ + ) + ) + + with open(toml_file, encoding="utf-8") as f: + config = toml_load(f) + + assert test_gates == get_decomposable_gates(config, False) + + +def test_get_decomp_gates_schema2(): + """Test native decomposition gates are properly obtained from the toml.""" + with TemporaryDirectory() as d: + test_gates = {"TestDecompGate": {}} + toml_file = join(d, "test.toml") + with open(toml_file, "w", encoding="utf-8") as f: + f.write( + dedent( + f""" + schema = 2 + [operators.gates] + decomp = {str(list(test_gates.keys()))} + """ + ) + ) + + with open(toml_file, encoding="utf-8") as f: + config = toml_load(f) + + assert test_gates == get_decomposable_gates(config, False) + + +def test_get_matrix_decomposable_gates_schema1(): + """Test native matrix gates are properly obtained from the toml.""" + with TemporaryDirectory() as d: + test_gates = {"TestMatrixGate": {}} + toml_file = join(d, "test.toml") + with open(toml_file, "w", encoding="utf-8") as f: + f.write( + dedent( + f""" + schema = 1 + [[operators.gates]] + matrix = {str(list(test_gates.keys()))} + """ + ) + ) + + with open(toml_file, encoding="utf-8") as f: + config = toml_load(f) + + assert test_gates == get_matrix_decomposable_gates(config, False) + + +def test_get_matrix_decomposable_gates_schema2(): """Test native matrix gates are properly obtained from the toml.""" - with tempfile.NamedTemporaryFile(mode="w+b") as f: - test_gates = ["TestMatrixGate"] - payload = f""" -[[operators.gates]] -matrix = {str(test_gates)} - """ - f.write(str.encode(payload)) - f.flush() - f.seek(0) - config = toml_load(f) - f.close() - assert test_gates == get_matrix_decomposable_gates(config) + with TemporaryDirectory() as d: + toml_file = join(d, "test.toml") + with open(toml_file, "w", encoding="utf-8") as f: + f.write( + dedent( + r""" + schema = 2 + [operators.gates.matrix] + TestMatrixGate = {} + """ + ) + ) + + with open(toml_file, encoding="utf-8") as f: + config = toml_load(f) + + assert {"TestMatrixGate": {}} == get_matrix_decomposable_gates(config, False) def test_check_overlap_msg(): """Test error is raised if there is an overlap in sets.""" - msg = "Device has overlapping gates in native and decomposable sets." + msg = "Device has overlapping gates." with pytest.raises(CompileError, match=msg): check_no_overlap(["A"], ["A"], ["A"]) @@ -139,12 +353,136 @@ def test_check_overlap_msg(): def test_check_full_overlap(): """Test that if there is no full overlap of operations, then an error is raised.""" - class Device: - operations = ["A", "B", "C"] - msg = f"Gates in qml.device.operations and specification file do not match" with pytest.raises(CompileError, match=msg): - check_full_overlap(Device(), ["A", "A", "A"], ["B", "B"]) + check_full_overlap({"A", "B", "C", "C(X)"}, {"A", "B", "Adjoint(Y)"}) + + +def test_config_invalid_attr(): + """Check the gate condition handling logic""" + with TemporaryDirectory() as d: + toml_file = join(d, "test.toml") + with open(toml_file, "w", encoding="utf-8") as f: + f.write( + dedent( + r""" + schema = 2 + [operators.gates.native] + TestGate = { unknown_attribute = 33 } + """ + ) + ) + + with open(toml_file, encoding="utf-8") as f: + config = toml_load(f) + + with pytest.raises( + CompileError, match="Configuration for gate 'TestGate' has unknown attributes" + ): + get_native_gates(config, True) + + +def test_config_invalid_condition_unknown(): + """Check the gate condition handling logic""" + with TemporaryDirectory() as d: + toml_file = join(d, "test.toml") + with open(toml_file, "w", encoding="utf-8") as f: + f.write( + dedent( + r""" + schema = 2 + [operators.gates.native] + TestGate = { condition = ["unknown", "analytic"] } + """ + ) + ) + + with open(toml_file, encoding="utf-8") as f: + config = toml_load(f) + + with pytest.raises( + CompileError, match="Configuration for gate 'TestGate' has unknown conditions" + ): + get_native_gates(config, True) + + +def test_config_invalid_property_unknown(): + """Check the gate condition handling logic""" + with TemporaryDirectory() as d: + toml_file = join(d, "test.toml") + with open(toml_file, "w", encoding="utf-8") as f: + f.write( + dedent( + r""" + schema = 2 + [operators.gates.native] + TestGate = { properties = ["unknown", "invertible"] } + """ + ) + ) + + with open(toml_file, encoding="utf-8") as f: + config = toml_load(f) + + with pytest.raises( + CompileError, match="Configuration for gate 'TestGate' has unknown properties" + ): + get_native_gates(config, True) + + +def test_config_invalid_condition_duplicate(): + """Check the gate condition handling logic""" + with TemporaryDirectory() as d: + toml_file = join(d, "test.toml") + with open(toml_file, "w", encoding="utf-8") as f: + f.write( + dedent( + r""" + schema = 2 + [operators.gates.native] + TestGate = { condition = ["finiteshots", "analytic"] } + """ + ) + ) + + with open(toml_file, encoding="utf-8") as f: + config = toml_load(f) + + with pytest.raises(CompileError, match="Configuration for gate 'TestGate'"): + get_native_gates(config, True) + + with pytest.raises(CompileError, match="Configuration for gate 'TestGate'"): + get_native_gates(config, False) + + +def test_config_unsupported_schema(): + """Test native matrix gates are properly obtained from the toml.""" + with TemporaryDirectory() as d: + toml_file = join(d, "test.toml") + with open(toml_file, "w", encoding="utf-8") as f: + f.write( + dedent( + r""" + schema = 999 + """ + ) + ) + + with open(toml_file, encoding="utf-8") as f: + config = toml_load(f) + + with pytest.raises(CompileError): + check_quantum_control_flag(config) + with pytest.raises(CompileError): + get_native_gates(config, False) + with pytest.raises(CompileError): + get_decomposable_gates(config, False) + with pytest.raises(CompileError): + get_matrix_decomposable_gates(config, False) + with pytest.raises(CompileError): + get_pennylane_operations(config, False, "device_name") + with pytest.raises(CompileError): + check_adjoint_flag(config, False) if __name__ == "__main__": diff --git a/frontend/test/pytest/test_custom_devices.py b/frontend/test/pytest/test_custom_devices.py index 60da42120c..367ef2a233 100644 --- a/frontend/test/pytest/test_custom_devices.py +++ b/frontend/test/pytest/test_custom_devices.py @@ -21,6 +21,7 @@ from catalyst import measure, qjit from catalyst.compiler import get_lib_path from catalyst.utils.exceptions import CompileError +from catalyst.utils.runtime import device_get_toml_config, extract_backend_info # These have to match the ones in the configuration file. OPERATIONS = [ @@ -81,6 +82,32 @@ "Adjoint(ISWAP)", "Adjoint(SISWAP)", "MultiControlledX", + "C(PauliY)", + "C(RY)", + "C(PauliX)", + "C(RX)", + "C(IsingXX)", + "C(Hadamard)", + "C(SWAP)", + "C(IsingYY)", + "C(S)", + "C(MultiRZ)", + "C(PhaseShift)", + "C(T)", + "C(IsingXY)", + "C(PauliZ)", + "C(Rot)", + "C(IsingZZ)", + "C(RZ)", + "C(SingleExcitationPlus)", + "C(GlobalPhase)", + "C(DoubleExcitationPlus)", + "C(SingleExcitationMinus)", + "C(DoubleExcitation)", + "GlobalPhase", + "C(SingleExcitation)", + "C(DoubleExcitationMinus)", + "BlockEncode", ] OBSERVABLES = [ "PauliX", @@ -91,18 +118,21 @@ "Identity", "Projector", "Hamiltonian", + "SparseHamiltonian", "Sum", "SProd", "Prod", "Exp", ] +RUNTIME_LIB_PATH = get_lib_path("runtime", "RUNTIME_LIB_DIR") + @pytest.mark.skipif( - not pathlib.Path(get_lib_path("runtime", "RUNTIME_LIB_DIR") + "/libdummy_device.so").is_file(), + not pathlib.Path(RUNTIME_LIB_PATH + "/libdummy_device.so").is_file(), reason="lib_dummydevice.so was not found.", ) -def test_custom_device(): +def test_custom_device_load(): """Test that custom device can run using Catalyst.""" class DummyDevice(qml.QubitDevice): @@ -114,12 +144,12 @@ class DummyDevice(qml.QubitDevice): version = "0.0.1" author = "Dummy" - # Doesn't matter as at the moment it is dictated by QJITDevice operations = OPERATIONS observables = OBSERVABLES def __init__(self, shots=None, wires=None): super().__init__(wires=wires, shots=shots) + self._option1 = 42 def apply(self, operations, **kwargs): """Unused""" @@ -133,8 +163,14 @@ def get_c_interface(): return "DummyDevice", get_lib_path("runtime", "RUNTIME_LIB_DIR") + "/libdummy_device.so" + device = DummyDevice(wires=1) + config = device_get_toml_config(device) + backend_info = extract_backend_info(device, config) + assert backend_info.kwargs["option1"] == 42 + assert "option2" not in backend_info.kwargs + @qjit - @qml.qnode(DummyDevice(wires=1)) + @qml.qnode(device) def f(): """This function would normally return False. However, DummyDevice as defined in libdummy_device.so @@ -182,3 +218,35 @@ def get_c_interface(): @qml.qnode(DummyDevice(wires=1)) def f(): return measure(0) + + +def test_custom_device_no_c_interface(): + """Test that custom device error.""" + + class DummyDevice(qml.QubitDevice): + """Dummy Device""" + + name = "Dummy Device" + short_name = "dummy.device" + pennylane_requires = "0.33.0" + version = "0.0.1" + author = "Dummy" + + operations = OPERATIONS + observables = OBSERVABLES + + def __init__(self, shots=None, wires=None): + super().__init__(wires=wires, shots=shots) + + def apply(self, operations, **kwargs): + """Unused.""" + raise RuntimeError("Dummy device") + + with pytest.raises( + CompileError, match="The dummy.device device does not provide C interface for compilation." + ): + + @qjit + @qml.qnode(DummyDevice(wires=1)) + def f(): + return measure(0) diff --git a/frontend/test/pytest/test_device_api.py b/frontend/test/pytest/test_device_api.py index 12a7ff5fc6..74643a94c3 100644 --- a/frontend/test/pytest/test_device_api.py +++ b/frontend/test/pytest/test_device_api.py @@ -25,7 +25,7 @@ from catalyst import qjit from catalyst.compiler import get_lib_path from catalyst.qjit_device import QJITDeviceNewAPI -from catalyst.utils.runtime import extract_backend_info +from catalyst.utils.runtime import device_get_toml_config, extract_backend_info class DummyDevice(Device): @@ -67,12 +67,12 @@ def test_qjit_device(): device = DummyDevice(wires=10, shots=2032) # Create qjit device - dev_args = extract_backend_info(device) - config, rest = dev_args[0], dev_args[1:] - device_qjit = QJITDeviceNewAPI(device, config, *rest) + config = device_get_toml_config(device) + backend_info = extract_backend_info(device, config) + device_qjit = QJITDeviceNewAPI(device, config, backend_info) # Check attributes of the new device - assert isinstance(device_qjit.config, dict) + assert isinstance(device_qjit.target_config, dict) assert device_qjit.shots == qml.measurements.Shots(2032) assert device_qjit.wires == qml.wires.Wires(range(0, 10)) @@ -88,6 +88,11 @@ def test_qjit_device(): with pytest.raises(RuntimeError, match="QJIT devices cannot execute tapes"): device_qjit.execute(10, 2) + assert isinstance(device_qjit.operations, set) + assert len(device_qjit.operations) > 0 + assert isinstance(device_qjit.observables, set) + assert len(device_qjit.observables) > 0 + @pytest.mark.skipif( not pathlib.Path(get_lib_path("runtime", "RUNTIME_LIB_DIR") + "/libdummy_device.so").is_file(), diff --git a/frontend/test/pytest/test_qnode.py b/frontend/test/pytest/test_qnode.py index 6b410ff784..72a4262aae 100644 --- a/frontend/test/pytest/test_qnode.py +++ b/frontend/test/pytest/test_qnode.py @@ -76,7 +76,7 @@ def test_unsupported_device(): def func(): return qml.probs() - regex = "Attempting to compile program for incompatible device .*" + regex = "Attempting to compile program for incompatible device.*" with pytest.raises(CompileError, match=regex): qjit(func) diff --git a/requirements.txt b/requirements.txt index 5e9d03522c..22ab4801ba 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,3 +22,4 @@ nbmake # optional rt/test dependencies pennylane-lightning[kokkos] amazon-braket-pennylane-plugin>=1.23.0 +lark diff --git a/runtime/lib/backend/lightning/lightning.kokkos.schema2.toml b/runtime/lib/backend/lightning/lightning.kokkos.schema2.toml new file mode 100644 index 0000000000..c709f97b6c --- /dev/null +++ b/runtime/lib/backend/lightning/lightning.kokkos.schema2.toml @@ -0,0 +1,109 @@ +schema = 2 + +# The union of all gate types listed in this section must match what +# the device considers "supported" through PennyLane's device API. +[operators.gates.native] + +CNOT = { properties = [ "invertible", "differentiable" ] } +ControlledPhaseShift = { properties = [ "invertible", "differentiable" ] } +ControlledQubitUnitary = { properties = [ "invertible", "differentiable" ] } +CRot = { properties = [ "invertible" ] } +CRX = { properties = [ "invertible", "differentiable" ] } +CRY = { properties = [ "invertible", "differentiable" ] } +CRZ = { properties = [ "invertible", "differentiable" ] } +CSWAP = { properties = [ "invertible", "differentiable" ] } +CY = { properties = [ "invertible", "differentiable" ] } +CZ = { properties = [ "invertible", "differentiable" ] } +DoubleExcitationMinus = { properties = [ "invertible", "differentiable" ] } +DoubleExcitationPlus = { properties = [ "invertible", "differentiable" ] } +DoubleExcitation = { properties = [ "invertible", "differentiable" ] } +GlobalPhase = { properties = [ "invertible", "controllable", "differentiable" ] } +Hadamard = { properties = [ "invertible", "differentiable" ] } +Identity = { properties = [ "invertible", "differentiable" ] } +IsingXX = { properties = [ "invertible", "differentiable" ] } +IsingXY = { properties = [ "invertible", "differentiable" ] } +IsingYY = { properties = [ "invertible", "differentiable" ] } +IsingZZ = { properties = [ "invertible", "differentiable" ] } +MultiRZ = { properties = [ "invertible", "differentiable" ] } +PauliX = { properties = [ "invertible", "differentiable" ] } +PauliY = { properties = [ "invertible", "differentiable" ] } +PauliZ = { properties = [ "invertible", "differentiable" ] } +PhaseShift = { properties = [ "invertible", "differentiable" ] } +QubitUnitary = { properties = [ "invertible", "differentiable" ] } +Rot = { properties = [ "invertible", "differentiable" ] } +RX = { properties = [ "invertible", "differentiable" ] } +RY = { properties = [ "invertible", "differentiable" ] } +RZ = { properties = [ "invertible", "differentiable" ] } +SingleExcitationMinus = { properties = [ "invertible", "differentiable" ] } +SingleExcitationPlus = { properties = [ "invertible", "differentiable" ] } +SingleExcitation = { properties = [ "invertible", "differentiable" ] } +S = { properties = [ "invertible", "differentiable" ] } +SWAP = { properties = [ "invertible", "differentiable" ] } +Toffoli = { properties = [ "invertible", "differentiable" ] } +T = { properties = [ "invertible", "differentiable" ] } + +# Operators that should be decomposed according to the algorithm used +# by PennyLane's device API. +# Optional, since gates not listed in this list will typically be decomposed by +# default, but can be useful to express a deviation from this device's regular +# strategy in PennyLane. +[operators.gates.decomp] + +BasisState = {} +MultiControlledX = {} +QFT = {} +QubitStateVector = {} +StatePrep = {} + +# Gates which should be translated to QubitUnitary +[operators.gates.matrix] + +BlockEncode = {} +CPhase = {} +DiagonalQubitUnitary = {} +ECR = {} +ISWAP = {} +OrbitalRotation = {} +PSWAP = {} +QubitCarry = {} +QubitSum = {} +SISWAP = {} +SQISW = {} +SX = {} + +# Observables supported by the device +[operators.observables] + +Exp = {} +Hadamard = {} +Hamiltonian = {} +Hermitian = {} +Identity = {} +PauliX = {} +PauliY = {} +PauliZ = {} +Prod = {} +SparseHamiltonian = {} +SProd = {} +Sum = {} + +[measurement_processes] + +Expval = {} +Var = {} +Probs = {} +State = { condition = [ "analytic" ] } +Sample = { condition = [ "finiteshots" ] } +Counts = { condition = [ "finiteshots" ] } + +[compilation] + +# If the device is compatible with qjit +qjit_compatible = true +# If the device requires run time generation of the quantum circuit. +runtime_code_generation = false +# If the device supports mid circuit measurements natively +mid_circuit_measurement = true +# This field is currently unchecked but it is reserved for the purpose of +# determining if the device supports dynamic qubit allocation/deallocation. +dynamic_qubit_management = false diff --git a/runtime/lib/backend/lightning/lightning.qubit.schema2.toml b/runtime/lib/backend/lightning/lightning.qubit.schema2.toml new file mode 100644 index 0000000000..35edd9b472 --- /dev/null +++ b/runtime/lib/backend/lightning/lightning.qubit.schema2.toml @@ -0,0 +1,114 @@ +schema = 2 + +[operators.gates.native] + +CNOT = { properties = [ "invertible", "differentiable" ] } +ControlledPhaseShift = { properties = [ "invertible", "differentiable" ] } +ControlledQubitUnitary = { properties = [ "invertible", "differentiable" ] } +CRot = { properties = [ "invertible" ] } +CRX = { properties = [ "invertible", "differentiable" ] } +CRY = { properties = [ "invertible", "differentiable" ] } +CRZ = { properties = [ "invertible", "differentiable" ] } +CSWAP = { properties = [ "invertible", "differentiable" ] } +CY = { properties = [ "invertible", "differentiable" ] } +CZ = { properties = [ "invertible", "differentiable" ] } +DoubleExcitationMinus = { properties = [ "invertible", "controllable", "differentiable"] } +DoubleExcitationPlus = { properties = [ "invertible", "controllable", "differentiable"] } +DoubleExcitation = { properties = [ "invertible", "controllable", "differentiable"] } +GlobalPhase = { properties = [ "controllable", "invertible", "differentiable" ] } +Hadamard = { properties = [ "controllable", "invertible", "differentiable" ] } +Identity = { properties = [ "invertible", "differentiable" ] } +IsingXX = { properties = [ "controllable", "invertible", "differentiable" ] } +IsingXY = { properties = [ "controllable", "invertible", "differentiable" ] } +IsingYY = { properties = [ "controllable", "invertible", "differentiable" ] } +IsingZZ = { properties = [ "controllable", "invertible", "differentiable" ] } +MultiRZ = { properties = [ "controllable", "invertible", "differentiable" ] } +PauliX = { properties = [ "controllable", "invertible", "differentiable" ] } +PauliY = { properties = [ "controllable", "invertible", "differentiable" ] } +PauliZ = { properties = [ "controllable", "invertible", "differentiable" ] } +PhaseShift = { properties = [ "controllable", "invertible", "differentiable" ] } +QubitUnitary = { properties = [ "invertible", "differentiable" ] } +Rot = { properties = [ "controllable", "invertible", "differentiable" ] } +RX = { properties = [ "controllable", "invertible", "differentiable" ] } +RY = { properties = [ "controllable", "invertible", "differentiable" ] } +RZ = { properties = [ "controllable", "invertible", "differentiable" ] } +SingleExcitationMinus = { properties = [ "invertible", "controllable", "differentiable"] } +SingleExcitationPlus = { properties = [ "invertible", "controllable", "differentiable"] } +SingleExcitation = { properties = [ "invertible", "controllable", "differentiable"] } +S = { properties = [ "controllable", "invertible", "differentiable" ] } +SWAP = { properties = [ "controllable", "invertible", "differentiable" ] } +Toffoli = { properties = [ "invertible", "differentiable" ] } +T = { properties = [ "controllable", "invertible", "differentiable" ] } + +[operators.gates.decomp] + +# Operators that should be decomposed according to the algorithm used +# by PennyLane's device API. +# Optional, since gates not listed in this list will typically be decomposed by +# default, but can be useful to express a deviation from this device's regular +# strategy in PennyLane. +BasisState = {} +MultiControlledX = {} +QFT = {} +QubitStateVector = {} +StatePrep = {} + +# Gates which should be translated to QubitUnitary +[operators.gates.matrix] + +BlockEncode = {} +CPhase = {} +DiagonalQubitUnitary = {} +ECR = {} +ISWAP = {} +OrbitalRotation = {} +PSWAP = {} +QubitCarry = {} +QubitSum = {} +SISWAP = {} +SQISW = {} +SX = {} + +# Observables supported by the device +[operators.observables] + +Exp = { properties = [ "differentiable" ] } +Hadamard = { properties = [ "differentiable" ] } +Hamiltonian = { properties = [ "differentiable" ] } +Hermitian = { properties = [ "differentiable" ] } +Identity = { properties = [ "differentiable" ] } +PauliX = { properties = [ "differentiable" ] } +PauliY = { properties = [ "differentiable" ] } +PauliZ = { properties = [ "differentiable" ] } +Prod = { properties = [ "differentiable" ] } +Projector = {} +SparseHamiltonian = { properties = [ "differentiable" ] } +SProd = { properties = [ "differentiable" ] } +Sum = { properties = [ "differentiable" ] } + +[measurement_processes] + +Expval = {} +Var = {} +Probs = {} +State = { condition = [ "analytic" ] } +Sample = { condition = [ "finiteshots" ] } +Count = { condition = [ "finiteshots" ] } + +[compilation] + +# If the device is compatible with qjit +qjit_compatible = true +# If the device requires run time generation of the quantum circuit. +runtime_code_generation = false +# If the device supports mid circuit measurements natively +mid_circuit_measurement = true +# This field is currently unchecked but it is reserved for the purpose of +# determining if the device supports dynamic qubit allocation/deallocation. +dynamic_qubit_management = false + +[options] + +mcmc = "_mcmc" +num_burnin = "_num_burnin" +kernel_name = "_kernel_name" diff --git a/runtime/lib/backend/openqasm/braket_aws_qubit.toml b/runtime/lib/backend/openqasm/braket_aws_qubit.toml index 40720ff7e4..500a596970 100644 --- a/runtime/lib/backend/openqasm/braket_aws_qubit.toml +++ b/runtime/lib/backend/openqasm/braket_aws_qubit.toml @@ -1,93 +1,75 @@ -schema = 1 - -[device] -name = "braket.aws.qubit" - -[operators] -# Observables supported by the device -observables = [ - "PauliX", - "PauliY", - "PauliZ", - "Hadamard", - "Hermitian", - "Tensor", -] +schema = 2 # The union of all gate types listed in this section must match what # the device considers "supported" through PennyLane's device API. -[[operators.gates]] -native = [ - "ISWAP", - "PSWAP", - "Hadamard", - "PauliX", - "PauliY", - "PauliZ", - "S", - "T", - "CNOT", - "CZ", - "SWAP", - "PhaseShift", - "RX", - "RY", - "RZ", - "CSWAP", - "MS", - "CY", - "SX", - "AAMS", - "ECR", - "GPi", - "GPi2", - "CPhaseShift00", - "CPhaseShift01", - "CPhaseShift10", - "Identity", - "IsingXX", - "IsingXY", - "IsingYY", - "IsingZZ", - "Toffoli", - "ControlledPhaseShift", -] +[operators.gates.native] +ISWAP = { } +PSWAP = { } +Hadamard = { } +PauliX = { } +PauliY = { } +PauliZ = { } +S = { } +T = { } +CNOT = { } +CZ = { } +SWAP = { } +PhaseShift = { } +RX = { } +RY = { } +RZ = { } +CSWAP = { } +MS = { } +CY = { } +SX = { } +AAMS = { } +ECR = { } +GPi = { } +GPi2 = { } +CPhaseShift00 = { } +CPhaseShift01 = { } +CPhaseShift10 = { } +Identity = { } +IsingXX = { } +IsingXY = { } +IsingYY = { } +IsingZZ = { } +Toffoli = { } +ControlledPhaseShift = { } # Operators that should be decomposed according to the algorithm used # by PennyLane's device API. # Optional, since gates not listed in this list will typically be decomposed by # default, but can be useful to express a deviation from this device's regular # strategy in PennyLane. -decomp = [] +[operators.gates.decomp] # Gates which should be translated to QubitUnitary -matrix = [ -] +[operators.gates.matrix] + +[operators.observables] +# Observables supported by the device +PauliX = {} +PauliY = {} +PauliZ = {} +Hadamard = {} +Hermitian = {} +Tensor = {} [measurement_processes] -exactshots = [ - "Expval", - "Var", - "Probs", - "State", -] -finiteshots = [ - "Expval", - "Var", - "Probs", - "Sample", - "Counts", -] +Expval = {} +Var = {} +Probs = {} +State = { condition = [ "analytic" ] } +Sample = { condition = [ "finiteshots" ] } +Count = { condition = [ "finiteshots" ] } + [compilation] # If the device is compatible with qjit qjit_compatible = true # If the device requires run time generation of the quantum circuit. runtime_code_generation = true -# If the device supports adjoint -quantum_adjoint = false -# If the device supports quantum control instructions natively -quantum_control = false # If the device supports mid circuit measurements natively mid_circuit_measurement = false diff --git a/runtime/lib/backend/openqasm/braket_local_qubit.toml b/runtime/lib/backend/openqasm/braket_local_qubit.toml index 702e6ac9e9..bdf2682efd 100644 --- a/runtime/lib/backend/openqasm/braket_local_qubit.toml +++ b/runtime/lib/backend/openqasm/braket_local_qubit.toml @@ -1,97 +1,85 @@ -schema = 1 - -[device] -name = "braket.aws.qubit" - -[operators] -# Observables supported by the device -observables = [ - "PauliX", - "PauliY", - "PauliZ", - "Hadamard", - "Hermitian", - "Tensor", -] +schema = 2 # The union of all gate types listed in this section must match what # the device considers "supported" through PennyLane's device API. -[[operators.gates]] -native = [ - "ISWAP", - "PSWAP", - "Hadamard", - "PauliX", - "PauliY", - "PauliZ", - "S", - "T", - "CNOT", - "CZ", - "SWAP", - "PhaseShift", - "RX", - "RY", - "RZ", - "CSWAP", - "QubitUnitary", - "MS", - "CY", - "SX", - "AAMS", - "ECR", - "GPi", - "GPi2", - "CPhaseShift00", - "CPhaseShift01", - "CPhaseShift10", - "Identity", - "IsingXX", - "IsingXY", - "IsingYY", - "IsingZZ", - "Toffoli", - "ControlledPhaseShift", -] +[operators.gates.native] + +ISWAP = { } +PSWAP = { } +Hadamard = { } +PauliX = { } +PauliY = { } +PauliZ = { } +S = { } +T = { } +CNOT = { } +CZ = { } +SWAP = { } +PhaseShift = { } +RX = { } +RY = { } +RZ = { } +CSWAP = { } +QubitUnitary = { } +MS = { } +CY = { } +SX = { } +AAMS = { } +ECR = { } +GPi = { } +GPi2 = { } +CPhaseShift00 = { } +CPhaseShift01 = { } +CPhaseShift10 = { } +Identity = { } +IsingXX = { } +IsingXY = { } +IsingYY = { } +IsingZZ = { } +Toffoli = { } +ControlledPhaseShift = { } # Operators that should be decomposed according to the algorithm used # by PennyLane's device API. # Optional, since gates not listed in this list will typically be decomposed by # default, but can be useful to express a deviation from this device's regular # strategy in PennyLane. -decomp = [] +[operators.gates.decomp] # Gates which should be translated to QubitUnitary -matrix = [ -] +[operators.gates.matrix] + +[operators.observables] +# Observables supported by the device +PauliX = {} +PauliY = {} +PauliZ = {} +Hadamard = {} +Hermitian = {} +# Tensor, +Projector = {} +Sprod = {} +Hamiltonian = { condition = [ "analytic" ] } +Sum = {} +Prod = {} [measurement_processes] -exactshots = [ - "Expval", - "Var", - "Probs", - "State", -] -finiteshots = [ - "Expval", - "Var", - "Probs", - "Sample", - "Counts", -] + +Expval = {} +Var = {} +Probs = {} +State = { condition = [ "analytic" ] } +Sample = { condition = [ "finiteshots" ] } +Count = { condition = [ "finiteshots" ] } [compilation] # If the device is compatible with qjit qjit_compatible = true # If the device requires run time generation of the quantum circuit. runtime_code_generation = true -# If the device supports adjoint -quantum_adjoint = false -# If the device supports quantum control instructions natively -quantum_control = false # If the device supports mid circuit measurements natively mid_circuit_measurement = false # This field is currently unchecked but it is reserved for the purpose of # determining if the device supports dynamic qubit allocation/deallocation. -dynamic_qubit_management = false +dynamic_qubit_management = false diff --git a/runtime/tests/third_party/dummy_device.toml b/runtime/tests/third_party/dummy_device.toml index de599c1ac4..e4e6ab94be 100644 --- a/runtime/tests/third_party/dummy_device.toml +++ b/runtime/tests/third_party/dummy_device.toml @@ -1,123 +1,116 @@ -schema = 1 +schema = 2 -[device] -name = "dummy.device.qubit" +[operators.gates.native] -[operators] -# Observables supported by the device -observables = [ - "PauliX", - "PauliY", - "PauliZ", - "Hadamard", - "Hermitian", - "Identity", - "Projector", - "SparseHamiltonian", - "Hamiltonian", - "Sum", - "SProd", - "Prod", - "Exp", -] - -# The union of all gate types listed in this section must match what -# the device considers "supported" through PennyLane's device API. -[[operators.gates]] -native = [ - "QubitUnitary", - "PauliX", - "PauliY", - "PauliZ", - "MultiRZ", - "Hadamard", - "S", - "T", - "CNOT", - "SWAP", - "CSWAP", - "Toffoli", - "CY", - "CZ", - "PhaseShift", - "ControlledPhaseShift", - "RX", - "RY", - "RZ", - "Rot", - "CRX", - "CRY", - "CRZ", - "CRot", - "Identity", - "IsingXX", - "IsingYY", - "IsingZZ", - "IsingXY", -] +QubitUnitary = { properties = [ "invertible", "differentiable" ] } +ControlledQubitUnitary = { properties = [ "invertible", "differentiable" ] } +PauliX = { properties = [ "controllable", "invertible", "differentiable" ] } +PauliY = { properties = [ "controllable", "invertible", "differentiable" ] } +PauliZ = { properties = [ "controllable", "invertible", "differentiable" ] } +MultiRZ = { properties = [ "controllable", "invertible", "differentiable" ] } +Hadamard = { properties = [ "controllable", "invertible", "differentiable" ] } +S = { properties = [ "controllable", "invertible", "differentiable" ] } +T = { properties = [ "controllable", "invertible", "differentiable" ] } +CNOT = { properties = [ "invertible", "differentiable" ] } +SWAP = { properties = [ "controllable", "invertible", "differentiable" ] } +CSWAP = { properties = [ "invertible", "differentiable" ] } +Toffoli = { properties = [ "invertible", "differentiable" ] } +CY = { properties = [ "invertible", "differentiable" ] } +CZ = { properties = [ "invertible", "differentiable" ] } +PhaseShift = { properties = [ "controllable", "invertible", "differentiable" ] } +ControlledPhaseShift = { properties = [ "invertible", "differentiable" ] } +RX = { properties = [ "controllable", "invertible", "differentiable" ] } +RY = { properties = [ "controllable", "invertible", "differentiable" ] } +RZ = { properties = [ "controllable", "invertible", "differentiable" ] } +Rot = { properties = [ "controllable", "invertible", "differentiable" ] } +CRX = { properties = [ "invertible", "differentiable" ] } +CRY = { properties = [ "invertible", "differentiable" ] } +CRZ = { properties = [ "invertible", "differentiable" ] } +CRot = { properties = [ "invertible" ] } +Identity = { properties = [ "invertible", "differentiable" ] } +IsingXX = { properties = [ "controllable", "invertible", "differentiable" ] } +IsingYY = { properties = [ "controllable", "invertible", "differentiable" ] } +IsingZZ = { properties = [ "controllable", "invertible", "differentiable" ] } +IsingXY = { properties = [ "controllable", "invertible", "differentiable" ] } +GlobalPhase = { properties = [ "controllable", "invertible", "differentiable" ] } +BlockEncode = { properties = [ "invertible", "differentiable" ] } +SingleExcitation = { properties = [ "invertible", "controllable", "differentiable"] } +SingleExcitationPlus = { properties = [ "invertible", "controllable", "differentiable"] } +SingleExcitationMinus = { properties = [ "invertible", "controllable", "differentiable"] } +DoubleExcitation = { properties = [ "invertible", "controllable", "differentiable"] } +DoubleExcitationPlus = { properties = [ "invertible", "controllable", "differentiable"] } +DoubleExcitationMinus = { properties = [ "invertible", "controllable", "differentiable"] } + +[operators.gates.decomp] # Operators that should be decomposed according to the algorithm used # by PennyLane's device API. # Optional, since gates not listed in this list will typically be decomposed by # default, but can be useful to express a deviation from this device's regular # strategy in PennyLane. -decomp = [ - "SX", - "ISWAP", - "PSWAP", - "SISWAP", - "SQISW", - "CPhase", - "BasisState", - "QubitStateVector", - "StatePrep", - "ControlledQubitUnitary", - "DiagonalQubitUnitary", - "SingleExcitation", - "SingleExcitationPlus", - "SingleExcitationMinus", - "DoubleExcitation", - "DoubleExcitationPlus", - "DoubleExcitationMinus", - "QubitCarry", - "QubitSum", - "OrbitalRotation", - "QFT", - "ECR", -] +SX = {} +ISWAP = {} +PSWAP = {} +SISWAP = {} +SQISW = {} +CPhase = {} +BasisState = {} +QubitStateVector = {} +StatePrep = {} +ControlledQubitUnitary = {} +DiagonalQubitUnitary = {} +QubitCarry = {} +QubitSum = {} +OrbitalRotation = {} +QFT = {} +ECR = {} # Gates which should be translated to QubitUnitary -matrix = [ - "MultiControlledX", -] +[operators.gates.matrix] + +MultiControlledX = {} + + +# Observables supported by the device +[operators.observables] + +PauliX = { properties = [ "differentiable" ] } +PauliY = { properties = [ "differentiable" ] } +PauliZ = { properties = [ "differentiable" ] } +Hadamard = { properties = [ "differentiable" ] } +Hermitian = { properties = [ "differentiable" ] } +Identity = { properties = [ "differentiable" ] } +Projector = {} +SparseHamiltonian = { properties = [ "differentiable" ] } +Hamiltonian = { properties = [ "differentiable" ] } +Sum = { properties = [ "differentiable" ] } +SProd = { properties = [ "differentiable" ] } +Prod = { properties = [ "differentiable" ] } +Exp = { properties = [ "differentiable" ] } [measurement_processes] -exactshots = [ - "Expval", - "Var", - "Probs", - "State", -] -finiteshots = [ - "Expval", - "Var", - "Probs", - "Sample", - "Counts", -] + +Expval = {} +Var = {} +Probs = {} +State = { condition = [ "analytic" ] } +Sample = { condition = [ "finiteshots" ] } +Count = { condition = [ "finiteshots" ] } [compilation] + # If the device is compatible with qjit qjit_compatible = true # If the device requires run time generation of the quantum circuit. runtime_code_generation = false -# If the device supports adjoint -quantum_adjoint = true -# If the device supports quantum control instructions natively -quantum_control = false # If the device supports mid circuit measurements natively mid_circuit_measurement = true - # This field is currently unchecked but it is reserved for the purpose of # determining if the device supports dynamic qubit allocation/deallocation. dynamic_qubit_management = false + +[options] + +option1 = "_option1" +option2 = "_option2" +