From 85cc93f7730703c99073b2620245ce609aa369f4 Mon Sep 17 00:00:00 2001 From: David Wierichs Date: Fri, 3 Jun 2022 10:24:32 +0200 Subject: [PATCH] Introduce device capability flag and default handler for parameter broadcasting (#2590) * introduce Operator.ndim_params, Operator.batch_size, QuantumTape.batch_size * linting * changelog * enable tf.function input_signature usage * black * test for unsilenced error * Apply suggestions from code review Co-authored-by: Josh Izaac * introduce device flag and batch_transform for unbroadcasting; use transform in device.batch_transform * black, [skip ci] * code review * string formatting [skip ci] * operation broadcasting interface tests * unbroadcast_expand * tests for expand function * tests * black * compatibility with TensorFlow 2.6 * builtins unstack * failing case coverage * stop using I in operation.py [skip ci] * Apply suggestions from code review Co-authored-by: Josh Izaac * review * Apply suggestions from code review Co-authored-by: Josh Izaac * review [skip ci] * move changelog section from "improvements" to "new features" * changelog * add missing files * namespace * linting variable names * pin protobuf<4.21.0 * docstring * unpin protobuf * Allow broadcasting in the numerical representations of standard operations (#2609) * commit old changes * intermed * clean up, move broadcast dimension first * update tests that manually set ndim_params for default ops * pin protobuf<4.21.0 * improve shape coersion order * changelog formatting * broadcasted pow tests * attribute test, ControlledQubitUnitary update * test kwargs attributes * Apply suggestions from code review Co-authored-by: Josh Izaac * changelog * review * remove prints * explicit attribute supports_broadcasting tests * tests disentangle * fix * PauliRot broadcasted identity compatible with TF * rename "batched" into "broadcasted" for uniform namespace * old TF version support in qubitunitary unitarity check * python3.7 support * Apply suggestions from code review Co-authored-by: Josh Izaac * linebreak Co-authored-by: Josh Izaac * black * black again * feature collision amend tests * black [skip ci] Co-authored-by: Josh Izaac --- doc/releases/changelog-dev.md | 134 ++- pennylane/_device.py | 42 +- pennylane/math/single_dispatch.py | 2 + pennylane/operation.py | 30 +- pennylane/ops/functions/matrix.py | 2 +- pennylane/ops/qubit/attributes.py | 32 + pennylane/ops/qubit/matrix_ops.py | 79 +- pennylane/ops/qubit/parametric_ops.py | 372 ++++++-- pennylane/transforms/__init__.py | 2 + pennylane/transforms/broadcast_expand.py | 104 ++ requirements-ci.txt | 2 +- tests/devices/test_default_gaussian.py | 1 + tests/devices/test_default_qubit.py | 69 +- tests/devices/test_default_qubit_autograd.py | 1 + tests/devices/test_default_qubit_jax.py | 1 + tests/devices/test_default_qubit_tf.py | 1 + tests/devices/test_default_qubit_torch.py | 1 + tests/ops/qubit/test_attributes.py | 244 +++++ tests/ops/qubit/test_matrix_ops.py | 298 +++++- tests/ops/qubit/test_parametric_ops.py | 945 +++++++++++++++++-- tests/qchem/test_observable_hf.py | 2 - tests/tape/test_tape.py | 32 +- tests/test_operation.py | 523 +++++++--- tests/test_qubit_device.py | 1 + tests/transforms/test_batch_partial.py | 18 +- tests/transforms/test_broadcast_expand.py | 203 ++++ tests/transforms/test_hamiltonian_expand.py | 2 +- 27 files changed, 2761 insertions(+), 382 deletions(-) create mode 100644 pennylane/transforms/broadcast_expand.py create mode 100644 tests/transforms/test_broadcast_expand.py diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 5751505469d..8865332822f 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -4,20 +4,119 @@

New features since last release

-* Operators have new attributes `ndim_params` and `batch_size`, and `QuantumTapes` have the new - attribute `batch_size`. - - `Operator.ndim_params` contains the expected number of dimensions per parameter of the operator, - - `Operator.batch_size` contains the size of an additional parameter broadcasting axis, if present, - - `QuantumTape.batch_size` contains the `batch_size` of its operations (see below). +* Parameter broadcasting within operations and tapes was introduced. [(#2575)](https://github.com/PennyLaneAI/pennylane/pull/2575) + [(#2590)](https://github.com/PennyLaneAI/pennylane/pull/2590) + [(#2609)](https://github.com/PennyLaneAI/pennylane/pull/2609) + + Parameter broadcasting refers to passing parameters with a (single) leading additional + dimension (compared to the expected parameter shape) to `Operator`'s. + Introducing this concept involves multiple changes: + + 1. New class attributes + - `Operator.ndim_params` can be specified by developers to provide the expected number of dimensions for each parameter + of an operator. + - `Operator.batch_size` returns the size of an additional parameter-broadcasting axis, + if present. + - `QuantumTape.batch_size` returns the `batch_size` of its operations (see logic below). + - `Device.capabilities()["supports_broadcasting"]` is a Boolean flag indicating whether a + device natively is able to apply broadcasted operators. + 2. New functionalities + - `Operator`s use their new `ndim_params` attribute to set their new attribute `batch_size` + at instantiation. `batch_size=None` corresponds to unbroadcasted operators. + - `QuantumTape`s automatically determine their new `batch_size` attribute from the + `batch_size`s of their operations. For this, all `Operators` in the tape must have the same + `batch_size` or `batch_size=None`. That is, mixing broadcasted and unbroadcasted `Operators` + is allowed, but mixing broadcasted `Operators` with differing `batch_size` is not, + similar to NumPy broadcasting. + - A new tape `batch_transform` called `broadcast_expand` was added. It transforms a single + tape with `batch_size!=None` (broadcasted) into multiple tapes with `batch_size=None` + (unbroadcasted) each. + - `Device`s natively can handle broadcasted `QuantumTape`s by using `broadcast_expand` if + the new flag `capabilities()["supports_broadcasting"]` is set to `False` (the default). + 3. Feature support + - Many parametrized operations now have the attribute `ndim_params` and + allow arguments with a broadcasting dimension in their numerical representations. + This includes all gates in `ops/qubit/parametric_ops` and `ops/qubit/matrix_ops`. + The broadcasted dimension is the first dimension in representations. + Note that the broadcasted parameter has to be passed as an `tensor` but not as a python + `list` or `tuple` for most operations. + + **Example** + + Instantiating a rotation gate with a one-dimensional array leads to a broadcasted `Operation`: - When providing an operator with the `ndim_params` attribute, it will - determine whether (and with which `batch_size`) its input parameter(s) - is/are broadcasted. - A `QuantumTape` can then infer from its operations whether it is batched. - For this, all `Operators` in the tape must have the same `batch_size` or `batch_size=None`. - That is, mixing broadcasted and unbroadcasted `Operators` is allowed, but mixing broadcasted - `Operators` with differing `batch_size` is not, similar to NumPy broadcasting. + ```pycon + >>> op = qml.RX(np.array([0.1, 0.2, 0.3], requires_grad=True), 0) + >>> op.batch_size + 3 + ``` + + It's matrix correspondingly is augmented by a leading dimension of size `batch_size`: + + ```pycon + >>> np.round(op.matrix(), 4) + tensor([[[0.9988+0.j , 0. -0.05j ], + [0. -0.05j , 0.9988+0.j ]], + [[0.995 +0.j , 0. -0.0998j], + [0. -0.0998j, 0.995 +0.j ]], + [[0.9888+0.j , 0. -0.1494j], + [0. -0.1494j, 0.9888+0.j ]]], requires_grad=True) + >>> op.matrix().shape + (3, 2, 2) + ``` + + A tape with such an operation will detect the `batch_size` and inherit it: + + ```pycon + >>> with qml.tape.QuantumTape() as tape: + >>> qml.apply(op) + >>> tape.batch_size + 3 + ``` + + A tape may contain broadcasted and unbroadcasted `Operation`s + + ```pycon + >>> with qml.tape.QuantumTape() as tape: + >>> qml.apply(op) + >>> qml.RY(1.9, 0) + >>> tape.batch_size + 3 + ``` + + but not `Operation`s with differing (non-`None`) `batch_size`s: + + ```pycon + >>> with qml.tape.QuantumTape() as tape: + >>> qml.apply(op) + >>> qml.RY(np.array([1.9, 2.4]), 0) + ValueError: The batch sizes of the tape operations do not match, they include 3 and 2. + ``` + + When creating a valid broadcasted tape, we can expand it into unbroadcasted tapes with + the new `broadcast_expand` transform, and execute the three tapes independently. + + ```pycon + >>> with qml.tape.QuantumTape() as tape: + >>> qml.apply(op) + >>> qml.RY(1.9, 0) + >>> qml.apply(op) + >>> qml.expval(qml.PauliZ(0)) + >>> tapes, fn = qml.transforms.broadcast_expand(tape) + >>> len(tapes) + 3 + >>> dev = qml.device("default.qubit", wires=1) + >>> fn(qml.execute(tapes, dev, None)) + array([-0.33003414, -0.34999899, -0.38238817]) + ``` + + However, devices will handle this automatically under the hood: + + ```pycon + >>> qml.execute([tape], dev, None)[0] + array([-0.33003414, -0.34999899, -0.38238817]) + ``` * Boolean mask indexing of the parameter-shift Hessian [(#2538)](https://github.com/PennyLaneAI/pennylane/pull/2538) @@ -133,11 +232,17 @@ for `qml.QueuingContext.update_info` in a variety of places. [(#2612)](https://github.com/PennyLaneAI/pennylane/pull/2612) -* `BasisEmbedding` can accept an int as argument instead of a list of bits (optionally). Example: `qml.BasisEmbedding(4, wires = range(4))` is now equivalent to `qml.BasisEmbedding([0,1,0,0], wires = range(4))` (because 4=0b100). +* `BasisEmbedding` can accept an int as argument instead of a list of bits (optionally). [(#2601)](https://github.com/PennyLaneAI/pennylane/pull/2601) + + Example: + + `qml.BasisEmbedding(4, wires = range(4))` is now equivalent to + `qml.BasisEmbedding([0,1,0,0], wires = range(4))` (because `4=0b100`). * Introduced a new `is_hermitian` property to determine if an operator can be used in a measurement process. [(#2629)](https://github.com/PennyLaneAI/pennylane/pull/2629) +

Breaking changes

* The `qml.queuing.Queue` class is now removed. @@ -179,7 +284,8 @@ as trainable do not have any impact on the QNode output. [(#2584)](https://github.com/PennyLaneAI/pennylane/pull/2584) -* `QNode`'s now can interpret variations on the interface name, like `"tensorflow"` or `"jax-jit"`, when requesting backpropagation. +* `QNode`'s now can interpret variations on the interface name, like `"tensorflow"` + or `"jax-jit"`, when requesting backpropagation. [(#2591)](https://github.com/PennyLaneAI/pennylane/pull/2591) * Fixed a bug for `diff_method="adjoint"` where incorrect gradients were diff --git a/pennylane/_device.py b/pennylane/_device.py index 36a0ce67f7d..6d291e96db8 100644 --- a/pennylane/_device.py +++ b/pennylane/_device.py @@ -108,7 +108,7 @@ class Device(abc.ABC): """ # pylint: disable=too-many-public-methods,too-many-instance-attributes - _capabilities = {"model": None} + _capabilities = {"model": None, "supports_broadcasting": False} """The capabilities dictionary stores the properties of a device. Devices can add their own custom properties and overwrite existing ones by overriding the ``capabilities()`` method.""" @@ -705,11 +705,6 @@ def batch_transform(self, circuit): the sequence of circuits to be executed, and a post-processing function to be applied to the list of evaluated circuit results. """ - - # If the observable contains a Hamiltonian and the device does not - # support Hamiltonians, or if the simulation uses finite shots, or - # if the Hamiltonian explicitly specifies an observable grouping, - # split tape into multiple tapes of diagonalizable known observables. supports_hamiltonian = self.supports_observable("Hamiltonian") finite_shots = self.shots is not None grouping_known = all( @@ -723,15 +718,18 @@ def batch_transform(self, circuit): return_types = [m.return_type for m in circuit.observables] if hamiltonian_in_obs and ((not supports_hamiltonian or finite_shots) or grouping_known): + # If the observable contains a Hamiltonian and the device does not + # support Hamiltonians, or if the simulation uses finite shots, or + # if the Hamiltonian explicitly specifies an observable grouping, + # split tape into multiple tapes of diagonalizable known observables. try: - return qml.transforms.hamiltonian_expand(circuit, group=False) + circuits, hamiltonian_fn = qml.transforms.hamiltonian_expand(circuit, group=False) except ValueError as e: raise ValueError( "Can only return the expectation of a single Hamiltonian observable" ) from e - - if ( + elif ( len(circuit._obs_sharing_wires) > 0 and not hamiltonian_in_obs and not qml.measurements.Sample in return_types @@ -739,10 +737,30 @@ def batch_transform(self, circuit): ): # Check for case of non-commuting terms and that there are no Hamiltonians # TODO: allow for Hamiltonians in list of observables as well. - return qml.transforms.split_non_commuting(circuit) + circuits, hamiltonian_fn = qml.transforms.split_non_commuting(circuit) + + else: + # otherwise, return the output of an identity transform + circuits, hamiltonian_fn = [circuit], lambda res: res[0] + + # Check whether the circuit was broadcasted (then the Hamiltonian-expanded + # ones will be as well) and whether broadcasting is supported + if circuit.batch_size is None or self.capabilities().get("supports_broadcasting"): + # If the circuit wasn't broadcasted or broadcasting is supported, no action required + return circuits, hamiltonian_fn + + # Expand each of the broadcasted Hamiltonian-expanded circuits + expanded_tapes, expanded_fn = qml.transforms.map_batch_transform( + qml.transforms.broadcast_expand, circuits + ) + + # Chain the postprocessing functions of the broadcasted-tape expansions and the Hamiltonian + # expansion. Note that the application order is reversed compared to the expansion order, + # i.e. while we first applied `hamiltonian_expand` to the tape, we need to process the + # results from the broadcast expansion first. + total_processing = lambda results: hamiltonian_fn(expanded_fn(results)) - # otherwise, return an identity transform - return [circuit], lambda res: res[0] + return expanded_tapes, total_processing @property def op_queue(self): diff --git a/pennylane/math/single_dispatch.py b/pennylane/math/single_dispatch.py index 312db0be1af..59bbc987019 100644 --- a/pennylane/math/single_dispatch.py +++ b/pennylane/math/single_dispatch.py @@ -42,11 +42,13 @@ def _i(name): ar.register_function("builtins", "block_diag", lambda x: _scipy_block_diag(*x)) ar.register_function("numpy", "gather", lambda x, indices: x[np.array(indices)]) ar.register_function("numpy", "unstack", list) +ar.register_function("builtins", "unstack", list) # the following is required to ensure that SciPy sparse Hamiltonians passed to # qml.SparseHamiltonian are not automatically 'unwrapped' to dense NumPy arrays. ar.register_function("scipy", "to_numpy", lambda x: x) ar.register_function("scipy", "shape", np.shape) +ar.register_function("scipy", "ndim", np.ndim) def _scatter_element_add_numpy(tensor, index, value): diff --git a/pennylane/operation.py b/pennylane/operation.py index 862f10a407e..73f2009a6f8 100644 --- a/pennylane/operation.py +++ b/pennylane/operation.py @@ -190,19 +190,23 @@ def expand_matrix(base_matrix, wires, wire_order): # TODO[Maria]: In future we should consider making ``utils.expand`` differentiable and calling it here. wire_order = Wires(wire_order) n = len(wires) - interface = qml.math._multi_dispatch(base_matrix) # pylint: disable=protected-access + shape = qml.math.shape(base_matrix) + batch_dim = shape[0] if len(shape) == 3 else None + interface = qml.math.get_interface(base_matrix) # pylint: disable=protected-access # operator's wire positions relative to wire ordering op_wire_pos = wire_order.indices(wires) identity = qml.math.reshape( - qml.math.eye(2 ** len(wire_order), like=interface), [2] * len(wire_order) * 2 + qml.math.eye(2 ** len(wire_order), like=interface), [2] * (len(wire_order) * 2) ) - axes = (list(range(n, 2 * n)), op_wire_pos) + # The first axis entries are range(n, 2n) for batch_dim=None and range(n+1, 2n+1) else + axes = (list(range(-n, 0)), op_wire_pos) # reshape op.matrix() op_matrix_interface = qml.math.convert_like(base_matrix, identity) - mat_op_reshaped = qml.math.reshape(op_matrix_interface, [2] * n * 2) + shape = [batch_dim] + [2] * (n * 2) if batch_dim else [2] * (n * 2) + mat_op_reshaped = qml.math.reshape(op_matrix_interface, shape) mat_tensordot = qml.math.tensordot( mat_op_reshaped, qml.math.cast_like(identity, mat_op_reshaped), axes ) @@ -210,9 +214,14 @@ def expand_matrix(base_matrix, wires, wire_order): unused_idxs = [idx for idx in range(len(wire_order)) if idx not in op_wire_pos] # permute matrix axes to match wire ordering perm = op_wire_pos + unused_idxs - mat = qml.math.moveaxis(mat_tensordot, wire_order.indices(wire_order), perm) + sources = wire_order.indices(wire_order) + if batch_dim: + perm = [p + 1 for p in perm] + sources = [s + 1 for s in sources] - mat = qml.math.reshape(mat, (2 ** len(wire_order), 2 ** len(wire_order))) + mat = qml.math.moveaxis(mat_tensordot, sources, perm) + shape = [batch_dim] + [2 ** len(wire_order)] * 2 if batch_dim else [2 ** len(wire_order)] * 2 + mat = qml.math.reshape(mat, shape) return mat @@ -804,7 +813,9 @@ def label(self, decimals=None, base_label=None, cache=None): if len(qml.math.shape(params[0])) != 0: # assume that if the first parameter is matrix-valued, there is only a single parameter - # this holds true for all current operations and templates + # this holds true for all current operations and templates unless parameter broadcasting + # is used + # TODO[dwierichs]: Implement a proper label for broadcasted operators if ( cache is None or not isinstance(cache.get("matrices", None), list) @@ -926,7 +937,8 @@ def _check_batching(self, params): ] if not qml.math.allclose(first_dims, first_dims[0]): raise ValueError( - f"Batching was attempted but the batched dimensions do not match: {first_dims}." + "Broadcasting was attempted but the broadcasted dimensions " + f"do not match: {first_dims}." ) self._batch_size = first_dims[0] @@ -1409,7 +1421,7 @@ def matrix(self, wire_order=None): canonical_matrix = self.compute_matrix(*self.parameters, **self.hyperparameters) if self.inverse: - canonical_matrix = qml.math.conj(qml.math.T(canonical_matrix)) + canonical_matrix = qml.math.conj(qml.math.moveaxis(canonical_matrix, -2, -1)) if wire_order is None or self.wires == Wires(wire_order): return canonical_matrix diff --git a/pennylane/ops/functions/matrix.py b/pennylane/ops/functions/matrix.py index 4924c7b0839..8a8ef680775 100644 --- a/pennylane/ops/functions/matrix.py +++ b/pennylane/ops/functions/matrix.py @@ -141,6 +141,6 @@ def _matrix(tape, wire_order=None): for op in tape.operations: U = matrix(op, wire_order=wire_order) - unitary_matrix = qml.math.dot(U, unitary_matrix) + unitary_matrix = qml.math.tensordot(U, unitary_matrix, axes=[[-1], [-2]]) return unitary_matrix diff --git a/pennylane/ops/qubit/attributes.py b/pennylane/ops/qubit/attributes.py index 62ef3c86cad..322b7455ddb 100644 --- a/pennylane/ops/qubit/attributes.py +++ b/pennylane/ops/qubit/attributes.py @@ -199,3 +199,35 @@ def __contains__(self, obj): representation using ``np.linalg.eigvals``, which fails for some tensor types that the matrix may be cast in on backpropagation devices. """ + +supports_broadcasting = Attribute( + [ + "QubitUnitary", + "ControlledQubitUnitary", + "DiagonalQubitUnitary", + "RX", + "RY", + "RZ", + "PhaseShift", + "ControlledPhaseShift", + "Rot", + "MultiRZ", + "PauliRot", + "CRX", + "CRY", + "CRZ", + "CRot", + "U1", + "U2", + "U3", + "IsingXX", + "IsingYY", + "IsingZZ", + ] +) +"""Attribute: Operations that support parameter broadcasting. + +For such operations, the input parameters are allowed to have a single leading additional +broadcasting dimension, creating the operation with a ``batch_size`` and leading to +broadcasted tapes when used in a ``QuantumTape``. +""" diff --git a/pennylane/ops/qubit/matrix_ops.py b/pennylane/ops/qubit/matrix_ops.py index b67ed245541..c492b123317 100644 --- a/pennylane/ops/qubit/matrix_ops.py +++ b/pennylane/ops/qubit/matrix_ops.py @@ -32,6 +32,7 @@ class QubitUnitary(Operation): * Number of wires: Any (the operation can act on any number of wires) * Number of parameters: 1 + * Number of dimensions per parameter: (2,) * Gradient recipe: None Args: @@ -55,6 +56,9 @@ class QubitUnitary(Operation): num_params = 1 """int: Number of trainable parameters that the operator depends on.""" + ndim_params = (2,) + """tuple[int]: Number of dimensions per trainable parameter that the operator depends on.""" + grad_method = None """Gradient computation method.""" @@ -65,20 +69,25 @@ def __init__(self, *params, wires, do_queue=True): # of wires fits the dimensions of the matrix if not isinstance(self, ControlledQubitUnitary): U = params[0] + U_shape = qml.math.shape(U) dim = 2 ** len(wires) - if qml.math.shape(U) != (dim, dim): + if not (len(U_shape) in {2, 3} and U_shape[-2:] == (dim, dim)): raise ValueError( - f"Input unitary must be of shape {(dim, dim)} to act on {len(wires)} wires." + f"Input unitary must be of shape {(dim, dim)} or (batch_size, {dim}, {dim}) " + f"to act on {len(wires)} wires." ) # Check for unitarity; due to variable precision across the different ML frameworks, # here we issue a warning to check the operation, instead of raising an error outright. - if not qml.math.is_abstract(U) and not qml.math.allclose( - qml.math.dot(U, qml.math.T(qml.math.conj(U))), - qml.math.eye(qml.math.shape(U)[0]), - atol=1e-6, + if not ( + qml.math.is_abstract(U) + or qml.math.allclose( + qml.math.einsum("...ij,...kj->...ik", U, qml.math.conj(U)), + qml.math.eye(dim), + atol=1e-6, + ) ): warnings.warn( f"Operator {U}\n may not be unitary." @@ -142,16 +151,24 @@ def compute_decomposition(U, wires): """ # Decomposes arbitrary single-qubit unitaries as Rot gates (RZ - RY - RZ format), # or a single RZ for diagonal matrices. - if qml.math.shape(U) == (2, 2): + shape = qml.math.shape(U) + if shape == (2, 2): return qml.transforms.decompositions.zyz_decomposition(U, Wires(wires)[0]) - if qml.math.shape(U) == (4, 4): + if shape == (4, 4): return qml.transforms.two_qubit_decomposition(U, Wires(wires)) + # TODO[dwierichs]: Implement decomposition of broadcasted unitary + if len(shape) == 3: + raise DecompositionUndefinedError( + "The decomposition of QubitUnitary does not support broadcasting." + ) + return super(QubitUnitary, QubitUnitary).compute_decomposition(U, wires=wires) def adjoint(self): - return QubitUnitary(qml.math.T(qml.math.conj(self.matrix())), wires=self.wires) + U = self.matrix() + return QubitUnitary(qml.math.moveaxis(qml.math.conj(U), -2, -1), wires=self.wires) def pow(self, z): if isinstance(z, int): @@ -179,6 +196,7 @@ class ControlledQubitUnitary(QubitUnitary): * Number of wires: Any (the operation can act on any number of wires) * Number of parameters: 1 + * Number of dimensions per parameter: (2,) * Gradient recipe: None Args: @@ -215,6 +233,9 @@ class ControlledQubitUnitary(QubitUnitary): num_params = 1 """int: Number of trainable parameters that the operator depends on.""" + ndim_params = (2,) + """tuple[int]: Number of dimensions per trainable parameter that the operator depends on.""" + grad_method = None """Gradient computation method.""" @@ -281,8 +302,12 @@ def compute_matrix( [ 0. +0.j 0. +0.j -0.31594146+0.j 0.94877869+0.j]] """ target_dim = 2 ** len(u_wires) - if len(U) != target_dim: - raise ValueError(f"Input unitary must be of shape {(target_dim, target_dim)}") + shape = qml.math.shape(U) + if not (len(shape) in {2, 3} and shape[-2:] == (target_dim, target_dim)): + raise ValueError( + f"Input unitary must be of shape {(target_dim, target_dim)} or " + f"(batch_size, {target_dim}, {target_dim})." + ) # A multi-controlled operation is a block-diagonal matrix partitioned into # blocks where the operation being applied sits in the block positioned at @@ -303,19 +328,21 @@ def compute_matrix( raise ValueError("Length of control bit string must equal number of control wires.") # Make sure all values are either 0 or 1 - if any(x not in ["0", "1"] for x in control_values): + if not set(control_values).issubset({"0", "1"}): raise ValueError("String of control values can contain only '0' or '1'.") control_int = int(control_values, 2) else: raise ValueError("Alternative control values must be passed as a binary string.") - padding_left = control_int * len(U) - padding_right = 2 ** len(total_wires) - len(U) - padding_left + padding_left = control_int * target_dim + padding_right = 2 ** len(total_wires) - target_dim - padding_left interface = qml.math.get_interface(U) left_pad = qml.math.cast_like(qml.math.eye(padding_left, like=interface), 1j) right_pad = qml.math.cast_like(qml.math.eye(padding_right, like=interface), 1j) + if len(qml.math.shape(U)) == 3: + return qml.math.stack([qml.math.block_diag([left_pad, _U, right_pad]) for _U in U]) return qml.math.block_diag([left_pad, U, right_pad]) @property @@ -348,6 +375,7 @@ class DiagonalQubitUnitary(Operation): * Number of wires: Any (the operation can act on any number of wires) * Number of parameters: 1 + * Number of dimensions per parameter: (1,) * Gradient recipe: None Args: @@ -360,6 +388,9 @@ class DiagonalQubitUnitary(Operation): num_params = 1 """int: Number of trainable parameters that the operator depends on.""" + ndim_params = (1,) + """tuple[int]: Number of dimensions per trainable parameter that the operator depends on.""" + grad_method = None """Gradient computation method.""" @@ -389,6 +420,10 @@ def compute_matrix(D): # pylint: disable=arguments-differ if not qml.math.allclose(D * qml.math.conj(D), qml.math.ones_like(D)): raise ValueError("Operator must be unitary.") + # The diagonal is supposed to have one-dimension. If it is broadcasted, it has two + if qml.math.ndim(D) == 2: + return qml.math.stack([qml.math.diag(_D) for _D in D]) + return qml.math.diag(D) @staticmethod @@ -419,8 +454,9 @@ def compute_eigvals(D): # pylint: disable=arguments-differ """ D = qml.math.asarray(D) - if not qml.math.is_abstract(D) and not qml.math.allclose( - D * qml.math.conj(D), qml.math.ones_like(D) + if not ( + qml.math.is_abstract(D) + or qml.math.allclose(D * qml.math.conj(D), qml.math.ones_like(D)) ): raise ValueError("Operator must be unitary.") @@ -450,20 +486,25 @@ def compute_decomposition(D, wires): [QubitUnitary(array([[1, 0], [0, 1]]), wires=[0])] """ - return [QubitUnitary(qml.math.diag(D), wires=wires)] + return [QubitUnitary(DiagonalQubitUnitary.compute_matrix(D), wires=wires)] def adjoint(self): return DiagonalQubitUnitary(qml.math.conj(self.parameters[0]), wires=self.wires) def pow(self, z): if isinstance(self.data[0], list): - return [DiagonalQubitUnitary([(x + 0.0j) ** z for x in self.data[0]], wires=self.wires)] + if isinstance(self.data[0][0], list): + # Support broadcasted list + new_data = [[(el + 0j) ** z for el in x] for x in self.data[0]] + else: + new_data = [(x + 0.0j) ** z for x in self.data[0]] + return [DiagonalQubitUnitary(new_data, wires=self.wires)] casted_data = qml.math.cast(self.data[0], np.complex128) return [DiagonalQubitUnitary(casted_data**z, wires=self.wires)] def _controlled(self, control): DiagonalQubitUnitary( - qml.math.concatenate([np.ones_like(self.parameters[0]), self.parameters[0]]), + qml.math.hstack([np.ones_like(self.parameters[0]), self.parameters[0]]), wires=Wires(control) + self.wires, ) diff --git a/pennylane/ops/qubit/parametric_ops.py b/pennylane/ops/qubit/parametric_ops.py index 09c39fbd29b..1c28c1f1443 100644 --- a/pennylane/ops/qubit/parametric_ops.py +++ b/pennylane/ops/qubit/parametric_ops.py @@ -26,11 +26,14 @@ import pennylane as qml from pennylane.operation import AnyWires, Operation from pennylane.ops.qubit.non_parametric_ops import PauliX, PauliY, PauliZ, Hadamard -from pennylane.utils import expand, pauli_eigs +from pennylane.operation import expand_matrix +from pennylane.utils import pauli_eigs from pennylane.wires import Wires INV_SQRT2 = 1 / math.sqrt(2) +stack_last = functools.partial(qml.math.stack, axis=-1) + class RX(Operation): r""" @@ -45,6 +48,7 @@ class RX(Operation): * Number of wires: 1 * Number of parameters: 1 + * Number of dimensions per parameter: (0,) * Gradient recipe: :math:`\frac{d}{d\phi}f(R_x(\phi)) = \frac{1}{2}\left[f(R_x(\phi+\pi/2)) - f(R_x(\phi-\pi/2))\right]` where :math:`f` is an expectation value depending on :math:`R_x(\phi)`. @@ -59,6 +63,9 @@ class RX(Operation): num_params = 1 """int: Number of trainable parameters that the operator depends on.""" + ndim_params = (0,) + """tuple[int]: Number of dimensions per trainable parameter that the operator depends on.""" + basis = "X" grad_method = "A" parameter_frequencies = [(1,)] @@ -97,11 +104,10 @@ def compute_matrix(theta): # pylint: disable=arguments-differ c = qml.math.cast_like(c, 1j) s = qml.math.cast_like(s, 1j) + # The following avoids casting an imaginary quantity to reals when backpropagating + c = (1 + 0j) * c js = -1j * s - - return qml.math.diag([c, c]) + qml.math.stack( - [qml.math.stack([0, js]), qml.math.stack([js, 0])] - ) + return qml.math.stack([stack_last([c, js]), stack_last([js, c])], axis=-2) def adjoint(self): return RX(-self.data[0], wires=self.wires) @@ -114,7 +120,8 @@ def _controlled(self, wire): def single_qubit_rot_angles(self): # RX(\theta) = RZ(-\pi/2) RY(\theta) RZ(\pi/2) - return [np.pi / 2, self.data[0], -np.pi / 2] + pi_half = qml.math.ones_like(self.data[0]) * (np.pi / 2) + return [pi_half, self.data[0], -pi_half] class RY(Operation): @@ -130,6 +137,7 @@ class RY(Operation): * Number of wires: 1 * Number of parameters: 1 + * Number of dimensions per parameter: (0,) * Gradient recipe: :math:`\frac{d}{d\phi}f(R_y(\phi)) = \frac{1}{2}\left[f(R_y(\phi+\pi/2)) - f(R_y(\phi-\pi/2))\right]` where :math:`f` is an expectation value depending on :math:`R_y(\phi)`. @@ -144,6 +152,9 @@ class RY(Operation): num_params = 1 """int: Number of trainable parameters that the operator depends on.""" + ndim_params = (0,) + """tuple[int]: Number of dimensions per trainable parameter that the operator depends on.""" + basis = "Y" grad_method = "A" parameter_frequencies = [(1,)] @@ -179,10 +190,13 @@ def compute_matrix(theta): # pylint: disable=arguments-differ c = qml.math.cos(theta / 2) s = qml.math.sin(theta / 2) - - return qml.math.diag([c, c]) + qml.math.stack( - [qml.math.stack([0, -s]), qml.math.stack([s, 0])] - ) + if qml.math.get_interface(theta) == "tensorflow": + c = qml.math.cast_like(c, 1j) + s = qml.math.cast_like(s, 1j) + # The following avoids casting an imaginary quantity to reals when backpropagating + c = (1 + 0j) * c + s = (1 + 0j) * s + return qml.math.stack([stack_last([c, -s]), stack_last([s, c])], axis=-2) def adjoint(self): return RY(-self.data[0], wires=self.wires) @@ -211,6 +225,7 @@ class RZ(Operation): * Number of wires: 1 * Number of parameters: 1 + * Number of dimensions per parameter: (0,) * Gradient recipe: :math:`\frac{d}{d\phi}f(R_z(\phi)) = \frac{1}{2}\left[f(R_z(\phi+\pi/2)) - f(R_z(\phi-\pi/2))\right]` where :math:`f` is an expectation value depending on :math:`R_z(\phi)`. @@ -225,6 +240,9 @@ class RZ(Operation): num_params = 1 """int: Number of trainable parameters that the operator depends on.""" + ndim_params = (0,) + """tuple[int]: Number of dimensions per trainable parameter that the operator depends on.""" + basis = "Z" grad_method = "A" parameter_frequencies = [(1,)] @@ -260,8 +278,9 @@ def compute_matrix(theta): # pylint: disable=arguments-differ theta = qml.math.cast_like(theta, 1j) p = qml.math.exp(-0.5j * theta) + z = qml.math.zeros_like(p) - return qml.math.diag([p, qml.math.conj(p)]) + return qml.math.stack([stack_last([p, z]), stack_last([z, qml.math.conj(p)])], axis=-2) @staticmethod def compute_eigvals(theta): # pylint: disable=arguments-differ @@ -296,7 +315,7 @@ def compute_eigvals(theta): # pylint: disable=arguments-differ p = qml.math.exp(-0.5j * theta) - return qml.math.stack([p, qml.math.conj(p)]) + return stack_last([p, qml.math.conj(p)]) def adjoint(self): return RZ(-self.data[0], wires=self.wires) @@ -325,6 +344,7 @@ class PhaseShift(Operation): * Number of wires: 1 * Number of parameters: 1 + * Number of dimensions per parameter: (0,) * Gradient recipe: :math:`\frac{d}{d\phi}f(R_\phi(\phi)) = \frac{1}{2}\left[f(R_\phi(\phi+\pi/2)) - f(R_\phi(\phi-\pi/2))\right]` where :math:`f` is an expectation value depending on :math:`R_{\phi}(\phi)`. @@ -339,6 +359,9 @@ class PhaseShift(Operation): num_params = 1 """int: Number of trainable parameters that the operator depends on.""" + ndim_params = (0,) + """tuple[int]: Number of dimensions per trainable parameter that the operator depends on.""" + basis = "Z" grad_method = "A" parameter_frequencies = [(1,)] @@ -377,9 +400,10 @@ def compute_matrix(phi): # pylint: disable=arguments-differ if qml.math.get_interface(phi) == "tensorflow": phi = qml.math.cast_like(phi, 1j) - exp_part = qml.math.exp(1j * phi) + p = qml.math.exp(1j * phi) + z = qml.math.zeros_like(p) - return qml.math.diag([1, exp_part]) + return qml.math.stack([stack_last([qml.math.ones_like(p), z]), stack_last([z, p])], axis=-2) @staticmethod def compute_eigvals(phi): # pylint: disable=arguments-differ @@ -411,9 +435,9 @@ def compute_eigvals(phi): # pylint: disable=arguments-differ if qml.math.get_interface(phi) == "tensorflow": phi = qml.math.cast_like(phi, 1j) - exp_part = qml.math.exp(1j * phi) + p = qml.math.exp(1j * phi) - return qml.math.stack([1, exp_part]) + return stack_last([qml.math.ones_like(p), p]) @staticmethod def compute_decomposition(phi, wires): @@ -470,6 +494,7 @@ class ControlledPhaseShift(Operation): * Number of wires: 2 * Number of parameters: 1 + * Number of dimensions per parameter: (0,) * Gradient recipe: :math:`\frac{d}{d\phi}f(CR_\phi(\phi)) = \frac{1}{2}\left[f(CR_\phi(\phi+\pi/2)) - f(CR_\phi(\phi-\pi/2))\right]` where :math:`f` is an expectation value depending on :math:`CR_{\phi}(\phi)`. @@ -484,6 +509,9 @@ class ControlledPhaseShift(Operation): num_params = 1 """int: Number of trainable parameters that the operator depends on.""" + ndim_params = (0,) + """tuple[int]: Number of dimensions per trainable parameter that the operator depends on.""" + basis = "Z" grad_method = "A" parameter_frequencies = [(1,)] @@ -525,6 +553,18 @@ def compute_matrix(phi): # pylint: disable=arguments-differ exp_part = qml.math.exp(1j * phi) + if qml.math.ndim(phi) > 0: + ones = qml.math.ones_like(exp_part) + zeros = qml.math.zeros_like(exp_part) + matrix = [ + [ones, zeros, zeros, zeros], + [zeros, ones, zeros, zeros], + [zeros, zeros, ones, zeros], + [zeros, zeros, zeros, exp_part], + ] + + return qml.math.stack([stack_last(row) for row in matrix], axis=-2) + return qml.math.diag([1, 1, 1, exp_part]) @staticmethod @@ -558,8 +598,8 @@ def compute_eigvals(phi): # pylint: disable=arguments-differ phi = qml.math.cast_like(phi, 1j) exp_part = qml.math.exp(1j * phi) - - return qml.math.stack([1, 1, 1, exp_part]) + ones = qml.math.ones_like(exp_part) + return stack_last([ones, ones, ones, exp_part]) @staticmethod def compute_decomposition(phi, wires): @@ -626,6 +666,7 @@ class Rot(Operation): * Number of wires: 1 * Number of parameters: 3 + * Number of dimensions per parameter: (0, 0, 0) * Gradient recipe: :math:`\frac{d}{d\phi}f(R(\phi, \theta, \omega)) = \frac{1}{2}\left[f(R(\phi+\pi/2, \theta, \omega)) - f(R(\phi-\pi/2, \theta, \omega))\right]` where :math:`f` is an expectation value depending on :math:`R(\phi, \theta, \omega)`. This gradient recipe applies for each angle argument :math:`\{\phi, \theta, \omega\}`. @@ -648,6 +689,9 @@ class Rot(Operation): num_params = 3 """int: Number of trainable parameters that the operator depends on.""" + ndim_params = (0, 0, 0) + """tuple[int]: Number of dimensions per trainable parameter that the operator depends on.""" + grad_method = "A" parameter_frequencies = [(1,), (1,), (1,)] @@ -694,6 +738,11 @@ def compute_matrix(phi, theta, omega): # pylint: disable=arguments-differ c = qml.math.cast_like(qml.math.asarray(c, like=interface), 1j) s = qml.math.cast_like(qml.math.asarray(s, like=interface), 1j) + # The following variable is used to assert the all terms to be stacked have same shape + one = qml.math.ones_like(phi) * qml.math.ones_like(omega) + c = c * one + s = s * one + mat = [ [ qml.math.exp(-0.5j * (phi + omega)) * c, @@ -705,7 +754,7 @@ def compute_matrix(phi, theta, omega): # pylint: disable=arguments-differ ], ] - return qml.math.stack([qml.math.stack(row) for row in mat]) + return qml.math.stack([stack_last(row) for row in mat], axis=-2) @staticmethod def compute_decomposition(phi, theta, omega, wires): @@ -761,6 +810,7 @@ class MultiRZ(Operation): * Number of wires: Any * Number of parameters: 1 + * Number of dimensions per parameter: (0,) * Gradient recipe: :math:`\frac{d}{d\theta}f(MultiRZ(\theta)) = \frac{1}{2}\left[f(MultiRZ(\theta +\pi/2)) - f(MultiRZ(\theta-\pi/2))\right]` where :math:`f` is an expectation value depending on :math:`MultiRZ(\theta)`. @@ -780,6 +830,9 @@ class MultiRZ(Operation): num_params = 1 """int: Number of trainable parameters that the operator depends on.""" + ndim_params = (0,) + """tuple[int]: Number of dimensions per trainable parameter that the operator depends on.""" + grad_method = "A" parameter_frequencies = [(1,)] @@ -818,7 +871,11 @@ def compute_matrix(theta, num_wires): # pylint: disable=arguments-differ theta = qml.math.cast_like(theta, 1j) eigs = qml.math.cast_like(eigs, 1j) - eigvals = qml.math.exp(-1j * theta / 2 * eigs) + if qml.math.ndim(theta) > 0: + eigvals = [qml.math.exp(-0.5j * t * eigs) for t in theta] + return qml.math.stack([qml.math.diag(eig) for eig in eigvals]) + + eigvals = qml.math.exp(-0.5j * theta * eigs) return qml.math.diag(eigvals) def generator(self): @@ -859,7 +916,10 @@ def compute_eigvals(theta, num_wires): # pylint: disable=arguments-differ theta = qml.math.cast_like(theta, 1j) eigs = qml.math.cast_like(eigs, 1j) - return qml.math.exp(-1j * theta / 2 * eigs) + if qml.math.ndim(theta) > 0: + return qml.math.exp(qml.math.tensordot(-0.5j * theta, eigs, axes=0)) + + return qml.math.exp(-0.5j * theta * eigs) @staticmethod def compute_decomposition( @@ -910,6 +970,7 @@ class PauliRot(Operation): * Number of wires: Any * Number of parameters: 1 + * Number of dimensions per parameter: (0,) * Gradient recipe: :math:`\frac{d}{d\theta}f(RP(\theta)) = \frac{1}{2}\left[f(RP(\theta +\pi/2)) - f(RP(\theta-\pi/2))\right]` where :math:`f` is an expectation value depending on :math:`RP(\theta)`. @@ -941,6 +1002,9 @@ class PauliRot(Operation): num_params = 1 """int: Number of trainable parameters that the operator depends on.""" + ndim_params = (0,) + """tuple[int]: Number of dimensions per trainable parameter that the operator depends on.""" + do_check_domain = False grad_method = "A" parameter_frequencies = [(1,)] @@ -967,7 +1031,8 @@ def __init__(self, theta, pauli_word, wires=None, do_queue=True, id=None): if not len(pauli_word) == num_wires: raise ValueError( - f"The given Pauli word has length {len(pauli_word)}, length {num_wires} was expected for wires {wires}" + f"The given Pauli word has length {len(pauli_word)}, length " + f"{num_wires} was expected for wires {wires}" ) def label(self, decimals=None, base_label=None, cache=None): @@ -1000,7 +1065,8 @@ def label(self, decimals=None, base_label=None, cache=None): if self.inverse: op_label += "⁻¹" - if decimals is not None: + # TODO[dwierichs]: Implement a proper label for parameter-broadcasted operators + if decimals is not None and self.batch_size is None: param_string = f"\n({qml.math.asarray(self.parameters[0]):.{decimals}f})" op_label += param_string @@ -1016,7 +1082,7 @@ def _check_pauli_word(pauli_word): Returns: bool: Whether the Pauli word has correct structure. """ - return all(pauli in PauliRot._ALLOWED_CHARACTERS for pauli in pauli_word) + return all(pauli in PauliRot._ALLOWED_CHARACTERS for pauli in set(pauli_word)) @staticmethod def compute_matrix(theta, pauli_word): # pylint: disable=arguments-differ @@ -1053,17 +1119,17 @@ def compute_matrix(theta, pauli_word): # pylint: disable=arguments-differ theta = qml.math.cast_like(theta, 1j) # Simplest case is if the Pauli is the identity matrix - if pauli_word == "I" * len(pauli_word): - - exp = qml.math.exp(-1j * theta / 2) - iden = qml.math.eye(2 ** len(pauli_word)) - if interface == "torch": - # Use convert_like to ensure that the tensor is put on the correct - # Torch device - iden = qml.math.convert_like(iden, theta) + if set(pauli_word) == {"I"}: + + exp = qml.math.exp(-0.5j * theta) + iden = qml.math.eye(2 ** len(pauli_word), like=theta) + if qml.math.get_interface(theta) == "tensorflow": + iden = qml.math.cast_like(iden, 1j) + + if qml.math.ndim(theta) == 0: return exp * iden - return qml.math.array(exp * iden, like=interface) + return qml.math.stack([e * iden for e in exp]) # We first generate the matrix excluding the identity parts and expand it afterwards. # To this end, we have to store on which wires the non-identity parts act @@ -1078,11 +1144,15 @@ def compute_matrix(theta, pauli_word): # pylint: disable=arguments-differ qml.math.kron, [PauliRot._PAULI_CONJUGATION_MATRICES[gate] for gate in non_identity_gates], ) - - return expand( - qml.math.dot( + if interface == "tensorflow": + conjugation_matrix = qml.math.cast_like(conjugation_matrix, 1j) + # Note: we use einsum with reverse arguments here because it is not multi-dispatched + # and the tensordot containing multi_Z_rot_matrix should decide about the interface + return expand_matrix( + qml.math.einsum( + "...jk,ij->...ik", + qml.math.tensordot(multi_Z_rot_matrix, conjugation_matrix, axes=[[-1], [0]]), qml.math.conj(conjugation_matrix), - qml.math.dot(multi_Z_rot_matrix, conjugation_matrix), ), non_identity_wires, list(range(len(pauli_word))), @@ -1121,8 +1191,16 @@ def compute_eigvals(theta, pauli_word): # pylint: disable=arguments-differ theta = qml.math.cast_like(theta, 1j) # Identity must be treated specially because its eigenvalues are all the same - if pauli_word == "I" * len(pauli_word): - return qml.math.exp(-1j * theta / 2) * qml.math.ones(2 ** len(pauli_word)) + if set(pauli_word) == {"I"}: + exp = qml.math.exp(-0.5j * theta) + ones = qml.math.ones(2 ** len(pauli_word), like=theta) + if qml.math.get_interface(theta) == "tensorflow": + ones = qml.math.cast_like(ones, 1j) + + if qml.math.ndim(theta) == 0: + return exp * ones + + return qml.math.tensordot(exp, ones, axes=0) return MultiRZ.compute_eigvals(theta, len(pauli_word)) @@ -1157,7 +1235,7 @@ def compute_decomposition(theta, wires, pauli_word): wires = [wires] # Check for identity and do nothing - if pauli_word == "I" * len(wires): + if set(pauli_word) == {"I"}: return [] active_wires, active_gates = zip( @@ -1207,6 +1285,7 @@ class CRX(Operation): * Number of wires: 2 * Number of parameters: 1 + * Number of dimensions per parameter: (0,) * Gradient recipe: The controlled-RX operator satisfies a four-term parameter-shift rule (see Appendix F, https://doi.org/10.1088/1367-2630/ac2cb3): @@ -1231,6 +1310,9 @@ class CRX(Operation): num_params = 1 """int: Number of trainable parameters that the operator depends on.""" + ndim_params = (0,) + """tuple[int]: Number of dimensions per trainable parameter that the operator depends on.""" + basis = "X" grad_method = "A" parameter_frequencies = [(0.5, 1.0)] @@ -1272,24 +1354,23 @@ def compute_matrix(theta): # pylint: disable=arguments-differ c = qml.math.cos(theta / 2) s = qml.math.sin(theta / 2) - if interface == "torch": - # Use convert_like to ensure that the tensor is put on the correct - # Torch device - z = qml.math.convert_like(qml.math.zeros([4]), theta) - else: - z = qml.math.zeros([4], like=interface) - if interface == "tensorflow": c = qml.math.cast_like(c, 1j) s = qml.math.cast_like(s, 1j) - z = qml.math.cast_like(z, 1j) + # The following avoids casting an imaginary quantity to reals when backpropagating + c = (1 + 0j) * c js = -1j * s + ones = qml.math.ones_like(js) + zeros = qml.math.zeros_like(js) + matrix = [ + [ones, zeros, zeros, zeros], + [zeros, ones, zeros, zeros], + [zeros, zeros, c, js], + [zeros, zeros, js, c], + ] - mat = qml.math.diag([1, 1, c, c]) - return mat + qml.math.stack( - [z, z, qml.math.stack([0, 0, 0, js]), qml.math.stack([0, 0, js, 0])] - ) + return qml.math.stack([stack_last(row) for row in matrix], axis=-2) @staticmethod def compute_decomposition(phi, wires): @@ -1318,13 +1399,14 @@ def compute_decomposition(phi, wires): RZ(-1.5707963267948966, wires=[1])] """ + pi_half = qml.math.ones_like(phi) * (np.pi / 2) decomp_ops = [ - RZ(np.pi / 2, wires=wires[1]), + RZ(pi_half, wires=wires[1]), RY(phi / 2, wires=wires[1]), qml.CNOT(wires=wires), RY(-phi / 2, wires=wires[1]), qml.CNOT(wires=wires), - RZ(-np.pi / 2, wires=wires[1]), + RZ(-pi_half, wires=wires[1]), ] return decomp_ops @@ -1359,6 +1441,7 @@ class CRY(Operation): * Number of wires: 2 * Number of parameters: 1 + * Number of dimensions per parameter: (0,) * Gradient recipe: The controlled-RY operator satisfies a four-term parameter-shift rule (see Appendix F, https://doi.org/10.1088/1367-2630/ac2cb3): @@ -1383,6 +1466,9 @@ class CRY(Operation): num_params = 1 """int: Number of trainable parameters that the operator depends on.""" + ndim_params = (0,) + """tuple[int]: Number of dimensions per trainable parameter that the operator depends on.""" + basis = "Y" grad_method = "A" parameter_frequencies = [(0.5, 1.0)] @@ -1425,17 +1511,23 @@ def compute_matrix(theta): # pylint: disable=arguments-differ c = qml.math.cos(theta / 2) s = qml.math.sin(theta / 2) - if interface == "torch": - # Use convert_like to ensure that the tensor is put on the correct - # Torch device - z = qml.math.convert_like(qml.math.zeros([4]), theta) - else: - z = qml.math.zeros([4], like=interface) + if interface == "tensorflow": + c = qml.math.cast_like(c, 1j) + s = qml.math.cast_like(s, 1j) - mat = qml.math.diag([1, 1, c, c]) - return mat + qml.math.stack( - [z, z, qml.math.stack([0, 0, 0, -s]), qml.math.stack([0, 0, s, 0])] - ) + # The following avoids casting an imaginary quantity to reals when backpropagating + c = (1 + 0j) * c + s = (1 + 0j) * s + ones = qml.math.ones_like(s) + zeros = qml.math.zeros_like(s) + matrix = [ + [ones, zeros, zeros, zeros], + [zeros, ones, zeros, zeros], + [zeros, zeros, c, -s], + [zeros, zeros, s, c], + ] + + return qml.math.stack([stack_last(row) for row in matrix], axis=-2) @staticmethod def compute_decomposition(phi, wires): @@ -1504,6 +1596,7 @@ class CRZ(Operation): * Number of wires: 2 * Number of parameters: 1 + * Number of dimensions per parameter: (0,) * Gradient recipe: The controlled-RZ operator satisfies a four-term parameter-shift rule (see Appendix F, https://doi.org/10.1088/1367-2630/ac2cb3): @@ -1528,6 +1621,9 @@ class CRZ(Operation): num_params = 1 """int: Number of trainable parameters that the operator depends on.""" + ndim_params = (0,) + """tuple[int]: Number of dimensions per trainable parameter that the operator depends on.""" + basis = "Z" grad_method = "A" parameter_frequencies = [(0.5, 1.0)] @@ -1567,9 +1663,18 @@ def compute_matrix(theta): # pylint: disable=arguments-differ if qml.math.get_interface(theta) == "tensorflow": theta = qml.math.cast_like(theta, 1j) - exp_part = qml.math.exp(-0.5j * theta) + exp_part = qml.math.exp(-1j * theta / 2) - return qml.math.diag([1, 1, exp_part, qml.math.conj(exp_part)]) + ones = qml.math.ones_like(exp_part) + zeros = qml.math.zeros_like(exp_part) + matrix = [ + [ones, zeros, zeros, zeros], + [zeros, ones, zeros, zeros], + [zeros, zeros, exp_part, zeros], + [zeros, zeros, zeros, qml.math.conj(exp_part)], + ] + + return qml.math.stack([stack_last(row) for row in matrix], axis=-2) @staticmethod def compute_eigvals(theta): # pylint: disable=arguments-differ @@ -1598,14 +1703,13 @@ def compute_eigvals(theta): # pylint: disable=arguments-differ >>> qml.CRZ.compute_eigvals(torch.tensor(0.5)) tensor([1.0000+0.0000j, 1.0000+0.0000j, 0.9689-0.2474j, 0.9689+0.2474j]) """ - theta = qml.math.flatten(qml.math.stack([theta]))[0] - if qml.math.get_interface(theta) == "tensorflow": theta = qml.math.cast_like(theta, 1j) exp_part = qml.math.exp(-0.5j * theta) + o = qml.math.ones_like(exp_part) - return qml.math.stack([1, 1, exp_part, qml.math.conj(exp_part)]) + return stack_last([o, o, exp_part, qml.math.conj(exp_part)]) @staticmethod def compute_decomposition(phi, wires): @@ -1668,6 +1772,7 @@ class CRot(Operation): * Number of wires: 2 * Number of parameters: 3 + * Number of dimensions per parameter: (0, 0, 0) * Gradient recipe: The controlled-Rot operator satisfies a four-term parameter-shift rule (see Appendix F, https://doi.org/10.1088/1367-2630/ac2cb3): @@ -1695,6 +1800,9 @@ class CRot(Operation): num_params = 3 """int: Number of trainable parameters that the operator depends on.""" + ndim_params = (0, 0, 0) + """tuple[int]: Number of dimensions per trainable parameter that the operator depends on.""" + grad_method = "A" parameter_frequencies = [(0.5, 1.0), (0.5, 1.0), (0.5, 1.0)] @@ -1731,7 +1839,7 @@ def compute_matrix(phi, theta, omega): # pylint: disable=arguments-differ [ 0.0+0.0j, 0.0+0.0j, 0.0993+0.0100j, 0.9752+0.1977j]]) """ # It might be that they are in different interfaces, e.g., - # Rot(0.2, 0.3, tf.Variable(0.5), wires=0) + # CRot(0.2, 0.3, tf.Variable(0.5), wires=[0, 1]) # So we need to make sure the matrix comes out having the right type interface = qml.math._multi_dispatch([phi, theta, omega]) @@ -1745,24 +1853,31 @@ def compute_matrix(phi, theta, omega): # pylint: disable=arguments-differ c = qml.math.cast_like(qml.math.asarray(c, like=interface), 1j) s = qml.math.cast_like(qml.math.asarray(s, like=interface), 1j) + # The following variable is used to assert the all terms to be stacked have same shape + one = qml.math.ones_like(phi) * qml.math.ones_like(omega) + c = c * one + s = s * one + + o = qml.math.ones_like(c) + z = qml.math.zeros_like(c) mat = [ - [1, 0, 0, 0], - [0, 1, 0, 0], + [o, z, z, z], + [z, o, z, z], [ - 0, - 0, + z, + z, qml.math.exp(-0.5j * (phi + omega)) * c, -qml.math.exp(0.5j * (phi - omega)) * s, ], [ - 0, - 0, + z, + z, qml.math.exp(-0.5j * (phi - omega)) * s, qml.math.exp(0.5j * (phi + omega)) * c, ], ] - return qml.math.stack([qml.math.stack(row) for row in mat]) + return qml.math.stack([stack_last(row) for row in mat], axis=-2) @staticmethod def compute_decomposition(phi, theta, omega, wires): @@ -1831,6 +1946,7 @@ class U1(Operation): * Number of wires: 1 * Number of parameters: 1 + * Number of dimensions per parameter: (0,) * Gradient recipe: :math:`\frac{d}{d\phi}f(U_1(\phi)) = \frac{1}{2}\left[f(U_1(\phi+\pi/2)) - f(U_1(\phi-\pi/2))\right]` where :math:`f` is an expectation value depending on :math:`U_1(\phi)`. @@ -1845,6 +1961,9 @@ class U1(Operation): num_params = 1 """int: Number of trainable parameters that the operator depends on.""" + ndim_params = (0,) + """tuple[int]: Number of dimensions per trainable parameter that the operator depends on.""" + grad_method = "A" parameter_frequencies = [(1,)] @@ -1878,9 +1997,10 @@ def compute_matrix(phi): # pylint: disable=arguments-differ if qml.math.get_interface(phi) == "tensorflow": phi = qml.math.cast_like(phi, 1j) - exp_part = qml.math.exp(1j * phi) + p = qml.math.exp(1j * phi) + z = qml.math.zeros_like(p) - return qml.math.diag([1, exp_part]) + return qml.math.stack([stack_last([qml.math.ones_like(p), z]), stack_last([z, p])], axis=-2) @staticmethod def compute_decomposition(phi, wires): @@ -1938,6 +2058,7 @@ class U2(Operation): * Number of wires: 1 * Number of parameters: 2 + * Number of dimensions per parameter: (0, 0) * Gradient recipe: :math:`\frac{d}{d\phi}f(U_2(\phi, \delta)) = \frac{1}{2}\left[f(U_2(\phi+\pi/2, \delta)) - f(U_2(\phi-\pi/2, \delta))\right]` where :math:`f` is an expectation value depending on :math:`U_2(\phi, \delta)`. This gradient recipe applies for each angle argument :math:`\{\phi, \delta\}`. @@ -1954,6 +2075,9 @@ class U2(Operation): num_params = 2 """int: Number of trainable parameters that the operator depends on.""" + ndim_params = (0, 0) + """tuple[int]: Number of dimensions per trainable parameter that the operator depends on.""" + grad_method = "A" parameter_frequencies = [(1,), (1,)] @@ -1989,12 +2113,13 @@ def compute_matrix(phi, delta): # pylint: disable=arguments-differ phi = qml.math.cast_like(qml.math.asarray(phi, like=interface), 1j) delta = qml.math.cast_like(qml.math.asarray(delta, like=interface), 1j) + one = qml.math.ones_like(phi) * qml.math.ones_like(delta) mat = [ - [1, -qml.math.exp(1j * delta)], - [qml.math.exp(1j * phi), qml.math.exp(1j * (phi + delta))], + [one, -qml.math.exp(1j * delta) * one], + [qml.math.exp(1j * phi) * one, qml.math.exp(1j * (phi + delta))], ] - return INV_SQRT2 * qml.math.stack([qml.math.stack(row) for row in mat]) + return INV_SQRT2 * qml.math.stack([stack_last(row) for row in mat], axis=-2) @staticmethod def compute_decomposition(phi, delta, wires): @@ -2020,8 +2145,9 @@ def compute_decomposition(phi, delta, wires): PhaseShift(1.23, wires=[0])] """ + pi_half = qml.math.ones_like(delta) * (np.pi / 2) decomp_ops = [ - Rot(delta, np.pi / 2, -delta, wires=wires), + Rot(delta, pi_half, -delta, wires=wires), PhaseShift(delta, wires=wires), PhaseShift(phi, wires=wires), ] @@ -2029,8 +2155,8 @@ def compute_decomposition(phi, delta, wires): def adjoint(self): phi, delta = self.parameters - new_delta = (np.pi - phi) % (2 * np.pi) - new_phi = (np.pi - delta) % (2 * np.pi) + new_delta = qml.math.mod((np.pi - phi), (2 * np.pi)) + new_phi = qml.math.mod((np.pi - delta), (2 * np.pi)) return U2(new_phi, new_delta, wires=self.wires) @@ -2059,6 +2185,7 @@ class U3(Operation): * Number of wires: 1 * Number of parameters: 3 + * Number of dimensions per parameter: (0, 0, 0) * Gradient recipe: :math:`\frac{d}{d\phi}f(U_3(\theta, \phi, \delta)) = \frac{1}{2}\left[f(U_3(\theta+\pi/2, \phi, \delta)) - f(U_3(\theta-\pi/2, \phi, \delta))\right]` where :math:`f` is an expectation value depending on :math:`U_3(\theta, \phi, \delta)`. This gradient recipe applies for each angle argument :math:`\{\theta, \phi, \delta\}`. @@ -2076,6 +2203,9 @@ class U3(Operation): num_params = 3 """int: Number of trainable parameters that the operator depends on.""" + ndim_params = (0, 0, 0) + """tuple[int]: Number of dimensions per trainable parameter that the operator depends on.""" + grad_method = "A" parameter_frequencies = [(1,), (1,), (1,)] @@ -2107,7 +2237,7 @@ def compute_matrix(theta, phi, delta): # pylint: disable=arguments-differ """ # It might be that they are in different interfaces, e.g., - # Rot(0.2, 0.3, tf.Variable(0.5), wires=0) + # U3(0.2, 0.3, tf.Variable(0.5), wires=0) # So we need to make sure the matrix comes out having the right type interface = qml.math._multi_dispatch([theta, phi, delta]) @@ -2121,12 +2251,17 @@ def compute_matrix(theta, phi, delta): # pylint: disable=arguments-differ c = qml.math.cast_like(qml.math.asarray(c, like=interface), 1j) s = qml.math.cast_like(qml.math.asarray(s, like=interface), 1j) + # The following variable is used to assert the all terms to be stacked have same shape + one = qml.math.ones_like(phi) * qml.math.ones_like(delta) + c = c * one + s = s * one + mat = [ [c, -s * qml.math.exp(1j * delta)], [s * qml.math.exp(1j * phi), c * qml.math.exp(1j * (phi + delta))], ] - return qml.math.stack([qml.math.stack(row) for row in mat]) + return qml.math.stack([stack_last(row) for row in mat], axis=-2) @staticmethod def compute_decomposition(theta, phi, delta, wires): @@ -2163,8 +2298,8 @@ def compute_decomposition(theta, phi, delta, wires): def adjoint(self): theta, phi, delta = self.parameters - new_delta = (np.pi - phi) % (2 * np.pi) - new_phi = (np.pi - delta) % (2 * np.pi) + new_delta = qml.math.mod((np.pi - phi), (2 * np.pi)) + new_phi = qml.math.mod((np.pi - delta), (2 * np.pi)) return U3(theta, new_phi, new_delta, wires=self.wires) @@ -2183,6 +2318,7 @@ class IsingXX(Operation): * Number of wires: 2 * Number of parameters: 1 + * Number of dimensions per parameter: (0,) * Gradient recipe: :math:`\frac{d}{d\phi}f(XX(\phi)) = \frac{1}{2}\left[f(XX(\phi +\pi/2)) - f(XX(\phi-\pi/2))\right]` where :math:`f` is an expectation value depending on :math:`XX(\phi)`. @@ -2197,6 +2333,9 @@ class IsingXX(Operation): num_params = 1 """int: Number of trainable parameters that the operator depends on.""" + ndim_params = (0,) + """tuple[int]: Number of dimensions per trainable parameter that the operator depends on.""" + grad_method = "A" parameter_frequencies = [(1,)] @@ -2232,15 +2371,23 @@ def compute_matrix(phi): # pylint: disable=arguments-differ """ c = qml.math.cos(phi / 2) s = qml.math.sin(phi / 2) - Y = qml.math.convert_like(np.eye(4)[::-1].copy(), phi) if qml.math.get_interface(phi) == "tensorflow": c = qml.math.cast_like(c, 1j) s = qml.math.cast_like(s, 1j) - Y = qml.math.cast_like(Y, 1j) - mat = qml.math.diag([c, c, c, c]) - 1j * s * Y - return mat + # The following avoids casting an imaginary quantity to reals when backpropagating + c = (1 + 0j) * c + js = -1j * s + z = qml.math.zeros_like(js) + + matrix = [ + [c, z, z, js], + [z, c, js, z], + [z, js, c, z], + [js, z, z, c], + ] + return qml.math.stack([stack_last(row) for row in matrix], axis=-2) @staticmethod def compute_decomposition(phi, wires): @@ -2294,6 +2441,7 @@ class IsingYY(Operation): * Number of wires: 2 * Number of parameters: 1 + * Number of dimensions per parameter: (0,) * Gradient recipe: :math:`\frac{d}{d\phi}f(YY(\phi)) = \frac{1}{2}\left[f(YY(\phi +\pi/2)) - f(YY(\phi-\pi/2))\right]` where :math:`f` is an expectation value depending on :math:`YY(\phi)`. @@ -2308,6 +2456,9 @@ class IsingYY(Operation): num_params = 1 """int: Number of trainable parameters that the operator depends on.""" + ndim_params = (0,) + """tuple[int]: Number of dimensions per trainable parameter that the operator depends on.""" + grad_method = "A" parameter_frequencies = [(1,)] @@ -2371,14 +2522,23 @@ def compute_matrix(phi): # pylint: disable=arguments-differ """ c = qml.math.cos(phi / 2) s = qml.math.sin(phi / 2) - Y = qml.math.convert_like(np.diag([1, -1, -1, 1])[::-1].copy(), phi) if qml.math.get_interface(phi) == "tensorflow": c = qml.math.cast_like(c, 1j) s = qml.math.cast_like(s, 1j) - Y = qml.math.cast_like(Y, 1j) - return qml.math.diag([c, c, c, c]) + 1j * s * Y + # The following avoids casting an imaginary quantity to reals when backpropagating + c = (1 + 0j) * c + js = 1j * s + z = qml.math.zeros_like(js) + + matrix = [ + [c, z, z, js], + [z, c, -js, z], + [z, -js, c, z], + [js, z, z, c], + ] + return qml.math.stack([stack_last(row) for row in matrix], axis=-2) def adjoint(self): (phi,) = self.parameters @@ -2403,6 +2563,7 @@ class IsingZZ(Operation): * Number of wires: 2 * Number of parameters: 1 + * Number of dimensions per parameter: (0,) * Gradient recipe: :math:`\frac{d}{d\phi}f(ZZ(\phi)) = \frac{1}{2}\left[f(ZZ(\phi +\pi/2)) - f(ZZ(\phi-\pi/2))\right]` where :math:`f` is an expectation value depending on :math:`ZZ(\theta)`. @@ -2417,6 +2578,9 @@ class IsingZZ(Operation): num_params = 1 """int: Number of trainable parameters that the operator depends on.""" + ndim_params = (0,) + """tuple[int]: Number of dimensions per trainable parameter that the operator depends on.""" + grad_method = "A" parameter_frequencies = [(1,)] @@ -2481,10 +2645,18 @@ def compute_matrix(phi): # pylint: disable=arguments-differ if qml.math.get_interface(phi) == "tensorflow": phi = qml.math.cast_like(phi, 1j) - pos_phase = qml.math.exp(1.0j * phi / 2) - neg_phase = qml.math.exp(-1.0j * phi / 2) + neg_phase = qml.math.exp(-0.5j * phi) + pos_phase = qml.math.exp(0.5j * phi) + + zeros = qml.math.zeros_like(pos_phase) + matrix = [ + [neg_phase, zeros, zeros, zeros], + [zeros, pos_phase, zeros, zeros], + [zeros, zeros, pos_phase, zeros], + [zeros, zeros, zeros, neg_phase], + ] - return qml.math.diag([neg_phase, pos_phase, pos_phase, neg_phase]) + return qml.math.stack([stack_last(row) for row in matrix], axis=-2) @staticmethod def compute_eigvals(phi): # pylint: disable=arguments-differ @@ -2519,7 +2691,7 @@ def compute_eigvals(phi): # pylint: disable=arguments-differ pos_phase = qml.math.exp(1.0j * phi / 2) neg_phase = qml.math.exp(-1.0j * phi / 2) - return qml.math.stack([neg_phase, pos_phase, pos_phase, neg_phase]) + return stack_last([neg_phase, pos_phase, pos_phase, neg_phase]) def adjoint(self): (phi,) = self.parameters diff --git a/pennylane/transforms/__init__.py b/pennylane/transforms/__init__.py index ca8ccdf6c18..8eea3ccb55c 100644 --- a/pennylane/transforms/__init__.py +++ b/pennylane/transforms/__init__.py @@ -149,6 +149,7 @@ .. autosummary:: :toctree: api + ~transforms.broadcast_expand ~transforms.measurement_grouping ~transforms.hamiltonian_expand @@ -228,3 +229,4 @@ from .transpile import transpile from . import qcut from .qcut import cut_circuit, cut_circuit_mc +from .broadcast_expand import broadcast_expand diff --git a/pennylane/transforms/broadcast_expand.py b/pennylane/transforms/broadcast_expand.py new file mode 100644 index 00000000000..fc6ce88bb60 --- /dev/null +++ b/pennylane/transforms/broadcast_expand.py @@ -0,0 +1,104 @@ +# Copyright 2018-2021 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 module contains the tape expansion function for expanding a +broadcasted tape into multiple tapes.""" +import pennylane as qml +from .batch_transform import batch_transform + + +@batch_transform +def broadcast_expand(tape): + r"""Expand a broadcasted tape into multiple tapes + and a function that stacks and squeezes the results. + + Args: + tape (.QuantumTape): Broadcasted tape to be expanded + + Returns: + tuple[list[.QuantumTape], function]: Returns a tuple containing a list of + quantum tapes that produce one of the results of the broadcasted tape each, + and a function that stacks and squeezes the tape execution results. + + This expansion function is used internally whenever a device does not + support broadcasting. + + **Example** + + We may use ``broadcast_expand`` on a ``QNode`` to separate it + into multiple calculations. For this we will provide ``qml.RX`` with + the ``ndim_params`` attribute that allows the operation to detect + broadcasting, and set up a simple ``QNode`` with a single operation and + returned expectation value: + + >>> qml.RX.ndim_params = (0,) + >>> dev = qml.device("default.qubit", wires=1) + >>> @qml.qnode(dev) + >>> def circuit(x): + ... qml.RX(x, wires=0) + ... return qml.expval(qml.PauliZ(0)) + + We can then call ``broadcast_expand`` on the QNode and store the + expanded ``QNode``: + + >>> expanded_circuit = qml.transforms.broadcast_expand(circuit) + + Let's use the expanded QNode and draw it for broadcasted parameters + with broadcasting axis of length ``3`` passed to ``qml.RX``: + + >>> x = pnp.array([0.2, 0.6, 1.0], requires_grad=True) + >>> qml.draw(expanded_circuit)(x) + 0: ──RX(0.20)─┤ + 0: ──RX(0.60)─┤ + 0: ──RX(1.00)─┤ + + Executing the expanded ``QNode`` results in three values, corresponding + to the three parameters in the broadcasted input ``x``: + + >>> expanded_circuit(x) + tensor([0.98006658, 0.82533561, 0.54030231], requires_grad=True) + + We also can call the transform manually on a tape: + + >>> with qml.tape.QuantumTape() as tape: + >>> qml.RX(np.array([0.2, 0.6, 1.0], requires_grad=True), wires=0) + >>> qml.expval(qml.PauliZ(0)) + >>> tapes, fn = qml.transforms.broadcast_expand(tape) + >>> tapes + [, + , + ] + >>> fn(qml.execute(tapes, qml.device("default.qubit", wires=1), None)) + array([0.98006658, 0.82533561, 0.54030231]) + """ + + num_tapes = tape.batch_size + if num_tapes is None: + raise ValueError("The provided tape is not broadcasted.") + + # Note that these unbatched_params will have shape (#params, num_tapes) + unbatched_params = [] + for op in tape.operations + tape.observables: + for j, p in enumerate(op.data): + if op.batch_size and qml.math.ndim(p) != op.ndim_params[j]: + unbatched_params.append(qml.math.unstack(p)) + else: + unbatched_params.append([p] * num_tapes) + + output_tapes = [] + for p in zip(*unbatched_params): + new_tape = tape.copy(copy_operations=True) + new_tape.set_parameters(p, trainable_only=False) + output_tapes.append(new_tape) + + return output_tapes, lambda x: qml.math.squeeze(qml.math.stack(x)) diff --git a/requirements-ci.txt b/requirements-ci.txt index 123122b9058..279c11a09fa 100644 --- a/requirements-ci.txt +++ b/requirements-ci.txt @@ -12,4 +12,4 @@ semantic_version==2.6 dask[delayed] autoray matplotlib -opt_einsum \ No newline at end of file +opt_einsum diff --git a/tests/devices/test_default_gaussian.py b/tests/devices/test_default_gaussian.py index c3c442e12ce..c083eca0ea1 100644 --- a/tests/devices/test_default_gaussian.py +++ b/tests/devices/test_default_gaussian.py @@ -738,6 +738,7 @@ def test_defines_correct_capabilities(self): "returns_state": False, "supports_reversible_diff": False, "supports_analytic_computation": True, + "supports_broadcasting": False, } assert cap == capabilities diff --git a/tests/devices/test_default_qubit.py b/tests/devices/test_default_qubit.py index 5918a017bc9..1997be7b088 100644 --- a/tests/devices/test_default_qubit.py +++ b/tests/devices/test_default_qubit.py @@ -346,7 +346,6 @@ def test_apply_operation_three_wires_no_parameters( assert np.allclose( qubit_device_3_wires._state.flatten(), np.array(expected_output), atol=tol, rtol=0 ) - print(qubit_device_3_wires.C_DTYPE) assert qubit_device_3_wires._state.dtype == qubit_device_3_wires.C_DTYPE @pytest.mark.parametrize("operation,input,expected_output", test_data_three_wires_no_parameters) @@ -1155,6 +1154,7 @@ def test_defines_correct_capabilities(self): "supports_reversible_diff": True, "supports_inverse_operations": True, "supports_analytic_computation": True, + "supports_broadcasting": False, "passthru_devices": { "torch": "default.qubit.torch", "tf": "default.qubit.tf", @@ -2527,3 +2527,70 @@ def test_error_hamiltonian_expval_finite_shots(self): with pytest.raises(AssertionError, match="Hamiltonian must be used with shots=None"): dev.expval(H) + + +class TestBroadcastingSupport: + """Tests that the device correctly makes use of ``broadcast_expand`` to + execute broadcasted tapes.""" + + @pytest.mark.parametrize("x", [0.2, [0.1, 0.6, 0.3], [0.1]]) + @pytest.mark.parametrize("shots", [None, 100000]) + def test_with_single_broadcasted_par(self, x, shots): + """Test that broadcasting on a circuit with a + single parametrized operation works.""" + dev = qml.device("default.qubit", wires=2, shots=shots) + + @qml.qnode(dev) + def circuit(x): + qml.RX(x, wires=0) + return qml.expval(qml.PauliZ(0)) + # return qml.expval(qml.Hamiltonian([0.3], [qml.PauliZ(0)])) + + out = circuit(np.array(x)) + + assert circuit.device.num_executions == (1 if isinstance(x, float) else len(x)) + tol = 1e-10 if shots is None else 1e-2 + assert qml.math.allclose(out, qml.math.cos(x), atol=tol, rtol=0) + + @pytest.mark.parametrize("x, y", [(0.2, [0.4]), ([0.1, 5.1], [0.1, -0.3])]) + @pytest.mark.parametrize("shots", [None, 1000000]) + def test_with_multiple_pars(self, x, y, shots): + """Test that broadcasting on a circuit with a + single parametrized operation works.""" + dev = qml.device("default.qubit", wires=2, shots=shots) + + @qml.qnode(dev) + def circuit(x, y): + qml.RX(x, wires=0) + qml.RX(y, wires=1) + return [qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliY(1))] + + out = circuit(x, y) + expected = qml.math.stack([qml.math.cos(x) * qml.math.ones_like(y), -qml.math.sin(y)]).T + + assert circuit.device.num_executions == len(y) + tol = 1e-10 if shots is None else 1e-2 + assert qml.math.allclose(out, expected, atol=tol, rtol=0) + + @pytest.mark.parametrize("x, y", [(0.2, [0.4]), ([0.1, 5.1], [0.1, -0.3])]) + @pytest.mark.parametrize("shots", [None, 1000000]) + def test_with_Hamiltonian(self, x, y, shots): + """Test that broadcasting on a circuit with a + single parametrized operation works.""" + dev = qml.device("default.qubit", wires=2, shots=shots) + + H = qml.Hamiltonian([0.3, 0.9], [qml.PauliZ(0), qml.PauliY(1)]) + H.compute_grouping() + + @qml.qnode(dev) + def circuit(x, y): + qml.RX(x, wires=0) + qml.RX(y, wires=1) + return qml.expval(H) + + out = circuit(x, y) + expected = 0.3 * qml.math.cos(x) * qml.math.ones_like(y) - 0.9 * qml.math.sin(y) + + assert circuit.device.num_executions == len(y) + tol = 1e-10 if shots is None else 1e-2 + assert qml.math.allclose(out, expected, atol=tol, rtol=0) diff --git a/tests/devices/test_default_qubit_autograd.py b/tests/devices/test_default_qubit_autograd.py index e07dcbb9c78..74e0a24e30f 100644 --- a/tests/devices/test_default_qubit_autograd.py +++ b/tests/devices/test_default_qubit_autograd.py @@ -55,6 +55,7 @@ def test_defines_correct_capabilities(self): "supports_inverse_operations": True, "supports_analytic_computation": True, "passthru_interface": "autograd", + "supports_broadcasting": False, "passthru_devices": { "torch": "default.qubit.torch", "tf": "default.qubit.tf", diff --git a/tests/devices/test_default_qubit_jax.py b/tests/devices/test_default_qubit_jax.py index 5db547a750f..c7a5598a8fa 100644 --- a/tests/devices/test_default_qubit_jax.py +++ b/tests/devices/test_default_qubit_jax.py @@ -54,6 +54,7 @@ def test_defines_correct_capabilities(self): "supports_reversible_diff": False, "supports_inverse_operations": True, "supports_analytic_computation": True, + "supports_broadcasting": False, "passthru_interface": "jax", "passthru_devices": { "torch": "default.qubit.torch", diff --git a/tests/devices/test_default_qubit_tf.py b/tests/devices/test_default_qubit_tf.py index c1bfe60c358..2f3d1c3b9b0 100644 --- a/tests/devices/test_default_qubit_tf.py +++ b/tests/devices/test_default_qubit_tf.py @@ -1018,6 +1018,7 @@ def test_defines_correct_capabilities(self): "supports_reversible_diff": False, "supports_inverse_operations": True, "supports_analytic_computation": True, + "supports_broadcasting": False, "passthru_interface": "tf", "passthru_devices": { "torch": "default.qubit.torch", diff --git a/tests/devices/test_default_qubit_torch.py b/tests/devices/test_default_qubit_torch.py index 2994e54d62a..438ce25191d 100644 --- a/tests/devices/test_default_qubit_torch.py +++ b/tests/devices/test_default_qubit_torch.py @@ -1130,6 +1130,7 @@ def test_defines_correct_capabilities(self, torch_device): "supports_reversible_diff": False, "supports_inverse_operations": True, "supports_analytic_computation": True, + "supports_broadcasting": False, "passthru_interface": "torch", "passthru_devices": { "torch": "default.qubit.torch", diff --git a/tests/ops/qubit/test_attributes.py b/tests/ops/qubit/test_attributes.py index 095e2e091bb..857faf2431d 100644 --- a/tests/ops/qubit/test_attributes.py +++ b/tests/ops/qubit/test_attributes.py @@ -14,7 +14,10 @@ """ Unit tests for the available qubit state preparation operations. """ +import itertools as it import pytest +import numpy as np +from scipy.stats import unitary_group import pennylane as qml from pennylane.ops.qubit.attributes import Attribute @@ -79,3 +82,244 @@ def test_inclusion_after_addition(self): def test_tensor_check(self): """Test that we can ask if a tensor is in the attribute.""" assert not qml.PauliX(wires=0) @ qml.PauliZ(wires=1) in new_attribute + + +single_scalar_single_wire_ops = [ + "RX", + "RY", + "RZ", + "PhaseShift", + "U1", +] + +single_scalar_multi_wire_ops = [ + "ControlledPhaseShift", + "CRX", + "CRY", + "CRZ", + "IsingXX", + "IsingYY", + "IsingZZ", +] + +two_scalar_single_wire_ops = [ + "U2", +] + +three_scalar_single_wire_ops = [ + "Rot", + "U3", +] + +three_scalar_multi_wire_ops = [ + "CRot", +] + +separately_tested_ops = [ + "QubitUnitary", + "ControlledQubitUnitary", + "DiagonalQubitUnitary", + "PauliRot", + "MultiRZ", +] + + +class TestSupportsBroadcasting: + """Test that all operations in the ``supports_broadcasting`` attribute + actually support broadcasting.""" + + def test_all_marked_operations_are_tested(self): + """Test that the subsets of the ``supports_broadcasting`` attribute + defined above cover the entire attribute.""" + tested_ops = set( + it.chain.from_iterable( + [ + single_scalar_single_wire_ops, + single_scalar_multi_wire_ops, + two_scalar_single_wire_ops, + three_scalar_single_wire_ops, + three_scalar_multi_wire_ops, + separately_tested_ops, + ] + ) + ) + + assert tested_ops == qml.ops.qubit.attributes.supports_broadcasting + + @pytest.mark.parametrize("name", single_scalar_single_wire_ops) + def test_single_scalar_single_wire_ops(self, name): + """Test that single-scalar-parameter operations on a single wire marked + as supporting parameter broadcasting actually do support broadcasting.""" + par = np.array([0.25, 2.1, -0.42]) + wires = ["wire0"] + + cls = getattr(qml, name) + op = cls(par, wires=wires) + + mat1 = op.matrix() + mat2 = cls.compute_matrix(par) + single_mats = [cls(p, wires=wires).matrix() for p in par] + + assert qml.math.allclose(mat1, single_mats) + assert qml.math.allclose(mat2, single_mats) + + @pytest.mark.parametrize("name", single_scalar_multi_wire_ops) + def test_single_scalar_multi_wire_ops(self, name): + """Test that single-scalar-parameter operations on multiple wires marked + as supporting parameter broadcasting actually do support broadcasting.""" + par = np.array([0.25, 2.1, -0.42]) + wires = ["wire0", 5] + + cls = getattr(qml, name) + op = cls(par, wires=wires) + + mat1 = op.matrix() + mat2 = cls.compute_matrix(par) + single_mats = [cls(p, wires=wires).matrix() for p in par] + + assert qml.math.allclose(mat1, single_mats) + assert qml.math.allclose(mat2, single_mats) + + @pytest.mark.parametrize("name", two_scalar_single_wire_ops) + def test_two_scalar_single_wire_ops(self, name): + """Test that two-scalar-parameter operations on a single wire marked + as supporting parameter broadcasting actually do support broadcasting.""" + par = (np.array([0.25, 2.1, -0.42]), np.array([-6.2, 0.12, 0.421])) + wires = ["wire0"] + + cls = getattr(qml, name) + op = cls(*par, wires=wires) + + mat1 = op.matrix() + mat2 = cls.compute_matrix(*par) + single_pars = [tuple(p[i] for p in par) for i in range(3)] + single_mats = [cls(*p, wires=wires).matrix() for p in single_pars] + + assert qml.math.allclose(mat1, single_mats) + assert qml.math.allclose(mat2, single_mats) + + @pytest.mark.parametrize("name", three_scalar_single_wire_ops) + def test_three_scalar_single_wire_ops(self, name): + """Test that three-scalar-parameter operations on a single wire marked + as supporting parameter broadcasting actually do support broadcasting.""" + par = ( + np.array([0.25, 2.1, -0.42]), + np.array([-6.2, 0.12, 0.421]), + np.array([0.2, 1.1, -5.2]), + ) + wires = ["wire0"] + + cls = getattr(qml, name) + op = cls(*par, wires=wires) + + mat1 = op.matrix() + mat2 = cls.compute_matrix(*par) + single_pars = [tuple(p[i] for p in par) for i in range(3)] + single_mats = [cls(*p, wires=wires).matrix() for p in single_pars] + + assert qml.math.allclose(mat1, single_mats) + assert qml.math.allclose(mat2, single_mats) + + @pytest.mark.parametrize("name", three_scalar_multi_wire_ops) + def test_three_scalar_multi_wire_ops(self, name): + """Test that three-scalar-parameter operations on multiple wires marked + as supporting parameter broadcasting actually do support broadcasting.""" + par = ( + np.array([0.25, 2.1, -0.42]), + np.array([-6.2, 0.12, 0.421]), + np.array([0.2, 1.1, -5.2]), + ) + wires = ["wire0", 214] + + cls = getattr(qml, name) + op = cls(*par, wires=wires) + + mat1 = op.matrix() + mat2 = cls.compute_matrix(*par) + single_pars = [tuple(p[i] for p in par) for i in range(3)] + single_mats = [cls(*p, wires=wires).matrix() for p in single_pars] + + assert qml.math.allclose(mat1, single_mats) + assert qml.math.allclose(mat2, single_mats) + + def test_qubit_unitary(self): + """Test that QubitUnitary, which is marked as supporting parameter broadcasting, + actually does support broadcasting.""" + + U = np.array([unitary_group.rvs(4, random_state=state) for state in [91, 1, 4]]) + wires = [0, "9"] + + op = qml.QubitUnitary(U, wires=wires) + + mat1 = op.matrix() + mat2 = qml.QubitUnitary.compute_matrix(U) + single_mats = [qml.QubitUnitary(_U, wires=wires).matrix() for _U in U] + + assert qml.math.allclose(mat1, single_mats) + assert qml.math.allclose(mat2, single_mats) + + def test_controlled_qubit_unitary(self): + """Test that ControlledQubitUnitary, which is marked as supporting parameter broadcasting, + actually does support broadcasting.""" + + U = np.array([unitary_group.rvs(4, random_state=state) for state in [91, 1, 4]]) + wires = [0, "9"] + + op = qml.ControlledQubitUnitary(U, wires=wires, control_wires=[1, "10"]) + + mat1 = op.matrix() + mat2 = qml.ControlledQubitUnitary.compute_matrix(U, u_wires=wires, control_wires=[1, "10"]) + single_mats = [ + qml.ControlledQubitUnitary(_U, wires=wires, control_wires=[1, "10"]).matrix() + for _U in U + ] + + assert qml.math.allclose(mat1, single_mats) + assert qml.math.allclose(mat2, single_mats) + + def test_diagonal_qubit_unitary(self): + """Test that DiagonalQubitUnitary, which is marked as supporting parameter broadcasting, + actually does support broadcasting.""" + diag = np.array([[1j, 1, 1, -1j], [-1j, 1j, 1, -1], [1j, -1j, 1.0, -1]]) + wires = ["a", 5] + + op = qml.DiagonalQubitUnitary(diag, wires=wires) + + mat1 = op.matrix() + mat2 = qml.DiagonalQubitUnitary.compute_matrix(diag) + single_mats = [qml.DiagonalQubitUnitary(d, wires=wires).matrix() for d in diag] + + assert qml.math.allclose(mat1, single_mats) + assert qml.math.allclose(mat2, single_mats) + + @pytest.mark.parametrize( + "pauli_word, wires", [("XYZ", [0, "4", 1]), ("II", [1, 5]), ("X", [7])] + ) + def test_pauli_rot(self, pauli_word, wires): + """Test that PauliRot, which is marked as supporting parameter broadcasting, + actually does support broadcasting.""" + par = np.array([0.25, 2.1, -0.42]) + + op = qml.PauliRot(par, pauli_word, wires=wires) + + mat1 = op.matrix() + mat2 = qml.PauliRot.compute_matrix(par, pauli_word=pauli_word) + single_mats = [qml.PauliRot(p, pauli_word, wires=wires).matrix() for p in par] + + assert qml.math.allclose(mat1, single_mats) + assert qml.math.allclose(mat2, single_mats) + + @pytest.mark.parametrize("wires", [[0, "4", 1], [1, 5], [7]]) + def test_pauli_rot(self, wires): + """Test that MultiRZ, which is marked as supporting parameter broadcasting, + actually does support broadcasting.""" + par = np.array([0.25, 2.1, -0.42]) + + op = qml.MultiRZ(par, wires=wires) + + mat1 = op.matrix() + mat2 = qml.MultiRZ.compute_matrix(par, num_wires=len(wires)) + single_mats = [qml.MultiRZ(p, wires=wires).matrix() for p in par] + + assert qml.math.allclose(mat1, single_mats) + assert qml.math.allclose(mat2, single_mats) diff --git a/tests/ops/qubit/test_matrix_ops.py b/tests/ops/qubit/test_matrix_ops.py index 1a03f97dc83..f1968b5e72c 100644 --- a/tests/ops/qubit/test_matrix_ops.py +++ b/tests/ops/qubit/test_matrix_ops.py @@ -20,6 +20,7 @@ import pennylane as qml from pennylane.wires import Wires +from pennylane.operation import DecompositionUndefinedError from gate_data import ( I, @@ -45,6 +46,20 @@ def test_qubit_unitary_noninteger_pow(self): with pytest.raises(qml.operation.PowUndefinedError): op.pow(0.123) + def test_qubit_unitary_noninteger_pow_broadcasted(self): + """Test broadcasted QubitUnitary raised to a non-integer power raises an error.""" + U = np.array( + [ + [[0.98877108 + 0.0j, 0.0 - 0.14943813j], [0.0 - 0.14943813j, 0.98877108 + 0.0j]], + [[0.98877108 + 0.0j, 0.0 - 0.14943813j], [0.0 - 0.14943813j, 0.98877108 + 0.0j]], + ] + ) + + op = qml.QubitUnitary(U, wires="a") + + with pytest.raises(qml.operation.PowUndefinedError): + op.pow(0.123) + @pytest.mark.parametrize("n", (1, 3, -1, -3)) def test_qubit_unitary_pow(self, n): """Test qubit unitary raised to an integer power.""" @@ -63,8 +78,31 @@ def test_qubit_unitary_pow(self, n): assert qml.math.allclose(mat_to_pow, new_mat) + @pytest.mark.parametrize("n", (1, 3, -1, -3)) + def test_qubit_unitary_pow_broadcasted(self, n): + """Test broadcasted qubit unitary raised to an integer power.""" + U = np.array( + [ + [[0.98877108 + 0.0j, 0.0 - 0.14943813j], [0.0 - 0.14943813j, 0.98877108 + 0.0j]], + [[0.4125124 + 0.0j, 0.0 - 0.91095199j], [0.0 - 0.91095199j, 0.4125124 + 0.0j]], + ] + ) + + op = qml.QubitUnitary(U, wires="a") + new_ops = op.pow(n) + + assert len(new_ops) == 1 + assert new_ops[0].wires == op.wires + + mat_to_pow = qml.math.linalg.matrix_power(qml.matrix(op), n) + new_mat = qml.matrix(new_ops[0]) + + assert qml.math.allclose(mat_to_pow, new_mat) + @pytest.mark.autograd - @pytest.mark.parametrize("U,num_wires", [(H, 1), (np.kron(H, H), 2)]) + @pytest.mark.parametrize( + "U,num_wires", [(H, 1), (np.kron(H, H), 2), (np.tensordot([1j, -1, 1], H, axes=0), 1)] + ) def test_qubit_unitary_autograd(self, U, num_wires): """Test that the unitary operator produces the correct output and catches incorrect input with autograd.""" @@ -79,7 +117,7 @@ def test_qubit_unitary_autograd(self, U, num_wires): # test non-square matrix with pytest.raises(ValueError, match="must be of shape"): - qml.QubitUnitary(U[1:], wires=range(num_wires)).matrix() + qml.QubitUnitary(U[:, 1:], wires=range(num_wires)).matrix() # test non-unitary matrix U3 = U.copy() @@ -92,7 +130,9 @@ def test_qubit_unitary_autograd(self, U, num_wires): qml.QubitUnitary(U, wires=range(num_wires + 1)).matrix() @pytest.mark.torch - @pytest.mark.parametrize("U,num_wires", [(H, 1), (np.kron(H, H), 2)]) + @pytest.mark.parametrize( + "U,num_wires", [(H, 1), (np.kron(H, H), 2), (np.tensordot([1j, -1, 1], H, axes=0), 1)] + ) def test_qubit_unitary_torch(self, U, num_wires): """Test that the unitary operator produces the correct output and catches incorrect input with torch.""" @@ -109,7 +149,7 @@ def test_qubit_unitary_torch(self, U, num_wires): # test non-square matrix with pytest.raises(ValueError, match="must be of shape"): - qml.QubitUnitary(U[1:], wires=range(num_wires)).matrix() + qml.QubitUnitary(U[:, 1:], wires=range(num_wires)).matrix() # test non-unitary matrix U3 = U.detach().clone() @@ -122,7 +162,9 @@ def test_qubit_unitary_torch(self, U, num_wires): qml.QubitUnitary(U, wires=range(num_wires + 1)).matrix() @pytest.mark.tf - @pytest.mark.parametrize("U,num_wires", [(H, 1), (np.kron(H, H), 2)]) + @pytest.mark.parametrize( + "U,num_wires", [(H, 1), (np.kron(H, H), 2), (np.tensordot([1j, -1, 1], H, axes=0), 1)] + ) def test_qubit_unitary_tf(self, U, num_wires): """Test that the unitary operator produces the correct output and catches incorrect input with tensorflow.""" @@ -139,7 +181,7 @@ def test_qubit_unitary_tf(self, U, num_wires): # test non-square matrix with pytest.raises(ValueError, match="must be of shape"): - qml.QubitUnitary(U[1:], wires=range(num_wires)).matrix() + qml.QubitUnitary(U[:, 1:], wires=range(num_wires)).matrix() # test non-unitary matrix U3 = tf.Variable(U + 0.5) @@ -151,7 +193,9 @@ def test_qubit_unitary_tf(self, U, num_wires): qml.QubitUnitary(U, wires=range(num_wires + 1)).matrix() @pytest.mark.jax - @pytest.mark.parametrize("U,num_wires", [(H, 1), (np.kron(H, H), 2)]) + @pytest.mark.parametrize( + "U,num_wires", [(H, 1), (np.kron(H, H), 2), (np.tensordot([1j, -1, 1], H, axes=0), 1)] + ) def test_qubit_unitary_jax(self, U, num_wires): """Test that the unitary operator produces the correct output and catches incorrect input with jax.""" @@ -168,7 +212,7 @@ def test_qubit_unitary_jax(self, U, num_wires): # test non-square matrix with pytest.raises(ValueError, match="must be of shape"): - qml.QubitUnitary(U[1:], wires=range(num_wires)).matrix() + qml.QubitUnitary(U[:, 1:], wires=range(num_wires)).matrix() # test non-unitary matrix U3 = U + 0.5 @@ -180,8 +224,10 @@ def test_qubit_unitary_jax(self, U, num_wires): qml.QubitUnitary(U, wires=range(num_wires + 1)).matrix() @pytest.mark.jax - @pytest.mark.parametrize("U, num_wires", [(H, 1), (np.kron(H, H), 2)]) - def test_qubit_unitary_jax(self, U, num_wires): + @pytest.mark.parametrize( + "U,num_wires", [(H, 1), (np.kron(H, H), 2), (np.tensordot([1j, -1, 1], H, axes=0), 1)] + ) + def test_qubit_unitary_jax_jit(self, U, num_wires): """Tests that QubitUnitary works with jitting.""" import jax from jax import numpy as jnp @@ -231,6 +277,17 @@ def test_qubit_unitary_decomposition(self, U, expected_gate, expected_params): assert isinstance(decomp2[0], expected_gate) assert np.allclose(decomp2[0].parameters, expected_params, atol=1e-7) + def test_error_qubit_unitary_decomposition_broadcasted(self): + """Tests that broadcasted QubitUnitary decompositions are not supported.""" + U = np.array( + [[0.98877108 + 0.0j, 0.0 - 0.14943813j], [0.0 - 0.14943813j, 0.98877108 + 0.0j]] + ) + U = np.tensordot([1j, -1.0, (1 + 1j) / np.sqrt(2)], U, axes=0) + with pytest.raises(DecompositionUndefinedError, match="QubitUnitary does not support"): + qml.QubitUnitary.compute_decomposition(U, wires=0) + with pytest.raises(DecompositionUndefinedError, match="QubitUnitary does not support"): + qml.QubitUnitary(U, wires=0).decomposition() + def test_qubit_unitary_decomposition_multiqubit_invalid(self): """Test that QubitUnitary is not decomposed for more than two qubits.""" U = qml.Toffoli(wires=[0, 1, 2]).matrix() @@ -249,6 +306,18 @@ def test_matrix_representation(self, tol): assert np.allclose(res_static, expected, atol=tol) assert np.allclose(res_dynamic, expected, atol=tol) + def test_matrix_representation_broadcasted(self, tol): + """Test that the matrix representation is defined correctly""" + U = np.array( + [[0.98877108 + 0.0j, 0.0 - 0.14943813j], [0.0 - 0.14943813j, 0.98877108 + 0.0j]] + ) + U = np.tensordot([1j, -1.0, (1 + 1j) / np.sqrt(2)], U, axes=0) + res_static = qml.QubitUnitary.compute_matrix(U) + res_dynamic = qml.QubitUnitary(U, wires=0).matrix() + expected = U + assert np.allclose(res_static, expected, atol=tol) + assert np.allclose(res_dynamic, expected, atol=tol) + class TestDiagonalQubitUnitary: """Test the DiagonalQubitUnitary operation.""" @@ -260,11 +329,27 @@ def test_decomposition(self): decomp = qml.DiagonalQubitUnitary.compute_decomposition(D, [0, 1, 2]) decomp2 = qml.DiagonalQubitUnitary(D, wires=[0, 1, 2]).decomposition() + assert len(decomp) == 1 == len(decomp2) assert decomp[0].name == "QubitUnitary" == decomp2[0].name assert decomp[0].wires == Wires([0, 1, 2]) == decomp2[0].wires assert np.allclose(decomp[0].data[0], np.diag(D)) assert np.allclose(decomp2[0].data[0], np.diag(D)) + def test_decomposition_broadcasted(self): + """Test that the broadcasted DiagonalQubitUnitary falls back to QubitUnitary.""" + D = np.outer([1.0, -1.0], [1.0, -1.0, 1j, 1.0]) + + decomp = qml.DiagonalQubitUnitary.compute_decomposition(D, [0, 1]) + decomp2 = qml.DiagonalQubitUnitary(D, wires=[0, 1]).decomposition() + + assert len(decomp) == 1 == len(decomp2) + assert decomp[0].name == "QubitUnitary" == decomp2[0].name + assert decomp[0].wires == Wires([0, 1]) == decomp2[0].wires + + expected = np.array([np.diag([1.0, -1.0, 1j, 1.0]), np.diag([-1.0, 1.0, -1j, -1.0])]) + assert np.allclose(decomp[0].data[0], expected) + assert np.allclose(decomp2[0].data[0], expected) + def test_controlled(self): """Test that the correct controlled operation is created when controlling a qml.DiagonalQubitUnitary.""" D = np.array([1j, 1, 1, -1, -1j, 1j, 1, -1]) @@ -276,6 +361,19 @@ def test_controlled(self): mat, qml.math.diag(qml.math.append(qml.math.ones(8, dtype=complex), D)) ) + def test_controlled_broadcasted(self): + """Test that the correct controlled operation is created when + controlling a qml.DiagonalQubitUnitary with a broadcasted diagonal.""" + D = np.array([[1j, 1, -1j, 1], [1, -1, 1j, -1]]) + op = qml.DiagonalQubitUnitary(D, wires=[1, 2]) + with qml.tape.QuantumTape() as tape: + op._controlled(control=0) + mat = qml.matrix(tape) + expected = np.array( + [np.diag([1, 1, 1, 1, 1j, 1, -1j, 1]), np.diag([1, 1, 1, 1, 1, -1, 1j, -1])] + ) + assert qml.math.allclose(mat, expected) + def test_matrix_representation(self, tol): """Test that the matrix representation is defined correctly""" diag = np.array([1, -1]) @@ -285,6 +383,15 @@ def test_matrix_representation(self, tol): assert np.allclose(res_static, expected, atol=tol) assert np.allclose(res_dynamic, expected, atol=tol) + def test_matrix_representation_broadcasted(self, tol): + """Test that the matrix representation is defined correctly for a broadcasted diagonal.""" + diag = np.array([[1, -1], [1j, -1], [-1j, -1]]) + res_static = qml.DiagonalQubitUnitary.compute_matrix(diag) + res_dynamic = qml.DiagonalQubitUnitary(diag, wires=0).matrix() + expected = np.array([[[1, 0], [0, -1]], [[1j, 0], [0, -1]], [[-1j, 0], [0, -1]]]) + assert np.allclose(res_static, expected, atol=tol) + assert np.allclose(res_dynamic, expected, atol=tol) + @pytest.mark.parametrize("n", (2, -1, 0.12345)) @pytest.mark.parametrize("diag", ([1.0, -1.0], np.array([1.0, -1.0]))) def test_pow(self, n, diag): @@ -296,16 +403,33 @@ def test_pow(self, n, diag): for x_op, x_pow in zip(op.data[0], pow_ops[0].data[0]): assert (x_op + 0.0j) ** n == x_pow - def test_error_matrix_not_unitary(self): + @pytest.mark.parametrize("n", (2, -1, 0.12345)) + @pytest.mark.parametrize( + "diag", ([[1.0, -1.0]] * 5, np.array([[1.0, -1j], [1j, 1j], [-1j, 1]])) + ) + def test_pow_broadcasted(self, n, diag): + """Test pow method returns expected results for broadcasted diagonals.""" + op = qml.DiagonalQubitUnitary(diag, wires="b") + pow_ops = op.pow(n) + assert len(pow_ops) == 1 + + qml.math.allclose(np.array(op.data[0], dtype=complex) ** n, pow_ops[0].data[0]) + + @pytest.mark.parametrize("D", [[1, 2], [[0.2, 1.0, -1.0], [1.0, -1j, 1j]]]) + def test_error_matrix_not_unitary(self, D): """Tests that error is raised if diagonal by `compute_matrix` does not lead to a unitary""" with pytest.raises(ValueError, match="Operator must be unitary"): - qml.DiagonalQubitUnitary.compute_matrix(np.array([1, 2])) + qml.DiagonalQubitUnitary.compute_matrix(np.array(D)) + with pytest.raises(ValueError, match="Operator must be unitary"): + qml.DiagonalQubitUnitary(np.array(D), wires=1).matrix() - @pytest.mark.jax - def test_error_eigvals_not_unitary(self): - """Tests that error is raised by `compute_eigvals` if diagonal does not lead to a unitary""" + @pytest.mark.parametrize("D", [[1, 2], [[0.2, 1.0, -1.0], [1.0, -1j, 1j]]]) + def test_error_eigvals_not_unitary(self, D): + """Tests that error is raised if diagonal by `compute_matrix` does not lead to a unitary""" + with pytest.raises(ValueError, match="Operator must be unitary"): + qml.DiagonalQubitUnitary.compute_eigvals(np.array(D)) with pytest.raises(ValueError, match="Operator must be unitary"): - qml.DiagonalQubitUnitary.compute_eigvals(np.array([1, 2])) + qml.DiagonalQubitUnitary(np.array(D), wires=0).eigvals() @pytest.mark.jax def test_jax_jit(self): @@ -330,6 +454,29 @@ def circuit(x): expected = -jnp.sin(x) assert np.allclose(grad, expected) + @pytest.mark.jax + def test_jax_jit_broadcasted(self): + """Test that the diagonal matrix unitary operation works + within a QNode that uses the JAX JIT""" + import jax + + jnp = jax.numpy + + dev = qml.device("default.qubit", wires=1, shots=None) + + @jax.jit + @qml.qnode(dev, interface="jax") + def circuit(x): + diag = jnp.exp(1j * jnp.outer(x, jnp.array([1, -1])) / 2) + qml.Hadamard(wires=0) + qml.DiagonalQubitUnitary(diag, wires=0) + return qml.expval(qml.PauliX(0)) + + x = jnp.array([0.654, 0.321]) + jac = jax.jacobian(circuit)(x) + expected = jnp.diag(-jnp.sin(x)) + assert np.allclose(jac, expected) + @pytest.mark.tf @pytest.mark.slow # test takes 12 seconds due to tf.function def test_tf_function(self): @@ -359,6 +506,7 @@ def circuit(x): X = np.array([[0, 1], [1, 0]]) +X_broadcasted = np.array([X] * 3) class TestControlledQubitUnitary: @@ -412,6 +560,42 @@ def f2(): assert np.allclose(state_1, state_2) + @pytest.mark.parametrize("target_wire", range(3)) + def test_toffoli_broadcasted(self, target_wire): + """Test if ControlledQubitUnitary acts like a Toffoli gate when the input unitary is a + broadcasted single-qubit X. Allows the target wire to be any of the three wires.""" + control_wires = list(range(3)) + del control_wires[target_wire] + + # pick some random unitaries (with a fixed seed) to make the circuit less trivial + U1 = unitary_group.rvs(8, random_state=1) + U2 = unitary_group.rvs(8, random_state=2) + + dev = qml.device("default.qubit", wires=3) + + @qml.qnode(dev) + def f1(): + qml.QubitUnitary(U1, wires=range(3)) + qml.ControlledQubitUnitary( + X_broadcasted, control_wires=control_wires, wires=target_wire + ) + qml.QubitUnitary(U2, wires=range(3)) + return qml.state() + + @qml.qnode(dev) + def f2(): + qml.QubitUnitary(U1, wires=range(3)) + qml.Toffoli(wires=control_wires + [target_wire]) + qml.QubitUnitary(U2, wires=range(3)) + return qml.state() + + state_1 = f1() + state_2 = f2() + + assert np.shape(state_1) == (3, 8) + assert np.allclose(state_1, state_1[0]) # Check that all broadcasted results are equal + assert np.allclose(state_1, state_2) + def test_arbitrary_multiqubit(self): """Test if ControlledQubitUnitary applies correctly for a 2-qubit unitary with 2-qubit control, where the control and target wires are not ordered.""" @@ -572,16 +756,55 @@ def test_matrix_representation(self, tol): assert np.allclose(res_static, expected, atol=tol) assert np.allclose(res_dynamic, expected, atol=tol) + def test_matrix_representation_broadcasted(self, tol): + """Test that the matrix representation is defined correctly""" + U = np.array( + [ + [[0.94877869, 0.31594146], [-0.31594146, 0.94877869]], + [[0.4125124, -0.91095199], [0.91095199, 0.4125124]], + [[0.31594146, 0.94877869j], [0.94877869j, 0.31594146]], + ] + ) + + res_static = qml.ControlledQubitUnitary.compute_matrix(U, control_wires=[1], u_wires=[0]) + res_dynamic = qml.ControlledQubitUnitary(U, control_wires=[1], wires=0).matrix() + expected = np.array( + [ + [ + [1.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], + [0.0 + 0.0j, 1.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], + [0.0 + 0.0j, 0.0 + 0.0j, 0.94877869 + 0.0j, 0.31594146 + 0.0j], + [0.0 + 0.0j, 0.0 + 0.0j, -0.31594146 + 0.0j, 0.94877869 + 0.0j], + ], + [ + [1.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], + [0.0 + 0.0j, 1.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], + [0.0 + 0.0j, 0.0 + 0.0j, 0.4125124 + 0.0j, -0.91095199 + 0.0j], + [0.0 + 0.0j, 0.0 + 0.0j, 0.91095199 + 0.0j, 0.4125124 + 0.0j], + ], + [ + [1.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], + [0.0 + 0.0j, 1.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], + [0.0 + 0.0j, 0.0 + 0.0j, 0.31594146 + 0.0j, 0.0 + 0.94877869j], + [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.94877869j, 0.31594146 + 0.0j], + ], + ] + ) + assert np.allclose(res_static, expected, atol=tol) + assert np.allclose(res_dynamic, expected, atol=tol) + def test_no_decomp(self): """Test that ControlledQubitUnitary raises a decomposition undefined error.""" mat = qml.PauliX(0).matrix() with pytest.raises(qml.operation.DecompositionUndefinedError): qml.ControlledQubitUnitary(mat, wires=0, control_wires=1).decomposition() + with pytest.raises(qml.operation.DecompositionUndefinedError): + qml.ControlledQubitUnitary(X_broadcasted, wires=0, control_wires=1).decomposition() @pytest.mark.parametrize("n", (2, -1, -2)) def test_pow(self, n): - """Tests the metadata and unitary for a controlledQubitUnitary raised to a power.""" + """Tests the metadata and unitary for a ControlledQubitUnitary raised to a power.""" U1 = np.array( [ [0.73708696 + 0.61324932j, 0.27034258 + 0.08685028j], @@ -600,6 +823,32 @@ def test_pow(self, n): op_mat_to_pow = qml.math.linalg.matrix_power(op.data[0], n) assert qml.math.allclose(pow_ops[0].data[0], op_mat_to_pow) + @pytest.mark.parametrize("n", (2, -1, -2)) + def test_pow_broadcasted(self, n): + """Tests the metadata and unitary for a broadcasted + ControlledQubitUnitary raised to a power.""" + U1 = np.tensordot( + np.array([1j, -1.0, 1j]), + np.array( + [ + [0.73708696 + 0.61324932j, 0.27034258 + 0.08685028j], + [-0.24979544 - 0.1350197j, 0.95278437 + 0.1075819j], + ] + ), + axes=0, + ) + + op = qml.ControlledQubitUnitary(U1, control_wires=("b", "c"), wires="a") + + pow_ops = op.pow(n) + assert len(pow_ops) == 1 + + assert pow_ops[0].hyperparameters["u_wires"] == op.hyperparameters["u_wires"] + assert pow_ops[0].control_wires == op.control_wires + + op_mat_to_pow = qml.math.linalg.matrix_power(op.data[0], n) + assert qml.math.allclose(pow_ops[0].data[0], op_mat_to_pow) + def test_noninteger_pow(self): """Test that a ControlledQubitUnitary raised to a non-integer power raises an error.""" U1 = np.array( @@ -614,6 +863,21 @@ def test_noninteger_pow(self): with pytest.raises(qml.operation.PowUndefinedError): op.pow(0.12) + def test_noninteger_pow_broadcasted(self): + """Test that a ControlledQubitUnitary raised to a non-integer power raises an error.""" + U1 = np.array( + [ + [0.73708696 + 0.61324932j, 0.27034258 + 0.08685028j], + [-0.24979544 - 0.1350197j, 0.95278437 + 0.1075819j], + ] + * 3 + ) + + op = qml.ControlledQubitUnitary(U1, control_wires=("b", "c"), wires="a") + + with pytest.raises(qml.operation.PowUndefinedError): + op.pow(0.12) + label_data = [ (X, qml.QubitUnitary(X, wires=0)), diff --git a/tests/ops/qubit/test_parametric_ops.py b/tests/ops/qubit/test_parametric_ops.py index 0fbda53a635..d1c1533768c 100644 --- a/tests/ops/qubit/test_parametric_ops.py +++ b/tests/ops/qubit/test_parametric_ops.py @@ -14,6 +14,7 @@ """ Unit tests for the available built-in parametric qubit operations. """ +from functools import reduce import pytest import copy import numpy as np @@ -55,6 +56,41 @@ qml.DoubleExcitationMinus(0.123, wires=[0, 1, 2, 3]), ] +BROADCASTED_OPERATIONS = [ + qml.RX(np.array([0.142, -0.61, 2.3]), wires=0), + qml.RY(np.array([1.291, -0.10, 5.2]), wires=0), + qml.RZ(np.array([4.239, -3.21, 1.1]), wires=0), + qml.PauliRot(np.array([0.142, -0.61, 2.3]), "Y", wires=0), + qml.IsingXX(np.array([0.142, -0.61, 2.3]), wires=[0, 1]), + qml.IsingYY(np.array([0.142, -0.61, 2.3]), wires=[0, 1]), + qml.IsingZZ(np.array([0.142, -0.61, 2.3]), wires=[0, 1]), + qml.Rot(np.array([0.142, -0.61, 2.3]), 0.456, 0.789, wires=0), + qml.PhaseShift(np.array([2.12, 0.21, -6.2]), wires=0), + qml.ControlledPhaseShift(np.array([1.777, -0.1, 5.29]), wires=[0, 2]), + qml.CPhase(np.array([1.777, -0.1, 5.29]), wires=[0, 2]), + qml.MultiRZ(np.array([1.124, -2.31, 0.112]), wires=[1, 2, 3]), + qml.CRX(np.array([0.836, 0.21, -3.57]), wires=[2, 3]), + qml.CRY(np.array([0.721, 2.31, 0.983]), wires=[2, 3]), + qml.CRZ(np.array([0.554, 1.11, 2.2]), wires=[2, 3]), + qml.U1(np.array([0.142, -0.61, 2.3]), wires=0), + qml.U2(np.array([9.23, 1.33, 3.556]), np.array([2.134, 1.2, 0.2]), wires=0), + qml.U3( + np.array([2.009, 1.33, 3.556]), + np.array([2.134, 1.2, 0.2]), + np.array([0.78, 0.48, 0.83]), + wires=0, + ), + qml.CRot( + np.array([0.142, -0.61, 2.3]), + np.array([9.82, 0.2, 0.53]), + np.array([0.12, 2.21, 0.789]), + wires=[0, 1], + ), + qml.QubitUnitary(1j * np.array([[[1, 0], [0, -1]], [[0, 1], [1, 0]]]), wires=0), + qml.DiagonalQubitUnitary(np.array([[1.0, 1.0j], [1.0j, 1.0j]]), wires=1), +] + + NON_PARAMETRIZED_OPERATIONS = [ qml.S(wires=0), qml.SX(wires=0), @@ -78,9 +114,12 @@ ALL_OPERATIONS = NON_PARAMETRIZED_OPERATIONS + PARAMETRIZED_OPERATIONS +dot_broadcasted = lambda a, b: np.einsum("...ij,...jk->...ik", a, b) +multi_dot_broadcasted = lambda matrices: reduce(dot_broadcasted, matrices) + class TestOperations: - @pytest.mark.parametrize("op", ALL_OPERATIONS) + @pytest.mark.parametrize("op", ALL_OPERATIONS + BROADCASTED_OPERATIONS) def test_parametrized_op_copy(self, op, tol): """Tests that copied parametrized ops function as expected""" copied_op = copy.copy(op) @@ -100,6 +139,16 @@ def test_adjoint_unitaries(self, op, tol): np.testing.assert_allclose(res2, np.eye(2 ** len(op.wires)), atol=tol) assert op.wires == op_d.wires + @pytest.mark.parametrize("op", BROADCASTED_OPERATIONS) + def test_adjoint_unitaries_broadcasted(self, op, tol): + op_d = op.adjoint() + res1 = dot_broadcasted(op.matrix(), op_d.matrix()) + res2 = dot_broadcasted(op_d.matrix(), op.matrix()) + I = [np.eye(2 ** len(op.wires))] * op.batch_size + np.testing.assert_allclose(res1, I, atol=tol) + np.testing.assert_allclose(res2, I, atol=tol) + assert op.wires == op_d.wires + class TestParameterFrequencies: @pytest.mark.parametrize("op", PARAMETRIZED_OPERATIONS) @@ -128,6 +177,22 @@ def test_parameter_frequencies_match_generator(self, op, tol): class TestDecompositions: + @pytest.mark.parametrize("phi", [0.3, np.array([0.4, 2.1, 0.2])]) + def test_phase_decomposition(self, phi, tol): + """Tests that the decomposition of the Phase gate is correct""" + op = qml.PhaseShift(phi, wires=0) + res = op.decomposition() + + assert len(res) == 1 + + assert res[0].name == "RZ" + + assert res[0].wires == Wires([0]) + assert np.allclose(res[0].data[0], phi) + + decomposed_matrix = res[0].matrix() + assert np.allclose(decomposed_matrix, global_phase * op.matrix(), atol=tol, rtol=0) + def test_phase_decomposition(self, tol): """Tests that the decomposition of the Phase gate is correct""" phi = 0.3 @@ -146,6 +211,24 @@ def test_phase_decomposition(self, tol): assert np.allclose(decomposed_matrix, global_phase * op.matrix(), atol=tol, rtol=0) + def test_phase_decomposition_broadcasted(self, tol): + """Tests that the decomposition of the broadcasted Phase gate is correct""" + phi = np.array([0.3, 2.1, 0.2]) + op = qml.PhaseShift(phi, wires=0) + res = op.decomposition() + + assert len(res) == 1 + + assert res[0].name == "RZ" + + assert res[0].wires == Wires([0]) + assert qml.math.allclose(res[0].data[0], np.array([0.3, 2.1, 0.2])) + + decomposed_matrix = res[0].matrix() + global_phase = np.exp(-1j * phi / 2)[..., np.newaxis, np.newaxis] + + assert np.allclose(decomposed_matrix, global_phase * op.matrix(), atol=tol, rtol=0) + def test_Rot_decomposition(self): """Test the decomposition of Rot.""" phi = 0.432 @@ -165,6 +248,25 @@ def test_Rot_decomposition(self): assert isinstance(op, c) assert op.parameters == p + def test_Rot_decomposition_broadcasted(self): + """Test the decomposition of broadcasted Rot.""" + phi = np.array([0.1, 2.1]) + theta = np.array([0.4, -0.2]) + omega = np.array([1.1, 0.2]) + + ops1 = qml.Rot.compute_decomposition(phi, theta, omega, wires=0) + ops2 = qml.Rot(phi, theta, omega, wires=0).decomposition() + + assert len(ops1) == len(ops2) == 3 + + classes = [qml.RZ, qml.RY, qml.RZ] + params = [[phi], [theta], [omega]] + + for ops in [ops1, ops2]: + for c, p, op in zip(classes, params, ops): + assert isinstance(op, c) + assert op.parameters == p + def test_CRX_decomposition(self): """Test the decomposition for CRX.""" phi = 0.432 @@ -182,6 +284,23 @@ def test_CRX_decomposition(self): assert op.parameters == p assert op.wires == w + def test_CRX_decomposition_broadcasted(self): + """Test the decomposition for broadcasted CRX.""" + phi = np.array([0.1, 2.1]) + + ops1 = qml.CRX.compute_decomposition(phi, wires=[0, 1]) + ops2 = qml.CRX(phi, wires=(0, 1)).decomposition() + + classes = [qml.RZ, qml.RY, qml.CNOT, qml.RY, qml.CNOT, qml.RZ] + params = [[np.pi / 2], [phi / 2], [], [-phi / 2], [], [-np.pi / 2]] + wires = [Wires(1), Wires(1), Wires((0, 1)), Wires(1), Wires((0, 1)), Wires(1)] + + for ops in [ops1, ops2]: + for op, c, p, w in zip(ops, classes, params, wires): + assert isinstance(op, c) + assert qml.math.allclose(op.parameters, p) + assert op.wires == w + def test_CRY_decomposition(self): """Test the decomposition for CRY.""" phi = 0.432 @@ -196,12 +315,29 @@ def test_CRY_decomposition(self): for ops in [ops1, ops2]: for op, c, p, w in zip(ops, classes, params, wires): assert isinstance(op, c) - assert op.parameters == p + assert np.allclose(op.parameters, p) + assert op.wires == w + + def test_CRY_decomposition_broadcasted(self): + """Test the decomposition for broadcastedCRY.""" + phi = np.array([2.1, 0.2]) + + ops1 = qml.CRY.compute_decomposition(phi, wires=[0, 1]) + ops2 = qml.CRY(phi, wires=(0, 1)).decomposition() + + classes = [qml.RY, qml.CNOT, qml.RY, qml.CNOT] + params = [[phi / 2], [], [-phi / 2], []] + wires = [Wires(1), Wires((0, 1)), Wires(1), Wires((0, 1))] + + for ops in [ops1, ops2]: + for op, c, p, w in zip(ops, classes, params, wires): + assert isinstance(op, c) + assert np.allclose(op.parameters, p) assert op.wires == w def test_CRZ_decomposition(self): """Test the decomposition for CRZ.""" - phi = 0.432 + phi = 0.321 ops1 = qml.CRZ.compute_decomposition(phi, wires=[0, 1]) ops2 = qml.CRZ(phi, wires=(0, 1)).decomposition() @@ -213,11 +349,28 @@ def test_CRZ_decomposition(self): for ops in [ops1, ops2]: for op, c, p, w in zip(ops, classes, params, wires): assert isinstance(op, c) - assert op.parameters == p + assert np.allclose(op.parameters, p) + assert op.wires == w + + def test_CRZ_decomposition_broadcasted(self): + """Test the decomposition for broadcasted CRZ.""" + phi = np.array([0.6, 2.1]) + + ops1 = qml.CRZ.compute_decomposition(phi, wires=[0, 1]) + ops2 = qml.CRZ(phi, wires=(0, 1)).decomposition() + + classes = [qml.PhaseShift, qml.CNOT, qml.PhaseShift, qml.CNOT] + params = [[phi / 2], [], [-phi / 2], []] + wires = [Wires(1), Wires((0, 1)), Wires(1), Wires((0, 1))] + + for ops in [ops1, ops2]: + for op, c, p, w in zip(ops, classes, params, wires): + assert isinstance(op, c) + assert np.allclose(op.parameters, p) assert op.wires == w @pytest.mark.parametrize("phi, theta, omega", [[0.5, 0.6, 0.7], [0.1, -0.4, 0.7], [-10, 5, -1]]) - def test_CRot_decomposition(self, tol, phi, theta, omega, monkeypatch): + def test_CRot_decomposition(self, tol, phi, theta, omega): """Tests that the decomposition of the CRot gate is correct""" op = qml.CRot(phi, theta, omega, wires=[0, 1]) res = op.decomposition() @@ -233,6 +386,31 @@ def test_CRot_decomposition(self, tol, phi, theta, omega, monkeypatch): assert np.allclose(decomposed_matrix, op.matrix(), atol=tol, rtol=0) + @pytest.mark.parametrize( + "phi, theta, omega", + [ + [np.array([0.1, 0.2]), np.array([-0.4, 2.19]), np.array([0.7, -0.7])], + [np.array([0.1, 0.2, 0.9]), -0.4, np.array([0.7, 0.0, -0.7])], + ], + ) + def test_CRot_decomposition_broadcasted(self, tol, phi, theta, omega): + """Tests that the decomposition of the broadcasted CRot gate is correct""" + op = qml.CRot(phi, theta, omega, wires=[0, 1]) + res = op.decomposition() + + mats = [] + for i in reversed(res): + mat = i.matrix() + if len(i.wires) == 1: + I = np.eye(2)[np.newaxis] if qml.math.ndim(mat) == 3 else np.eye(2) + mats.append(np.kron(I, mat)) + else: + mats.append(mat) + + decomposed_matrix = multi_dot_broadcasted(mats) + + assert np.allclose(decomposed_matrix, op.matrix(), atol=tol, rtol=0) + def test_U1_decomposition(self): """Test the decomposition for U1.""" phi = 0.432 @@ -243,6 +421,17 @@ def test_U1_decomposition(self): assert res[0].name == res2[0].name == "PhaseShift" assert res[0].parameters == res2[0].parameters == [phi] + def test_U1_decomposition_broadcasted(self): + """Test the decomposition for broadcasted U1.""" + phi = np.array([0.6, 1.2, 9.5]) + res = qml.U1(phi, wires=0).decomposition() + res2 = qml.U1.compute_decomposition(phi, wires=0) + + assert len(res) == len(res2) == 1 + assert res[0].name == res2[0].name == "PhaseShift" + assert qml.math.allclose(res[0].parameters[0], phi) + assert qml.math.allclose(res2[0].parameters[0], phi) + def test_U2_decomposition(self): """Test the decomposition for U2.""" phi = 0.432 @@ -252,13 +441,29 @@ def test_U2_decomposition(self): ops2 = qml.U2(phi, lam, wires=0).decomposition() classes = [qml.Rot, qml.PhaseShift, qml.PhaseShift] - params = [[lam, np.pi / 2, -lam], [lam], [phi]] + params = [[lam, np.ones_like(lam) * np.pi / 2, -lam], [lam], [phi]] for ops in [ops1, ops2]: for op, c, p in zip(ops, classes, params): assert isinstance(op, c) assert op.parameters == p + def test_U2_decomposition_broadcasted(self): + """Test the decomposition for broadcasted U2.""" + phi = np.array([0.1, 2.1]) + lam = np.array([1.2, 4.9]) + + ops1 = qml.U2.compute_decomposition(phi, lam, wires=0) + ops2 = qml.U2(phi, lam, wires=0).decomposition() + + classes = [qml.Rot, qml.PhaseShift, qml.PhaseShift] + params = [[lam, np.ones_like(lam) * np.pi / 2, -lam], [lam], [phi]] + + for ops in [ops1, ops2]: + for op, c, p in zip(ops, classes, params): + assert isinstance(op, c) + assert np.allclose(op.parameters, p) + def test_U3_decomposition(self): """Test the decomposition for U3.""" theta = 0.654 @@ -276,6 +481,23 @@ def test_U3_decomposition(self): assert isinstance(op, c) assert op.parameters == p + def test_U3_decomposition_broadcasted(self): + """Test the decomposition for broadcasted U3.""" + theta = np.array([0.1, 2.1]) + phi = np.array([1.2, 4.9]) + lam = np.array([-1.7, 3.2]) + + ops1 = qml.U3.compute_decomposition(theta, phi, lam, wires=0) + ops2 = qml.U3(theta, phi, lam, wires=0).decomposition() + + classes = [qml.Rot, qml.PhaseShift, qml.PhaseShift] + params = [[lam, theta, -lam], [lam], [phi]] + + for ops in [ops1, ops2]: + for op, c, p in zip(ops, classes, params): + assert isinstance(op, c) + assert np.allclose(op.parameters, p) + def test_isingxx_decomposition(self, tol): """Tests that the decomposition of the IsingXX gate is correct""" param = 0.1234 @@ -304,6 +526,36 @@ def test_isingxx_decomposition(self, tol): assert np.allclose(decomposed_matrix, op.matrix(), atol=tol, rtol=0) + def test_isingxx_decomposition_broadcasted(self, tol): + """Tests that the decomposition of the broadcasted IsingXX gate is correct""" + param = np.array([-0.1, 0.2, 0.5]) + op = qml.IsingXX(param, wires=[3, 2]) + res = op.decomposition() + + assert len(res) == 3 + + assert res[0].wires == Wires([3, 2]) + assert res[1].wires == Wires([3]) + assert res[2].wires == Wires([3, 2]) + + assert res[0].name == "CNOT" + assert res[1].name == "RX" + assert res[2].name == "CNOT" + + mats = [] + for i in reversed(res): + mat = i.matrix() + if i.wires == Wires([3]): + # RX gate + I = np.eye(2)[np.newaxis] if len(mat.shape) == 3 else np.eye(2) + mats.append(np.kron(mat, I)) + else: + mats.append(mat) + + decomposed_matrix = multi_dot_broadcasted(mats) + + assert np.allclose(decomposed_matrix, op.matrix(), atol=tol, rtol=0) + def test_isingyy_decomposition(self, tol): """Tests that the decomposition of the IsingYY gate is correct""" param = 0.1234 @@ -332,6 +584,36 @@ def test_isingyy_decomposition(self, tol): assert np.allclose(decomposed_matrix, op.matrix(), atol=tol, rtol=0) + def test_isingyy_decomposition_broadcasted(self, tol): + """Tests that the decomposition of the broadcasted IsingYY gate is correct""" + param = np.array([-0.1, 0.2, 0.5]) + op = qml.IsingYY(param, wires=[3, 2]) + res = op.decomposition() + + assert len(res) == 3 + + assert res[0].wires == Wires([3, 2]) + assert res[1].wires == Wires([3]) + assert res[2].wires == Wires([3, 2]) + + assert res[0].name == "CY" + assert res[1].name == "RY" + assert res[2].name == "CY" + + mats = [] + for i in reversed(res): + mat = i.matrix() + if i.wires == Wires([3]): + # RY gate + I = np.eye(2)[np.newaxis] if len(mat.shape) == 3 else np.eye(2) + mats.append(np.kron(mat, I)) + else: + mats.append(mat) + + decomposed_matrix = multi_dot_broadcasted(mats) + + assert np.allclose(decomposed_matrix, op.matrix(), atol=tol, rtol=0) + def test_isingzz_decomposition(self, tol): """Tests that the decomposition of the IsingZZ gate is correct""" param = 0.1234 @@ -360,6 +642,36 @@ def test_isingzz_decomposition(self, tol): assert np.allclose(decomposed_matrix, op.matrix(), atol=tol, rtol=0) + def test_isingzz_decomposition_broadcasted(self, tol): + """Tests that the decomposition of the broadcasted IsingZZ gate is correct""" + param = np.array([-0.1, 0.2, 0.5]) + op = qml.IsingZZ(param, wires=[3, 2]) + res = op.decomposition() + + assert len(res) == 3 + + assert res[0].wires == Wires([3, 2]) + assert res[1].wires == Wires([2]) + assert res[2].wires == Wires([3, 2]) + + assert res[0].name == "CNOT" + assert res[1].name == "RZ" + assert res[2].name == "CNOT" + + mats = [] + for i in reversed(res): + mat = i.matrix() + if i.wires == Wires([2]): + # RX gate + I = np.eye(2)[np.newaxis] if len(mat.shape) == 3 else np.eye(2) + mats.append(np.kron(I, mat)) + else: + mats.append(mat) + + decomposed_matrix = multi_dot_broadcasted(mats) + + assert np.allclose(decomposed_matrix, op.matrix(), atol=tol, rtol=0) + @pytest.mark.parametrize("phi", [-0.1, 0.2, 0.5]) @pytest.mark.parametrize("cphase_op", [qml.ControlledPhaseShift, qml.CPhase]) def test_controlled_phase_shift_decomp(self, phi, cphase_op): @@ -411,6 +723,48 @@ def test_controlled_phase_shift_decomp(self, phi, cphase_op): assert np.allclose(decomposed_matrix, exp) + @pytest.mark.parametrize("cphase_op", [qml.ControlledPhaseShift, qml.CPhase]) + def test_controlled_phase_shift_decomp(self, cphase_op): + """Tests that the ControlledPhaseShift and CPhase operation + calculates the correct decomposition""" + phi = np.array([-0.2, 4.2, 1.8]) + op = cphase_op(phi, wires=[0, 2]) + decomp = op.decomposition() + + mats = [] + for i in reversed(decomp): + mat = i.matrix() + eye = np.eye(2)[np.newaxis] if np.ndim(mat) == 3 else np.eye(2) + if i.wires.tolist() == [0]: + mats.append(np.kron(mat, np.kron(eye, eye))) + elif i.wires.tolist() == [1]: + mats.append(np.kron(eye, np.kron(mat, eye))) + elif i.wires.tolist() == [2]: + mats.append(np.kron(np.kron(eye, eye), mat)) + elif isinstance(i, qml.CNOT) and i.wires.tolist() == [0, 1]: + mats.append(np.kron(mat, eye)) + elif isinstance(i, qml.CNOT) and i.wires.tolist() == [0, 2]: + mats.append( + np.array( + [ + [1, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 0, 1, 0], + ] + ) + ) + + decomposed_matrix = multi_dot_broadcasted(mats) + lam = np.exp(1j * phi) + exp = np.array([np.diag([1, 1, 1, 1, 1, el, 1, el]) for el in lam]) + + assert np.allclose(decomposed_matrix, exp) + class TestMatrix: def test_phase_shift(self, tol): @@ -418,6 +772,7 @@ def test_phase_shift(self, tol): # test identity for theta=0 assert np.allclose(qml.PhaseShift.compute_matrix(0), np.identity(2), atol=tol, rtol=0) + assert np.allclose(qml.PhaseShift(0, wires=0).matrix(), np.identity(2), atol=tol, rtol=0) assert np.allclose(qml.U1.compute_matrix(0), np.identity(2), atol=tol, rtol=0) # test arbitrary phase shift @@ -426,6 +781,12 @@ def test_phase_shift(self, tol): assert np.allclose(qml.PhaseShift.compute_matrix(phi), expected, atol=tol, rtol=0) assert np.allclose(qml.U1.compute_matrix(phi), expected, atol=tol, rtol=0) + # test arbitrary broadcasted phase shift + phi = np.array([0.5, 0.4, 0.3]) + expected = np.array([[[1, 0], [0, np.exp(1j * p)]] for p in phi]) + assert np.allclose(qml.PhaseShift.compute_matrix(phi), expected, atol=tol, rtol=0) + assert np.allclose(qml.U1.compute_matrix(phi), expected, atol=tol, rtol=0) + def test_rx(self, tol): """Test x rotation is correct""" @@ -438,6 +799,12 @@ def test_rx(self, tol): assert np.allclose(qml.RX.compute_matrix(np.pi / 2), expected, atol=tol, rtol=0) assert np.allclose(qml.RX(np.pi / 2, wires=0).matrix(), expected, atol=tol, rtol=0) + # test identity for broadcasted theta=pi/2 + expected = np.tensordot([1, 1], np.array([[1, -1j], [-1j, 1]]) / np.sqrt(2), axes=0) + pi_half = np.array([np.pi / 2, np.pi / 2]) + assert np.allclose(qml.RX.compute_matrix(pi_half), expected, atol=tol, rtol=0) + assert np.allclose(qml.RX(pi_half, wires=0).matrix(), expected, atol=tol, rtol=0) + # test identity for theta=pi expected = -1j * np.array([[0, 1], [1, 0]]) assert np.allclose(qml.RX.compute_matrix(np.pi), expected, atol=tol, rtol=0) @@ -455,6 +822,12 @@ def test_ry(self, tol): assert np.allclose(qml.RY.compute_matrix(np.pi / 2), expected, atol=tol, rtol=0) assert np.allclose(qml.RY(np.pi / 2, wires=0).matrix(), expected, atol=tol, rtol=0) + # test identity for broadcasted theta=pi/2 + expected = np.tensordot([1, 1], np.array([[1, -1], [1, 1]]) / np.sqrt(2), axes=0) + pi_half = np.array([np.pi / 2, np.pi / 2]) + assert np.allclose(qml.RY.compute_matrix(pi_half), expected, atol=tol, rtol=0) + assert np.allclose(qml.RY(pi_half, wires=0).matrix(), expected, atol=tol, rtol=0) + # test identity for theta=pi expected = np.array([[0, -1], [1, 0]]) assert np.allclose(qml.RY.compute_matrix(np.pi), expected, atol=tol, rtol=0) @@ -472,6 +845,12 @@ def test_rz(self, tol): assert np.allclose(qml.RZ.compute_matrix(np.pi / 2), expected, atol=tol, rtol=0) assert np.allclose(qml.RZ(np.pi / 2, wires=0).matrix(), expected, atol=tol, rtol=0) + # test identity for broadcasted theta=pi/2 + expected = np.tensordot([1, 1], np.diag(np.exp([-1j * np.pi / 4, 1j * np.pi / 4])), axes=0) + pi_half = np.array([np.pi / 2, np.pi / 2]) + assert np.allclose(qml.RZ.compute_matrix(pi_half), expected, atol=tol, rtol=0) + assert np.allclose(qml.RZ(pi_half, wires=0).matrix(), expected, atol=tol, rtol=0) + # test identity for theta=pi assert np.allclose(qml.RZ.compute_matrix(np.pi), -1j * Z, atol=tol, rtol=0) assert np.allclose(qml.RZ(np.pi, wires=0).matrix(), -1j * Z, atol=tol, rtol=0) @@ -502,6 +881,86 @@ def get_expected(theta): qml.IsingXX(param, wires=[0, 1]).matrix(), get_expected(param), atol=tol, rtol=0 ) + def test_isingxx_broadcasted(self, tol): + """Test that the broadcasted IsingXX operation is correct""" + z = np.zeros(3) + assert np.allclose(qml.IsingXX.compute_matrix(z), np.identity(4), atol=tol, rtol=0) + assert np.allclose(qml.IsingXX(z, wires=[0, 1]).matrix(), np.identity(4), atol=tol, rtol=0) + + def get_expected(theta): + expected = np.array([np.diag([np.cos(t / 2)] * 4) for t in theta], dtype=np.complex128) + sin_coeff = -1j * np.sin(theta / 2) + expected[:, 3, 0] = sin_coeff + expected[:, 2, 1] = sin_coeff + expected[:, 1, 2] = sin_coeff + expected[:, 0, 3] = sin_coeff + return expected + + param = np.array([np.pi / 2, np.pi]) + assert np.allclose(qml.IsingXX.compute_matrix(param), get_expected(param), atol=tol, rtol=0) + assert np.allclose( + qml.IsingXX(param, wires=[0, 1]).matrix(), get_expected(param), atol=tol, rtol=0 + ) + + param = np.array([2.152, np.pi / 2, 0.213]) + assert np.allclose(qml.IsingXX.compute_matrix(param), get_expected(param), atol=tol, rtol=0) + assert np.allclose( + qml.IsingXX(param, wires=[0, 1]).matrix(), get_expected(param), atol=tol, rtol=0 + ) + + def test_isingyy(self, tol): + """Test that the IsingYY operation is correct""" + assert np.allclose(qml.IsingYY.compute_matrix(0), np.identity(4), atol=tol, rtol=0) + assert np.allclose(qml.IsingYY(0, wires=[0, 1]).matrix(), np.identity(4), atol=tol, rtol=0) + + def get_expected(theta): + expected = np.array(np.diag([np.cos(theta / 2)] * 4), dtype=np.complex128) + sin_coeff = 1j * np.sin(theta / 2) + expected[3, 0] = sin_coeff + expected[2, 1] = -sin_coeff + expected[1, 2] = -sin_coeff + expected[0, 3] = sin_coeff + return expected + + param = np.pi / 2 + assert np.allclose(qml.IsingYY.compute_matrix(param), get_expected(param), atol=tol, rtol=0) + assert np.allclose( + qml.IsingYY(param, wires=[0, 1]).matrix(), get_expected(param), atol=tol, rtol=0 + ) + + param = np.pi + assert np.allclose(qml.IsingYY.compute_matrix(param), get_expected(param), atol=tol, rtol=0) + assert np.allclose( + qml.IsingYY(param, wires=[0, 1]).matrix(), get_expected(param), atol=tol, rtol=0 + ) + + def test_isingyy_broadcasted(self, tol): + """Test that the broadcasted IsingYY operation is correct""" + z = np.zeros(3) + assert np.allclose(qml.IsingYY.compute_matrix(z), np.identity(4), atol=tol, rtol=0) + assert np.allclose(qml.IsingYY(z, wires=[0, 1]).matrix(), np.identity(4), atol=tol, rtol=0) + + def get_expected(theta): + expected = np.array([np.diag([np.cos(t / 2)] * 4) for t in theta], dtype=np.complex128) + sin_coeff = 1j * np.sin(theta / 2) + expected[:, 3, 0] = sin_coeff + expected[:, 2, 1] = -sin_coeff + expected[:, 1, 2] = -sin_coeff + expected[:, 0, 3] = sin_coeff + return expected + + param = np.array([np.pi / 2, np.pi]) + assert np.allclose(qml.IsingYY.compute_matrix(param), get_expected(param), atol=tol, rtol=0) + assert np.allclose( + qml.IsingYY(param, wires=[0, 1]).matrix(), get_expected(param), atol=tol, rtol=0 + ) + + param = np.array([2.152, np.pi / 2, 0.213]) + assert np.allclose(qml.IsingYY.compute_matrix(param), get_expected(param), atol=tol, rtol=0) + assert np.allclose( + qml.IsingYY(param, wires=[0, 1]).matrix(), get_expected(param), atol=tol, rtol=0 + ) + def test_isingzz(self, tol): """Test that the IsingZZ operation is correct""" assert np.allclose(qml.IsingZZ.compute_matrix(0), np.identity(4), atol=tol, rtol=0) @@ -536,6 +995,37 @@ def get_expected(theta): qml.IsingZZ.compute_eigvals(param), np.diagonal(get_expected(param)), atol=tol, rtol=0 ) + def test_isingzz_broadcasted(self, tol): + """Test that the broadcasted IsingZZ operation is correct""" + z = np.zeros(3) + assert np.allclose(qml.IsingZZ.compute_matrix(z), np.identity(4), atol=tol, rtol=0) + assert np.allclose(qml.IsingZZ(z, wires=[0, 1]).matrix(), np.identity(4), atol=tol, rtol=0) + assert np.allclose( + qml.IsingZZ.compute_eigvals(z), np.diagonal(np.identity(4)), atol=tol, rtol=0 + ) + + def get_expected(theta): + neg_imag = np.exp(-1j * theta / 2) + plus_imag = np.exp(1j * theta / 2) + expected = np.array([np.diag([n, p, p, n]) for n, p in zip(neg_imag, plus_imag)]) + return expected + + param = np.array([np.pi / 2, np.pi]) + assert np.allclose(qml.IsingZZ.compute_matrix(param), get_expected(param), atol=tol, rtol=0) + assert np.allclose( + qml.IsingZZ(param, wires=[0, 1]).matrix(), get_expected(param), atol=tol, rtol=0 + ) + expected_eigvals = np.array([np.diag(m) for m in get_expected(param)]) + assert np.allclose(qml.IsingZZ.compute_eigvals(param), expected_eigvals, atol=tol, rtol=0) + + param = np.array([0.5, 1.2, np.pi / 8]) + assert np.allclose(qml.IsingZZ.compute_matrix(param), get_expected(param), atol=tol, rtol=0) + assert np.allclose( + qml.IsingZZ(param, wires=[0, 1]).matrix(), get_expected(param), atol=tol, rtol=0 + ) + expected_eigvals = np.array([np.diag(m) for m in get_expected(param)]) + assert np.allclose(qml.IsingZZ.compute_eigvals(param), expected_eigvals, atol=tol, rtol=0) + @pytest.mark.tf def test_isingzz_matrix_tf(self, tol): """Tests the matrix representation for IsingZZ for tensorflow, since the method contains @@ -553,6 +1043,22 @@ def get_expected(theta): param = tf.Variable(np.pi) assert np.allclose(qml.IsingZZ.compute_matrix(param), get_expected(np.pi), atol=tol, rtol=0) + @pytest.mark.tf + def test_isingzz_matrix_tf_broadcasted(self, tol): + """Tests the matrix representation for broadcasted IsingZZ for tensorflow, + since the method contains different logic for this framework""" + import tensorflow as tf + + def get_expected(theta): + neg_imag = np.exp(-1j * theta / 2) + plus_imag = np.exp(1j * theta / 2) + expected = np.array([np.diag([n, p, p, n]) for n, p in zip(neg_imag, plus_imag)]) + return expected + + param = np.array([np.pi, 0.1242]) + param_tf = tf.Variable(param) + assert np.allclose(qml.IsingZZ.compute_matrix(param), get_expected(param), atol=tol, rtol=0) + def test_Rot(self, tol): """Test arbitrary single qubit rotation is correct""" @@ -580,7 +1086,38 @@ def arbitrary_rotation(x, y, z): qml.Rot(a, b, c, wires=0).matrix(), arbitrary_rotation(a, b, c), atol=tol, rtol=0 ) - def test_CRx(self, tol): + def test_Rot_broadcasted(self, tol): + """Test broadcasted arbitrary single qubit rotation is correct""" + + # test identity for phi,theta,omega=0 + z = np.zeros(5) + assert np.allclose(qml.Rot.compute_matrix(z, z, z), np.identity(2), atol=tol, rtol=0) + assert np.allclose(qml.Rot(z, z, z, wires=0).matrix(), np.identity(2), atol=tol, rtol=0) + + # expected result + def arbitrary_rotation(x, y, z): + """arbitrary single qubit rotation""" + c = np.cos(y / 2) + s = np.sin(y / 2) + return np.array( + [ + [ + [np.exp(-0.5j * (_x + _z)) * _c, -np.exp(0.5j * (_x - _z)) * _s], + [np.exp(-0.5j * (_x - _z)) * _s, np.exp(0.5j * (_x + _z)) * _c], + ] + for _x, _z, _c, _s in zip(x, z, c, s) + ] + ) + + a, b, c = np.array([0.432, -0.124]), np.array([-0.152, 2.912]), np.array([0.9234, -9.2]) + assert np.allclose( + qml.Rot.compute_matrix(a, b, c), arbitrary_rotation(a, b, c), atol=tol, rtol=0 + ) + assert np.allclose( + qml.Rot(a, b, c, wires=0).matrix(), arbitrary_rotation(a, b, c), atol=tol, rtol=0 + ) + + def test_CRX(self, tol): """Test controlled x rotation is correct""" # test identity for theta=0 @@ -588,7 +1125,7 @@ def test_CRx(self, tol): assert np.allclose(qml.CRX(0, wires=[0, 1]).matrix(), np.identity(4), atol=tol, rtol=0) # test identity for theta=pi/2 - expected = np.array( + expected_pi_half = np.array( [ [1, 0, 0, 0], [0, 1, 0, 0], @@ -596,13 +1133,21 @@ def test_CRx(self, tol): [0, 0, -1j / np.sqrt(2), 1 / np.sqrt(2)], ] ) - assert np.allclose(qml.CRX.compute_matrix(np.pi / 2), expected, atol=tol, rtol=0) - assert np.allclose(qml.CRX(np.pi / 2, wires=[0, 1]).matrix(), expected, atol=tol, rtol=0) + assert np.allclose(qml.CRX.compute_matrix(np.pi / 2), expected_pi_half, atol=tol, rtol=0) + assert np.allclose( + qml.CRX(np.pi / 2, wires=[0, 1]).matrix(), expected_pi_half, atol=tol, rtol=0 + ) # test identity for theta=pi - expected = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, -1j], [0, 0, -1j, 0]]) - assert np.allclose(qml.CRX.compute_matrix(np.pi), expected, atol=tol, rtol=0) - assert np.allclose(qml.CRX(np.pi, wires=[0, 1]).matrix(), expected, atol=tol, rtol=0) + expected_pi = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, -1j], [0, 0, -1j, 0]]) + assert np.allclose(qml.CRX.compute_matrix(np.pi), expected_pi, atol=tol, rtol=0) + assert np.allclose(qml.CRX(np.pi, wires=[0, 1]).matrix(), expected_pi, atol=tol, rtol=0) + + # test broadcasting + param = np.array([np.pi / 2, np.pi]) + expected = [expected_pi_half, expected_pi] + assert np.allclose(qml.CRX.compute_matrix(param), expected, atol=tol, rtol=0) + assert np.allclose(qml.CRX(param, wires=[0, 1]).matrix(), expected, atol=tol, rtol=0) def test_CRY(self, tol): """Test controlled y rotation is correct""" @@ -612,7 +1157,7 @@ def test_CRY(self, tol): assert np.allclose(qml.CRY(0, wires=[0, 1]).matrix(), np.identity(4), atol=tol, rtol=0) # test identity for theta=pi/2 - expected = np.array( + expected_pi_half = np.array( [ [1, 0, 0, 0], [0, 1, 0, 0], @@ -620,13 +1165,21 @@ def test_CRY(self, tol): [0, 0, 1 / np.sqrt(2), 1 / np.sqrt(2)], ] ) - assert np.allclose(qml.CRY.compute_matrix(np.pi / 2), expected, atol=tol, rtol=0) - assert np.allclose(qml.CRY(np.pi / 2, wires=[0, 1]).matrix(), expected, atol=tol, rtol=0) + assert np.allclose(qml.CRY.compute_matrix(np.pi / 2), expected_pi_half, atol=tol, rtol=0) + assert np.allclose( + qml.CRY(np.pi / 2, wires=[0, 1]).matrix(), expected_pi_half, atol=tol, rtol=0 + ) # test identity for theta=pi - expected = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, -1], [0, 0, 1, 0]]) - assert np.allclose(qml.CRY.compute_matrix(np.pi), expected, atol=tol, rtol=0) - assert np.allclose(qml.CRY(np.pi, wires=[0, 1]).matrix(), expected, atol=tol, rtol=0) + expected_pi = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, -1], [0, 0, 1, 0]]) + assert np.allclose(qml.CRY.compute_matrix(np.pi), expected_pi, atol=tol, rtol=0) + assert np.allclose(qml.CRY(np.pi, wires=[0, 1]).matrix(), expected_pi, atol=tol, rtol=0) + + # test broadcasting + param = np.array([np.pi / 2, np.pi]) + expected = [expected_pi_half, expected_pi] + assert np.allclose(qml.CRY.compute_matrix(param), expected, atol=tol, rtol=0) + assert np.allclose(qml.CRY(param, wires=[0, 1]).matrix(), expected, atol=tol, rtol=0) def test_CRZ(self, tol): """Test controlled z rotation is correct""" @@ -636,7 +1189,7 @@ def test_CRZ(self, tol): assert np.allclose(qml.CRZ(0, wires=[0, 1]).matrix(), np.identity(4), atol=tol, rtol=0) # test identity for theta=pi/2 - expected = np.array( + expected_pi_half = np.array( [ [1, 0, 0, 0], [0, 1, 0, 0], @@ -644,13 +1197,21 @@ def test_CRZ(self, tol): [0, 0, 0, np.exp(1j * np.pi / 4)], ] ) - assert np.allclose(qml.CRZ.compute_matrix(np.pi / 2), expected, atol=tol, rtol=0) - assert np.allclose(qml.CRZ(np.pi / 2, wires=[0, 1]).matrix(), expected, atol=tol, rtol=0) + assert np.allclose(qml.CRZ.compute_matrix(np.pi / 2), expected_pi_half, atol=tol, rtol=0) + assert np.allclose( + qml.CRZ(np.pi / 2, wires=[0, 1]).matrix(), expected_pi_half, atol=tol, rtol=0 + ) # test identity for theta=pi - expected = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, -1j, 0], [0, 0, 0, 1j]]) - assert np.allclose(qml.CRZ.compute_matrix(np.pi), expected, atol=tol, rtol=0) - assert np.allclose(qml.CRZ(np.pi, wires=[0, 1]).matrix(), expected, atol=tol, rtol=0) + expected_pi = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, -1j, 0], [0, 0, 0, 1j]]) + assert np.allclose(qml.CRZ.compute_matrix(np.pi), expected_pi, atol=tol, rtol=0) + assert np.allclose(qml.CRZ(np.pi, wires=[0, 1]).matrix(), expected_pi, atol=tol, rtol=0) + + # test broadcasting + param = np.array([np.pi / 2, np.pi]) + expected = [expected_pi_half, expected_pi] + assert np.allclose(qml.CRZ.compute_matrix(param), expected, atol=tol, rtol=0) + assert np.allclose(qml.CRZ(param, wires=[0, 1]).matrix(), expected, atol=tol, rtol=0) def test_CRot(self, tol): """Test controlled arbitrary rotation is correct""" @@ -692,6 +1253,49 @@ def arbitrary_Crotation(x, y, z): rtol=0, ) + def test_CRot_broadcasted(self, tol): + """Test broadcasted controlled arbitrary rotation is correct""" + + # test identity for phi,theta,omega=0 + z = np.zeros(5) + assert np.allclose(qml.CRot.compute_matrix(z, z, z), np.identity(4), atol=tol, rtol=0) + assert np.allclose( + qml.CRot(z, z, z, wires=[0, 1]).matrix(), np.identity(4), atol=tol, rtol=0 + ) + + # test -i*CY for phi,theta,omega=pi + expected = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, -1], [0, 0, 1, 0]]) + pi = np.ones(3) * np.pi + assert np.allclose(qml.CRot.compute_matrix(pi, pi, pi), expected, atol=tol, rtol=0) + assert np.allclose(qml.CRot(pi, pi, pi, wires=[0, 1]).matrix(), expected, atol=tol, rtol=0) + + def arbitrary_Crotation(x, y, z): + """controlled arbitrary single qubit rotation""" + c = np.cos(y / 2) + s = np.sin(y / 2) + return np.array( + [ + [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, np.exp(-0.5j * (_x + _z)) * _c, -np.exp(0.5j * (_x - _z)) * _s], + [0, 0, np.exp(-0.5j * (_x - _z)) * _s, np.exp(0.5j * (_x + _z)) * _c], + ] + for _x, _z, _c, _s in zip(x, z, c, s) + ] + ) + + a, b, c = np.array([0.432, -0.124]), np.array([-0.152, 2.912]), np.array([0.9234, -9.2]) + assert np.allclose( + qml.CRot.compute_matrix(a, b, c), arbitrary_Crotation(a, b, c), atol=tol, rtol=0 + ) + assert np.allclose( + qml.CRot(a, b, c, wires=[0, 1]).matrix(), + arbitrary_Crotation(a, b, c), + atol=tol, + rtol=0, + ) + def test_U2_gate(self, tol): """Test U2 gate matrix matches the documentation""" phi = 0.432 @@ -702,6 +1306,34 @@ def test_U2_gate(self, tol): assert np.allclose(qml.U2.compute_matrix(phi, lam), expected, atol=tol, rtol=0) assert np.allclose(qml.U2(phi, lam, wires=[0]).matrix(), expected, atol=tol, rtol=0) + def test_U2_gate_broadcasted(self, tol): + """Test U2 gate matrix matches the documentation""" + + def get_expected(phi, lam): + one = np.ones_like(phi) * np.ones_like(lam) + expected = np.array( + [[one, -np.exp(1j * lam * one)], [np.exp(1j * phi * one), np.exp(1j * (phi + lam))]] + ) / np.sqrt(2) + return np.transpose(expected, (2, 0, 1)) + + phi = np.array([0.1, 2.1, -0.6]) + lam = np.array([1.2, 4.9, 0.7]) + expected = get_expected(phi, lam) + assert np.allclose(qml.U2.compute_matrix(phi, lam), expected, atol=tol, rtol=0) + assert np.allclose(qml.U2(phi, lam, wires=[0]).matrix(), expected, atol=tol, rtol=0) + + phi = 0.432 + lam = np.array([1.2, 4.9, 0.7]) + expected = get_expected(phi, lam) + assert np.allclose(qml.U2.compute_matrix(phi, lam), expected, atol=tol, rtol=0) + assert np.allclose(qml.U2(phi, lam, wires=[0]).matrix(), expected, atol=tol, rtol=0) + + phi = np.array([0.1, 2.1, -0.6]) + lam = -0.12 + expected = get_expected(phi, lam) + assert np.allclose(qml.U2.compute_matrix(phi, lam), expected, atol=tol, rtol=0) + assert np.allclose(qml.U2(phi, lam, wires=[0]).matrix(), expected, atol=tol, rtol=0) + def test_U3_gate(self, tol): """Test U3 gate matrix matches the documentation""" theta = 0.65 @@ -721,11 +1353,32 @@ def test_U3_gate(self, tol): assert np.allclose(qml.U3.compute_matrix(theta, phi, lam), expected, atol=tol, rtol=0) assert np.allclose(qml.U3(theta, phi, lam, wires=[0]).matrix(), expected, atol=tol, rtol=0) + @pytest.mark.parametrize("theta", [0.432, np.array([0.1, 2.1, -0.6])]) + @pytest.mark.parametrize("phi", [0.654, np.array([1.2, 4.9, 0.7])]) + @pytest.mark.parametrize("lam", [0.218, np.array([-1.7, 3.2, 1.9])]) + def test_U3_gate_broadcasted(self, tol, theta, phi, lam): + """Test broadcasted U3 gate matrix matches the documentation""" + if np.ndim(theta) == np.ndim(phi) == np.ndim(lam) == 0: + pytest.skip("The scalars-only case is covered in a separate test.") + one = np.ones_like(phi) * np.ones_like(lam) * np.ones_like(theta) + expected = np.array( + [ + [one * np.cos(theta / 2), one * -np.exp(1j * lam) * np.sin(theta / 2)], + [ + one * np.exp(1j * phi) * np.sin(theta / 2), + np.exp(1j * (phi + lam)) * np.cos(theta / 2), + ], + ] + ) + expected = np.transpose(expected, (2, 0, 1)) + assert np.allclose(qml.U3.compute_matrix(theta, phi, lam), expected, atol=tol, rtol=0) + assert np.allclose(qml.U3(theta, phi, lam, wires=[0]).matrix(), expected, atol=tol, rtol=0) + @pytest.mark.parametrize("phi", [-0.1, 0.2, 0.5]) @pytest.mark.parametrize("cphase_op", [qml.ControlledPhaseShift, qml.CPhase]) def test_controlled_phase_shift_matrix_and_eigvals(self, phi, cphase_op): - """Tests that the ControlledPhaseShift and CPhase operation calculate the correct matrix and - eigenvalues""" + """Tests that the ControlledPhaseShift and CPhase operation calculate the correct + matrix and eigenvalues""" op = cphase_op(phi, wires=[0, 1]) res = op.matrix() exp = ControlledPhaseShift(phi) @@ -734,6 +1387,22 @@ def test_controlled_phase_shift_matrix_and_eigvals(self, phi, cphase_op): res = op.eigvals() assert np.allclose(res, np.diag(exp)) + @pytest.mark.parametrize("cphase_op", [qml.ControlledPhaseShift, qml.CPhase]) + def test_controlled_phase_shift_matrix_and_eigvals_broadcasted(self, cphase_op): + """Tests that the ControlledPhaseShift and CPhase operation calculate the + correct matrix and eigenvalues for broadcasted parameters""" + phi = np.array([0.2, np.pi / 2, -0.1]) + op = cphase_op(phi, wires=[0, 1]) + res = op.matrix() + expected = np.array([np.eye(4, dtype=complex)] * 3) + expected[..., 3, 3] = np.exp(1j * phi) + assert np.allclose(res, expected) + + res = op.eigvals() + exp_eigvals = np.ones((3, 4), dtype=complex) + exp_eigvals[..., 3] = np.exp(1j * phi) + assert np.allclose(res, exp_eigvals) + class TestGrad: device_methods = [ @@ -1212,6 +1881,12 @@ def test_PauliRot_matrix_parametric(self, theta, pauli_word, expected_matrix, to assert np.allclose(res, expected, atol=tol, rtol=0) + # Test broadcasted matrix + res = qml.PauliRot.compute_matrix(np.ones(3) * theta, pauli_word) + expected = [expected_matrix(theta)] * 3 + + assert np.allclose(res, expected, atol=tol, rtol=0) + @pytest.mark.parametrize( "theta,pauli_word,expected_matrix", PAULI_ROT_MATRIX_TEST_DATA, @@ -1224,6 +1899,11 @@ def test_PauliRot_matrix(self, theta, pauli_word, expected_matrix, tol): assert np.allclose(res, expected, atol=tol, rtol=0) + res = qml.PauliRot.compute_matrix(np.ones(5) * theta, pauli_word) + expected = [expected_matrix] * 5 + + assert np.allclose(res, expected, atol=tol, rtol=0) + @pytest.mark.parametrize( "theta,pauli_word,compressed_pauli_word,wires,compressed_wires", [ @@ -1247,6 +1927,14 @@ def test_PauliRot_matrix_identity( assert np.allclose(res, expected, atol=tol, rtol=0) + batch = np.ones(3) * theta + res = qml.PauliRot.compute_matrix(batch, pauli_word) + expected = qml.operation.expand_matrix( + qml.PauliRot.compute_matrix(batch, compressed_pauli_word), compressed_wires, wires + ) + + assert np.allclose(res, expected, atol=tol, rtol=0) + def test_PauliRot_wire_as_int(self): """Test that passing a single wire as an integer works.""" @@ -1278,10 +1966,24 @@ def test_PauliRot_all_Identity(self): assert len(decomp_ops) == 0 - def test_PauliRot_decomposition_ZZ(self): - """Test that the decomposition for a ZZ rotation is correct.""" + def test_PauliRot_all_Identity_broadcasted(self): + """Test handling of the broadcasted all-identity Pauli.""" - theta = 0.4 + theta = np.array([0.4, 0.9, 1.2]) + op = qml.PauliRot(theta, "II", wires=[0, 1]) + decomp_ops = op.decomposition() + + phases = np.exp(-1j * theta / 2) + assert np.allclose(op.eigvals(), np.outer(phases, np.ones(4))) + mat = op.matrix() + for phase, sub_mat in zip(phases, mat): + assert np.allclose(sub_mat, phase * np.eye(4)) + + assert len(decomp_ops) == 0 + + @pytest.mark.parametrize("theta", [0.4, np.array([np.pi / 3, 0.1, -0.9])]) + def test_PauliRot_decomposition_ZZ(self, theta): + """Test that the decomposition for a ZZ rotation is correct.""" op = qml.PauliRot(theta, "ZZ", wires=[0, 1]) decomp_ops = op.decomposition() @@ -1290,12 +1992,12 @@ def test_PauliRot_decomposition_ZZ(self): assert decomp_ops[0].name == "MultiRZ" assert decomp_ops[0].wires == Wires([0, 1]) - assert decomp_ops[0].data[0] == theta + assert np.allclose(decomp_ops[0].data[0], theta) - def test_PauliRot_decomposition_XY(self): + @pytest.mark.parametrize("theta", [0.4, np.array([np.pi / 3, 0.1, -0.9])]) + def test_PauliRot_decomposition_XY(self, theta): """Test that the decomposition for a XY rotation is correct.""" - theta = 0.4 op = qml.PauliRot(theta, "XY", wires=[0, 1]) decomp_ops = op.decomposition() @@ -1305,26 +2007,24 @@ def test_PauliRot_decomposition_XY(self): assert decomp_ops[0].wires == Wires([0]) assert decomp_ops[1].name == "RX" - assert decomp_ops[1].wires == Wires([1]) assert decomp_ops[1].data[0] == np.pi / 2 assert decomp_ops[2].name == "MultiRZ" assert decomp_ops[2].wires == Wires([0, 1]) - assert decomp_ops[2].data[0] == theta + assert np.allclose(decomp_ops[2].data[0], theta) assert decomp_ops[3].name == "Hadamard" assert decomp_ops[3].wires == Wires([0]) assert decomp_ops[4].name == "RX" - assert decomp_ops[4].wires == Wires([1]) assert decomp_ops[4].data[0] == -np.pi / 2 - def test_PauliRot_decomposition_XIYZ(self): + @pytest.mark.parametrize("theta", [0.4, np.array([np.pi / 3, 0.1, -0.9])]) + def test_PauliRot_decomposition_XIYZ(self, theta): """Test that the decomposition for a XIYZ rotation is correct.""" - theta = 0.4 op = qml.PauliRot(theta, "XIYZ", wires=[0, 1, 2, 3]) decomp_ops = op.decomposition() @@ -1340,7 +2040,7 @@ def test_PauliRot_decomposition_XIYZ(self): assert decomp_ops[2].name == "MultiRZ" assert decomp_ops[2].wires == Wires([0, 2, 3]) - assert decomp_ops[2].data[0] == theta + assert np.allclose(decomp_ops[2].data[0], theta) assert decomp_ops[3].name == "Hadamard" assert decomp_ops[3].wires == Wires([0]) @@ -1361,7 +2061,7 @@ def test_differentiability(self, angle, pauli_word, tol): def circuit(theta): qml.PauliRot(theta, pauli_word, wires=[0, 1]) - return qml.expval(qml.PauliZ(0)) + return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) res = circuit(angle) gradient = np.squeeze(qml.grad(circuit)(angle)) @@ -1370,6 +2070,27 @@ def circuit(theta): 0.5 * (circuit(angle + np.pi / 2) - circuit(angle - np.pi / 2)), abs=tol ) + @pytest.mark.parametrize("pauli_word", ["XX", "YY", "ZZ"]) + def test_differentiability_broadcasted(self, pauli_word, tol): + """Test that differentiation of PauliRot works with broadcasted parameters.""" + + dev = qml.device("default.qubit", wires=2) + + @qml.qnode(dev) + def circuit(theta): + qml.PauliRot(theta, pauli_word, wires=[0, 1]) + return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) + + angle = npp.linspace(0, 2 * np.pi, 7, requires_grad=True) + res = circuit(angle) + jac = qml.jacobian(circuit)(angle) + + assert np.allclose( + jac, + 0.5 * (circuit(angle + np.pi / 2) - circuit(angle - np.pi / 2)), + atol=tol, + ) + @pytest.mark.parametrize("angle", npp.linspace(0, 2 * np.pi, 7, requires_grad=True)) def test_decomposition_integration(self, angle, tol): """Test that the decompositon of PauliRot yields the same results.""" @@ -1522,25 +2243,23 @@ def test_MultiRZ_matrix_parametric(self, theta, wires, expected_matrix, tol): assert np.allclose(res_static, expected, atol=tol, rtol=0) assert np.allclose(res_dynamic, expected, atol=tol, rtol=0) - def test_MultiRZ_matrix_expand(self, tol): - """Test that the MultiRZ matrix respects the wire order.""" + @pytest.mark.parametrize("num_wires", [1, 2, 3]) + def test_MultiRZ_matrix_broadcasted(self, num_wires, tol): + """Test that the MultiRZ matrix is correct for broadcasted parameters.""" - res = qml.MultiRZ(0.1, wires=[0, 1]).matrix(wire_order=[1, 0]) - expected = np.array( - [ - [0.99875026 - 0.04997917j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], - [0.0 + 0.0j, 0.99875026 + 0.04997917j, 0.0 + 0.0j, 0.0 + 0.0j], - [0.0 + 0.0j, 0.0 + 0.0j, 0.99875026 + 0.04997917j, 0.0 + 0.0j], - [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.99875026 - 0.04997917j], - ] - ) + theta = np.linspace(0, 2 * np.pi, 7)[:3] + res_static = qml.MultiRZ.compute_matrix(theta, num_wires) + res_dynamic = qml.MultiRZ(theta, wires=list(range(num_wires))).matrix() + signs = reduce(np.kron, [np.array([1, -1])] * num_wires) / 2 + expected = [np.diag(np.exp(-1j * signs * p)) for p in theta] - assert np.allclose(res, expected, atol=tol, rtol=0) + assert np.allclose(res_static, expected, atol=tol, rtol=0) + assert np.allclose(res_dynamic, expected, atol=tol, rtol=0) - def test_MultiRZ_decomposition_ZZ(self): + @pytest.mark.parametrize("theta", [0.4, np.array([np.pi / 3, 0.1, -0.9])]) + def test_MultiRZ_decomposition_ZZ(self, theta): """Test that the decomposition for a ZZ rotation is correct.""" - theta = 0.4 op = qml.MultiRZ(theta, wires=[0, 1]) decomp_ops = op.decomposition() @@ -1550,15 +2269,15 @@ def test_MultiRZ_decomposition_ZZ(self): assert decomp_ops[1].name == "RZ" assert decomp_ops[1].wires == Wires([0]) - assert decomp_ops[1].data[0] == theta + assert np.allclose(decomp_ops[1].data[0], theta) assert decomp_ops[2].name == "CNOT" assert decomp_ops[2].wires == Wires([1, 0]) - def test_MultiRZ_decomposition_ZZZ(self): + @pytest.mark.parametrize("theta", [0.4, np.array([np.pi / 3, 0.1, -0.9])]) + def test_MultiRZ_decomposition_ZZZ(self, theta): """Test that the decomposition for a ZZZ rotation is correct.""" - theta = 0.4 op = qml.MultiRZ(theta, wires=[0, 2, 3]) decomp_ops = op.decomposition() @@ -1571,7 +2290,7 @@ def test_MultiRZ_decomposition_ZZZ(self): assert decomp_ops[2].name == "RZ" assert decomp_ops[2].wires == Wires([0]) - assert decomp_ops[2].data[0] == theta + assert np.allclose(decomp_ops[2].data[0], theta) assert decomp_ops[3].name == "CNOT" assert decomp_ops[3].wires == Wires([2, 0]) @@ -1599,6 +2318,27 @@ def circuit(theta): 0.5 * (circuit(angle + np.pi / 2) - circuit(angle - np.pi / 2)), abs=tol ) + def test_differentiability_broadcasted(self, tol): + """Test that differentiation of MultiRZ works.""" + + dev = qml.device("default.qubit", wires=2) + + @qml.qnode(dev) + def circuit(theta): + qml.Hadamard(0) + qml.Hadamard(1) + qml.MultiRZ(theta, wires=[0, 1]) + + return qml.expval(qml.PauliX(0) @ qml.PauliX(1)) + + angle = npp.linspace(0, 2 * np.pi, 7, requires_grad=True) + res = circuit(angle) + jac = qml.jacobian(circuit)(angle) + + assert np.allclose( + jac, 0.5 * (circuit(angle + np.pi / 2) - circuit(angle - np.pi / 2)), atol=tol + ) + @pytest.mark.parametrize("angle", npp.linspace(0, 2 * np.pi, 7, requires_grad=True)) def test_decomposition_integration(self, angle, tol): """Test that the decompositon of MultiRZ yields the same results.""" @@ -1639,6 +2379,28 @@ def test_multirz_generator(self, qubits, mocker): op.generator() spy.assert_not_called() + @pytest.mark.parametrize("theta", [0.4, np.array([np.pi / 3, 0.1, -0.9])]) + def test_multirz_eigvals(self, theta, tol): + """Test that the eigenvalues of the MultiRZ gate are correct.""" + op = qml.MultiRZ(theta, wires=range(3)) + + pos_phase = np.exp(1j * theta / 2) + neg_phase = np.exp(-1j * theta / 2) + expected = np.array( + [ + neg_phase, + pos_phase, + pos_phase, + neg_phase, + pos_phase, + neg_phase, + neg_phase, + pos_phase, + ] + ).T + eigvals = op.eigvals() + assert np.allclose(eigvals, expected) + label_data = [ ( @@ -1715,6 +2477,19 @@ def test_multirz_generator(self, qubits, mocker): ), ] +# labels with broadcasted parameters are not implemented properly yet, the parameters are truncated +label_data_broadcasted = [ + (qml.RX(np.array([1.23, 4.56]), wires=0), "RX", "RX", "RX", "RX⁻¹"), + (qml.PauliRot(np.array([1.23, 4.5]), "XYZ", wires=(0, 1, 2)), "RXYZ", "RXYZ", "RXYZ", "RXYZ⁻¹"), + ( + qml.U3(np.array([0.1, 0.2]), np.array([-0.1, -0.2]), np.array([1.2, -0.1]), wires=0), + "U3", + "U3", + "U3", + "U3⁻¹", + ), +] + class TestLabel: """Test the label method on parametric ops""" @@ -1731,6 +2506,18 @@ def test_label_method(self, op, label1, label2, label3, label4): assert op.label(decimals=0) == label4 op.inv() + @pytest.mark.parametrize("op, label1, label2, label3, label4", label_data_broadcasted) + def test_label_method_broadcasted(self, op, label1, label2, label3, label4): + """Test label method with plain scalers.""" + + assert op.label() == label1 + assert op.label(decimals=2) == label2 + assert op.label(decimals=0) == label3 + + op.inv() + assert op.label(decimals=0) == label4 + op.inv() + @pytest.mark.tf def test_label_tf(self): """Test label methods work with tensorflow variables""" @@ -1786,6 +2573,24 @@ def test_string_parameter(self): op3 = qml.Rot("x", "y", "z", wires=0) assert op3.label(decimals=0) == "Rot\n(x,\ny,\nz)" + def test_string_parameter_broadcasted(self): + """Test labelling works (i.e. does not raise an Error) if variable is a + string instead of a float.""" + + x = np.array(["x0", "x1", "x2"]) + y = np.array(["y0", "y1", "y2"]) + z = np.array(["z0", "z1", "z2"]) + + op1 = qml.RX(x, wires=0) + assert op1.label() == "RX" + assert op1.label(decimals=0) == "RX" + + op2 = qml.CRX(y, wires=(0, 1)) + assert op2.label(decimals=0) == "RX" + + op3 = qml.Rot(x, y, z, wires=0) + assert op3.label(decimals=0) == "Rot" + pow_parametric_ops = ( qml.RX(1.234, wires=0), @@ -1802,6 +2607,21 @@ def test_string_parameter(self): qml.IsingXX(-2.345, wires=(0, 1)), qml.IsingYY(3.1652, wires=(0, 1)), qml.IsingZZ(1.789, wires=("a", "b")), + # broadcasted ops + qml.RX(np.array([1.234, 4.129]), wires=0), + qml.RY(np.array([2.345, 6, 789]), wires=0), + qml.RZ(np.array([3.456]), wires=0), + qml.PhaseShift(np.array([6.0, 7.0, 8.0]), wires=0), + qml.ControlledPhaseShift(np.array([0.234]), wires=(0, 1)), + qml.MultiRZ(np.array([-0.4432, -0.231, 0.251]), wires=(0, 1, 2)), + qml.PauliRot(np.array([0.5, 0.9]), "X", wires=0), + qml.CRX(np.array([-6.5432, 0.7653]), wires=(0, 1)), + qml.CRY(np.array([-0.543, 0.21]), wires=(0, 1)), + qml.CRZ(np.array([1.234, 5.678]), wires=(0, 1)), + qml.U1(np.array([1.23, 0.241]), wires=0), + qml.IsingXX(np.array([9.32, -2.345]), wires=(0, 1)), + qml.IsingYY(np.array([3.1652]), wires=(0, 1)), + qml.IsingZZ(np.array([1.789, 2.52, 0.211]), wires=("a", "b")), ) @@ -1809,12 +2629,13 @@ class TestParametricPow: @pytest.mark.parametrize("op", pow_parametric_ops) @pytest.mark.parametrize("n", (2, -1, 0.2631, -0.987)) def test_pow_method_parametric_ops(self, op, n): - """Assert that a matrix raised to a power is the same as multiplying the data by n for relevant ops.""" + """Assert that a matrix raised to a power is the same as + multiplying the data by n for relevant ops.""" pow_op = op.pow(n) assert len(pow_op) == 1 assert pow_op[0].__class__ is op.__class__ - assert all((d1 == d2 * n for d1, d2 in zip(pow_op[0].data, op.data))) + assert all((qml.math.allclose(d1, d2 * n) for d1, d2 in zip(pow_op[0].data, op.data))) @pytest.mark.parametrize("op", pow_parametric_ops) @pytest.mark.parametrize("n", (3, -2)) @@ -1838,14 +2659,14 @@ def test_pow_matrix(self, op, n): (qml.U2(1.234, 2.345, wires=0), Wires([])), (qml.U3(1.234, 2.345, 3.456, wires=0), Wires([])), (qml.IsingXX(1.234, wires=(0, 1)), Wires([])), - (qml.IsingYY(1.234, wires=(0, 1)), Wires([])), + (qml.IsingYY(np.array([-5.1, 0.219]), wires=(0, 1)), Wires([])), (qml.IsingZZ(1.234, wires=(0, 1)), Wires([])), ### Controlled Ops (qml.ControlledPhaseShift(1.234, wires=(0, 1)), Wires(0)), (qml.CPhase(1.234, wires=(0, 1)), Wires(0)), (qml.CRX(1.234, wires=(0, 1)), Wires(0)), (qml.CRY(1.234, wires=(0, 1)), Wires(0)), - (qml.CRZ(1.234, wires=(0, 1)), Wires(0)), + (qml.CRZ(np.array([1.234, 0.219]), wires=(0, 1)), Wires(0)), (qml.CRot(1.234, 2.2345, 3.456, wires=(0, 1)), Wires(0)), ] diff --git a/tests/qchem/test_observable_hf.py b/tests/qchem/test_observable_hf.py index 337564a8e3c..fd1116cc716 100644 --- a/tests/qchem/test_observable_hf.py +++ b/tests/qchem/test_observable_hf.py @@ -190,7 +190,6 @@ def test_fermionic_observable(core_constant, integral_one, integral_two, f_ref): r"""Test that fermionic_observable returns the correct fermionic observable.""" f = qchem.fermionic_observable(core_constant, integral_one, integral_two) - print(f) assert np.allclose(f[0], f_ref[0]) # fermionic coefficients assert f[1] == f_ref[1] # fermionic operators @@ -237,7 +236,6 @@ def test_fermionic_observable(core_constant, integral_one, integral_two, f_ref): ) def test_qubit_observable(f_observable, q_observable): r"""Test that qubit_observable returns the correct operator.""" - print(f_observable) h = qchem.qubit_observable(f_observable) h_ref = qml.Hamiltonian(q_observable[0], q_observable[1]) diff --git a/tests/tape/test_tape.py b/tests/tape/test_tape.py index cbb1661dc32..79d84eee8cb 100644 --- a/tests/tape/test_tape.py +++ b/tests/tape/test_tape.py @@ -326,16 +326,10 @@ def test_update_batch_size(self, x, rot, exp_batch_size): """Test that the batch size is correctly inferred from all operation's batch_size, when creating and when using `set_parameters`.""" - class RXWithNdim(qml.RX): - ndim_params = (0,) - - class RotWithNdim(qml.Rot): - ndim_params = (0, 0, 0) - # Test with tape construction with qml.tape.QuantumTape() as tape: - RXWithNdim(x, wires=0) - RotWithNdim(*rot, wires=1) + qml.RX(x, wires=0) + qml.Rot(*rot, wires=1) qml.apply(qml.expval(qml.PauliZ(0))) qml.apply(qml.expval(qml.PauliX(1))) @@ -343,8 +337,8 @@ class RotWithNdim(qml.Rot): # Test with set_parameters with qml.tape.QuantumTape() as tape: - RXWithNdim(0.2, wires=0) - RotWithNdim(1.0, 0.2, -0.3, wires=1) + qml.RX(0.2, wires=0) + qml.Rot(1.0, 0.2, -0.3, wires=1) qml.apply(qml.expval(qml.PauliZ(0))) qml.apply(qml.expval(qml.PauliX(1))) @@ -364,23 +358,17 @@ def test_error_inconsistent_batch_sizes(self, x, rot, y): """Test that the batch size is correctly inferred from all operation's batch_size, when creating and when using `set_parameters`.""" - class RXWithNdim(qml.RX): - ndim_params = (0,) - - class RotWithNdim(qml.Rot): - ndim_params = (0, 0, 0) - with pytest.raises(ValueError, match="batch sizes of the tape operations do not match."): with qml.tape.QuantumTape() as tape: - RXWithNdim(x, wires=0) - RotWithNdim(*rot, wires=1) - RXWithNdim(y, wires=1) + qml.RX(x, wires=0) + qml.Rot(*rot, wires=1) + qml.RX(y, wires=1) qml.apply(qml.expval(qml.PauliZ(0))) with qml.tape.QuantumTape() as tape: - RXWithNdim(0.2, wires=0) - RotWithNdim(1.0, 0.2, -0.3, wires=1) - RXWithNdim(0.2, wires=1) + qml.RX(0.2, wires=0) + qml.Rot(1.0, 0.2, -0.3, wires=1) + qml.RX(0.2, wires=1) qml.apply(qml.expval(qml.PauliZ(0))) with pytest.raises(ValueError, match="batch sizes of the tape operations do not match."): tape.set_parameters([x] + rot + [y]) diff --git a/tests/test_operation.py b/tests/test_operation.py index 1ec39e1fed8..3a384dfa434 100644 --- a/tests/test_operation.py +++ b/tests/test_operation.py @@ -26,12 +26,16 @@ import pennylane as qml from pennylane.operation import Tensor, operation_derivative, Operator, Operation -from gate_data import I, X, CNOT +from gate_data import I, X, CNOT, Toffoli, SWAP, II from pennylane.wires import Wires # pylint: disable=no-self-use, no-member, protected-access, pointless-statement +Toffoli_broadcasted = np.tensordot([0.1, -4.2j], Toffoli, axes=0) +CNOT_broadcasted = np.tensordot([1.4], CNOT, axes=0) +I_broadcasted = I[pnp.newaxis] + @pytest.mark.parametrize( "return_type", ("Sample", "Variance", "Expectation", "Probability", "State", "MidMeasure") @@ -162,25 +166,25 @@ class DummyOp4(qml.operation.Operator): num_wires = 1 # Test with mismatching batch dimensions - with pytest.raises(ValueError, match="Batching was attempted but the batched dimensions"): + with pytest.raises(ValueError, match="Broadcasting was attempted but the broadcasted"): DummyOp4([0.3] * 4, [[[0.3, 1.2]]] * 3, wires=0) - batched_params_test_data = [ - # Test with no parameter batched + broadcasted_params_test_data = [ + # Test with no parameter broadcasted ((0.3, [[0.3, 1.2]]), None), - # Test with both parameters batched with same dimension + # Test with both parameters broadcasted with same dimension (([0.3], [[[0.3, 1.2]]]), 1), (([0.3] * 3, [[[0.3, 1.2]]] * 3), 3), - # Test with one parameter batched + # Test with one parameter broadcasted ((0.3, [[[0.3, 1.2]]]), 1), ((0.3, [[[0.3, 1.2]]] * 3), 3), (([0.3], [[0.3, 1.2]]), 1), (([0.3] * 3, [[0.3, 1.2]]), 3), ] - @pytest.mark.parametrize("params, exp_batch_size", batched_params_test_data) - def test_batched_params(self, params, exp_batch_size): - r"""Test that initialization of an operator with batched parameters + @pytest.mark.parametrize("params, exp_batch_size", broadcasted_params_test_data) + def test_broadcasted_params(self, params, exp_batch_size): + r"""Test that initialization of an operator with broadcasted parameters works and sets the ``batch_size`` correctly.""" class DummyOp(qml.operation.Operator): @@ -193,9 +197,9 @@ class DummyOp(qml.operation.Operator): assert op._batch_size == exp_batch_size @pytest.mark.autograd - @pytest.mark.parametrize("params, exp_batch_size", batched_params_test_data) - def test_batched_params(self, params, exp_batch_size): - r"""Test that initialization of an operator with batched parameters + @pytest.mark.parametrize("params, exp_batch_size", broadcasted_params_test_data) + def test_broadcasted_params(self, params, exp_batch_size): + r"""Test that initialization of an operator with broadcasted parameters works and sets the ``batch_size`` correctly with Autograd parameters.""" class DummyOp(qml.operation.Operator): @@ -209,9 +213,9 @@ class DummyOp(qml.operation.Operator): assert op._batch_size == exp_batch_size @pytest.mark.jax - @pytest.mark.parametrize("params, exp_batch_size", batched_params_test_data) - def test_batched_params(self, params, exp_batch_size): - r"""Test that initialization of an operator with batched parameters + @pytest.mark.parametrize("params, exp_batch_size", broadcasted_params_test_data) + def test_broadcasted_params(self, params, exp_batch_size): + r"""Test that initialization of an operator with broadcasted parameters works and sets the ``batch_size`` correctly with JAX parameters.""" import jax @@ -226,9 +230,9 @@ class DummyOp(qml.operation.Operator): assert op._batch_size == exp_batch_size @pytest.mark.tf - @pytest.mark.parametrize("params, exp_batch_size", batched_params_test_data) - def test_batched_params(self, params, exp_batch_size): - r"""Test that initialization of an operator with batched parameters + @pytest.mark.parametrize("params, exp_batch_size", broadcasted_params_test_data) + def test_broadcasted_params(self, params, exp_batch_size): + r"""Test that initialization of an operator with broadcasted parameters works and sets the ``batch_size`` correctly with TensorFlow parameters.""" import tensorflow as tf @@ -243,9 +247,9 @@ class DummyOp(qml.operation.Operator): assert op._batch_size == exp_batch_size @pytest.mark.torch - @pytest.mark.parametrize("params, exp_batch_size", batched_params_test_data) - def test_batched_params(self, params, exp_batch_size): - r"""Test that initialization of an operator with batched parameters + @pytest.mark.parametrize("params, exp_batch_size", broadcasted_params_test_data) + def test_broadcasted_params(self, params, exp_batch_size): + r"""Test that initialization of an operator with broadcasted parameters works and sets the ``batch_size`` correctly with Torch parameters.""" import torch @@ -260,7 +264,7 @@ class DummyOp(qml.operation.Operator): assert op._batch_size == exp_batch_size @pytest.mark.filterwarnings("ignore:Creating an ndarray from ragged nested sequences") - def test_error_batched_params_not_silenced(self): + def test_error_broadcasted_params_not_silenced(self): """Handling tf.function properly requires us to catch a specific error and to silence it. Here we test it does not silence others.""" @@ -370,7 +374,9 @@ def test_with_tf_function(self, jit_compile): import tensorflow as tf class MyRX(qml.RX): - ndim_params = (0,) + @property + def ndim_params(self): + return self._ndim_params def fun(x): op0 = qml.RX(x, 0) @@ -381,7 +387,9 @@ def fun(x): fun0(tf.Variable(0.2)) fun0(tf.Variable([0.2, 0.5])) - fun1 = tf.function(fun, input_signature=(tf.TensorSpec(shape=None, dtype=tf.float32),)) + # With kwargs + signature = (tf.TensorSpec(shape=None, dtype=tf.float32),) + fun1 = tf.function(fun, jit_compile=jit_compile, input_signature=signature) fun1(tf.Variable(0.2)) fun1(tf.Variable([0.2, 0.5])) @@ -1691,6 +1699,33 @@ def test_pow_undefined(self): gate.pow(1.234) +class MyOpWithMat(Operator): + num_wires = 1 + + @staticmethod + def compute_matrix(theta): + return np.tensordot(theta, np.array([[0.4, 1.2], [1.2, 0.4]]), axes=0) + + +class TestInheritedRepresentations: + """Tests that the default representations allow for + inheritance from other representations""" + + def test_eigvals_from_matrix(self): + """Test that eigvals can be extracted when a matrix is defined.""" + # Test with scalar parameter + theta = 0.3 + op = MyOpWithMat(theta, wires=1) + eigvals = op.eigvals() + assert np.allclose(eigvals, [1.6 * theta, -0.8 * theta]) + + # Test with broadcasted parameter + theta = np.array([0.3, 0.9, 1.2]) + op = MyOpWithMat(theta, wires=1) + eigvals = op.eigvals() + assert np.allclose(eigvals, np.array([1.6 * theta, -0.8 * theta]).T) + + class TestChannel: """Unit tests for the Channel class""" @@ -1928,99 +1963,216 @@ def test_composed(self): class TestExpandMatrix: """Tests for the expand_matrix helper function.""" + base_matrix_1 = np.arange(1, 5).reshape((2, 2)) + base_matrix_1_broadcasted = np.arange(1, 13).reshape((3, 2, 2)) + base_matrix_2 = np.arange(1, 17).reshape((4, 4)) + base_matrix_2_broadcasted = np.arange(1, 49).reshape((3, 4, 4)) + def test_no_expansion(self): """Tests the case where the original matrix is not changed""" - base_matrix = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]]) - res = qml.operation.expand_matrix(base_matrix, wires=[0, 2], wire_order=[0, 2]) - assert np.allclose(base_matrix, res) + res = qml.operation.expand_matrix(self.base_matrix_2, wires=[0, 2], wire_order=[0, 2]) + assert np.allclose(self.base_matrix_2, res) + + def test_no_expansion_broadcasted(self): + """Tests the case where the broadcasted original matrix is not changed""" + res = qml.operation.expand_matrix( + self.base_matrix_2_broadcasted, wires=[0, 2], wire_order=[0, 2] + ) + assert np.allclose(self.base_matrix_2_broadcasted, res) def test_permutation(self): """Tests the case where the original matrix is permuted""" - base_matrix = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]]) - res = qml.operation.expand_matrix(base_matrix, wires=[0, 2], wire_order=[2, 0]) + res = qml.operation.expand_matrix(self.base_matrix_2, wires=[0, 2], wire_order=[2, 0]) expected = np.array([[1, 3, 2, 4], [9, 11, 10, 12], [5, 7, 6, 8], [13, 15, 14, 16]]) assert np.allclose(expected, res) + def test_permutation_broadcasted(self): + """Tests the case where the broadcasted original matrix is permuted""" + res = qml.operation.expand_matrix( + self.base_matrix_2_broadcasted, wires=[0, 2], wire_order=[2, 0] + ) + + perm = [0, 2, 1, 3] + expected = self.base_matrix_2_broadcasted[:, perm][:, :, perm] + assert np.allclose(expected, res) + def test_expansion(self): """Tests the case where the original matrix is expanded""" - base_matrix = np.array([[0, 1], [1, 0]]) - res = qml.operation.expand_matrix(base_matrix, wires=[2], wire_order=[0, 2]) - expected = np.array([[0, 1, 0, 0], [1, 0, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]]) + res = qml.operation.expand_matrix(self.base_matrix_1, wires=[2], wire_order=[0, 2]) + expected = np.array([[1, 2, 0, 0], [3, 4, 0, 0], [0, 0, 1, 2], [0, 0, 3, 4]]) assert np.allclose(expected, res) - res = qml.operation.expand_matrix(base_matrix, wires=[2], wire_order=[2, 0]) - expected = np.array([[0, 0, 1, 0], [0, 0, 0, 1], [1, 0, 0, 0], [0, 1, 0, 0]]) + res = qml.operation.expand_matrix(self.base_matrix_1, wires=[2], wire_order=[2, 0]) + expected = np.array([[1, 0, 2, 0], [0, 1, 0, 2], [3, 0, 4, 0], [0, 3, 0, 4]]) assert np.allclose(expected, res) - @pytest.mark.autograd - def test_autograd(self, tol): - """Tests differentiation in autograd by checking how a specific element of the expanded matrix depends on the - canonical matrix.""" + def test_expansion_broadcasted(self): + """Tests the case where the broadcasted original matrix is expanded""" + res = qml.operation.expand_matrix( + self.base_matrix_1_broadcasted, wires=[2], wire_order=[0, 2] + ) + expected = np.array( + [ + [ + [1, 2, 0, 0], + [3, 4, 0, 0], + [0, 0, 1, 2], + [0, 0, 3, 4], + ], + [ + [5, 6, 0, 0], + [7, 8, 0, 0], + [0, 0, 5, 6], + [0, 0, 7, 8], + ], + [ + [9, 10, 0, 0], + [11, 12, 0, 0], + [0, 0, 9, 10], + [0, 0, 11, 12], + ], + ] + ) + assert np.allclose(expected, res) + + res = qml.operation.expand_matrix( + self.base_matrix_1_broadcasted, wires=[2], wire_order=[2, 0] + ) + expected = np.array( + [ + [ + [1, 0, 2, 0], + [0, 1, 0, 2], + [3, 0, 4, 0], + [0, 3, 0, 4], + ], + [ + [5, 0, 6, 0], + [0, 5, 0, 6], + [7, 0, 8, 0], + [0, 7, 0, 8], + ], + [ + [9, 0, 10, 0], + [0, 9, 0, 10], + [11, 0, 12, 0], + [0, 11, 0, 12], + ], + ] + ) + assert np.allclose(expected, res) - def func(mat): - res = qml.operation.expand_matrix(mat, wires=[2], wire_order=[0, 2]) - return res[0, 1] + @staticmethod + def func_for_autodiff(mat): + """Expand a single-qubit matrix to two qubits where the + matrix acts on the latter qubit.""" + return qml.operation.expand_matrix(mat, wires=[2], wire_order=[0, 2]) + + # the entries should be mapped by func_for_autodiff via + # source -> destinations + # (0, 0) -> (0, 0), (2, 2) + # (0, 1) -> (0, 1), (2, 3) + # (1, 0) -> (1, 0), (3, 2) + # (1, 1) -> (1, 1), (3, 3) + # so that the expected Jacobian is 0 everywhere except for the entries + # (dest, source) from the above list, where it is 1. + expected_autodiff_nobatch = np.zeros((4, 4, 2, 2), dtype=float) + indices = [ + (0, 0, 0, 0), + (2, 2, 0, 0), + (0, 1, 0, 1), + (2, 3, 0, 1), + (1, 0, 1, 0), + (3, 2, 1, 0), + (1, 1, 1, 1), + (3, 3, 1, 1), + ] + for ind in indices: + expected_autodiff_nobatch[ind] = 1.0 - base_matrix = pnp.array([[0.0, 1.0], [1.0, 0.0]], requires_grad=True) - grad_fn = qml.grad(func) - gradient = grad_fn(base_matrix) + # When using broadcasting, the expected Jacobian + # of func_for_autodiff is diagonal in the dimensions 0 and 3 + expected_autodiff_broadcasted = np.zeros((3, 4, 4, 3, 2, 2), dtype=float) + for ind in indices: + expected_autodiff_broadcasted[:, ind[0], ind[1], :, ind[2], ind[3]] = np.eye(3) - # the entry should propagate from position (0, 1) in the original tensor - expected = np.array([[0.0, 1.0], [0.0, 0.0]]) - assert np.allclose(gradient, expected, atol=tol) + expected_autodiff = [expected_autodiff_nobatch, expected_autodiff_broadcasted] + + @pytest.mark.autograd + @pytest.mark.parametrize( + "i, base_matrix", + [ + (0, [[0.2, 1.1], [-1.3, 1.9]]), + (1, [[[0.2, 0.5], [1.2, 1.1]], [[-0.3, -0.2], [-1.3, 1.9]], [[0.2, 0.1], [0.2, 0.7]]]), + ], + ) + def test_autograd(self, i, base_matrix, tol): + """Tests differentiation in autograd by computing the Jacobian of + the expanded matrix with respect to the canonical matrix.""" + + base_matrix = pnp.array(base_matrix, requires_grad=True) + jac_fn = qml.jacobian(self.func_for_autodiff) + jac = jac_fn(base_matrix) + + assert np.allclose(jac, self.expected_autodiff[i], atol=tol) @pytest.mark.torch - def test_torch(self, tol): - """Tests differentiation in torch by checking how a specific element of the expanded matrix depends on the - canonical matrix.""" + @pytest.mark.parametrize( + "i, base_matrix", + [ + (0, [[0.2, 1.1], [-1.3, 1.9]]), + (1, [[[0.2, 0.5], [1.2, 1.1]], [[-0.3, -0.2], [-1.3, 1.9]], [[0.2, 0.1], [0.2, 0.7]]]), + ], + ) + def test_torch(self, i, base_matrix, tol): + """Tests differentiation in torch by computing the Jacobian of + the expanded matrix with respect to the canonical matrix.""" import torch - base_matrix = torch.tensor([[0.0, 1.0], [1.0, 0.0]], requires_grad=True) - res = qml.operation.expand_matrix(base_matrix, wires=[2], wire_order=[0, 2]) - element = res[0, 1] - element.backward() - gradient = base_matrix.grad + base_matrix = torch.tensor(base_matrix, requires_grad=True) + jac = torch.autograd.functional.jacobian(self.func_for_autodiff, base_matrix) - # the entry should propagate from position (0, 1) in the original tensor - expected = torch.tensor([[0.0, 1.0], [0.0, 0.0]]) - assert np.allclose(gradient, expected, atol=tol) + assert np.allclose(jac, self.expected_autodiff[i], atol=tol) @pytest.mark.jax - def test_jax(self, tol): - """Tests differentiation in jax by checking how a specific element of the expanded matrix depends on the - canonical matrix.""" + @pytest.mark.parametrize( + "i, base_matrix", + [ + (0, [[0.2, 1.1], [-1.3, 1.9]]), + (1, [[[0.2, 0.5], [1.2, 1.1]], [[-0.3, -0.2], [-1.3, 1.9]], [[0.2, 0.1], [0.2, 0.7]]]), + ], + ) + def test_jax(self, i, base_matrix, tol): + """Tests differentiation in jax by computing the Jacobian of + the expanded matrix with respect to the canonical matrix.""" import jax - from jax import numpy as jnp - - def func(mat): - res = qml.operation.expand_matrix(mat, wires=[2], wire_order=[0, 2]) - return res[0, 1] - base_matrix = jnp.array([[0.0, 1.0], [1.0, 0.0]]) - grad_fn = jax.grad(func) - gradient = grad_fn(base_matrix) + base_matrix = jax.numpy.array(base_matrix) + jac_fn = jax.jacobian(self.func_for_autodiff) + jac = jac_fn(base_matrix) - # the entry should propagate from position (0, 1) in the original tensor - expected = np.array([[0.0, 1.0], [0.0, 0.0]]) - assert np.allclose(gradient, expected, atol=tol) + assert np.allclose(jac, self.expected_autodiff[i], atol=tol) @pytest.mark.tf - def test_tf(self, tol): - """Tests differentiation in TensorFlow by checking how a specific element of the expanded matrix depends on the - canonical matrix.""" + @pytest.mark.parametrize( + "i, base_matrix", + [ + (0, [[0.2, 1.1], [-1.3, 1.9]]), + (1, [[[0.2, 0.5], [1.2, 1.1]], [[-0.3, -0.2], [-1.3, 1.9]], [[0.2, 0.1], [0.2, 0.7]]]), + ], + ) + def test_tf(self, i, base_matrix, tol): + """Tests differentiation in TensorFlow by computing the Jacobian of + the expanded matrix with respect to the canonical matrix.""" import tensorflow as tf - base_matrix = tf.Variable([[0.0, 1.0], [1.0, 0.0]]) + base_matrix = tf.Variable(base_matrix) with tf.GradientTape() as tape: - res = qml.operation.expand_matrix(base_matrix, wires=[2], wire_order=[0, 2]) - element = res[0, 1] + res = self.func_for_autodiff(base_matrix) - gradient = tape.gradient(element, base_matrix) - - # the entry should propagate from position (0, 1) in the original tensor - expected = tf.constant([[0.0, 1.0], [0.0, 0.0]]) - assert np.allclose(gradient, expected, atol=tol) + jac = tape.jacobian(res, base_matrix) + assert np.allclose(jac, self.expected_autodiff[i], atol=tol) def test_expand_one(self, tol): """Test that a 1 qubit gate correctly expands to 3 qubits.""" @@ -2045,6 +2197,31 @@ def test_expand_one(self, tol): expected = np.kron(np.kron(I, I), U) assert np.allclose(res, expected, atol=tol, rtol=0) + def test_expand_one_broadcasted(self, tol): + """Test that a broadcasted 1 qubit gate correctly expands to 3 qubits.""" + U = np.array( + [ + [0.83645892 - 0.40533293j, -0.20215326 + 0.30850569j], + [-0.23889780 - 0.28101519j, -0.88031770 - 0.29832709j], + ] + ) + # outer product with batch vector + U = np.tensordot([0.14, -0.23, 1.3j], U, axes=0) + # test applied to wire 0 + res = qml.operation.expand_matrix(U, [0], [0, 4, 9]) + expected = np.kron(np.kron(U, I_broadcasted), I_broadcasted) + assert np.allclose(res, expected, atol=tol, rtol=0) + + # test applied to wire 4 + res = qml.operation.expand_matrix(U, [4], [0, 4, 9]) + expected = np.kron(np.kron(I_broadcasted, U), I_broadcasted) + assert np.allclose(res, expected, atol=tol, rtol=0) + + # test applied to wire 9 + res = qml.operation.expand_matrix(U, [9], [0, 4, 9]) + expected = np.kron(np.kron(I_broadcasted, I_broadcasted), U) + assert np.allclose(res, expected, atol=tol, rtol=0) + def test_expand_two_consecutive_wires(self, tol): """Test that a 2 qubit gate on consecutive wires correctly expands to 4 qubits.""" @@ -2065,6 +2242,27 @@ def test_expand_two_consecutive_wires(self, tol): expected = np.kron(np.kron(I, I), U2) assert np.allclose(res, expected, atol=tol, rtol=0) + def test_expand_two_consecutive_wires_broadcasted(self, tol): + """Test that a broadcasted 2 qubit gate on consecutive wires correctly + expands to 4 qubits.""" + U2 = np.array([[0, 1, 1, 1], [1, 0, 1, -1], [1, -1, 0, 1], [1, 1, -1, 0]]) / np.sqrt(3) + U2 = np.tensordot([2.31, 1.53, 0.7 - 1.9j], U2, axes=0) + + # test applied to wire 0+1 + res = qml.operation.expand_matrix(U2, [0, 1], [0, 1, 2, 3]) + expected = np.kron(np.kron(U2, I_broadcasted), I_broadcasted) + assert np.allclose(res, expected, atol=tol, rtol=0) + + # test applied to wire 1+2 + res = qml.operation.expand_matrix(U2, [1, 2], [0, 1, 2, 3]) + expected = np.kron(np.kron(I_broadcasted, U2), I_broadcasted) + assert np.allclose(res, expected, atol=tol, rtol=0) + + # test applied to wire 2+3 + res = qml.operation.expand_matrix(U2, [2, 3], [0, 1, 2, 3]) + expected = np.kron(np.kron(I_broadcasted, I_broadcasted), U2) + assert np.allclose(res, expected, atol=tol, rtol=0) + def test_expand_two_reversed_wires(self, tol): """Test that a 2 qubit gate on reversed consecutive wires correctly expands to 4 qubits.""" @@ -2074,74 +2272,136 @@ def test_expand_two_reversed_wires(self, tol): expected = np.kron(np.kron(CNOT[:, rows][rows], I), I) assert np.allclose(res, expected, atol=tol, rtol=0) + def test_expand_two_reversed_wires_broadcasted(self, tol): + """Test that a broadcasted 2 qubit gate on reversed consecutive wires correctly + expands to 4 qubits.""" + # CNOT with target on wire 1 and a batch dimension of size 1 + res = qml.operation.expand_matrix(CNOT_broadcasted, [1, 0], [0, 1, 2, 3]) + rows = [0, 2, 1, 3] + expected = np.kron( + np.kron(CNOT_broadcasted[:, :, rows][:, rows], I_broadcasted), I_broadcasted + ) + assert np.allclose(res, expected, atol=tol, rtol=0) + def test_expand_three_consecutive_wires(self, tol): """Test that a 3 qubit gate on consecutive wires correctly expands to 4 qubits.""" - U_toffoli = np.diag([1 for i in range(8)]) - U_toffoli[6:8, 6:8] = np.array([[0, 1], [1, 0]]) # test applied to wire 0,1,2 - res = qml.operation.expand_matrix(U_toffoli, [0, 1, 2], [0, 1, 2, 3]) - expected = np.kron(U_toffoli, I) + res = qml.operation.expand_matrix(Toffoli, [0, 1, 2], [0, 1, 2, 3]) + expected = np.kron(Toffoli, I) assert np.allclose(res, expected, atol=tol, rtol=0) # test applied to wire 1,2,3 - res = qml.operation.expand_matrix(U_toffoli, [1, 2, 3], [0, 1, 2, 3]) - expected = np.kron(I, U_toffoli) + res = qml.operation.expand_matrix(Toffoli, [1, 2, 3], [0, 1, 2, 3]) + expected = np.kron(I, Toffoli) + assert np.allclose(res, expected, atol=tol, rtol=0) + + def test_expand_three_consecutive_wires_broadcasted(self, tol): + """Test that a broadcasted 3 qubit gate on consecutive + wires correctly expands to 4 qubits.""" + # test applied to wire 0,1,2 + res = qml.operation.expand_matrix(Toffoli_broadcasted, [0, 1, 2], [0, 1, 2, 3]) + expected = np.kron(Toffoli_broadcasted, I_broadcasted) + assert np.allclose(res, expected, atol=tol, rtol=0) + + # test applied to wire 1,2,3 + res = qml.operation.expand_matrix(Toffoli_broadcasted, [1, 2, 3], [0, 1, 2, 3]) + expected = np.kron(I_broadcasted, Toffoli_broadcasted) assert np.allclose(res, expected, atol=tol, rtol=0) def test_expand_three_nonconsecutive_ascending_wires(self, tol): """Test that a 3 qubit gate on non-consecutive but ascending wires correctly expands to 4 qubits.""" - U_toffoli = np.diag([1 for i in range(8)]) - U_toffoli[6:8, 6:8] = np.array([[0, 1], [1, 0]]) # test applied to wire 0,2,3 - res = qml.operation.expand_matrix(U_toffoli, [0, 2, 3], [0, 1, 2, 3]) - expected = ( - np.kron(qml.SWAP.compute_matrix(), np.kron(I, I)) - @ np.kron(I, U_toffoli) - @ np.kron(qml.SWAP.compute_matrix(), np.kron(I, I)) + res = qml.operation.expand_matrix(Toffoli, [0, 2, 3], [0, 1, 2, 3]) + expected = np.kron(SWAP, II) @ np.kron(I, Toffoli) @ np.kron(SWAP, II) + assert np.allclose(res, expected, atol=tol, rtol=0) + + # test applied to wire 0,1,3 + res = qml.operation.expand_matrix(Toffoli, [0, 1, 3], [0, 1, 2, 3]) + expected = np.kron(II, SWAP) @ np.kron(Toffoli, I) @ np.kron(II, SWAP) + assert np.allclose(res, expected, atol=tol, rtol=0) + + def test_expand_three_nonconsecutive_ascending_wires_broadcasted(self, tol): + """Test that a broadcasted 3 qubit gate on non-consecutive but ascending + wires correctly expands to 4 qubits.""" + # test applied to wire 0,2,3 + res = qml.operation.expand_matrix(Toffoli_broadcasted[:1], [0, 2, 3], [0, 1, 2, 3]) + expected = np.tensordot( + np.tensordot( + np.kron(SWAP, II), + np.kron(I_broadcasted, Toffoli_broadcasted[:1]), + axes=[[1], [1]], + ), + np.kron(SWAP, II), + axes=[[2], [0]], ) + expected = np.moveaxis(expected, 0, -2) assert np.allclose(res, expected, atol=tol, rtol=0) # test applied to wire 0,1,3 - res = qml.operation.expand_matrix(U_toffoli, [0, 1, 3], [0, 1, 2, 3]) - expected = ( - np.kron(np.kron(I, I), qml.SWAP.compute_matrix()) - @ np.kron(U_toffoli, I) - @ np.kron(np.kron(I, I), qml.SWAP.compute_matrix()) + res = qml.operation.expand_matrix(Toffoli_broadcasted, [0, 1, 3], [0, 1, 2, 3]) + expected = np.tensordot( + np.tensordot( + np.kron(II, SWAP), + np.kron(Toffoli_broadcasted, I_broadcasted), + axes=[[1], [1]], + ), + np.kron(II, SWAP), + axes=[[2], [0]], ) + expected = np.moveaxis(expected, 0, -2) assert np.allclose(res, expected, atol=tol, rtol=0) def test_expand_three_nonconsecutive_nonascending_wires(self, tol): """Test that a 3 qubit gate on non-consecutive non-ascending wires correctly expands to 4 qubits""" - U_toffoli = np.diag([1 for i in range(8)]) - U_toffoli[6:8, 6:8] = np.array([[0, 1], [1, 0]]) # test applied to wire 3, 1, 2 - res = qml.operation.expand_matrix(U_toffoli, [3, 1, 2], [0, 1, 2, 3]) + res = qml.operation.expand_matrix(Toffoli, [3, 1, 2], [0, 1, 2, 3]) + # change the control qubit on the Toffoli gate + rows = [0, 4, 1, 5, 2, 6, 3, 7] + Toffoli_perm = Toffoli[:, rows][rows] + expected = np.kron(I, Toffoli_perm) + assert np.allclose(res, expected, atol=tol, rtol=0) + + # test applied to wire 3, 0, 2 + res = qml.operation.expand_matrix(Toffoli, [3, 0, 2], [0, 1, 2, 3]) + # change the control qubit on the Toffoli gate + expected = np.kron(SWAP, II) @ np.kron(I, Toffoli_perm) @ np.kron(SWAP, II) + assert np.allclose(res, expected, atol=tol, rtol=0) + + def test_expand_three_nonconsecutive_nonascending_wires_broadcasted(self, tol): + """Test that a broadcasted 3 qubit gate on non-consecutive non-ascending + wires correctly expands to 4 qubits""" + # test applied to wire 3, 1, 2 + res = qml.operation.expand_matrix(Toffoli_broadcasted, [3, 1, 2], [0, 1, 2, 3]) # change the control qubit on the Toffoli gate - rows = np.array([0, 4, 1, 5, 2, 6, 3, 7]) - expected = np.kron(I, U_toffoli[:, rows][rows]) + rows = [0, 4, 1, 5, 2, 6, 3, 7] + Toffoli_broadcasted_perm = Toffoli_broadcasted[:, :, rows][:, rows] + expected = np.kron(I_broadcasted, Toffoli_broadcasted_perm) assert np.allclose(res, expected, atol=tol, rtol=0) # test applied to wire 3, 0, 2 - res = qml.operation.expand_matrix(U_toffoli, [3, 0, 2], [0, 1, 2, 3]) + res = qml.operation.expand_matrix(Toffoli_broadcasted, [3, 0, 2], [0, 1, 2, 3]) # change the control qubit on the Toffoli gate - rows = np.array([0, 4, 1, 5, 2, 6, 3, 7]) - expected = ( - np.kron(qml.SWAP.compute_matrix(), np.kron(I, I)) - @ np.kron(I, U_toffoli[:, rows][rows]) - @ np.kron(qml.SWAP.compute_matrix(), np.kron(I, I)) + expected = np.tensordot( + np.tensordot( + np.kron(SWAP, II), + np.kron(I_broadcasted, Toffoli_broadcasted_perm), + axes=[[1], [1]], + ), + np.kron(SWAP, II), + axes=[[2], [0]], ) + expected = np.moveaxis(expected, 0, -2) assert np.allclose(res, expected, atol=tol, rtol=0) def test_expand_matrix_usage_in_operator_class(self, tol): """Tests that the method is used correctly by defining a dummy operator and checking the permutation/expansion.""" - base_matrix = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]]) - - permuted_matrix = np.array([[1, 3, 2, 4], [9, 11, 10, 12], [5, 7, 6, 8], [13, 15, 14, 16]]) + perm = [0, 2, 1, 3] + permuted_matrix = self.base_matrix_2[perm][:, perm] expanded_matrix = np.array( [ @@ -2160,10 +2420,39 @@ class DummyOp(qml.operation.Operator): num_wires = 2 def compute_matrix(*params, **hyperparams): - return base_matrix + return self.base_matrix_2 + + op = DummyOp(wires=[0, 2]) + assert np.allclose(op.matrix(), self.base_matrix_2, atol=tol) + assert np.allclose(op.matrix(wire_order=[2, 0]), permuted_matrix, atol=tol) + assert np.allclose(op.matrix(wire_order=[0, 1, 2]), expanded_matrix, atol=tol) + + def test_expand_matrix_usage_in_operator_class_broadcasted(self, tol): + """Tests that the method is used correctly with a broadcasted matrix by defining + a dummy operator and checking the permutation/expansion.""" + + perm = [0, 2, 1, 3] + permuted_matrix = self.base_matrix_2_broadcasted[:, perm][:, :, perm] + + expanded_matrix = np.tensordot( + np.tensordot( + np.kron(SWAP, I), + np.kron(I_broadcasted, self.base_matrix_2_broadcasted), + axes=[[1], [1]], + ), + np.kron(SWAP, I), + axes=[[2], [0]], + ) + expanded_matrix = np.moveaxis(expanded_matrix, 0, -2) + + class DummyOp(qml.operation.Operator): + num_wires = 2 + + def compute_matrix(*params, **hyperparams): + return self.base_matrix_2_broadcasted op = DummyOp(wires=[0, 2]) - assert np.allclose(op.matrix(), base_matrix, atol=tol) + assert np.allclose(op.matrix(), self.base_matrix_2_broadcasted, atol=tol) assert np.allclose(op.matrix(wire_order=[2, 0]), permuted_matrix, atol=tol) assert np.allclose(op.matrix(wire_order=[0, 1, 2]), expanded_matrix, atol=tol) diff --git a/tests/test_qubit_device.py b/tests/test_qubit_device.py index 1dfbcc4e675..5aaa4db1778 100644 --- a/tests/test_qubit_device.py +++ b/tests/test_qubit_device.py @@ -770,6 +770,7 @@ def test_defines_correct_capabilities(self): "supports_finite_shots": True, "supports_tensor_observables": True, "returns_probs": True, + "supports_broadcasting": False, } assert capabilities == QubitDevice.capabilities() diff --git a/tests/transforms/test_batch_partial.py b/tests/transforms/test_batch_partial.py index f4a3dba440c..9c6f18ad592 100644 --- a/tests/transforms/test_batch_partial.py +++ b/tests/transforms/test_batch_partial.py @@ -730,12 +730,22 @@ def test_different_batchdim_error(): dimensions are given to the decorated QNode""" dev = qml.device("default.qubit", wires=2) + # To test this error message, we need to use operations that do + # not report problematic broadcasting dimensions (in place of problematic + # batch dimensions) at tape creation. For this, we "delete" `ndim_params`. + + class RX_no_ndim(qml.RX): + ndim_params = property(lambda self: self._ndim_params) + + class RY_no_ndim(qml.RY): + ndim_params = property(lambda self: self._ndim_params) + @qml.qnode(dev) def circuit(x, y, z): - qml.RX(x, wires=0) - qml.RY(y[..., 0], wires=0) - qml.RY(y[..., 1], wires=1) - qml.RX(z, wires=1) + RX_no_ndim(x, wires=0) + RY_no_ndim(y[..., 0], wires=0) + RY_no_ndim(y[..., 1], wires=1) + RX_no_ndim(z, wires=1) return qml.expval(qml.PauliZ(wires=0) @ qml.PauliZ(wires=1)) batch_size1, batch_size2 = 5, 4 diff --git a/tests/transforms/test_broadcast_expand.py b/tests/transforms/test_broadcast_expand.py new file mode 100644 index 00000000000..018df262079 --- /dev/null +++ b/tests/transforms/test_broadcast_expand.py @@ -0,0 +1,203 @@ +# Copyright 2018-2021 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. + +import pytest +import numpy as np +import pennylane as qml +from pennylane import numpy as pnp + + +class RX_broadcasted(qml.RX): + """A version of qml.RX that detects batching.""" + + ndim_params = (0,) + compute_decomposition = staticmethod(lambda theta, wires=None: qml.RX(theta, wires=wires)) + + +class RZ_broadcasted(qml.RZ): + """A version of qml.RZ that detects batching.""" + + ndim_params = (0,) + compute_decomposition = staticmethod(lambda theta, wires=None: qml.RZ(theta, wires=wires)) + + +dev = qml.device("default.qubit", wires=2) +"""Defines the device used for all tests""" + + +def make_tape(x, y, z, obs): + """Construct a tape with three parametrized, two unparametrized + operations and expvals of provided observables.""" + with qml.tape.QuantumTape() as tape: + RX_broadcasted(x, wires=0) + qml.PauliY(0) + RX_broadcasted(y, wires=1) + RZ_broadcasted(z, wires=1) + qml.Hadamard(1) + for ob in obs: + qml.expval(ob) + + return tape + + +parameters_and_size = [ + [(0.2, np.array([0.1, 0.8, 2.1]), -1.5), 3], + [(0.2, np.array([0.1]), np.array([-0.3])), 1], + [ + ( + 0.2, + pnp.array([0.1, 0.3], requires_grad=True), + pnp.array([-0.3, 2.1], requires_grad=False), + ), + 2, + ], +] + +coeffs0 = [0.3, -5.1] +H0 = qml.Hamiltonian(qml.math.array(coeffs0), [qml.PauliZ(0), qml.PauliY(1)]) + +# Here we exploit the product structure of our circuit +exp_fn_Z0 = lambda x, y, z: -qml.math.cos(x) * qml.math.ones_like(y) * qml.math.ones_like(z) +exp_fn_Y1 = lambda x, y, z: qml.math.sin(y) * qml.math.cos(z) * qml.math.ones_like(x) +exp_fn_Z0Y1 = lambda x, y, z: exp_fn_Z0(x, y, z) * exp_fn_Y1(x, y, z) +exp_fn_Z0_and_Y1 = lambda x, y, z: qml.math.transpose( + qml.math.array( + [exp_fn_Z0(x, y, z), exp_fn_Y1(x, y, z)], + like=exp_fn_Z0(x, y, z) + exp_fn_Y1(x, y, z), + ) +) +exp_fn_H0 = lambda x, y, z: exp_fn_Z0(x, y, z) * coeffs0[0] + exp_fn_Y1(x, y, z) * coeffs0[1] + +observables_and_exp_fns = [ + ([qml.PauliZ(0)], exp_fn_Z0), + ([qml.PauliZ(0) @ qml.PauliY(1)], exp_fn_Z0Y1), + ([qml.PauliZ(0), qml.PauliY(1)], exp_fn_Z0_and_Y1), + ([H0], exp_fn_H0), +] + + +class TestBroadcastExpand: + """Tests for the broadcast_expand transform""" + + @pytest.mark.parametrize("params, size", parameters_and_size) + @pytest.mark.parametrize("obs, exp_fn", observables_and_exp_fns) + def test_expansion(self, params, size, obs, exp_fn): + """Test that the expansion works as expected.""" + tape = make_tape(*params, obs) + assert tape.batch_size == size + + tapes, fn = qml.transforms.broadcast_expand(tape) + assert len(tapes) == size + assert all(_tape.batch_size is None for _tape in tapes) + + result = fn(qml.execute(tapes, dev, None)) + assert isinstance(result, np.ndarray) + assert qml.math.allclose(result, exp_fn(*params)) + + def test_without_broadcasting(self): + tape = make_tape(0.2, 0.1, 0.5, [qml.PauliZ(0)]) + with pytest.raises(ValueError, match="The provided tape is not broadcasted."): + qml.transforms.broadcast_expand(tape) + + @pytest.mark.autograd + @pytest.mark.filterwarnings("ignore:Output seems independent of input") + @pytest.mark.parametrize("params, size", parameters_and_size) + @pytest.mark.parametrize("obs, exp_fn", observables_and_exp_fns) + def test_autograd(self, params, size, obs, exp_fn): + """Test that the expansion works with autograd and is differentiable.""" + params = tuple(pnp.array(p, requires_grad=True) for p in params) + + def cost(*params): + tape = make_tape(*params, obs) + tapes, fn = qml.transforms.broadcast_expand(tape) + return fn(qml.execute(tapes, dev, qml.gradients.param_shift)) + + assert qml.math.allclose(cost(*params), exp_fn(*params)) + + jac = qml.jacobian(cost)(*params) + exp_jac = qml.jacobian(exp_fn)(*params) + + assert all(qml.math.allclose(_jac, e_jac) for _jac, e_jac in zip(jac, exp_jac)) + + @pytest.mark.jax + @pytest.mark.parametrize("params, size", parameters_and_size) + @pytest.mark.parametrize("obs, exp_fn", observables_and_exp_fns) + def test_jax(self, params, size, obs, exp_fn): + """Test that the expansion works with jax and is differentiable.""" + import jax + + params = tuple(jax.numpy.array(p) for p in params) + + def cost(*params): + tape = make_tape(*params, obs) + tapes, fn = qml.transforms.broadcast_expand(tape) + return fn(qml.execute(tapes, dev, qml.gradients.param_shift, interface="jax")) + + assert qml.math.allclose(cost(*params), exp_fn(*params)) + + jac = jax.jacobian(cost, argnums=[0, 1, 2])(*params) + exp_jac = jax.jacobian(exp_fn, argnums=[0, 1, 2])(*params) + + assert all(qml.math.allclose(_jac, e_jac) for _jac, e_jac in zip(jac, exp_jac)) + + @pytest.mark.tf + @pytest.mark.parametrize("params, size", parameters_and_size) + @pytest.mark.parametrize("obs, exp_fn", observables_and_exp_fns) + def test_tf(self, params, size, obs, exp_fn): + """Test that the expansion works with TensorFlow and is differentiable.""" + import tensorflow as tf + + params = tuple(tf.Variable(p, dtype=tf.float64) for p in params) + + def cost(*params): + tape = make_tape(*params, obs) + tapes, fn = qml.transforms.broadcast_expand(tape) + return fn(qml.execute(tapes, dev, qml.gradients.param_shift, interface="tf")) + + with tf.GradientTape(persistent=True) as t: + out = cost(*params) + exp = exp_fn(*params) + + jac = t.jacobian(out, params) + exp_jac = t.jacobian(exp, params) + + assert qml.math.allclose(out, exp) + for _jac, e_jac in zip(jac, exp_jac): + if e_jac is None: + assert qml.math.allclose(_jac, 0.0) + else: + assert qml.math.allclose(_jac, e_jac) + + @pytest.mark.torch + @pytest.mark.filterwarnings("ignore:Output seems independent of input") + @pytest.mark.parametrize("params, size", parameters_and_size) + @pytest.mark.parametrize("obs, exp_fn", observables_and_exp_fns) + def test_torch(self, params, size, obs, exp_fn): + """Test that the expansion works with torch and is differentiable.""" + import torch + + torch_params = tuple(torch.tensor(p, requires_grad=True) for p in params) + params = tuple(pnp.array(p, requires_grad=True) for p in params) + + def cost(*params): + tape = make_tape(*params, obs) + tapes, fn = qml.transforms.broadcast_expand(tape) + return fn(qml.execute(tapes, dev, qml.gradients.param_shift, interface="torch")) + + assert qml.math.allclose(cost(*torch_params), exp_fn(*params)) + + jac = torch.autograd.functional.jacobian(cost, torch_params) + exp_jac = qml.jacobian(exp_fn)(*params) + + assert all(qml.math.allclose(_jac, e_jac) for _jac, e_jac in zip(jac, exp_jac)) diff --git a/tests/transforms/test_hamiltonian_expand.py b/tests/transforms/test_hamiltonian_expand.py index b41b712bd5e..f04e464f0ff 100644 --- a/tests/transforms/test_hamiltonian_expand.py +++ b/tests/transforms/test_hamiltonian_expand.py @@ -74,7 +74,7 @@ OUTPUTS = [-1.5, -6, -1.5, -8] -class TestHamiltonianExpval: +class TestHamiltonianExpand: """Tests for the hamiltonian_expand transform""" @pytest.mark.parametrize(("tape", "output"), zip(TAPES, OUTPUTS))