From 080e5eddf4846c65241b61036e3684346c4edb9f Mon Sep 17 00:00:00 2001 From: "Yushao Chen (Jerry)" Date: Fri, 15 Nov 2024 16:49:43 -0500 Subject: [PATCH] Move contents of `pennylane.utils` to appropriate modules (#6588) **Context:** The legacy remains `qml.utils` module has been hanging around for long. Now, finally we are to delete them and move the remaining folks to wherever they're supposed to be. Specifically, the following 4 sets of functions have been either moved or removed: * `qml.utils._flatten`, `qml.utils.unflatten` has been moved and renamed to `qml.pytrees.flatten_np` and `qml.pytrees.unflatten_np` respectively. * `qml.utils._inv_dict` and `qml._get_default_args` have been removed. * `qml.utils.pauli_eigs` has been moved to `qml.pauli.utils`. * `qml.utils.expand_vector` has been moved to `qml.math.expand_vector`. **Description of the Change:** **Benefits:** Less redundancy **Possible Drawbacks:** Rare chance that some downstreaming repo might be using these funcationalities: - [x] lightning - [x] catalyst - [x] qml - [x] plugins We will come back to check them one by one to make sure nothing is to break **Related GitHub Issues:** **Related Shortcut Stories:** [sc-76906] --------- Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> --- doc/code/qml_utils.rst | 14 -- doc/index.rst | 1 - doc/releases/changelog-dev.md | 11 + pennylane/math/__init__.py | 3 +- pennylane/math/matrix_manipulation.py | 54 ++++ pennylane/operation.py | 5 +- pennylane/ops/op_math/composite.py | 4 +- pennylane/ops/op_math/linear_combination.py | 4 +- pennylane/ops/qubit/non_parametric_ops.py | 9 +- .../ops/qubit/parametric_ops_multi_qubit.py | 5 +- pennylane/optimize/momentum_qng.py | 7 +- pennylane/optimize/qng.py | 85 ++++++- pennylane/optimize/rotoselect.py | 9 +- pennylane/pauli/__init__.py | 1 + pennylane/pauli/utils.py | 18 ++ pennylane/pytrees/__init__.py | 9 +- .../transforms/sign_expand/sign_expand.py | 1 - pennylane/utils.py | 208 --------------- tests/math/test_matrix_manipulation.py | 66 ++++- tests/ops/qubit/test_parametric_ops.py | 2 +- tests/optimize/test_qng.py | 64 +++++ tests/optimize/test_rotosolve.py | 32 +-- tests/pauli/test_pauli_utils.py | 44 +++- tests/test_utils.py | 237 ------------------ 24 files changed, 385 insertions(+), 508 deletions(-) delete mode 100644 doc/code/qml_utils.rst delete mode 100644 pennylane/utils.py delete mode 100644 tests/test_utils.py diff --git a/doc/code/qml_utils.rst b/doc/code/qml_utils.rst deleted file mode 100644 index 5faeb61440d..00000000000 --- a/doc/code/qml_utils.rst +++ /dev/null @@ -1,14 +0,0 @@ -qml.utils -========= - -.. currentmodule:: pennylane.utils - -.. warning:: - - Unless you are a PennyLane or plugin developer, you likely do not need - to use these utility functions. - -.. automodapi:: pennylane.utils - :no-heading: - :include-all-objects: - :skip: Iterable diff --git a/doc/index.rst b/doc/index.rst index 8bde3d6470f..4398685fdf5 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -230,6 +230,5 @@ PennyLane is **free** and **open source**, released under the Apache License, Ve code/qml_operation code/qml_queuing code/qml_tape - code/qml_utils code/qml_wires code/qml_workflow diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 164775e1d26..d7c504dfed4 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -66,6 +66,17 @@

Breaking changes 💔

+* The developer-facing `qml.utils` module has been removed. Specifically, the +following 4 sets of functions have been either moved or removed[(#6588)](https://github.com/PennyLaneAI/pennylane/pull/6588): + + * `qml.utils._flatten`, `qml.utils.unflatten` has been moved and renamed to `qml.optimize.qng._flatten_np` and `qml.optimize.qng._unflatten_np` respectively. + + * `qml.utils._inv_dict` and `qml._get_default_args` have been removed. + + * `qml.utils.pauli_eigs` has been moved to `qml.pauli.utils`. + + * `qml.utils.expand_vector` has been moved to `qml.math.expand_vector`. + * The `qml.qinfo` module has been removed. Please see the respective functions in the `qml.math` and `qml.measurements` modules instead. [(#6584)](https://github.com/PennyLaneAI/pennylane/pull/6584) diff --git a/pennylane/math/__init__.py b/pennylane/math/__init__.py index 731979daac9..afcf55316f1 100644 --- a/pennylane/math/__init__.py +++ b/pennylane/math/__init__.py @@ -34,7 +34,7 @@ import autoray as ar from .is_independent import is_independent -from .matrix_manipulation import expand_matrix, reduce_matrices, get_batch_size +from .matrix_manipulation import expand_matrix, expand_vector, reduce_matrices, get_batch_size from .multi_dispatch import ( add, array, @@ -152,6 +152,7 @@ def __getattr__(name): "dot", "einsum", "expand_matrix", + "expand_vector", "expectation_value", "eye", "fidelity", diff --git a/pennylane/math/matrix_manipulation.py b/pennylane/math/matrix_manipulation.py index 98bea694eae..0e4149c21f0 100644 --- a/pennylane/math/matrix_manipulation.py +++ b/pennylane/math/matrix_manipulation.py @@ -14,6 +14,7 @@ """This module contains methods to expand the matrix representation of an operator to a higher hilbert space with re-ordered wires.""" import itertools +import numbers from collections.abc import Callable, Generator, Iterable from functools import reduce @@ -348,3 +349,56 @@ def get_batch_size(tensor, expected_shape, expected_size): raise err return None + + +def expand_vector(vector, original_wires, expanded_wires): + r"""Expand a vector to more wires. + + Args: + vector (array): :math:`2^n` vector where n = len(original_wires). + original_wires (Sequence[int]): original wires of vector + expanded_wires (Union[Sequence[int], int]): expanded wires of vector, can be shuffled + If a single int m is given, corresponds to list(range(m)) + + Returns: + array: :math:`2^m` vector where m = len(expanded_wires). + """ + if len(original_wires) == 0: + val = qml.math.squeeze(vector) + return val * qml.math.ones(2 ** len(expanded_wires)) + if isinstance(expanded_wires, numbers.Integral): + expanded_wires = list(range(expanded_wires)) + + N = len(original_wires) + M = len(expanded_wires) + D = M - N + + len_vector = qml.math.shape(vector)[0] + qudit_order = int(2 ** (np.log2(len_vector) / N)) + + if not set(expanded_wires).issuperset(original_wires): + raise ValueError("Invalid target subsystems provided in 'original_wires' argument.") + + if qml.math.shape(vector) != (qudit_order**N,): + raise ValueError(f"Vector parameter must be of length {qudit_order}**len(original_wires)") + + dims = [qudit_order] * N + tensor = qml.math.reshape(vector, dims) + + if D > 0: + extra_dims = [qudit_order] * D + ones = qml.math.ones(qudit_order**D).reshape(extra_dims) + expanded_tensor = qml.math.tensordot(tensor, ones, axes=0) + else: + expanded_tensor = tensor + + wire_indices = [expanded_wires.index(wire) for wire in original_wires] + wire_indices = np.array(wire_indices) + + # Order tensor factors according to wires + original_indices = np.array(range(N)) + expanded_tensor = qml.math.moveaxis( + expanded_tensor, tuple(original_indices), tuple(wire_indices) + ) + + return qml.math.reshape(expanded_tensor, (qudit_order**M,)) diff --git a/pennylane/operation.py b/pennylane/operation.py index f405ec259b8..1cc41052f6f 100644 --- a/pennylane/operation.py +++ b/pennylane/operation.py @@ -264,7 +264,6 @@ from pennylane.wires import Wires, WiresLike from .pytrees import register_pytree -from .utils import pauli_eigs # ============================================================================= # Errors @@ -2346,7 +2345,7 @@ def eigvals(self): standard_observables = {"PauliX", "PauliY", "PauliZ", "Hadamard"} # observable should be Z^{\otimes n} - self._eigvals_cache = pauli_eigs(len(self.wires)) + self._eigvals_cache = qml.pauli.pauli_eigs(len(self.wires)) # check if there are any non-standard observables (such as Identity) if set(self.name) - standard_observables: @@ -2357,7 +2356,7 @@ def eigvals(self): if k: # Subgroup g contains only standard observables. self._eigvals_cache = qml.math.kron( - self._eigvals_cache, pauli_eigs(len(list(g))) + self._eigvals_cache, qml.pauli.pauli_eigs(len(list(g))) ) else: # Subgroup g contains only non-standard observables. diff --git a/pennylane/ops/op_math/composite.py b/pennylane/ops/op_math/composite.py index 51dd94fb605..132fa3cd516 100644 --- a/pennylane/ops/op_math/composite.py +++ b/pennylane/ops/op_math/composite.py @@ -193,12 +193,12 @@ def eigvals(self): for ops in self.overlapping_ops: if len(ops) == 1: eigvals.append( - qml.utils.expand_vector(ops[0].eigvals(), list(ops[0].wires), list(self.wires)) + math.expand_vector(ops[0].eigvals(), list(ops[0].wires), list(self.wires)) ) else: tmp_composite = self.__class__(*ops) eigvals.append( - qml.utils.expand_vector( + math.expand_vector( tmp_composite.eigendecomposition["eigval"], list(tmp_composite.wires), list(self.wires), diff --git a/pennylane/ops/op_math/linear_combination.py b/pennylane/ops/op_math/linear_combination.py index bbada7385f7..348a95cfab0 100644 --- a/pennylane/ops/op_math/linear_combination.py +++ b/pennylane/ops/op_math/linear_combination.py @@ -471,12 +471,12 @@ def eigvals(self): for ops in self.overlapping_ops: if len(ops) == 1: eigvals.append( - qml.utils.expand_vector(ops[0].eigvals(), list(ops[0].wires), list(self.wires)) + qml.math.expand_vector(ops[0].eigvals(), list(ops[0].wires), list(self.wires)) ) else: tmp_composite = Sum(*ops) # only change compared to CompositeOp.eigvals() eigvals.append( - qml.utils.expand_vector( + qml.math.expand_vector( tmp_composite.eigendecomposition["eigval"], list(tmp_composite.wires), list(self.wires), diff --git a/pennylane/ops/qubit/non_parametric_ops.py b/pennylane/ops/qubit/non_parametric_ops.py index 8e12f6d333c..1855e0217a2 100644 --- a/pennylane/ops/qubit/non_parametric_ops.py +++ b/pennylane/ops/qubit/non_parametric_ops.py @@ -27,7 +27,6 @@ import pennylane as qml from pennylane.operation import Observable, Operation from pennylane.typing import TensorLike -from pennylane.utils import pauli_eigs from pennylane.wires import Wires, WiresLike INV_SQRT2 = 1 / qml.math.sqrt(2) @@ -126,7 +125,7 @@ def compute_eigvals() -> np.ndarray: # pylint: disable=arguments-differ >>> print(qml.Hadamard.compute_eigvals()) [ 1 -1] """ - return pauli_eigs(1) + return qml.pauli.pauli_eigs(1) @staticmethod def compute_diagonalizing_gates(wires: WiresLike) -> list[qml.operation.Operator]: @@ -315,7 +314,7 @@ def compute_eigvals() -> np.ndarray: # pylint: disable=arguments-differ >>> print(qml.X.compute_eigvals()) [ 1 -1] """ - return pauli_eigs(1) + return qml.pauli.pauli_eigs(1) @staticmethod def compute_diagonalizing_gates(wires: WiresLike) -> list[qml.operation.Operator]: @@ -506,7 +505,7 @@ def compute_eigvals() -> np.ndarray: # pylint: disable=arguments-differ >>> print(qml.Y.compute_eigvals()) [ 1 -1] """ - return pauli_eigs(1) + return qml.pauli.pauli_eigs(1) @staticmethod def compute_diagonalizing_gates(wires: WiresLike) -> list[qml.operation.Operator]: @@ -695,7 +694,7 @@ def compute_eigvals() -> np.ndarray: # pylint: disable=arguments-differ >>> print(qml.Z.compute_eigvals()) [ 1 -1] """ - return pauli_eigs(1) + return qml.pauli.pauli_eigs(1) @staticmethod def compute_diagonalizing_gates( # pylint: disable=unused-argument diff --git a/pennylane/ops/qubit/parametric_ops_multi_qubit.py b/pennylane/ops/qubit/parametric_ops_multi_qubit.py index 39278435ad1..1064315965c 100644 --- a/pennylane/ops/qubit/parametric_ops_multi_qubit.py +++ b/pennylane/ops/qubit/parametric_ops_multi_qubit.py @@ -27,7 +27,6 @@ from pennylane.math import expand_matrix from pennylane.operation import AnyWires, FlatPytree, Operation from pennylane.typing import TensorLike -from pennylane.utils import pauli_eigs from pennylane.wires import Wires, WiresLike from .non_parametric_ops import Hadamard, PauliX, PauliY, PauliZ @@ -105,7 +104,7 @@ def compute_matrix( [0.0000+0.0000j, 0.0000+0.0000j, 0.9988+0.0500j, 0.0000+0.0000j], [0.0000+0.0000j, 0.0000+0.0000j, 0.0000+0.0000j, 0.9988-0.0500j]]) """ - eigs = qml.math.convert_like(pauli_eigs(num_wires), theta) + eigs = qml.math.convert_like(qml.pauli.pauli_eigs(num_wires), theta) if qml.math.get_interface(theta) == "tensorflow": theta = qml.math.cast_like(theta, 1j) @@ -153,7 +152,7 @@ def compute_eigvals( tensor([0.9689-0.2474j, 0.9689+0.2474j, 0.9689+0.2474j, 0.9689-0.2474j, 0.9689+0.2474j, 0.9689-0.2474j, 0.9689-0.2474j, 0.9689+0.2474j]) """ - eigs = qml.math.convert_like(pauli_eigs(num_wires), theta) + eigs = qml.math.convert_like(qml.pauli.pauli_eigs(num_wires), theta) if qml.math.get_interface(theta) == "tensorflow": theta = qml.math.cast_like(theta, 1j) diff --git a/pennylane/optimize/momentum_qng.py b/pennylane/optimize/momentum_qng.py index 611c204b2bd..376b18d60c1 100644 --- a/pennylane/optimize/momentum_qng.py +++ b/pennylane/optimize/momentum_qng.py @@ -16,9 +16,8 @@ # pylint: disable=too-many-branches # pylint: disable=too-many-arguments from pennylane import numpy as pnp -from pennylane.utils import _flatten, unflatten -from .qng import QNGOptimizer +from .qng import QNGOptimizer, _flatten_np, _unflatten_np class MomentumQNGOptimizer(QNGOptimizer): @@ -131,12 +130,12 @@ def apply_grad(self, grad, args): for index, arg in enumerate(args): if getattr(arg, "requires_grad", False): - grad_flat = pnp.array(list(_flatten(grad[trained_index]))) + grad_flat = pnp.array(list(_flatten_np(grad[trained_index]))) # self.metric_tensor has already been reshaped to 2D, matching flat gradient. qng_update = pnp.linalg.pinv(metric_tensor[trained_index]) @ grad_flat self.accumulation[trained_index] *= self.momentum - self.accumulation[trained_index] += self.stepsize * unflatten( + self.accumulation[trained_index] += self.stepsize * _unflatten_np( qng_update, grad[trained_index] ) args_new[index] = arg - self.accumulation[trained_index] diff --git a/pennylane/optimize/qng.py b/pennylane/optimize/qng.py index a5c31e7218f..c55a5cb06bd 100644 --- a/pennylane/optimize/qng.py +++ b/pennylane/optimize/qng.py @@ -12,12 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. """Quantum natural gradient optimizer""" +import numbers +from collections.abc import Iterable + import pennylane as qml # pylint: disable=too-many-branches # pylint: disable=too-many-arguments from pennylane import numpy as pnp -from pennylane.utils import _flatten, unflatten from .gradient_descent import GradientDescentOptimizer @@ -277,11 +279,88 @@ def apply_grad(self, grad, args): trained_index = 0 for index, arg in enumerate(args): if getattr(arg, "requires_grad", False): - grad_flat = pnp.array(list(_flatten(grad[trained_index]))) + grad_flat = pnp.array(list(_flatten_np(grad[trained_index]))) # self.metric_tensor has already been reshaped to 2D, matching flat gradient. update = pnp.linalg.pinv(mt[trained_index]) @ grad_flat - args_new[index] = arg - self.stepsize * unflatten(update, grad[trained_index]) + args_new[index] = arg - self.stepsize * _unflatten_np(update, grad[trained_index]) trained_index += 1 return tuple(args_new) + + +def _flatten_np(x): + """Iterate recursively through an arbitrarily nested structure in depth-first order. + + See also :func:`_unflatten`. + + Args: + x (array, Iterable, Any): each element of an array or an Iterable may itself be any of these types + + Yields: + Any: elements of x in depth-first order + """ + if isinstance(x, pnp.ndarray): + yield from _flatten_np( + x.flat + ) # should we allow object arrays? or just "yield from x.flat"? + elif isinstance(x, qml.wires.Wires): + # Reursive calls to flatten `Wires` will cause infinite recursion (`Wires` atoms are `Wires`). + # Since Wires are always flat, just yield. + yield from x + elif isinstance(x, Iterable) and not isinstance(x, (str, bytes)): + for item in x: + yield from _flatten_np(item) + else: + yield x + + +def _unflatten_np_dispatch(flat, model): + """Restores an arbitrary nested structure to a flattened iterable. + + See also :func:`_flatten`. + + Args: + flat (array): 1D array of items + model (array, Iterable, Number): model nested structure + + Raises: + TypeError: if ``model`` contains an object of unsupported type + + Returns: + Union[array, list, Any], array: first elements of flat arranged into the nested + structure of model, unused elements of flat + """ + if isinstance(model, (numbers.Number, str)): + return flat[0], flat[1:] + + if isinstance(model, pnp.ndarray): + idx = model.size + res = pnp.array(flat)[:idx].reshape(model.shape) + return res, flat[idx:] + + if isinstance(model, Iterable): + res = [] + for x in model: + val, flat = _unflatten_np_dispatch(flat, x) + res.append(val) + return res, flat + + raise TypeError(f"Unsupported type in the model: {type(model)}") + + +def _unflatten_np(flat, model): + """Wrapper for :func:`_unflatten`. + + Args: + flat (array): 1D array of items + model (array, Iterable, Number): model nested structure + + Raises: + ValueError: if ``flat`` has more elements than ``model`` + """ + # pylint:disable=len-as-condition + res, tail = _unflatten_np_dispatch(pnp.asarray(flat), model) + if len(tail) != 0: + raise ValueError("Flattened iterable has more elements than the model.") + return res diff --git a/pennylane/optimize/rotoselect.py b/pennylane/optimize/rotoselect.py index 1d1669648a9..c61b27ba628 100644 --- a/pennylane/optimize/rotoselect.py +++ b/pennylane/optimize/rotoselect.py @@ -16,7 +16,8 @@ import numpy as np import pennylane as qml -from pennylane.utils import _flatten, unflatten + +from .qng import _flatten_np, _unflatten_np class RotoselectOptimizer: @@ -132,11 +133,11 @@ def step(self, objective_fn, x, generators, **kwargs): Returns: array: The new variable values :math:`x^{(t+1)}` as well as the new generators. """ - x_flat = np.fromiter(_flatten(x), dtype=float) + x_flat = np.fromiter(_flatten_np(x), dtype=float) # wrap the objective function so that it accepts the flattened parameter array # pylint:disable=unnecessary-lambda-assignment objective_fn_flat = lambda x_flat, gen: objective_fn( - unflatten(x_flat, x), generators=gen, **kwargs + _unflatten_np(x_flat, x), generators=gen, **kwargs ) try: @@ -151,7 +152,7 @@ def step(self, objective_fn, x, generators, **kwargs): objective_fn_flat, x_flat, generators, d ) - return unflatten(x_flat, x), generators + return _unflatten_np(x_flat, x), generators def _find_optimal_generators(self, objective_fn, x, generators, d): r"""Optimizer for the generators. diff --git a/pennylane/pauli/__init__.py b/pennylane/pauli/__init__.py index ab90d27df81..394be72cb77 100644 --- a/pennylane/pauli/__init__.py +++ b/pennylane/pauli/__init__.py @@ -34,6 +34,7 @@ diagonalize_qwc_pauli_words, diagonalize_qwc_groupings, simplify, + pauli_eigs, ) from .pauli_interface import pauli_word_prefactor diff --git a/pennylane/pauli/utils.py b/pennylane/pauli/utils.py index 7365e3131e1..8f0ec975ec0 100644 --- a/pennylane/pauli/utils.py +++ b/pennylane/pauli/utils.py @@ -1335,3 +1335,21 @@ def _binary_matrix_from_pws(terms, num_qubits, wire_map=None): binary_matrix[idx][wire_map[wire]] = 1 return binary_matrix + + +@lru_cache() +def pauli_eigs(n): + r"""Eigenvalues for :math:`A^{\otimes n}`, where :math:`A` is + Pauli operator, or shares its eigenvalues. + + As an example if n==2, then the eigenvalues of a tensor product consisting + of two matrices sharing the eigenvalues with Pauli matrices is returned. + + Args: + n (int): the number of qubits the matrix acts on + Returns: + list: the eigenvalues of the specified observable + """ + if n == 1: + return np.array([1.0, -1.0]) + return np.concatenate([pauli_eigs(n - 1), -pauli_eigs(n - 1)]) diff --git a/pennylane/pytrees/__init__.py b/pennylane/pytrees/__init__.py index 6180de919f5..420bd25503e 100644 --- a/pennylane/pytrees/__init__.py +++ b/pennylane/pytrees/__init__.py @@ -15,7 +15,14 @@ An internal module for working with pytrees. """ -from .pytrees import PyTreeStructure, flatten, is_pytree, leaf, register_pytree, unflatten +from .pytrees import ( + PyTreeStructure, + flatten, + is_pytree, + leaf, + register_pytree, + unflatten, +) __all__ = [ "PyTreeStructure", diff --git a/pennylane/transforms/sign_expand/sign_expand.py b/pennylane/transforms/sign_expand/sign_expand.py index 928079f29c5..aaad22cb63e 100644 --- a/pennylane/transforms/sign_expand/sign_expand.py +++ b/pennylane/transforms/sign_expand/sign_expand.py @@ -310,7 +310,6 @@ def circuit(): hamiltonian = tape.measurements[0].obs wires = hamiltonian.wires - # TODO qml.utils.sparse_hamiltonian at the moment does not allow autograd to push gradients through if ( not isinstance(hamiltonian, (qml.ops.Hamiltonian, qml.ops.LinearCombination)) or len(tape.measurements) > 1 diff --git a/pennylane/utils.py b/pennylane/utils.py deleted file mode 100644 index e9eab2c3171..00000000000 --- a/pennylane/utils.py +++ /dev/null @@ -1,208 +0,0 @@ -# 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 utilities and auxiliary functions which are shared -across the PennyLane submodules. -""" -import functools -import inspect -import numbers - -# pylint: disable=protected-access,too-many-branches -from collections.abc import Iterable - -import numpy as np - -import pennylane as qml - - -def _flatten(x): - """Iterate recursively through an arbitrarily nested structure in depth-first order. - - See also :func:`_unflatten`. - - Args: - x (array, Iterable, Any): each element of an array or an Iterable may itself be any of these types - - Yields: - Any: elements of x in depth-first order - """ - if isinstance(x, np.ndarray): - yield from _flatten(x.flat) # should we allow object arrays? or just "yield from x.flat"? - elif isinstance(x, qml.wires.Wires): - # Reursive calls to flatten `Wires` will cause infinite recursion (`Wires` atoms are `Wires`). - # Since Wires are always flat, just yield. - yield from x - elif isinstance(x, Iterable) and not isinstance(x, (str, bytes)): - for item in x: - yield from _flatten(item) - else: - yield x - - -def _unflatten(flat, model): - """Restores an arbitrary nested structure to a flattened iterable. - - See also :func:`_flatten`. - - Args: - flat (array): 1D array of items - model (array, Iterable, Number): model nested structure - - Raises: - TypeError: if ``model`` contains an object of unsupported type - - Returns: - Union[array, list, Any], array: first elements of flat arranged into the nested - structure of model, unused elements of flat - """ - if isinstance(model, (numbers.Number, str)): - return flat[0], flat[1:] - - if isinstance(model, np.ndarray): - idx = model.size - res = np.array(flat)[:idx].reshape(model.shape) - return res, flat[idx:] - - if isinstance(model, Iterable): - res = [] - for x in model: - val, flat = _unflatten(flat, x) - res.append(val) - return res, flat - - raise TypeError(f"Unsupported type in the model: {type(model)}") - - -def unflatten(flat, model): - """Wrapper for :func:`_unflatten`. - - Args: - flat (array): 1D array of items - model (array, Iterable, Number): model nested structure - - Raises: - ValueError: if ``flat`` has more elements than ``model`` - """ - # pylint:disable=len-as-condition - res, tail = _unflatten(np.asarray(flat), model) - if len(tail) != 0: - raise ValueError("Flattened iterable has more elements than the model.") - return res - - -def _inv_dict(d): - """Reverse a dictionary mapping. - - Returns multimap where the keys are the former values, - and values are sets of the former keys. - - Args: - d (dict[a->b]): mapping to reverse - - Returns: - dict[b->set[a]]: reversed mapping - """ - ret = {} - for k, v in d.items(): - ret.setdefault(v, set()).add(k) - return ret - - -def _get_default_args(func): - """Get the default arguments of a function. - - Args: - func (callable): a function - - Returns: - dict[str, tuple]: mapping from argument name to (positional idx, default value) - """ - signature = inspect.signature(func) - return { - k: (idx, v.default) - for idx, (k, v) in enumerate(signature.parameters.items()) - if v.default is not inspect.Parameter.empty - } - - -@functools.lru_cache() -def pauli_eigs(n): - r"""Eigenvalues for :math:`A^{\otimes n}`, where :math:`A` is - Pauli operator, or shares its eigenvalues. - - As an example if n==2, then the eigenvalues of a tensor product consisting - of two matrices sharing the eigenvalues with Pauli matrices is returned. - - Args: - n (int): the number of qubits the matrix acts on - Returns: - list: the eigenvalues of the specified observable - """ - if n == 1: - return np.array([1.0, -1.0]) - return np.concatenate([pauli_eigs(n - 1), -pauli_eigs(n - 1)]) - - -def expand_vector(vector, original_wires, expanded_wires): - r"""Expand a vector to more wires. - - Args: - vector (array): :math:`2^n` vector where n = len(original_wires). - original_wires (Sequence[int]): original wires of vector - expanded_wires (Union[Sequence[int], int]): expanded wires of vector, can be shuffled - If a single int m is given, corresponds to list(range(m)) - - Returns: - array: :math:`2^m` vector where m = len(expanded_wires). - """ - if len(original_wires) == 0: - val = qml.math.squeeze(vector) - return val * qml.math.ones(2 ** len(expanded_wires)) - if isinstance(expanded_wires, numbers.Integral): - expanded_wires = list(range(expanded_wires)) - - N = len(original_wires) - M = len(expanded_wires) - D = M - N - - len_vector = qml.math.shape(vector)[0] - qudit_order = int(2 ** (np.log2(len_vector) / N)) - - if not set(expanded_wires).issuperset(original_wires): - raise ValueError("Invalid target subsystems provided in 'original_wires' argument.") - - if qml.math.shape(vector) != (qudit_order**N,): - raise ValueError(f"Vector parameter must be of length {qudit_order}**len(original_wires)") - - dims = [qudit_order] * N - tensor = qml.math.reshape(vector, dims) - - if D > 0: - extra_dims = [qudit_order] * D - ones = qml.math.ones(qudit_order**D).reshape(extra_dims) - expanded_tensor = qml.math.tensordot(tensor, ones, axes=0) - else: - expanded_tensor = tensor - - wire_indices = [expanded_wires.index(wire) for wire in original_wires] - wire_indices = np.array(wire_indices) - - # Order tensor factors according to wires - original_indices = np.array(range(N)) - expanded_tensor = qml.math.moveaxis( - expanded_tensor, tuple(original_indices), tuple(wire_indices) - ) - - return qml.math.reshape(expanded_tensor, (qudit_order**M,)) diff --git a/tests/math/test_matrix_manipulation.py b/tests/math/test_matrix_manipulation.py index d6b4029a0ca..8ea97f2ee18 100644 --- a/tests/math/test_matrix_manipulation.py +++ b/tests/math/test_matrix_manipulation.py @@ -22,7 +22,7 @@ import pennylane as qml from pennylane import numpy as pnp -from pennylane.math import expand_matrix +from pennylane.math import expand_matrix, expand_vector # Define a list of dtypes to test dtypes = ["complex64", "complex128"] @@ -1004,3 +1004,67 @@ def test_partial_trace_single_matrix(self, ml_framework, c_dtype): expected = qml.math.asarray(np.array([[1, 0], [0, 0]], dtype=c_dtype), like=ml_framework) assert qml.math.allclose(result, expected) + + +class TestExpandVector: + """Tests vector expansion to more wires""" + + VECTOR1 = np.array([1, -1]) + ONES = np.array([1, 1]) + + @pytest.mark.parametrize( + "original_wires,expanded_wires,expected", + [ + ([0], 3, np.kron(np.kron(VECTOR1, ONES), ONES)), + ([1], 3, np.kron(np.kron(ONES, VECTOR1), ONES)), + ([2], 3, np.kron(np.kron(ONES, ONES), VECTOR1)), + ([0], [0, 4, 7], np.kron(np.kron(VECTOR1, ONES), ONES)), + ([4], [0, 4, 7], np.kron(np.kron(ONES, VECTOR1), ONES)), + ([7], [0, 4, 7], np.kron(np.kron(ONES, ONES), VECTOR1)), + ([0], [0, 4, 7], np.kron(np.kron(VECTOR1, ONES), ONES)), + ([4], [4, 0, 7], np.kron(np.kron(VECTOR1, ONES), ONES)), + ([7], [7, 4, 0], np.kron(np.kron(VECTOR1, ONES), ONES)), + ], + ) + def test_expand_vector_single_wire(self, original_wires, expanded_wires, expected, tol): + """Test that expand_vector works with a single-wire vector.""" + + res = expand_vector(TestExpandVector.VECTOR1, original_wires, expanded_wires) + + assert np.allclose(res, expected, atol=tol, rtol=0) + + VECTOR2 = np.array([1, 2, 3, 4]) + ONES = np.array([1, 1]) + + @pytest.mark.parametrize( + "original_wires,expanded_wires,expected", + [ + ([0, 1], 3, np.kron(VECTOR2, ONES)), + ([1, 2], 3, np.kron(ONES, VECTOR2)), + ([0, 2], 3, np.array([1, 2, 1, 2, 3, 4, 3, 4])), + ([0, 5], [0, 5, 9], np.kron(VECTOR2, ONES)), + ([5, 9], [0, 5, 9], np.kron(ONES, VECTOR2)), + ([0, 9], [0, 5, 9], np.array([1, 2, 1, 2, 3, 4, 3, 4])), + ([9, 0], [0, 5, 9], np.array([1, 3, 1, 3, 2, 4, 2, 4])), + ([0, 1], [0, 1], VECTOR2), + ], + ) + def test_expand_vector_two_wires(self, original_wires, expanded_wires, expected, tol): + """Test that expand_vector works with a single-wire vector.""" + + res = expand_vector(TestExpandVector.VECTOR2, original_wires, expanded_wires) + + assert np.allclose(res, expected, atol=tol, rtol=0) + + def test_expand_vector_invalid_wires(self): + """Test exception raised if unphysical subsystems provided.""" + with pytest.raises( + ValueError, + match="Invalid target subsystems provided in 'original_wires' argument", + ): + expand_vector(TestExpandVector.VECTOR2, [-1, 5], 4) + + def test_expand_vector_invalid_vector(self): + """Test exception raised if incorrect sized vector provided.""" + with pytest.raises(ValueError, match="Vector parameter must be of length"): + expand_vector(TestExpandVector.VECTOR1, [0, 1], 4) diff --git a/tests/ops/qubit/test_parametric_ops.py b/tests/ops/qubit/test_parametric_ops.py index f9fe0e6d9ab..66a44b98e9b 100644 --- a/tests/ops/qubit/test_parametric_ops.py +++ b/tests/ops/qubit/test_parametric_ops.py @@ -3260,7 +3260,7 @@ def test_multirz_generator(self, qubits, mocker): qml.assert_equal(gen, qml.Hamiltonian([-0.5], [expected_gen])) - spy = mocker.spy(qml.utils, "pauli_eigs") + spy = mocker.spy(qml.pauli.utils, "pauli_eigs") op.generator() spy.assert_not_called() diff --git a/tests/optimize/test_qng.py b/tests/optimize/test_qng.py index 3347fdca290..a382a73aef3 100644 --- a/tests/optimize/test_qng.py +++ b/tests/optimize/test_qng.py @@ -18,6 +18,7 @@ import pennylane as qml from pennylane import numpy as np +from pennylane.optimize.qng import _flatten_np, _unflatten_np class TestBasics: @@ -394,3 +395,66 @@ def gradient(params): assert np.allclose(circuit(x, y), qml.eigvals(H).min(), atol=tol, rtol=0) if qml.operation.active_new_opmath(): assert len(recwarn) == 0 + + +flat_dummy_array = np.linspace(-1, 1, 64) +test_shapes = [ + (64,), + (64, 1), + (32, 2), + (16, 4), + (8, 8), + (16, 2, 2), + (8, 2, 2, 2), + (4, 2, 2, 2, 2), + (2, 2, 2, 2, 2, 2), +] + + +class TestFlatten: + """Tests the flatten and unflatten functions""" + + @pytest.mark.parametrize("shape", test_shapes) + def test_flatten(self, shape): + """Tests that _flatten successfully flattens multidimensional arrays.""" + + reshaped = np.reshape(flat_dummy_array, shape) + flattened = np.array(list(_flatten_np(reshaped))) + + assert flattened.shape == flat_dummy_array.shape + assert np.array_equal(flattened, flat_dummy_array) + + @pytest.mark.parametrize("shape", test_shapes) + def test_unflatten(self, shape): + """Tests that _unflatten successfully unflattens multidimensional arrays.""" + + reshaped = np.reshape(flat_dummy_array, shape) + unflattened = np.array(list(_unflatten_np(flat_dummy_array, reshaped))) + + assert unflattened.shape == reshaped.shape + assert np.array_equal(unflattened, reshaped) + + def test_unflatten_error_unsupported_model(self): + """Tests that unflatten raises an error if the given model is not supported""" + + with pytest.raises(TypeError, match="Unsupported type in the model"): + model = lambda x: x # not a valid model for unflatten + _unflatten_np(flat_dummy_array, model) + + def test_unflatten_error_too_many_elements(self): + """Tests that unflatten raises an error if the given iterable has + more elements than the model""" + + reshaped = np.reshape(flat_dummy_array, (16, 2, 2)) + + with pytest.raises(ValueError, match="Flattened iterable has more elements than the model"): + _unflatten_np(np.concatenate([flat_dummy_array, flat_dummy_array]), reshaped) + + def test_flatten_wires(self): + """Tests flattening a Wires object.""" + wires = qml.wires.Wires([3, 4]) + wires_int = [3, 4] + + wires = _flatten_np(wires) + for i, wire in enumerate(wires): + assert wires_int[i] == wire diff --git a/tests/optimize/test_rotosolve.py b/tests/optimize/test_rotosolve.py index 2db9075e5bc..e000ea5f03c 100644 --- a/tests/optimize/test_rotosolve.py +++ b/tests/optimize/test_rotosolve.py @@ -23,7 +23,7 @@ import pennylane as qml from pennylane import numpy as np from pennylane.optimize import RotosolveOptimizer -from pennylane.utils import _flatten, unflatten +from pennylane.optimize.qng import _flatten_np, _unflatten_np def expand_num_freq(num_freq, param): @@ -47,11 +47,11 @@ def expand_num_freq(num_freq, param): def successive_params(par1, par2): """Return a list of parameter configurations, successively walking from par1 to par2 coordinate-wise.""" - par1_flat = np.fromiter(_flatten(par1), dtype=float) - par2_flat = np.fromiter(_flatten(par2), dtype=float) + par1_flat = np.fromiter(_flatten_np(par1), dtype=float) + par2_flat = np.fromiter(_flatten_np(par2), dtype=float) walking_param = [] for i in range(len(par1_flat) + 1): - walking_param.append(unflatten(np.append(par2_flat[:i], par1_flat[i:]), par1)) + walking_param.append(_unflatten_np(np.append(par2_flat[:i], par1_flat[i:]), par1)) return walking_param @@ -253,8 +253,8 @@ def test_single_step_convergence( assert len(x_min) == len(new_param_step) assert np.allclose( - np.fromiter(_flatten(x_min), dtype=float), - np.fromiter(_flatten(new_param_step), dtype=float), + np.fromiter(_flatten_np(x_min), dtype=float), + np.fromiter(_flatten_np(new_param_step), dtype=float), atol=1e-5, ) @@ -271,8 +271,8 @@ def test_single_step_convergence( assert len(x_min) == len(new_param_step_and_cost) assert np.allclose( - np.fromiter(_flatten(new_param_step_and_cost), dtype=float), - np.fromiter(_flatten(new_param_step), dtype=float), + np.fromiter(_flatten_np(new_param_step_and_cost), dtype=float), + np.fromiter(_flatten_np(new_param_step), dtype=float), atol=1e-5, ) assert np.isclose(old_cost, fun(*param)) @@ -331,8 +331,8 @@ def test_multiple_steps(fun, x_min, param, num_freq): assert (np.isscalar(x_min) and np.isscalar(param)) or len(x_min) == len(param) assert np.allclose( - np.fromiter(_flatten(x_min), dtype=float), - np.fromiter(_flatten(param), dtype=float), + np.fromiter(_flatten_np(x_min), dtype=float), + np.fromiter(_flatten_np(param), dtype=float), atol=1e-5, ) @@ -390,8 +390,8 @@ def test_single_step(self, fun, x_min, param, num_freq): assert len(x_min) == len(new_param_step) assert np.allclose( - np.fromiter(_flatten(x_min), dtype=float), - np.fromiter(_flatten(new_param_step), dtype=float), + np.fromiter(_flatten_np(x_min), dtype=float), + np.fromiter(_flatten_np(new_param_step), dtype=float), atol=1e-5, ) @@ -407,8 +407,8 @@ def test_single_step(self, fun, x_min, param, num_freq): assert len(x_min) == len(new_param_step_and_cost) assert np.allclose( - np.fromiter(_flatten(new_param_step_and_cost), dtype=float), - np.fromiter(_flatten(new_param_step), dtype=float), + np.fromiter(_flatten_np(new_param_step_and_cost), dtype=float), + np.fromiter(_flatten_np(new_param_step), dtype=float), atol=1e-5, ) assert np.isclose(old_cost, fun(*param)) @@ -517,8 +517,8 @@ def test_single_step( new_param_step_and_cost = (new_param_step_and_cost,) assert np.allclose( - np.fromiter(_flatten(new_param_step_and_cost), dtype=float), - np.fromiter(_flatten(new_param_step), dtype=float), + np.fromiter(_flatten_np(new_param_step_and_cost), dtype=float), + np.fromiter(_flatten_np(new_param_step), dtype=float), ) assert np.isclose(qnode(*param), old_cost) diff --git a/tests/pauli/test_pauli_utils.py b/tests/pauli/test_pauli_utils.py index cfba6dc1195..a8c7e3a060a 100644 --- a/tests/pauli/test_pauli_utils.py +++ b/tests/pauli/test_pauli_utils.py @@ -14,9 +14,11 @@ """ Unit tests for the :mod:`pauli` utility functions in ``pauli/utils.py``. """ +# pylint: disable=too-few-public-methods,too-many-public-methods +import functools +import itertools import warnings -# pylint: disable=too-few-public-methods,too-many-public-methods import numpy as np import pytest @@ -45,6 +47,7 @@ is_qwc, observables_to_binary_matrix, partition_pauli_group, + pauli_eigs, pauli_group, pauli_to_binary, pauli_word_to_matrix, @@ -1135,3 +1138,42 @@ def test_binary_matrix_from_pws(self, terms, num_qubits, result): pws_lst = [list(qml.pauli.pauli_sentence(t))[0] for t in terms] binary_matrix = qml.pauli.utils._binary_matrix_from_pws(pws_lst, num_qubits) assert (binary_matrix == result).all() + + +class TestPauliEigs: + """Tests for the auxiliary function to return the eigenvalues for Paulis""" + + paulix = np.array([[0, 1], [1, 0]]) + pauliy = np.array([[0, -1j], [1j, 0]]) + pauliz = np.array([[1, 0], [0, -1]]) + hadamard = 1 / np.sqrt(2) * np.array([[1, 1], [1, -1]]) + + standard_observables = [paulix, pauliy, pauliz, hadamard] + + matrix_pairs = [ + np.kron(x, y) + for x, y in list(itertools.product(standard_observables, standard_observables)) + ] + + def test_correct_eigenvalues_paulis(self): + """Test the paulieigs function for one qubit""" + assert np.array_equal(pauli_eigs(1), np.diag(self.pauliz)) + + def test_correct_eigenvalues_pauli_kronecker_products_two_qubits(self): + """Test the paulieigs function for two qubits""" + assert np.array_equal(pauli_eigs(2), np.diag(np.kron(self.pauliz, self.pauliz))) + + def test_correct_eigenvalues_pauli_kronecker_products_three_qubits(self): + """Test the paulieigs function for three qubits""" + assert np.array_equal( + pauli_eigs(3), + np.diag(np.kron(self.pauliz, np.kron(self.pauliz, self.pauliz))), + ) + + @pytest.mark.parametrize("depth", list(range(1, 6))) + def test_cache_usage(self, depth): + """Test that the right number of cachings have been executed after clearing the cache""" + pauli_eigs.cache_clear() + pauli_eigs(depth) + # pylint: disable=protected-access + assert functools._CacheInfo(depth - 1, depth, 128, depth) == pauli_eigs.cache_info() diff --git a/tests/test_utils.py b/tests/test_utils.py deleted file mode 100644 index 47dac02148f..00000000000 --- a/tests/test_utils.py +++ /dev/null @@ -1,237 +0,0 @@ -# Copyright 2018-2020 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. -""" -Unit tests for the :mod:`pennylane.utils` module. -""" -# pylint: disable=no-self-use,too-many-arguments,protected-access -import functools -import itertools - -import numpy as np -import pytest - -import pennylane as qml -import pennylane.utils as pu - -flat_dummy_array = np.linspace(-1, 1, 64) -test_shapes = [ - (64,), - (64, 1), - (32, 2), - (16, 4), - (8, 8), - (16, 2, 2), - (8, 2, 2, 2), - (4, 2, 2, 2, 2), - (2, 2, 2, 2, 2, 2), -] - - -class TestFlatten: - """Tests the flatten and unflatten functions""" - - @pytest.mark.parametrize("shape", test_shapes) - def test_flatten(self, shape): - """Tests that _flatten successfully flattens multidimensional arrays.""" - - reshaped = np.reshape(flat_dummy_array, shape) - flattened = np.array(list(pu._flatten(reshaped))) - - assert flattened.shape == flat_dummy_array.shape - assert np.array_equal(flattened, flat_dummy_array) - - @pytest.mark.parametrize("shape", test_shapes) - def test_unflatten(self, shape): - """Tests that _unflatten successfully unflattens multidimensional arrays.""" - - reshaped = np.reshape(flat_dummy_array, shape) - unflattened = np.array(list(pu.unflatten(flat_dummy_array, reshaped))) - - assert unflattened.shape == reshaped.shape - assert np.array_equal(unflattened, reshaped) - - def test_unflatten_error_unsupported_model(self): - """Tests that unflatten raises an error if the given model is not supported""" - - with pytest.raises(TypeError, match="Unsupported type in the model"): - model = lambda x: x # not a valid model for unflatten - pu.unflatten(flat_dummy_array, model) - - def test_unflatten_error_too_many_elements(self): - """Tests that unflatten raises an error if the given iterable has - more elements than the model""" - - reshaped = np.reshape(flat_dummy_array, (16, 2, 2)) - - with pytest.raises(ValueError, match="Flattened iterable has more elements than the model"): - pu.unflatten(np.concatenate([flat_dummy_array, flat_dummy_array]), reshaped) - - def test_flatten_wires(self): - """Tests flattening a Wires object.""" - wires = qml.wires.Wires([3, 4]) - wires_int = [3, 4] - - wires = qml.utils._flatten(wires) - for i, wire in enumerate(wires): - assert wires_int[i] == wire - - -class TestPauliEigs: - """Tests for the auxiliary function to return the eigenvalues for Paulis""" - - paulix = np.array([[0, 1], [1, 0]]) - pauliy = np.array([[0, -1j], [1j, 0]]) - pauliz = np.array([[1, 0], [0, -1]]) - hadamard = 1 / np.sqrt(2) * np.array([[1, 1], [1, -1]]) - - standard_observables = [paulix, pauliy, pauliz, hadamard] - - matrix_pairs = [ - np.kron(x, y) - for x, y in list(itertools.product(standard_observables, standard_observables)) - ] - - def test_correct_eigenvalues_paulis(self): - """Test the paulieigs function for one qubit""" - assert np.array_equal(pu.pauli_eigs(1), np.diag(self.pauliz)) - - def test_correct_eigenvalues_pauli_kronecker_products_two_qubits(self): - """Test the paulieigs function for two qubits""" - assert np.array_equal(pu.pauli_eigs(2), np.diag(np.kron(self.pauliz, self.pauliz))) - - def test_correct_eigenvalues_pauli_kronecker_products_three_qubits(self): - """Test the paulieigs function for three qubits""" - assert np.array_equal( - pu.pauli_eigs(3), - np.diag(np.kron(self.pauliz, np.kron(self.pauliz, self.pauliz))), - ) - - @pytest.mark.parametrize("depth", list(range(1, 6))) - def test_cache_usage(self, depth): - """Test that the right number of cachings have been executed after clearing the cache""" - pu.pauli_eigs.cache_clear() - pu.pauli_eigs(depth) - assert functools._CacheInfo(depth - 1, depth, 128, depth) == pu.pauli_eigs.cache_info() - - -class TestArgumentHelpers: - """Tests for auxiliary functions to help with parsing - Python function arguments""" - - def test_no_default_args(self): - """Test that empty dict is returned if function has - no default arguments""" - - def dummy_func(a, b): # pylint: disable=unused-argument - pass - - res = pu._get_default_args(dummy_func) - assert not res - - def test_get_default_args(self): - """Test that default arguments are correctly extracted""" - - def dummy_func( - a, b, c=8, d=[0, 0.65], e=np.array([4]), f=None - ): # pylint: disable=unused-argument,dangerous-default-value - pass - - res = pu._get_default_args(dummy_func) - expected = { - "c": (2, 8), - "d": (3, [0, 0.65]), - "e": (4, np.array([4])), - "f": (5, None), - } - - assert res == expected - - def test_inv_dict(self): - """Test _inv_dict correctly inverts a dictionary""" - test_data = {"c": 8, "d": (0, 0.65), "e": "hi", "f": None, "g": 8} - res = pu._inv_dict(test_data) - expected = {8: {"g", "c"}, (0, 0.65): {"d"}, "hi": {"e"}, None: {"f"}} - - assert res == expected - - def test_inv_dict_unhashable_key(self): - """Test _inv_dict raises an exception if a dictionary value is unhashable""" - test_data = {"c": 8, "d": [0, 0.65], "e": "hi", "f": None, "g": 8} - - with pytest.raises(TypeError, match="unhashable type"): - pu._inv_dict(test_data) - - -class TestExpandVector: - """Tests vector expansion to more wires""" - - VECTOR1 = np.array([1, -1]) - ONES = np.array([1, 1]) - - @pytest.mark.parametrize( - "original_wires,expanded_wires,expected", - [ - ([0], 3, np.kron(np.kron(VECTOR1, ONES), ONES)), - ([1], 3, np.kron(np.kron(ONES, VECTOR1), ONES)), - ([2], 3, np.kron(np.kron(ONES, ONES), VECTOR1)), - ([0], [0, 4, 7], np.kron(np.kron(VECTOR1, ONES), ONES)), - ([4], [0, 4, 7], np.kron(np.kron(ONES, VECTOR1), ONES)), - ([7], [0, 4, 7], np.kron(np.kron(ONES, ONES), VECTOR1)), - ([0], [0, 4, 7], np.kron(np.kron(VECTOR1, ONES), ONES)), - ([4], [4, 0, 7], np.kron(np.kron(VECTOR1, ONES), ONES)), - ([7], [7, 4, 0], np.kron(np.kron(VECTOR1, ONES), ONES)), - ], - ) - def test_expand_vector_single_wire(self, original_wires, expanded_wires, expected, tol): - """Test that expand_vector works with a single-wire vector.""" - - res = pu.expand_vector(TestExpandVector.VECTOR1, original_wires, expanded_wires) - - assert np.allclose(res, expected, atol=tol, rtol=0) - - VECTOR2 = np.array([1, 2, 3, 4]) - ONES = np.array([1, 1]) - - @pytest.mark.parametrize( - "original_wires,expanded_wires,expected", - [ - ([0, 1], 3, np.kron(VECTOR2, ONES)), - ([1, 2], 3, np.kron(ONES, VECTOR2)), - ([0, 2], 3, np.array([1, 2, 1, 2, 3, 4, 3, 4])), - ([0, 5], [0, 5, 9], np.kron(VECTOR2, ONES)), - ([5, 9], [0, 5, 9], np.kron(ONES, VECTOR2)), - ([0, 9], [0, 5, 9], np.array([1, 2, 1, 2, 3, 4, 3, 4])), - ([9, 0], [0, 5, 9], np.array([1, 3, 1, 3, 2, 4, 2, 4])), - ([0, 1], [0, 1], VECTOR2), - ], - ) - def test_expand_vector_two_wires(self, original_wires, expanded_wires, expected, tol): - """Test that expand_vector works with a single-wire vector.""" - - res = pu.expand_vector(TestExpandVector.VECTOR2, original_wires, expanded_wires) - - assert np.allclose(res, expected, atol=tol, rtol=0) - - def test_expand_vector_invalid_wires(self): - """Test exception raised if unphysical subsystems provided.""" - with pytest.raises( - ValueError, - match="Invalid target subsystems provided in 'original_wires' argument", - ): - pu.expand_vector(TestExpandVector.VECTOR2, [-1, 5], 4) - - def test_expand_vector_invalid_vector(self): - """Test exception raised if incorrect sized vector provided.""" - with pytest.raises(ValueError, match="Vector parameter must be of length"): - pu.expand_vector(TestExpandVector.VECTOR1, [0, 1], 4)