From 45a67d495552ca9aca3c05467b16b1b6d2e1d4ab Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Fri, 22 Nov 2024 12:56:55 -0500 Subject: [PATCH] Update Lightning device TOML files to the new schema (#988) **Context:** This PR is part of the new device capabilities initiative to improve feature parity across the ecosystem. See [this ADR](https://github.com/PennyLaneAI/adrs/pull/78) for more context. A new TOML schema has been defined, and the relevant module implemented in PennyLane: - https://github.com/PennyLaneAI/pennylane/pull/6407 - https://github.com/PennyLaneAI/pennylane/pull/6433. As well as updates made to Catalyst: - https://github.com/PennyLaneAI/catalyst/pull/1275 **Description of the Change:** - Updates `lightning_qubit.toml`, `lightning_kokkos.toml`, `lightning_gpu.toml` to the new schema - Removes `_operations` and `_observables` from `LightningQubit`, `LightningKokkos`, and `LightningGPU` as they are now available via `Device.capabilities` that is loaded from the TOML file. **Benefits:** A step towards feature parity across the ecosystem. **Possible Drawbacks:** - Per discussions when the ADR was developed, `operator.gates.decomp` and `operator.gates.matrix` are removed, and the TOML file no longer prescribes to the framework how an operator should be handled. To ensure consistency of behaviour, this information is temporarily moved to a `_to_matrix_ops` class property to be used by Catalyst, until we have better support for customizable multi-pathway decompositions. **Related GitHub Issues:** [sc-71729] --------- Co-authored-by: ringo-but-quantum Co-authored-by: Ali Asadi <10773383+maliasadi@users.noreply.github.com> Co-authored-by: Joseph Lee Co-authored-by: Joseph Lee <40768758+josephleekl@users.noreply.github.com> --- .github/CHANGELOG.md | 5 +- doc/requirements.txt | 1 + pennylane_lightning/core/_version.py | 2 +- .../lightning_gpu/lightning_gpu.py | 171 ++++------------- .../lightning_gpu/lightning_gpu.toml | 88 +++++---- .../lightning_kokkos/lightning_kokkos.py | 147 ++++----------- .../lightning_kokkos/lightning_kokkos.toml | 81 ++++---- .../lightning_qubit/lightning_qubit.py | 176 ++++-------------- .../lightning_qubit/lightning_qubit.toml | 87 +++++---- tests/test_gates.py | 25 ++- 10 files changed, 276 insertions(+), 507 deletions(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 465a065d79..8b3916b8d7 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -23,6 +23,9 @@ ### Improvements +* The TOML files for the devices are updated to use the new schema for declaring device capabilities. + [(#988)](https://github.com/PennyLaneAI/pennylane-lightning/pull/988) + * Unify excitation gates memory layout to row-major for both LGPU and LT. [(#959)](https://github.com/PennyLaneAI/pennylane-lightning/pull/959) @@ -52,7 +55,7 @@ This release contains contributions from (in alphabetical order): -Ali Asadi, Joseph Lee, Anton Naim Ibrahim, Luis Alfredo Nuñez Meneses, Andrija Paurevic, Shuli Shu, Raul Torres, Haochen Paul Wang +Ali Asadi, Astral Cai, Joseph Lee, Anton Naim Ibrahim, Luis Alfredo Nuñez Meneses, Andrija Paurevic, Shuli Shu, Raul Torres, Haochen Paul Wang --- diff --git a/doc/requirements.txt b/doc/requirements.txt index 07f3a33b9c..70f89d4032 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -18,3 +18,4 @@ cutensornet-cu12 wheel sphinxext-opengraph matplotlib +git+https://github.com/PennyLaneAI/pennylane.git@master diff --git a/pennylane_lightning/core/_version.py b/pennylane_lightning/core/_version.py index fec481f46d..46600effac 100644 --- a/pennylane_lightning/core/_version.py +++ b/pennylane_lightning/core/_version.py @@ -16,4 +16,4 @@ Version number (major.minor.patch[-label]) """ -__version__ = "0.40.0-dev13" +__version__ = "0.40.0-dev14" diff --git a/pennylane_lightning/lightning_gpu/lightning_gpu.py b/pennylane_lightning/lightning_gpu/lightning_gpu.py index 47efd2bf6a..6a8dfff594 100644 --- a/pennylane_lightning/lightning_gpu/lightning_gpu.py +++ b/pennylane_lightning/lightning_gpu/lightning_gpu.py @@ -30,7 +30,7 @@ import numpy as np import pennylane as qml from pennylane.devices import DefaultExecutionConfig, ExecutionConfig -from pennylane.devices.default_qubit import adjoint_ops +from pennylane.devices.capabilities import OperatorProperties from pennylane.devices.modifiers import simulator_tracking, single_tape_support from pennylane.devices.preprocess import ( decompose, @@ -43,7 +43,7 @@ ) from pennylane.measurements import MidMeasureMP from pennylane.operation import DecompositionUndefinedError, Operator -from pennylane.ops import Prod, SProd, Sum +from pennylane.ops import Conditional, PauliRot, Prod, SProd, Sum from pennylane.tape import QuantumScript from pennylane.transforms.core import TransformProgram from pennylane.typing import Result @@ -74,135 +74,25 @@ from ._mpi_handler import MPIHandler from ._state_vector import LightningGPUStateVector -# The set of supported operations. -_operations = frozenset( - { - "Identity", - "QubitUnitary", - "ControlledQubitUnitary", - "MultiControlledX", - "DiagonalQubitUnitary", - "PauliX", - "PauliY", - "PauliZ", - "MultiRZ", - "GlobalPhase", - "C(PauliX)", - "C(PauliY)", - "C(PauliZ)", - "C(Hadamard)", - "C(S)", - "C(T)", - "C(PhaseShift)", - "C(RX)", - "C(RY)", - "C(RZ)", - "C(Rot)", - "C(SWAP)", - "C(IsingXX)", - "C(IsingXY)", - "C(IsingYY)", - "C(IsingZZ)", - "C(SingleExcitation)", - "C(SingleExcitationMinus)", - "C(SingleExcitationPlus)", - "C(DoubleExcitation)", - "C(DoubleExcitationMinus)", - "C(DoubleExcitationPlus)", - "C(MultiRZ)", - "C(GlobalPhase)", - "Hadamard", - "S", - "Adjoint(S)", - "T", - "Adjoint(T)", - "SX", - "Adjoint(SX)", - "CNOT", - "SWAP", - "ISWAP", - "PSWAP", - "Adjoint(ISWAP)", - "SISWAP", - "Adjoint(SISWAP)", - "SQISW", - "CSWAP", - "Toffoli", - "CY", - "CZ", - "PhaseShift", - "ControlledPhaseShift", - "RX", - "RY", - "RZ", - "Rot", - "CRX", - "CRY", - "CRZ", - "CRot", - "IsingXX", - "IsingYY", - "IsingZZ", - "IsingXY", - "SingleExcitation", - "SingleExcitationPlus", - "SingleExcitationMinus", - "DoubleExcitation", - "DoubleExcitationPlus", - "DoubleExcitationMinus", - "Adjoint(MultiRZ)", - "Adjoint(GlobalPhase)", - "Adjoint(PhaseShift)", - "Adjoint(ControlledPhaseShift)", - "Adjoint(RX)", - "Adjoint(RY)", - "Adjoint(RZ)", - "Adjoint(CRX)", - "Adjoint(CRY)", - "Adjoint(CRZ)", - "Adjoint(IsingXX)", - "Adjoint(IsingYY)", - "Adjoint(IsingZZ)", - "Adjoint(IsingXY)", - "Adjoint(SingleExcitation)", - "Adjoint(SingleExcitationPlus)", - "Adjoint(SingleExcitationMinus)", - "Adjoint(DoubleExcitation)", - "Adjoint(DoubleExcitationPlus)", - "Adjoint(DoubleExcitationMinus)", - "QubitCarry", - "QubitSum", - "OrbitalRotation", - "ECR", - "BlockEncode", - "C(BlockEncode)", - } -) -# End the set of supported operations. - -# The set of supported observables. -_observables = frozenset( - { - "PauliX", - "PauliY", - "PauliZ", - "Hadamard", - "SparseHamiltonian", - "LinearCombination", - "Hermitian", - "Identity", - "Projector", - "Sum", - "Prod", - "SProd", - "Exp", - } -) +_to_matrix_ops = { + "BlockEncode": OperatorProperties(controllable=True), + "ControlledQubitUnitary": OperatorProperties(), + "ECR": OperatorProperties(), + "SX": OperatorProperties(), + "ISWAP": OperatorProperties(), + "PSWAP": OperatorProperties(), + "SISWAP": OperatorProperties(), + "SQISW": OperatorProperties(), + "OrbitalRotation": OperatorProperties(), + "QubitCarry": OperatorProperties(), + "QubitSum": OperatorProperties(), + "DiagonalQubitUnitary": OperatorProperties(), +} def stopping_condition(op: Operator) -> bool: """A function that determines whether or not an operation is supported by ``lightning.gpu``.""" - return op.name in _operations + return _supports_operation(op.name) def stopping_condition_shots(op: Operator) -> bool: @@ -213,7 +103,7 @@ def stopping_condition_shots(op: Operator) -> bool: def accepted_observables(obs: Operator) -> bool: """A function that determines whether or not an observable is supported by ``lightning.gpu``.""" - return obs.name in _observables + return _supports_observable(obs.name) def adjoint_observables(obs: Operator) -> bool: @@ -228,7 +118,7 @@ def adjoint_observables(obs: Operator) -> bool: if isinstance(obs, (Sum, Prod)): return all(adjoint_observables(o) for o in obs) - return obs.name in _observables + return _supports_observable(obs.name) def adjoint_measurements(mp: qml.measurements.MeasurementProcess) -> bool: @@ -252,7 +142,10 @@ def _supports_adjoint(circuit): def _adjoint_ops(op: qml.operation.Operator) -> bool: """Specify whether or not an Operator is supported by adjoint differentiation.""" - return adjoint_ops(op) and not isinstance(op, qml.PauliRot) + + return not isinstance(op, (Conditional, MidMeasureMP, PauliRot)) and ( + not qml.operation.is_trainable(op) or (op.num_params == 1 and op.has_generator) + ) def _add_adjoint_transforms(program: TransformProgram) -> None: @@ -333,15 +226,13 @@ class LightningGPU(LightningBase): _CPP_BINARY_AVAILABLE = LGPU_CPP_BINARY_AVAILABLE _backend_info = backend_info if LGPU_CPP_BINARY_AVAILABLE else None - # This `config` is used in Catalyst-Frontend - config = Path(__file__).parent / "lightning_gpu.toml" + # TODO: This is to communicate to Catalyst in qjit-compiled workflows that these operations + # should be converted to QubitUnitary instead of their original decompositions. Remove + # this when customizable multiple decomposition pathways are implemented + _to_matrix_ops = _to_matrix_ops - # TODO: Move supported ops/obs to TOML file - operations = _operations - # The names of the supported operations. - - observables = _observables - # The names of the supported observables. + # This configuration file declares capabilities of the device + config_filepath = Path(__file__).parent / "lightning_gpu.toml" def __init__( # pylint: disable=too-many-arguments self, @@ -607,3 +498,7 @@ def get_c_interface(): return "LightningGPUSimulator", lib_location raise RuntimeError("'LightningGPUSimulator' shared library not found") # pragma: no cover + + +_supports_operation = LightningGPU.capabilities.supports_operation +_supports_observable = LightningGPU.capabilities.supports_observable diff --git a/pennylane_lightning/lightning_gpu/lightning_gpu.toml b/pennylane_lightning/lightning_gpu/lightning_gpu.toml index 8f80ea1676..480b56c9c7 100644 --- a/pennylane_lightning/lightning_gpu/lightning_gpu.toml +++ b/pennylane_lightning/lightning_gpu/lightning_gpu.toml @@ -1,8 +1,25 @@ -schema = 2 +schema = 3 -# 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] +# The set of all gate types supported at the runtime execution interface of the +# device, i.e., what is supported by the `execute` method of the Device API. +# The gate definition has the following format: +# +# GATE = { properties = [ PROPS ], conditions = [ CONDS ] } +# +# where PROPS and CONS are zero or more comma separated quoted strings. +# +# PROPS: zero or more comma-separated quoted strings: +# - "controllable": if a controlled version of this gate is supported. +# - "invertible": if the adjoint of this operation is supported. +# - "differentiable": if device gradient is supported for this gate. +# CONDS: zero or more comma-separated quoted strings: +# - "analytic" or "finiteshots": if this operation is only supported in +# either analytic execution or with shots, respectively. +# - "terms-commute": if this composite operator is only supported +# given that its terms commute. Only relevant for Prod, SProd, Sum, +# LinearCombination, and Hamiltonian. +# +[operators.gates] Identity = { properties = [ "invertible", "differentiable" ] } PauliX = { properties = [ "invertible", "controllable", "differentiable" ] } @@ -15,7 +32,7 @@ PhaseShift = { properties = [ "invertible", "controllable", "differe RX = { properties = [ "invertible", "controllable", "differentiable" ] } RY = { properties = [ "invertible", "controllable", "differentiable" ] } RZ = { properties = [ "invertible", "controllable", "differentiable" ] } -Rot = { properties = [ "invertible", "controllable", "differentiable" ] } +Rot = { properties = [ "controllable", "differentiable" ] } CNOT = { properties = [ "invertible", "differentiable" ] } CY = { properties = [ "invertible", "differentiable" ] } CZ = { properties = [ "invertible", "differentiable" ] } @@ -30,7 +47,7 @@ ControlledPhaseShift = { properties = [ "invertible", "differe CRX = { properties = [ "invertible", "differentiable" ] } CRY = { properties = [ "invertible", "differentiable" ] } CRZ = { properties = [ "invertible", "differentiable" ] } -CRot = { properties = [ "invertible" ] } +CRot = { } SingleExcitation = { properties = [ "invertible", "controllable", "differentiable" ] } SingleExcitationPlus = { properties = [ "invertible", "controllable", "differentiable" ] } SingleExcitationMinus = { properties = [ "invertible", "controllable", "differentiable" ] } @@ -41,21 +58,10 @@ MultiRZ = { properties = [ "invertible", "controllable", "differe QubitUnitary = { properties = [ "invertible", "controllable" ] } GlobalPhase = { properties = [ "invertible", "controllable", "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] +# Operations supported by the execution in Python but not directly supported by the backend +[pennylane.operators.gates] -BasisState = {} -StatePrep = {} -MultiControlledX = {} - -# Gates which should be translated to QubitUnitary -[operators.gates.matrix] - -BlockEncode = {properties = [ "controllable" ]} +BlockEncode = { properties = [ "controllable" ] } ControlledQubitUnitary = {} ECR = {} SX = {} @@ -67,6 +73,7 @@ OrbitalRotation = {} QubitCarry = {} QubitSum = {} DiagonalQubitUnitary = {} +MultiControlledX = {} # Observables supported by the device [operators.observables] @@ -84,30 +91,35 @@ Prod = { properties = [ "differentiable" ] } Exp = { properties = [ "differentiable" ] } LinearCombination = { properties = [ "differentiable" ] } +[pennylane.operators.observables] + +Projector = {} + [measurement_processes] -Expval = {} -Var = {} -Probs = {} -State = { condition = [ "analytic" ] } -Sample = { condition = [ "finiteshots" ] } -Counts = { condition = [ "finiteshots" ] } +ExpectationMP = {} +VarianceMP = {} +ProbabilityMP = {} +StateMP = { conditions = [ "analytic" ] } +SampleMP = { conditions = [ "finiteshots" ] } +CountsMP = { conditions = [ "finiteshots" ] } +# Additional support that the device may provide. All accepted fields and their +# default values are listed below. Any fields missing from the TOML file will be +# set to their default values. [compilation] -# If the device is compatible with qjit + +# Whether the device is compatible with qjit. qjit_compatible = true -# If the device requires run time generation of the quantum circuit. +# Whether 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. +# The methods of handling mid-circuit measurements that the device supports, e.g., +# "one-shot", "device", "tree-traversal", etc. An empty list indicates that the device +# does not support mid-circuit measurements. +supported_mcm_methods = [ "one-shot" ] +# Whether the device supports dynamic qubit allocation/deallocation. dynamic_qubit_management = false - -# whether the device can support non-commuting measurements together -# in a single execution +# Whether simultaneous measurements of non-commuting observables is supported. non_commuting_observables = true - -# Whether the device supports (arbitrary) initial state preparation. +# Whether the device supports initial state preparation. initial_state_prep = true diff --git a/pennylane_lightning/lightning_kokkos/lightning_kokkos.py b/pennylane_lightning/lightning_kokkos/lightning_kokkos.py index 046bb6c547..e799878549 100644 --- a/pennylane_lightning/lightning_kokkos/lightning_kokkos.py +++ b/pennylane_lightning/lightning_kokkos/lightning_kokkos.py @@ -26,7 +26,7 @@ import numpy as np import pennylane as qml from pennylane.devices import DefaultExecutionConfig, ExecutionConfig -from pennylane.devices.default_qubit import adjoint_ops +from pennylane.devices.capabilities import OperatorProperties from pennylane.devices.modifiers import simulator_tracking, single_tape_support from pennylane.devices.preprocess import ( decompose, @@ -39,7 +39,7 @@ ) from pennylane.measurements import MidMeasureMP from pennylane.operation import DecompositionUndefinedError, Operator -from pennylane.ops import Prod, SProd, Sum +from pennylane.ops import Conditional, PauliRot, Prod, SProd, Sum from pennylane.tape import QuantumScript from pennylane.transforms.core import TransformProgram from pennylane.typing import Result @@ -63,107 +63,19 @@ from ._measurements import LightningKokkosMeasurements from ._state_vector import LightningKokkosStateVector -# The set of supported operations. -_operations = frozenset( - { - "Identity", - "QubitUnitary", - "ControlledQubitUnitary", - "MultiControlledX", - "DiagonalQubitUnitary", - "PauliX", - "PauliY", - "PauliZ", - "MultiRZ", - "GlobalPhase", - "C(GlobalPhase)", - "Hadamard", - "S", - "Adjoint(S)", - "T", - "Adjoint(T)", - "SX", - "Adjoint(SX)", - "CNOT", - "SWAP", - "ISWAP", - "PSWAP", - "Adjoint(ISWAP)", - "SISWAP", - "Adjoint(SISWAP)", - "SQISW", - "CSWAP", - "Toffoli", - "CY", - "CZ", - "PhaseShift", - "ControlledPhaseShift", - "RX", - "RY", - "RZ", - "Rot", - "CRX", - "CRY", - "CRZ", - "CRot", - "IsingXX", - "IsingYY", - "IsingZZ", - "IsingXY", - "SingleExcitation", - "SingleExcitationPlus", - "SingleExcitationMinus", - "DoubleExcitation", - "DoubleExcitationPlus", - "DoubleExcitationMinus", - "Adjoint(MultiRZ)", - "Adjoint(GlobalPhase)", - "Adjoint(PhaseShift)", - "Adjoint(ControlledPhaseShift)", - "Adjoint(RX)", - "Adjoint(RY)", - "Adjoint(RZ)", - "Adjoint(CRX)", - "Adjoint(CRY)", - "Adjoint(CRZ)", - "Adjoint(IsingXX)", - "Adjoint(IsingYY)", - "Adjoint(IsingZZ)", - "Adjoint(IsingXY)", - "Adjoint(SingleExcitation)", - "Adjoint(SingleExcitationPlus)", - "Adjoint(SingleExcitationMinus)", - "Adjoint(DoubleExcitation)", - "Adjoint(DoubleExcitationPlus)", - "Adjoint(DoubleExcitationMinus)", - "QubitCarry", - "QubitSum", - "OrbitalRotation", - "ECR", - "BlockEncode", - "C(BlockEncode)", - } -) -# End the set of supported operations. - -# The set of supported observables. -_observables = frozenset( - { - "PauliX", - "PauliY", - "PauliZ", - "Hadamard", - "Hermitian", - "Identity", - "Projector", - "SparseHamiltonian", - "LinearCombination", - "Sum", - "SProd", - "Prod", - "Exp", - } -) +_to_matrix_ops = { + "BlockEncode": OperatorProperties(), + "DiagonalQubitUnitary": OperatorProperties(), + "ECR": OperatorProperties(), + "ISWAP": OperatorProperties(), + "OrbitalRotation": OperatorProperties(), + "PSWAP": OperatorProperties(), + "QubitCarry": OperatorProperties(), + "QubitSum": OperatorProperties(), + "SISWAP": OperatorProperties(), + "SQISW": OperatorProperties(), + "SX": OperatorProperties(), +} def stopping_condition(op: Operator) -> bool: @@ -172,7 +84,7 @@ def stopping_condition(op: Operator) -> bool: word = op._hyperparameters["pauli_word"] # pylint: disable=protected-access # decomposes to IsingXX, etc. for n <= 2 return reduce(lambda x, y: x + (y != "I"), word, 0) > 2 - return op.name in _operations + return _supports_operation(op.name) def stopping_condition_shots(op: Operator) -> bool: @@ -183,7 +95,7 @@ def stopping_condition_shots(op: Operator) -> bool: def accepted_observables(obs: Operator) -> bool: """A function that determines whether or not an observable is supported by ``lightning.kokkos``.""" - return obs.name in _observables + return _supports_observable(obs.name) def adjoint_observables(obs: Operator) -> bool: @@ -198,7 +110,7 @@ def adjoint_observables(obs: Operator) -> bool: if isinstance(obs, (Sum, Prod)): return all(adjoint_observables(o) for o in obs) - return obs.name in _observables + return _supports_observable(obs.name) def adjoint_measurements(mp: qml.measurements.MeasurementProcess) -> bool: @@ -222,7 +134,10 @@ def _supports_adjoint(circuit): def _adjoint_ops(op: qml.operation.Operator) -> bool: """Specify whether or not an Operator is supported by adjoint differentiation.""" - return not isinstance(op, qml.PauliRot) and adjoint_ops(op) + + return not isinstance(op, (Conditional, MidMeasureMP, PauliRot)) and ( + not qml.operation.is_trainable(op) or (op.num_params == 1 and op.has_generator) + ) def _add_adjoint_transforms(program: TransformProgram) -> None: @@ -290,15 +205,13 @@ class LightningKokkos(LightningBase): _backend_info = backend_info if LK_CPP_BINARY_AVAILABLE else None kokkos_config = {} - # This `config` is used in Catalyst-Frontend - config = Path(__file__).parent / "lightning_kokkos.toml" + # The configuration file declares the capabilities of the device + config_filepath = Path(__file__).parent / "lightning_kokkos.toml" - # TODO: Move supported ops/obs to TOML file - operations = _operations - # The names of the supported operations. - - observables = _observables - # The names of the supported observables. + # TODO: This is to communicate to Catalyst in qjit-compiled workflows that these operations + # should be converted to QubitUnitary instead of their original decompositions. Remove + # this when customizable multiple decomposition pathways are implemented + _to_matrix_ops = _to_matrix_ops def __init__( # pylint: disable=too-many-arguments self, @@ -554,3 +467,7 @@ def get_c_interface(): return "LightningKokkosSimulator", lib_location raise RuntimeError("'LightningKokkosSimulator' shared library not found") + + +_supports_operation = LightningKokkos.capabilities.supports_operation +_supports_observable = LightningKokkos.capabilities.supports_observable diff --git a/pennylane_lightning/lightning_kokkos/lightning_kokkos.toml b/pennylane_lightning/lightning_kokkos/lightning_kokkos.toml index 98901ee5eb..801add040d 100644 --- a/pennylane_lightning/lightning_kokkos/lightning_kokkos.toml +++ b/pennylane_lightning/lightning_kokkos/lightning_kokkos.toml @@ -1,8 +1,25 @@ -schema = 2 +schema = 3 -# 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] +# The set of all gate types supported at the runtime execution interface of the +# device, i.e., what is supported by the `execute` method of the Device API. +# The gate definition has the following format: +# +# GATE = { properties = [ PROPS ], conditions = [ CONDS ] } +# +# where PROPS and CONS are zero or more comma separated quoted strings. +# +# PROPS: zero or more comma-separated quoted strings: +# - "controllable": if a controlled version of this gate is supported. +# - "invertible": if the adjoint of this operation is supported. +# - "differentiable": if device gradient is supported for this gate. +# CONDS: zero or more comma-separated quoted strings: +# - "analytic" or "finiteshots": if this operation is only supported in +# either analytic execution or with shots, respectively. +# - "terms-commute": if this composite operator is only supported +# given that its terms commute. Only relevant for Prod, SProd, Sum, +# LinearCombination, and Hamiltonian. +# +[operators.gates] CNOT = { properties = [ "invertible", "differentiable" ] } ControlledPhaseShift = { properties = [ "invertible", "differentiable" ] } @@ -41,22 +58,13 @@ SWAP = { properties = [ "invertible", "differe 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] +# Operations supported by the execution in Python but not directly supported by the backend +[pennylane.operators.gates] -BasisState = {} MultiControlledX = {} -StatePrep = {} ControlledQubitUnitary = {} - -# Gates which should be translated to QubitUnitary -[operators.gates.matrix] - -BlockEncode = {} +GlobalPhase = { properties = [ "invertible", "controllable", "differentiable" ] } +BlockEncode = { properties = [ "controllable" ] } DiagonalQubitUnitary = {} ECR = {} ISWAP = {} @@ -84,30 +92,35 @@ Prod = { properties = [ "differentiable" ] } Exp = { properties = [ "differentiable" ] } LinearCombination = { properties = [ "differentiable" ] } +[pennylane.operators.observables] + +Projector = {} + [measurement_processes] -Expval = {} -Var = {} -Probs = {} -State = { condition = [ "analytic" ] } -Sample = { condition = [ "finiteshots" ] } -Counts = { condition = [ "finiteshots" ] } +ExpectationMP = {} +VarianceMP = {} +ProbabilityMP = {} +StateMP = { conditions = [ "analytic" ] } +SampleMP = { conditions = [ "finiteshots" ] } +CountsMP = { conditions = [ "finiteshots" ] } +# Additional support that the device may provide. All accepted fields and their +# default values are listed below. Any fields missing from the TOML file will be +# set to their default values. [compilation] -# If the device is compatible with qjit +# Whether the device is compatible with qjit. qjit_compatible = true -# If the device requires run time generation of the quantum circuit. +# Whether 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. +# The methods of handling mid-circuit measurements that the device supports, e.g., +# "one-shot", "device", "tree-traversal", etc. An empty list indicates that the device +# does not support mid-circuit measurements. +supported_mcm_methods = [ "one-shot" ] +# Whether the device supports dynamic qubit allocation/deallocation. dynamic_qubit_management = false - -# whether the device can support non-commuting measurements together -# in a single execution +# Whether simultaneous measurements of non-commuting observables is supported. non_commuting_observables = true - -# Whether the device supports (arbitrary) initial state preparation. +# Whether the device supports initial state preparation. initial_state_prep = true diff --git a/pennylane_lightning/lightning_qubit/lightning_qubit.py b/pennylane_lightning/lightning_qubit/lightning_qubit.py index 3abff13254..3475fccfbd 100644 --- a/pennylane_lightning/lightning_qubit/lightning_qubit.py +++ b/pennylane_lightning/lightning_qubit/lightning_qubit.py @@ -26,7 +26,7 @@ import numpy as np import pennylane as qml from pennylane.devices import DefaultExecutionConfig, ExecutionConfig -from pennylane.devices.default_qubit import adjoint_ops +from pennylane.devices.capabilities import OperatorProperties from pennylane.devices.modifiers import simulator_tracking, single_tape_support from pennylane.devices.preprocess import ( decompose, @@ -39,7 +39,7 @@ ) from pennylane.measurements import MidMeasureMP from pennylane.operation import DecompositionUndefinedError, Operator -from pennylane.ops import Prod, SProd, Sum +from pennylane.ops import Conditional, PauliRot, Prod, SProd, Sum from pennylane.tape import QuantumScript from pennylane.transforms.core import TransformProgram from pennylane.typing import Result @@ -62,130 +62,19 @@ from ._measurements import LightningMeasurements from ._state_vector import LightningStateVector -# The set of supported operations. -_operations = frozenset( - { - "Identity", - "QubitUnitary", - "MultiControlledX", - "DiagonalQubitUnitary", - "PauliX", - "PauliY", - "PauliZ", - "MultiRZ", - "GlobalPhase", - "Hadamard", - "S", - "Adjoint(S)", - "T", - "Adjoint(T)", - "SX", - "Adjoint(SX)", - "CNOT", - "SWAP", - "ISWAP", - "PSWAP", - "Adjoint(ISWAP)", - "SISWAP", - "Adjoint(SISWAP)", - "SQISW", - "CSWAP", - "Toffoli", - "CY", - "CZ", - "PhaseShift", - "ControlledPhaseShift", - "RX", - "RY", - "RZ", - "Adjoint(PhaseShift)", - "Adjoint(ControlledPhaseShift)", - "Adjoint(RX)", - "Adjoint(RY)", - "Adjoint(RZ)", - "Rot", - "CRX", - "CRY", - "CRZ", - "Adjoint(CRX)", - "Adjoint(CRY)", - "Adjoint(CRZ)", - "C(PauliX)", - "C(PauliY)", - "C(PauliZ)", - "C(Hadamard)", - "C(S)", - "C(T)", - "C(PhaseShift)", - "C(RX)", - "C(RY)", - "C(RZ)", - "C(Rot)", - "C(SWAP)", - "C(IsingXX)", - "C(IsingXY)", - "C(IsingYY)", - "C(IsingZZ)", - "C(SingleExcitation)", - "C(SingleExcitationMinus)", - "C(SingleExcitationPlus)", - "C(DoubleExcitation)", - "C(DoubleExcitationMinus)", - "C(DoubleExcitationPlus)", - "C(MultiRZ)", - "C(GlobalPhase)", - "C(QubitUnitary)", - "CRot", - "IsingXX", - "IsingYY", - "IsingZZ", - "IsingXY", - "Adjoint(IsingXX)", - "Adjoint(IsingXY)", - "Adjoint(IsingYY)", - "Adjoint(IsingZZ)", - "SingleExcitation", - "SingleExcitationPlus", - "SingleExcitationMinus", - "DoubleExcitation", - "DoubleExcitationPlus", - "DoubleExcitationMinus", - "Adjoint(SingleExcitation)", - "Adjoint(SingleExcitationMinus)", - "Adjoint(SingleExcitationPlus)", - "Adjoint(DoubleExcitation)", - "Adjoint(DoubleExcitationMinus)", - "Adjoint(DoubleExcitationPlus)", - "Adjoint(MultiRZ)", - "Adjoint(GlobalPhase)", - "QubitCarry", - "QubitSum", - "OrbitalRotation", - "ECR", - "BlockEncode", - "C(BlockEncode)", - } -) -# End the set of supported operations. - -# The set of supported observables. -_observables = frozenset( - { - "PauliX", - "PauliY", - "PauliZ", - "Hadamard", - "Hermitian", - "Identity", - "Projector", - "SparseHamiltonian", - "LinearCombination", - "Sum", - "SProd", - "Prod", - "Exp", - } -) +_to_matrix_ops = { + "BlockEncode": OperatorProperties(controllable=True), + "DiagonalQubitUnitary": OperatorProperties(), + "ECR": OperatorProperties(), + "ISWAP": OperatorProperties(), + "OrbitalRotation": OperatorProperties(), + "PSWAP": OperatorProperties(), + "QubitCarry": OperatorProperties(), + "QubitSum": OperatorProperties(), + "SISWAP": OperatorProperties(), + "SQISW": OperatorProperties(), + "SX": OperatorProperties(), +} def stopping_condition(op: Operator) -> bool: @@ -199,7 +88,7 @@ def stopping_condition(op: Operator) -> bool: word = op._hyperparameters["pauli_word"] # pylint: disable=protected-access # decomposes to IsingXX, etc. for n <= 2 return reduce(lambda x, y: x + (y != "I"), word, 0) > 2 - return op.name in _operations + return _supports_operation(op.name) def stopping_condition_shots(op: Operator) -> bool: @@ -210,7 +99,7 @@ def stopping_condition_shots(op: Operator) -> bool: def accepted_observables(obs: Operator) -> bool: """A function that determines whether or not an observable is supported by ``lightning.qubit``.""" - return obs.name in _observables + return _supports_observable(obs.name) def adjoint_observables(obs: Operator) -> bool: @@ -225,7 +114,7 @@ def adjoint_observables(obs: Operator) -> bool: if isinstance(obs, (Sum, Prod)): return all(adjoint_observables(o) for o in obs) - return obs.name in _observables + return _supports_observable(obs.name) def adjoint_measurements(mp: qml.measurements.MeasurementProcess) -> bool: @@ -249,7 +138,10 @@ def _supports_adjoint(circuit): def _adjoint_ops(op: qml.operation.Operator) -> bool: """Specify whether or not an Operator is supported by adjoint differentiation.""" - return not isinstance(op, qml.PauliRot) and adjoint_ops(op) + + return not isinstance(op, (Conditional, MidMeasureMP, PauliRot)) and ( + not qml.operation.is_trainable(op) or (op.num_params == 1 and op.has_generator) + ) def _add_adjoint_transforms(program: TransformProgram) -> None: @@ -327,15 +219,13 @@ class LightningQubit(LightningBase): _CPP_BINARY_AVAILABLE = LQ_CPP_BINARY_AVAILABLE _backend_info = backend_info if LQ_CPP_BINARY_AVAILABLE else None - # This `config` is used in Catalyst-Frontend - config = Path(__file__).parent / "lightning_qubit.toml" - - # TODO: Move supported ops/obs to TOML file - operations = _operations - # The names of the supported operations. + # This configuration file declares the device capabilities + config_filepath = Path(__file__).parent / "lightning_qubit.toml" - observables = _observables - # The names of the supported observables. + # TODO: This is to communicate to Catalyst in qjit-compiled workflows that these operations + # should be converted to QubitUnitary instead of their original decompositions. Remove + # this when customizable multiple decomposition pathways are implemented + _to_matrix_ops = _to_matrix_ops def __init__( # pylint: disable=too-many-arguments self, @@ -391,6 +281,12 @@ def __init__( # pylint: disable=too-many-arguments self._kernel_name = None self._num_burnin = 0 + self.device_kwargs = { + "mcmc": self._mcmc, + "num_burnin": self._num_burnin, + "kernel_name": self._kernel_name, + } + # Creating the state vector self._statevector = self.LightningStateVector(num_wires=len(self.wires), dtype=c_dtype) @@ -618,3 +514,7 @@ def get_c_interface(): return "LightningSimulator", lib_location raise RuntimeError("'LightningSimulator' shared library not found") # pragma: no cover + + +_supports_operation = LightningQubit.capabilities.supports_operation +_supports_observable = LightningQubit.capabilities.supports_observable diff --git a/pennylane_lightning/lightning_qubit/lightning_qubit.toml b/pennylane_lightning/lightning_qubit/lightning_qubit.toml index cedc0b3682..4f8a153b4f 100644 --- a/pennylane_lightning/lightning_qubit/lightning_qubit.toml +++ b/pennylane_lightning/lightning_qubit/lightning_qubit.toml @@ -1,6 +1,25 @@ -schema = 2 - -[operators.gates.native] +schema = 3 + +# The set of all gate types supported at the runtime execution interface of the +# device, i.e., what is supported by the `execute` method of the Device API. +# The gate definition has the following format: +# +# GATE = { properties = [ PROPS ], conditions = [ CONDS ] } +# +# where PROPS and CONS are zero or more comma separated quoted strings. +# +# PROPS: zero or more comma-separated quoted strings: +# - "controllable": if a controlled version of this gate is supported. +# - "invertible": if the adjoint of this operation is supported. +# - "differentiable": if device gradient is supported for this gate. +# CONDS: zero or more comma-separated quoted strings: +# - "analytic" or "finiteshots": if this operation is only supported in +# either analytic execution or with shots, respectively. +# - "terms-commute": if this composite operator is only supported +# given that its terms commute. Only relevant for Prod, SProd, Sum, +# LinearCombination, and Hamiltonian. +# +[operators.gates] CNOT = { properties = [ "invertible", "differentiable" ] } ControlledPhaseShift = { properties = [ "invertible", "differentiable" ] } @@ -39,19 +58,10 @@ SWAP = { properties = [ "invertible", "controllable", "differe Toffoli = { properties = [ "invertible", "differentiable" ] } T = { 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. -MultiControlledX = {} - -# Gates which should be translated to QubitUnitary -[operators.gates.matrix] +# Operations supported by the execution in Python but not directly supported by the backend +[pennylane.operators.gates] -BlockEncode = {properties = [ "controllable" ]} +BlockEncode = { properties = [ "controllable" ] } DiagonalQubitUnitary = {} ECR = {} ISWAP = {} @@ -62,6 +72,7 @@ QubitSum = {} SISWAP = {} SQISW = {} SX = {} +MultiControlledX = {} # Observables supported by the device [operators.observables] @@ -72,7 +83,6 @@ PauliY = { properties = [ "differentiable" ] } PauliZ = { properties = [ "differentiable" ] } Hadamard = { properties = [ "differentiable" ] } Hermitian = { properties = [ "differentiable" ] } -Hamiltonian = { properties = [ "differentiable" ] } SparseHamiltonian = { properties = [ "differentiable" ] } Projector = { properties = [ "differentiable" ] } Sum = { properties = [ "differentiable" ] } @@ -81,36 +91,35 @@ Prod = { properties = [ "differentiable" ] } Exp = { properties = [ "differentiable" ] } LinearCombination = { properties = [ "differentiable" ] } +[pennylane.operators.observables] + +Projector = {} + [measurement_processes] -Expval = {} -Var = {} -Probs = {} -State = { condition = [ "analytic" ] } -Sample = { condition = [ "finiteshots" ] } -Counts = { condition = [ "finiteshots" ] } +ExpectationMP = {} +VarianceMP = {} +ProbabilityMP = {} +StateMP = { conditions = [ "analytic" ] } +SampleMP = { conditions = [ "finiteshots" ] } +CountsMP = { conditions = [ "finiteshots" ] } +# Additional support that the device may provide. All accepted fields and their +# default values are listed below. Any fields missing from the TOML file will be +# set to their default values. [compilation] -# If the device is compatible with qjit +# Whether the device is compatible with qjit. qjit_compatible = true -# If the device requires run time generation of the quantum circuit. +# Whether 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. +# The methods of handling mid-circuit measurements that the device supports, e.g., +# "one-shot", "device", "tree-traversal", etc. An empty list indicates that the device +# does not support mid-circuit measurements. +supported_mcm_methods = [ "one-shot" ] +# Whether the device supports dynamic qubit allocation/deallocation. dynamic_qubit_management = false - -# whether the device can support non-commuting measurements together -# in a single execution +# Whether simultaneous measurements of non-commuting observables is supported. non_commuting_observables = true - -# Whether the device supports (arbitrary) initial state preparation. +# Whether the device supports initial state preparation. initial_state_prep = true - -[options] - -mcmc = "_mcmc" -num_burnin = "_num_burnin" -kernel_name = "_kernel_name" diff --git a/tests/test_gates.py b/tests/test_gates.py index 3433f2ceec..f589423951 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -28,6 +28,25 @@ pytest.skip("No binary module found. Skipping.", allow_module_level=True) +def _get_ld_operations(): + """Gets a set of supported operations by LightningDevice.""" + + if ld.capabilities is None and hasattr(ld, "operations"): + return ld.operations + + operations = set() + for op, prop in ld.capabilities.operations.items(): + operations.add(op) + if prop.controllable: + operations.add(f"C({op})") + if prop.invertible: + operations.add(f"Adjoint({op})") + return operations + + +ld_operations = _get_ld_operations() + + @pytest.fixture def op(op_name): ops_list = { @@ -87,7 +106,7 @@ def op(op_name): return ops_list.get(op_name) -@pytest.mark.parametrize("op_name", ld.operations) +@pytest.mark.parametrize("op_name", ld_operations) def test_gate_unitary_correct(op, op_name): """Test if lightning device correctly applies gates by reconstructing the unitary matrix and comparing to the expected version""" @@ -153,7 +172,7 @@ def output(input): assert np.allclose(unitary, unitary_expected) -@pytest.mark.parametrize("op_name", ld.operations) +@pytest.mark.parametrize("op_name", ld_operations) def test_gate_unitary_correct_lt(op, op_name): """Test if lightning device correctly applies gates by reconstructing the unitary matrix and comparing to the expected version""" @@ -187,7 +206,7 @@ def output(input): assert np.allclose(unitary, unitary_expected) -@pytest.mark.parametrize("op_name", ld.operations) +@pytest.mark.parametrize("op_name", ld_operations) def test_inverse_unitary_correct(op, op_name): """Test if lightning device correctly applies inverse gates by reconstructing the unitary matrix and comparing to the expected version"""