From 734303832c36b8c71aef65fc0004cc611f0e1055 Mon Sep 17 00:00:00 2001 From: Korbinian Kottmann <43949391+Qottmann@users.noreply.github.com> Date: Mon, 25 Mar 2024 16:18:44 +0100 Subject: [PATCH] [new opmath 2] New `LinearCombination` class to succeed `qml.Hamiltonian` (#5216) Branching: https://github.com/PennyLaneAI/pennylane/pull/5269 > https://github.com/PennyLaneAI/pennylane/pull/5216 > https://github.com/PennyLaneAI/pennylane/pull/5322 > https://github.com/PennyLaneAI/pennylane/pull/5335 The basic idea is to have a clone of `qml.Hamiltonian` with a better name, `LinearCombination` - that inherits from `CompositeOp` and plays nice with new opmath. The motivation is that sometimes it is still handy to have an operator class for which you _know_ there is no funny nesting and that has `coeffs` and `ops` separately on demand without processing. ToDo - [x] basic init - [x] `tests/ops/op_math/test_linear_combination.py` pass - [x] update map_wires - [x] all tests pass - [x] add pauli_rep - [x] add operands attribute and make iterable (?) - [x] update string repr - [x] upgrade to internally use new opmath - [x] update tests with default new opmath - [x] dunder math methods support with new opmath - [x] simplify and speed-up `simplify()`, also make sure to not act in-place - [x] Could we make the matrix generation workflow more general like Sum? - [x] Also, deferring to the pauli rep method for matrix generation leads to substantial performance improvements. - [x] toggle `__use_new_opmath` - [x] tests_linear_combination pass - [x] Integration tests - [x] get all linear combination tests to pass locally - [x] utilize super().sparse_matrix - [x] use grouping functionality from Sum that Mudit is adding in https://github.com/PennyLaneAI/pennylane/pull/5179 - [x] update matmul to handle other LinearCombination instances - [x] remove top level import - [x] remove unnecessary copies - [x] add diagonalizing gates - [x] bugfix trivial case of simplify empty LinearCombination - [x] add trivial case for is_hermitian - [ ] torch differentiation with simplify=True in constructor - [x] changelog Optional features (likely to be included in a follow-up) - [x] `qml.Hamiltonian` points to either old Hamiltonian or LinearCombination depending on `__use_new_opmath` - [x] support Hermitian - [ ] matrix method (optional) - [ ] adjoint method (optional) - [ ] batching support (optional) - [ ] qml.dot points to LinearCombination - [ ] take care of xfails (mostly about raiseing errors in other parts of the codebase) - [ ] update logic of adjoint differentiation to catch attempt to differentiate lincomb coeffs - [x] support in qml.equal - [x] add special matmul with other product or non-composite ops - [x] add special matmul with other LinearCombination [sc-56704] --------- Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> Co-authored-by: Astral Cai Co-authored-by: Mudit Pandey Co-authored-by: Christina Lee Co-authored-by: Pietropaolo Frisoni Co-authored-by: albi3ro Co-authored-by: Utkarsh Co-authored-by: lillian542 Co-authored-by: Alex Preciado Co-authored-by: Thomas R. Bromley <49409390+trbromley@users.noreply.github.com> Co-authored-by: Vincent Michaud-Rioux Co-authored-by: Josh Izaac Co-authored-by: Nathan Killoran Co-authored-by: Matthew Silverman Co-authored-by: Mikhail Andrenkov Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- doc/development/deprecations.rst | 14 + doc/releases/changelog-dev.md | 33 +- pennylane/__init__.py | 12 + pennylane/_device.py | 12 +- pennylane/_qubit_device.py | 2 +- .../data/attributes/operator/operator.py | 32 +- pennylane/devices/default_clifford.py | 7 +- pennylane/devices/default_qubit.py | 1 + pennylane/devices/default_qubit_legacy.py | 10 +- pennylane/devices/default_qutrit.py | 7 +- pennylane/devices/qubit/measure.py | 4 +- pennylane/devices/qubit/sampling.py | 12 +- pennylane/devices/qutrit_mixed/sampling.py | 8 +- pennylane/devices/tests/test_measurements.py | 8 +- pennylane/gradients/gradient_transform.py | 2 +- pennylane/gradients/hadamard_gradient.py | 6 +- pennylane/gradients/hamiltonian_grad.py | 12 +- pennylane/gradients/parameter_shift.py | 87 +- pennylane/measurements/probs.py | 2 +- pennylane/operation.py | 103 +- .../ops/functions/bind_new_parameters.py | 7 +- pennylane/ops/functions/eigvals.py | 3 +- pennylane/ops/functions/equal.py | 13 +- pennylane/ops/functions/generator.py | 12 +- pennylane/ops/functions/matrix.py | 3 +- pennylane/ops/identity.py | 2 + pennylane/ops/op_math/__init__.py | 2 + pennylane/ops/op_math/adjoint.py | 4 +- pennylane/ops/op_math/composite.py | 5 +- pennylane/ops/op_math/controlled.py | 2 + pennylane/ops/op_math/exp.py | 16 +- pennylane/ops/op_math/linear_combination.py | 588 +++++ pennylane/ops/op_math/pow.py | 2 +- pennylane/ops/op_math/prod.py | 6 +- pennylane/ops/op_math/sprod.py | 2 +- pennylane/ops/op_math/sum.py | 36 +- pennylane/ops/op_math/symbolicop.py | 4 +- pennylane/ops/qubit/hamiltonian.py | 33 +- .../ops/qubit/parametric_ops_multi_qubit.py | 26 +- .../ops/qubit/parametric_ops_single_qubit.py | 6 +- pennylane/ops/qubit/qchem_ops.py | 64 +- pennylane/ops/qutrit/parametric_ops.py | 6 + pennylane/optimize/riemannian_gradient.py | 8 +- pennylane/optimize/shot_adaptive.py | 4 +- pennylane/pauli/conversion.py | 24 +- pennylane/pauli/pauli_arithmetic.py | 12 +- pennylane/pauli/pauli_interface.py | 16 +- pennylane/pauli/utils.py | 77 +- pennylane/pulse/hardware_hamiltonian.py | 9 +- pennylane/pulse/parametrized_evolution.py | 2 +- pennylane/pulse/parametrized_hamiltonian.py | 5 +- pennylane/qaoa/cycle.py | 33 +- pennylane/qaoa/layers.py | 11 +- pennylane/qchem/factorization.py | 9 +- pennylane/qchem/tapering.py | 6 +- pennylane/qcut/cutcircuit.py | 4 +- pennylane/qcut/tapes.py | 10 +- pennylane/shadows/classical_shadow.py | 30 +- pennylane/templates/subroutines/qdrift.py | 6 +- pennylane/templates/subroutines/trotter.py | 3 +- pennylane/transforms/hamiltonian_expand.py | 5 +- .../transforms/sign_expand/sign_expand.py | 2 +- pennylane/transforms/transpile.py | 10 +- pennylane/utils.py | 20 +- .../circuit_graph/test_circuit_graph_hash.py | 85 +- tests/conftest.py | 29 + .../data/attributes/operator/test_operator.py | 43 +- .../test_default_qubit_preprocessing.py | 35 +- .../test_default_qubit_tracking.py | 37 +- .../test_qutrit_mixed_sampling.py | 16 +- tests/devices/test_default_clifford.py | 3 +- tests/devices/test_default_qubit_jax.py | 17 +- tests/devices/test_default_qubit_legacy.py | 6 +- tests/devices/test_default_qubit_tf.py | 17 +- tests/devices/test_default_qubit_torch.py | 22 +- tests/devices/test_preprocess.py | 3 +- tests/drawer/test_tape_text.py | 7 +- tests/gradients/core/test_adjoint_diff.py | 21 +- .../gradients/core/test_hadamard_gradient.py | 8 +- tests/gradients/core/test_pulse_gradient.py | 3 +- tests/gradients/core/test_pulse_odegen.py | 10 +- .../test_parameter_shift_shot_vec.py | 3 +- tests/interfaces/test_autograd.py | 4 +- .../legacy/test_classical_shadow_legacy.py | 2 +- tests/measurements/test_classical_shadow.py | 2 +- tests/measurements/test_sample.py | 43 +- tests/ops/functions/conftest.py | 3 +- tests/ops/functions/test_assert_valid.py | 6 +- tests/ops/functions/test_dot.py | 4 +- tests/ops/functions/test_eigvals.py | 19 +- tests/ops/functions/test_equal.py | 20 +- tests/ops/functions/test_generator.py | 47 +- tests/ops/op_math/test_adjoint.py | 8 +- tests/ops/op_math/test_composite.py | 1 + tests/ops/op_math/test_controlled.py | 34 +- tests/ops/op_math/test_evolution.py | 10 +- tests/ops/op_math/test_exp.py | 7 +- tests/ops/op_math/test_linear_combination.py | 2021 +++++++++++++++++ tests/ops/op_math/test_pow_op.py | 26 +- tests/ops/op_math/test_prod.py | 21 + tests/ops/op_math/test_sum.py | 25 +- tests/ops/qubit/test_hamiltonian.py | 1290 ++++++----- tests/ops/qubit/test_parametric_ops.py | 26 +- tests/ops/qubit/test_qchem_ops.py | 15 +- tests/ops/qutrit/test_qutrit_observables.py | 1 + tests/optimize/test_qng.py | 26 + tests/optimize/test_spsa.py | 39 +- .../grouping/test_pauli_group_observables.py | 21 +- tests/pauli/test_conversion.py | 57 +- tests/pauli/test_pauli_arithmetic.py | 9 + tests/pauli/test_pauli_utils.py | 219 +- tests/pulse/test_rydberg.py | 1 + tests/qchem/of_tests/test_convert.py | 61 +- tests/qchem/of_tests/test_dipole_of.py | 21 +- .../of_tests/test_molecular_hamiltonian.py | 109 +- tests/qchem/test_dipole.py | 29 +- tests/qchem/test_factorization.py | 4 +- tests/qchem/test_hamiltonians.py | 29 +- tests/qchem/test_observable_hf.py | 32 +- tests/qchem/test_particle_number.py | 13 +- tests/qchem/test_spin.py | 32 +- tests/qchem/test_tapering.py | 114 +- tests/resource/test_error/test_error.py | 2 +- tests/shadow/test_shadow_class.py | 17 + tests/tape/test_tape.py | 1 + tests/test_debugging.py | 2 +- tests/test_device.py | 25 +- tests/test_operation.py | 490 ++-- tests/test_qaoa.py | 1859 +++++++++------ tests/test_qubit_device.py | 22 +- tests/test_queuing.py | 13 + tests/test_vqe.py | 30 + tests/transforms/test_batch_transform.py | 8 +- tests/transforms/test_defer_measurements.py | 2 + .../test_transform_dispatcher.py | 5 +- tests/transforms/test_hamiltonian_expand.py | 24 +- tests/transforms/test_insert_ops.py | 72 +- .../test_optimization/test_merge_rotations.py | 15 + tests/transforms/test_qcut.py | 286 ++- tests/transforms/test_sign_expand.py | 13 +- tests/transforms/test_tape_expand.py | 2 +- tests/transforms/test_transpile.py | 25 +- tests/workflow/test_construct_batch.py | 14 +- 143 files changed, 6858 insertions(+), 2407 deletions(-) create mode 100644 pennylane/ops/op_math/linear_combination.py create mode 100644 tests/ops/op_math/test_linear_combination.py diff --git a/doc/development/deprecations.rst b/doc/development/deprecations.rst index 7876396bd35..2d74dc913b0 100644 --- a/doc/development/deprecations.rst +++ b/doc/development/deprecations.rst @@ -34,6 +34,20 @@ Pending deprecations - Added and deprecated for ``Sum`` and ``Prod`` instances in v0.35 +* Accessing ``qml.ops.Hamiltonian`` with new operator arithmetic enabled is deprecated. Using ``qml.Hamiltonian`` + with new operator arithmetic enabled now returns a ``LinearCombination`` instance. Some functionality + may not work as expected, and use of the Hamiltonian class with the new operator arithmetic will not + be supported in future releases of PennyLane. + + You can update your code to the new operator arithmetic by using ``qml.Hamiltonian`` instead of importing + the Hamiltonian class directly or via ``qml.ops.Hamiltonian``. When the new operator arithmetic is enabled, + ``qml.Hamiltonian`` will access the new corresponding implementation. + + Alternatively, to continue accessing the legacy functionality, you can use + ``qml.operation.disable_new_opmath()``. + + - Deprecated in v0.36 + Completed deprecation cycles ---------------------------- diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 1e579a934d0..7f70c7b1d5c 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -64,6 +64,14 @@ * Added new function `qml.operation.convert_to_legacy_H` to convert `Sum`, `SProd`, and `Prod` to `Hamiltonian` instances. [(#5309)](https://github.com/PennyLaneAI/pennylane/pull/5309) +

Improvements πŸ› 

+ +* The `qml.is_commuting` function now accepts `Sum`, `SProd`, and `Prod` instances. + [(#5351)](https://github.com/PennyLaneAI/pennylane/pull/5351) + +* Operators can now be left multiplied `x * op` by numpy arrays. + [(#5361)](https://github.com/PennyLaneAI/pennylane/pull/5361) + * Create the `qml.Reflection` operator, useful for amplitude amplification and its variants. [(#5159)](https://github.com/PennyLaneAI/pennylane/pull/5159) @@ -123,6 +131,10 @@ ``` +* A new class `qml.ops.LinearCombination` is introduced. In essence, this class is an updated equivalent of `qml.ops.Hamiltonian` + but for usage with new operator arithmetic. + [(#5216)](https://github.com/PennyLaneAI/pennylane/pull/5216) + * The `qml.TrotterProduct` operator now supports error estimation functionality. [(#5384)](https://github.com/PennyLaneAI/pennylane/pull/5384) @@ -147,9 +159,14 @@ * The `molecular_hamiltonian` function calls `PySCF` directly when `method='pyscf'` is selected. [(#5118)](https://github.com/PennyLaneAI/pennylane/pull/5118) -* All generators in the source code (except those in the `qchem` module) no longer return - `Hamiltonian` or `Tensor` instances. Wherever possible, these return `Sum`, `SProd`, and `Prod` instances. +* The generators in the source code return operators consistent with the global setting for + `qml.operator.active_new_opmath()` wherever possible. `Sum`, `SProd` and `Prod` instances + will be returned even after disabling the new operator arithmetic in cases where they offer + additional functionality not available using legacy operators. [(#5253)](https://github.com/PennyLaneAI/pennylane/pull/5253) + [(#5410)](https://github.com/PennyLaneAI/pennylane/pull/5410) + [(#5411)](https://github.com/PennyLaneAI/pennylane/pull/5411) + [(#5421)](https://github.com/PennyLaneAI/pennylane/pull/5421) * Upgraded `null.qubit` to the new device API. Also, added support for all measurements and various modes of differentiation. [(#5211)](https://github.com/PennyLaneAI/pennylane/pull/5211) @@ -160,6 +177,9 @@ * `Hamiltonian.pauli_rep` is now defined if the hamiltonian is a linear combination of paulis. [(#5377)](https://github.com/PennyLaneAI/pennylane/pull/5377) +* `Prod.eigvals()` is now compatible with Qudit operators. + [(#5400)](https://github.com/PennyLaneAI/pennylane/pull/5400) + * Obtaining classical shadows using the `default.clifford` device is now compatible with [stim](https://github.com/quantumlib/Stim) `v1.13.0`. [(#5409)](https://github.com/PennyLaneAI/pennylane/pull/5409) @@ -226,6 +246,10 @@ * Attempting to multiply `PauliWord` and `PauliSentence` with `*` will raise an error. Instead, use `@` to conform with the PennyLane convention. [(#5341)](https://github.com/PennyLaneAI/pennylane/pull/5341) +* When new operator arithmetic is enabled, `qml.Hamiltonian` is now an alias for `qml.ops.LinearCombination`. + `Hamiltonian` will still be accessible as `qml.ops.Hamiltonian`. + [(#5393)](https://github.com/PennyLaneAI/pennylane/pull/5393) + * Since `default.mixed` does not support snapshots with measurements, attempting to do so will result in a `DeviceError` instead of getting the density matrix. [(#5416)](https://github.com/PennyLaneAI/pennylane/pull/5416) @@ -245,6 +269,11 @@ ... circuit = qml.from_qasm(f.read()) ``` +* Accessing `qml.ops.Hamiltonian` with new operator arithmetic is deprecated. Using `qml.Hamiltonian` with new operator arithmetic enabled now + returns a `LinearCombination` instance. Some functionality may not work as expected. To continue using the `Hamiltonian` class, you can use + `qml.operation.disable_new_opmath()` to disable the new operator arithmetic. + [(#5393)](https://github.com/PennyLaneAI/pennylane/pull/5393) +

Documentation πŸ“

* Removed some redundant documentation for the `evolve` function. diff --git a/pennylane/__init__.py b/pennylane/__init__.py index 6eed9004466..b3a50d287a5 100644 --- a/pennylane/__init__.py +++ b/pennylane/__init__.py @@ -145,6 +145,18 @@ class PennyLaneDeprecationWarning(UserWarning): """Warning raised when a PennyLane feature is being deprecated.""" +del globals()["Hamiltonian"] + + +def __getattr__(name): + if name == "Hamiltonian": + if pennylane.operation.active_new_opmath(): + return pennylane.ops.LinearCombination + return pennylane.ops.Hamiltonian + + raise AttributeError(f"module 'pennylane' has no attribute '{name}'") + + def _get_device_entrypoints(): """Returns a dictionary mapping the device short name to the loadable entrypoint""" diff --git a/pennylane/_device.py b/pennylane/_device.py index 3553e2b8c0d..18b0c533878 100644 --- a/pennylane/_device.py +++ b/pennylane/_device.py @@ -42,7 +42,7 @@ ) from pennylane.operation import Observable, Operation, Tensor, Operator, StatePrepBase -from pennylane.ops import Hamiltonian, Sum +from pennylane.ops import Hamiltonian, Sum, LinearCombination from pennylane.tape import QuantumScript, QuantumTape, expand_tape_state_prep from pennylane.wires import WireError, Wires from pennylane.queuing import QueuingManager @@ -678,9 +678,8 @@ def default_expand_fn(self, circuit, max_expansion=10): ) obs_on_same_wire = len(circuit._obs_sharing_wires) > 0 or comp_basis_sampled_multi_measure obs_on_same_wire &= not any( - isinstance(o, qml.Hamiltonian) for o in circuit._obs_sharing_wires + isinstance(o, (Hamiltonian, LinearCombination)) for o in circuit._obs_sharing_wires ) - ops_not_supported = not all(self.stopping_condition(op) for op in circuit.operations) if obs_on_same_wire: @@ -745,17 +744,20 @@ def batch_transform(self, circuit: QuantumTape): to be applied to the list of evaluated circuit results. """ supports_hamiltonian = self.supports_observable("Hamiltonian") + supports_sum = self.supports_observable("Sum") finite_shots = self.shots is not None grouping_known = all( obs.grouping_indices is not None for obs in circuit.observables - if isinstance(obs, Hamiltonian) + if isinstance(obs, (Hamiltonian, LinearCombination)) ) # device property present in braket plugin use_grouping = getattr(self, "use_grouping", True) - hamiltonian_in_obs = any(isinstance(obs, Hamiltonian) for obs in circuit.observables) + hamiltonian_in_obs = any( + isinstance(obs, (Hamiltonian, LinearCombination)) for obs in circuit.observables + ) expval_sum_in_obs = any( isinstance(m.obs, Sum) and isinstance(m, ExpectationMP) for m in circuit.measurements ) diff --git a/pennylane/_qubit_device.py b/pennylane/_qubit_device.py index 2f96fa1925c..e52a3a66704 100644 --- a/pennylane/_qubit_device.py +++ b/pennylane/_qubit_device.py @@ -1631,7 +1631,7 @@ def adjoint_jacobian( f" measurement {m.__class__.__name__}" ) - if m.obs.name == "Hamiltonian": + if not m.obs.has_matrix: raise qml.QuantumFunctionError( "Adjoint differentiation method does not support Hamiltonian observables." ) diff --git a/pennylane/data/attributes/operator/operator.py b/pennylane/data/attributes/operator/operator.py index 6b96ef28d69..6a5c79d5ebf 100644 --- a/pennylane/data/attributes/operator/operator.py +++ b/pennylane/data/attributes/operator/operator.py @@ -61,7 +61,13 @@ def consumes_types(cls) -> FrozenSet[Type[Operator]]: qml.QubitCarry, qml.QubitSum, # pennylane/ops/qubit/hamiltonian.py - qml.Hamiltonian, + qml.ops.Hamiltonian, + # pennylane/ops/op_math/linear_combination.py + qml.ops.LinearCombination, + # pennylane/ops/op_math - prod.py, s_prod.py, sum.py + qml.ops.Prod, + qml.ops.SProd, + qml.ops.Sum, # pennylane/ops/qubit/matrix_qml.py qml.QubitUnitary, qml.DiagonalQubitUnitary, @@ -206,6 +212,8 @@ def _ops_to_hdf5( op_class_names = [] for i, op in enumerate(value): op_key = f"op_{i}" + if isinstance(op, (qml.ops.Prod, qml.ops.SProd, qml.ops.Sum)): + op = op.simplify() if type(op) not in self.consumes_types(): raise TypeError( f"Serialization of operator type '{type(op).__name__}' is not supported." @@ -214,11 +222,19 @@ def _ops_to_hdf5( if isinstance(op, Tensor): self._ops_to_hdf5(bind, op_key, op.obs) op_wire_labels.append("null") - elif isinstance(op, qml.Hamiltonian): + elif isinstance(op, (qml.ops.Hamiltonian, qml.ops.LinearCombination)): coeffs, ops = op.terms() ham_grp = self._ops_to_hdf5(bind, op_key, ops) ham_grp["hamiltonian_coeffs"] = coeffs op_wire_labels.append("null") + elif isinstance(op, (qml.ops.Prod, qml.ops.Sum)): + self._ops_to_hdf5(bind, op_key, op.operands) + op_wire_labels.append("null") + elif isinstance(op, qml.ops.SProd): + coeffs, ops = op.terms() + sprod_grp = self._ops_to_hdf5(bind, op_key, ops) + sprod_grp["sprod_scalar"] = coeffs + op_wire_labels.append("null") else: bind[op_key] = op.data if len(op.data) else h5py.Empty("f") op_wire_labels.append(wires_to_json(op.wires)) @@ -238,7 +254,6 @@ def _hdf5_to_ops(self, bind: HDF5Group) -> List[Operator]: wires_bind = bind["op_wire_labels"] op_class_names = [] if names_bind.shape == (0,) else names_bind.asstr() op_wire_labels = [] if wires_bind.shape == (0,) else wires_bind.asstr() - with qml.QueuingManager.stop_recording(): for i, op_class_name in enumerate(op_class_names): op_key = f"op_{i}" @@ -246,13 +261,22 @@ def _hdf5_to_ops(self, bind: HDF5Group) -> List[Operator]: op_cls = self._supported_ops_dict()[op_class_name] if op_cls is Tensor: ops.append(Tensor(*self._hdf5_to_ops(bind[op_key]))) - elif op_cls is qml.Hamiltonian: + elif op_cls in (qml.ops.Hamiltonian, qml.ops.LinearCombination): ops.append( qml.Hamiltonian( coeffs=list(bind[op_key]["hamiltonian_coeffs"]), observables=self._hdf5_to_ops(bind[op_key]), ) ) + elif op_cls in (qml.ops.Prod, qml.ops.Sum): + ops.append(op_cls(*self._hdf5_to_ops(bind[op_key]))) + elif op_cls is qml.ops.SProd: + ops.append( + qml.ops.s_prod( + scalar=bind[op_key]["sprod_scalar"][0], + operator=self._hdf5_to_ops(bind[op_key])[0], + ) + ) else: wire_labels = json.loads(op_wire_labels[i]) op_data = bind[op_key] diff --git a/pennylane/devices/default_clifford.py b/pennylane/devices/default_clifford.py index 63442c63250..0559b033471 100644 --- a/pennylane/devices/default_clifford.py +++ b/pennylane/devices/default_clifford.py @@ -75,6 +75,7 @@ "Identity", "Projector", "Hamiltonian", + "LinearCombination", "Sum", "SProd", "Prod", @@ -1017,9 +1018,9 @@ def _measure_single_sample(stim_ct, meas_ops, meas_idx, meas_wire): """Sample a single qubit Pauli measurement from a stim circuit""" stim_sm = stim.TableauSimulator() stim_sm.do_circuit(stim_ct) - return stim_sm.measure_observable( - stim.PauliString([0] * meas_idx + meas_ops + [0] * (meas_wire - meas_idx - 1)) - ) + res = [0] * meas_idx + meas_ops + [0] * (meas_wire - meas_idx - 1) + res = [int(r) for r in res] + return stim_sm.measure_observable(stim.PauliString(res)) def _sample_classical_shadow(self, meas, stim_circuit, shots, seed): """Measures classical shadows from the state of simulator device""" diff --git a/pennylane/devices/default_qubit.py b/pennylane/devices/default_qubit.py index e4794dd3f5b..32a8455615b 100644 --- a/pennylane/devices/default_qubit.py +++ b/pennylane/devices/default_qubit.py @@ -68,6 +68,7 @@ "Projector", "SparseHamiltonian", "Hamiltonian", + "LinearCombination", "Sum", "SProd", "Prod", diff --git a/pennylane/devices/default_qubit_legacy.py b/pennylane/devices/default_qubit_legacy.py index 52860be1615..54cd1eaf1fd 100644 --- a/pennylane/devices/default_qubit_legacy.py +++ b/pennylane/devices/default_qubit_legacy.py @@ -196,6 +196,7 @@ class DefaultQubitLegacy(QubitDevice): "Projector", "SparseHamiltonian", "Hamiltonian", + "LinearCombination", "Sum", "SProd", "Prod", @@ -597,7 +598,7 @@ def expval(self, observable, shot_range=None, bin_size=None): # intercept other Hamiltonians # TODO: Ideally, this logic should not live in the Device, but be moved # to a component that can be re-used by devices as needed. - if observable.name not in ("Hamiltonian", "SparseHamiltonian"): + if observable.name not in ("Hamiltonian", "SparseHamiltonian", "LinearCombination"): return super().expval(observable, shot_range=shot_range, bin_size=bin_size) assert self.shots is None, f"{observable.name} must be used with shots=None" @@ -606,7 +607,7 @@ def expval(self, observable, shot_range=None, bin_size=None): backprop_mode = ( not isinstance(self.state, np.ndarray) or any(not isinstance(d, (float, np.ndarray)) for d in observable.data) - ) and observable.name == "Hamiltonian" + ) and observable.name in ["Hamiltonian", "LinearCombination"] if backprop_mode: # TODO[dwierichs]: This branch is not adapted to broadcasting yet @@ -666,7 +667,7 @@ def expval(self, observable, shot_range=None, bin_size=None): csr_matrix.dot(Hmat, csr_matrix(state[..., None])), ).toarray()[0] - if observable.name == "Hamiltonian": + if observable.name in ["Hamiltonian", "LinearCombination"]: res = qml.math.squeeze(res) return self._real(res) @@ -1086,6 +1087,7 @@ def _get_diagonalizing_gates(self, circuit: qml.tape.QuantumTape) -> List[Operat meas_filtered = [ m for m in circuit.measurements - if m.obs is None or not isinstance(m.obs, qml.Hamiltonian) + if m.obs is None + or not isinstance(m.obs, (qml.ops.Hamiltonian, qml.ops.LinearCombination)) ] return super()._get_diagonalizing_gates(qml.tape.QuantumScript(measurements=meas_filtered)) diff --git a/pennylane/devices/default_qutrit.py b/pennylane/devices/default_qutrit.py index 41d56cd391e..3c6bc44027a 100644 --- a/pennylane/devices/default_qutrit.py +++ b/pennylane/devices/default_qutrit.py @@ -33,6 +33,7 @@ OMEGA = qml.math.exp(2 * np.pi * 1j / 3) +# pylint: disable=too-many-arguments class DefaultQutrit(QutritDevice): """Default qutrit device for PennyLane. @@ -87,11 +88,7 @@ class DefaultQutrit(QutritDevice): # Identity is supported as an observable for qml.state() to work correctly. However, any # measurement types that rely on eigenvalue decomposition will not work with qml.Identity - observables = { - "THermitian", - "GellMann", - "Identity", - } + observables = {"THermitian", "GellMann", "Identity", "Prod"} # Static methods to use qml.math to allow for backprop differentiation _reshape = staticmethod(qml.math.reshape) diff --git a/pennylane/devices/qubit/measure.py b/pennylane/devices/qubit/measure.py index bf2b58263b8..184a5582ea5 100644 --- a/pennylane/devices/qubit/measure.py +++ b/pennylane/devices/qubit/measure.py @@ -19,7 +19,7 @@ from scipy.sparse import csr_matrix from pennylane import math -from pennylane.ops import Sum, Hamiltonian +from pennylane.ops import Sum, Hamiltonian, LinearCombination from pennylane.measurements import ( StateMeasurement, MeasurementProcess, @@ -194,7 +194,7 @@ def get_measurement_function( return full_dot_products backprop_mode = math.get_interface(state, *measurementprocess.obs.data) != "numpy" - if isinstance(measurementprocess.obs, Hamiltonian): + if isinstance(measurementprocess.obs, (Hamiltonian, LinearCombination)): # need to work out thresholds for when its faster to use "backprop mode" measurements return sum_of_terms_method if backprop_mode else csr_dot_products diff --git a/pennylane/devices/qubit/sampling.py b/pennylane/devices/qubit/sampling.py index 856d0cf592b..96223486f8d 100644 --- a/pennylane/devices/qubit/sampling.py +++ b/pennylane/devices/qubit/sampling.py @@ -16,7 +16,7 @@ import numpy as np import pennylane as qml -from pennylane.ops import Sum, Hamiltonian, SProd, Prod +from pennylane.ops import Sum, Hamiltonian, SProd, Prod, LinearCombination from pennylane.measurements import ( SampleMeasurement, Shots, @@ -59,7 +59,7 @@ def _group_measurements(mps: List[Union[SampleMeasurement, ClassicalShadowMP, Sh elif mp.obs is None: mp_no_obs.append(mp) mp_no_obs_indices.append(i) - elif isinstance(mp.obs, (Sum, Hamiltonian, SProd, Prod)): + elif isinstance(mp.obs, (Hamiltonian, Sum, SProd, Prod)): # Sum, Hamiltonian, SProd, and Prod are treated as valid Pauli words, but # aren't accepted in qml.pauli.group_observables mp_other_obs.append([mp]) @@ -108,7 +108,9 @@ def get_num_shots_and_executions(tape: qml.tape.QuantumTape) -> Tuple[int, int]: num_executions = 0 num_shots = 0 for group in groups: - if isinstance(group[0], ExpectationMP) and isinstance(group[0].obs, qml.Hamiltonian): + if isinstance(group[0], ExpectationMP) and isinstance( + group[0].obs, (qml.ops.Hamiltonian, qml.ops.LinearCombination) + ): indices = group[0].obs.grouping_indices H_executions = len(indices) if indices else len(group[0].obs.ops) num_executions += H_executions @@ -184,7 +186,9 @@ def measure_with_samples( all_res = [] for group in groups: - if isinstance(group[0], ExpectationMP) and isinstance(group[0].obs, Hamiltonian): + if isinstance(group[0], ExpectationMP) and isinstance( + group[0].obs, (Hamiltonian, LinearCombination) + ): measure_fn = _measure_hamiltonian_with_samples elif isinstance(group[0], ExpectationMP) and isinstance(group[0].obs, Sum): measure_fn = _measure_sum_with_samples diff --git a/pennylane/devices/qutrit_mixed/sampling.py b/pennylane/devices/qutrit_mixed/sampling.py index 89aec13783c..3b8b9293098 100644 --- a/pennylane/devices/qutrit_mixed/sampling.py +++ b/pennylane/devices/qutrit_mixed/sampling.py @@ -18,7 +18,7 @@ import numpy as np import pennylane as qml from pennylane import math -from pennylane.ops import Sum, Hamiltonian +from pennylane.ops import Sum from pennylane.measurements import ( Shots, SampleMeasurement, @@ -203,13 +203,15 @@ def _sum_for_single_shot(s): ) ) - if isinstance(mp.obs, Hamiltonian): + if isinstance(mp.obs, qml.ops.Hamiltonian): # If Hamiltonian apply coefficients return sum((c * res for c, res in zip(mp.obs.terms()[0], results))) + return sum(results) if shots.has_partitioned_shots: return tuple(_sum_for_single_shot(type(shots)(s)) for s in shots) + return _sum_for_single_shot(shots) @@ -350,7 +352,7 @@ def measure_with_samples( TensorLike[Any]: Sample measurement results """ - if isinstance(mp, ExpectationMP) and isinstance(mp.obs, (Hamiltonian, Sum)): + if isinstance(mp, ExpectationMP) and isinstance(mp.obs, (qml.ops.Hamiltonian, Sum)): measure_fn = _measure_sum_with_samples else: # measure with the usual method (rotate into the measurement basis) diff --git a/pennylane/devices/tests/test_measurements.py b/pennylane/devices/tests/test_measurements.py index 2a2b8b07dde..54f1813f701 100644 --- a/pennylane/devices/tests/test_measurements.py +++ b/pennylane/devices/tests/test_measurements.py @@ -50,6 +50,7 @@ ], "SparseHamiltonian": qml.SparseHamiltonian(csr_matrix(np.eye(8)), wires=[0, 1, 2]), "Hamiltonian": qml.Hamiltonian([1, 1], [qml.Z(0), qml.X(0)]), + "LinearCombination": qml.ops.LinearCombination([1, 1], [qml.Z(0), qml.X(0)]), } all_obs = obs.keys() @@ -59,7 +60,7 @@ # Note that the identity is not technically a qubit observable all_available_obs |= {"Identity"} -if not set(all_obs) == all_available_obs: +if not set(all_obs) == all_available_obs | {"LinearCombination"}: raise ValueError( "A qubit observable has been added that is not being tested in the " "device test suite. Please add to the obs dictionary in " @@ -152,7 +153,8 @@ def circuit(): class TestHamiltonianSupport: """Separate test to ensure that the device can differentiate Hamiltonian observables.""" - def test_hamiltonian_diff(self, device_kwargs, tol): + @pytest.mark.parametrize("ham_constructor", [qml.ops.Hamiltonian, qml.ops.LinearCombination]) + def test_hamiltonian_diff(self, ham_constructor, device_kwargs, tol): """Tests a simple VQE gradient using parameter-shift rules.""" device_kwargs["wires"] = 1 dev = qml.device(**device_kwargs) @@ -164,7 +166,7 @@ def circuit(coeffs, param): qml.RX(param, wires=0) qml.RY(param, wires=0) return qml.expval( - qml.Hamiltonian( + ham_constructor( coeffs, [qml.X(0), qml.Z(0)], ) diff --git a/pennylane/gradients/gradient_transform.py b/pennylane/gradients/gradient_transform.py index 3fc77d4ebf7..cfb50b3f2c4 100644 --- a/pennylane/gradients/gradient_transform.py +++ b/pennylane/gradients/gradient_transform.py @@ -166,7 +166,7 @@ def _try_zero_grad_from_graph_or_get_grad_method(tape, param_index, use_graph=Tr # there is no influence of this operation on any of the observables return "0" - return par_info["op"].grad_method + return getattr(par_info["op"], "grad_method", None) def _find_gradient_methods(tape, trainable_param_indices, use_graph=True): diff --git a/pennylane/gradients/hadamard_gradient.py b/pennylane/gradients/hadamard_gradient.py index 56d9ef0a501..dea644caf71 100644 --- a/pennylane/gradients/hadamard_gradient.py +++ b/pennylane/gradients/hadamard_gradient.py @@ -329,7 +329,8 @@ def _expval_hadamard_grad(tape, argnum, aux_wire): obs_new = [qml.Z(i) for i in m.wires] obs_new.append(qml.Y(aux_wire)) - obs_new = qml.operation.Tensor(*obs_new) + obs_type = qml.prod if qml.operation.active_new_opmath() else qml.operation.Tensor + obs_new = obs_type(*obs_new) if isinstance(m, qml.measurements.ExpectationMP): measurements.append(qml.expval(op=obs_new)) @@ -460,9 +461,6 @@ def _get_generators(trainable_op): elif isinstance(trainable_op, qml.Rot): generators = [qml.Z(trainable_op.wires)] coeffs = [-0.5] - elif isinstance(trainable_op, (qml.RX, qml.RY, qml.RZ)): - generators = [trainable_op.generator().base] - coeffs = [trainable_op.generator().scalar] else: generators = trainable_op.generator().ops coeffs = trainable_op.generator().coeffs diff --git a/pennylane/gradients/hamiltonian_grad.py b/pennylane/gradients/hamiltonian_grad.py index 211b186580d..7c9b5c4c229 100644 --- a/pennylane/gradients/hamiltonian_grad.py +++ b/pennylane/gradients/hamiltonian_grad.py @@ -30,7 +30,13 @@ def hamiltonian_grad(tape, idx): # get position in queue queue_position = m_pos - len(tape.operations) new_measurements = list(tape.measurements) - new_measurements[queue_position] = qml.expval(op.ops[p_idx]) + + new_parameters = [0 * d for d in op.data] + new_parameters[p_idx] = qml.math.ones_like(op.data[p_idx]) + new_obs = qml.ops.functions.bind_new_parameters(op, new_parameters) + new_obs = qml.simplify(new_obs) + + new_measurements[queue_position] = qml.expval(new_obs) new_tape = qml.tape.QuantumScript(tape.operations, new_measurements, shots=tape.shots) @@ -40,9 +46,7 @@ def processing_fn(results): res = results[0][queue_position] zeros = qml.math.zeros_like(res) - final = [] - for i, _ in enumerate(tape.measurements): - final.append(res if i == queue_position else zeros) + final = [res if i == queue_position else zeros for i, _ in enumerate(tape.measurements)] return qml.math.expand_dims(qml.math.stack(final), 0) diff --git a/pennylane/gradients/parameter_shift.py b/pennylane/gradients/parameter_shift.py index 9b19c365de2..1654d28f700 100644 --- a/pennylane/gradients/parameter_shift.py +++ b/pennylane/gradients/parameter_shift.py @@ -63,18 +63,17 @@ def _square_observable(obs): # Observable is a tensor, we must consider its # component observables independently. Note that # we assume all component observables are on distinct wires. - - components_squared = [] - - for comp in obs.obs: - try: - components_squared.append(NONINVOLUTORY_OBS[comp.name](comp)) - except KeyError: - # component is involutory - pass - + components_squared = [ + NONINVOLUTORY_OBS[o.name](o) for o in obs if o.name in NONINVOLUTORY_OBS + ] return qml.operation.Tensor(*components_squared) + if isinstance(obs, qml.ops.Prod): + components_squared = [ + NONINVOLUTORY_OBS[o.name](o) for o in obs if o.name in NONINVOLUTORY_OBS + ] + return qml.prod(*components_squared) + return NONINVOLUTORY_OBS[obs.name](obs) @@ -379,7 +378,7 @@ def expval_param_shift( op, op_idx, _ = tape.get_operation(idx) - if op.name == "Hamiltonian": + if op.name in ["Hamiltonian", "LinearCombination"]: # operation is a Hamiltonian if tape[op_idx].return_type is not qml.measurements.Expectation: raise ValueError( @@ -627,6 +626,27 @@ def processing_fn(results): return processing_fn +def _get_non_involuntory_indices(tape, var_indices): + non_involutory_indices = [] + + for i in var_indices: + obs = tape.measurements[i].obs + + if isinstance(obs, qml.operation.Tensor): + # Observable is a tensor product, we must investigate all constituent observables. + if any(o.name in NONINVOLUTORY_OBS for o in tape.measurements[i].obs.obs): + non_involutory_indices.append(i) + + elif isinstance(tape.measurements[i].obs, qml.ops.Prod): + if any(o.name in NONINVOLUTORY_OBS for o in tape.measurements[i].obs): + non_involutory_indices.append(i) + + elif obs.name in NONINVOLUTORY_OBS: + non_involutory_indices.append(i) + + return non_involutory_indices + + def var_param_shift(tape, argnum, shifts=None, gradient_recipes=None, f0=None, broadcast=False): r"""Generate the parameter-shift tapes and postprocessing methods required to compute the gradient of a gate parameter with respect to a @@ -666,16 +686,17 @@ def var_param_shift(tape, argnum, shifts=None, gradient_recipes=None, f0=None, b var_indices = np.where(var_mask)[0] # Get , the expectation value of the tape with unshifted parameters. - expval_tape = tape.copy(copy_operations=True) + new_measurements = list(tape.measurements) # Convert all variance measurements on the tape into expectation values + for i in var_indices: - obs = expval_tape.measurements[i].obs - expval_tape._measurements[i] = qml.expval(op=obs) - if obs.name == "Hamiltonian": - first_obs_idx = len(expval_tape.operations) - for t_idx in reversed(range(len(expval_tape.trainable_params))): - op, op_idx, _ = expval_tape.get_operation(t_idx) + obs = new_measurements[i].obs + new_measurements[i] = qml.expval(op=obs) + if obs.name in {"Hamiltonian", "LinearCombination", "Sum"}: + first_obs_idx = len(tape.operations) + for t_idx in reversed(range(len(tape.trainable_params))): + op, op_idx, _ = tape.get_operation(t_idx) if op_idx < first_obs_idx: break # already seen all observables if op is obs: @@ -683,6 +704,10 @@ def var_param_shift(tape, argnum, shifts=None, gradient_recipes=None, f0=None, b "Can only differentiate Hamiltonian coefficients for expectations, not variances" ) + expval_tape = qml.tape.QuantumScript( + tape.operations, new_measurements, shots=tape.shots, trainable_params=tape.trainable_params + ) + # evaluate the analytic derivative of pdA_tapes, pdA_fn = expval_param_shift( expval_tape, argnum, shifts, gradient_recipes, f0, broadcast @@ -697,29 +722,25 @@ def var_param_shift(tape, argnum, shifts=None, gradient_recipes=None, f0=None, b # If there are non-involutory observables A present, we must compute d/dp. # Get the indices in the measurement queue of all non-involutory # observables. - non_involutory_indices = [] - for i in var_indices: - obs_name = tape.observables[i].name - - if isinstance(obs_name, list): - # Observable is a tensor product, we must investigate all constituent observables. - if any(name in NONINVOLUTORY_OBS for name in obs_name): - non_involutory_indices.append(i) - - elif obs_name in NONINVOLUTORY_OBS: - non_involutory_indices.append(i) + non_involutory_indices = _get_non_involuntory_indices(tape, var_indices) pdA2_fn = None if non_involutory_indices: - tape_with_obs_squared_expval = tape.copy(copy_operations=True) + new_measurements = list(tape.measurements) for i in non_involutory_indices: # We need to calculate d/dp; to do so, we replace the # involutory observables A in the queue with A^2. - obs = _square_observable(tape_with_obs_squared_expval.measurements[i].obs) - tape_with_obs_squared_expval._measurements[i] = qml.expval(op=obs) - + obs = _square_observable(tape.measurements[i].obs) + new_measurements[i] = qml.expval(obs) + + tape_with_obs_squared_expval = qml.tape.QuantumScript( + tape.operations, + new_measurements, + shots=tape.shots, + trainable_params=tape.trainable_params, + ) # Non-involutory observables are present; the partial derivative of # may be non-zero. Here, we calculate the analytic derivatives of the # observables. diff --git a/pennylane/measurements/probs.py b/pennylane/measurements/probs.py index 387580fe1ca..2d1d0e005ae 100644 --- a/pennylane/measurements/probs.py +++ b/pennylane/measurements/probs.py @@ -111,7 +111,7 @@ def circuit(): return ProbabilityMP(obs=op) - if isinstance(op, qml.Hamiltonian): + if isinstance(op, (qml.ops.Hamiltonian, qml.ops.LinearCombination)): raise qml.QuantumFunctionError("Hamiltonians are not supported for rotating probabilities.") if op is not None and not op.has_diagonalizing_gates: diff --git a/pennylane/operation.py b/pennylane/operation.py index 6b6421cee19..7d478b0d679 100644 --- a/pennylane/operation.py +++ b/pennylane/operation.py @@ -10,7 +10,7 @@ # 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. -# pylint: disable=protected-access +# pylint: disable=protected-access, no-member r""" This module contains the abstract base classes for defining PennyLane operations and observables. @@ -192,6 +192,7 @@ ~disable_new_opmath ~active_new_opmath ~convert_to_opmath + ~convert_to_legacy_H Other ~~~~~ @@ -248,6 +249,7 @@ import warnings from enum import IntEnum from typing import List +from contextlib import contextmanager import numpy as np from numpy.linalg import multi_dot @@ -1084,7 +1086,14 @@ def __init__(self, *params, wires=None, id=None): if ( not isinstance( self, - (qml.Barrier, qml.Snapshot, qml.Hamiltonian, qml.GlobalPhase, qml.Identity), + ( + qml.Barrier, + qml.Snapshot, + qml.ops.Hamiltonian, + qml.ops.LinearCombination, + qml.GlobalPhase, + qml.Identity, + ), ) and len(qml.wires.Wires(wires)) == 0 ): @@ -1897,7 +1906,7 @@ def __matmul__(self, other): if active_new_opmath(): return super().__matmul__(other=other) - if isinstance(other, (Tensor, qml.Hamiltonian)): + if isinstance(other, (Tensor, qml.ops.Hamiltonian, qml.ops.LinearCombination)): return other.__rmatmul__(self) if isinstance(other, Observable): @@ -1958,7 +1967,7 @@ def compare(self, other): >>> ob1.compare(ob2) False """ - if isinstance(other, qml.Hamiltonian): + if isinstance(other, (qml.ops.Hamiltonian, qml.ops.LinearCombination)): return other.compare(self) if isinstance(other, (Tensor, Observable)): return other._obs_data() == self._obs_data() @@ -1972,7 +1981,7 @@ def __add__(self, other): if active_new_opmath(): return super().__add__(other=other) - if isinstance(other, qml.Hamiltonian): + if isinstance(other, (qml.ops.Hamiltonian, qml.ops.LinearCombination)): return other + self if isinstance(other, (Observable, Tensor)): return qml.Hamiltonian([1, 1], [self, other], simplify=True) @@ -1998,7 +2007,7 @@ def __sub__(self, other): if active_new_opmath(): return super().__sub__(other=other) - if isinstance(other, (Observable, Tensor, qml.Hamiltonian)): + if isinstance(other, (Observable, Tensor, qml.ops.Hamiltonian, qml.ops.LinearCombination)): return self + (-1 * other) return super().__sub__(other=other) @@ -2216,7 +2225,7 @@ def arithmetic_depth(self) -> int: return 1 + max(o.arithmetic_depth for o in self.obs) def __matmul__(self, other): - if isinstance(other, qml.Hamiltonian): + if isinstance(other, (qml.ops.Hamiltonian, qml.ops.LinearCombination)): return other.__rmatmul__(self) if isinstance(other, Observable): @@ -2936,7 +2945,7 @@ def gen_is_multi_term_hamiltonian(obj): except (AttributeError, OperatorPropertyUndefined, GeneratorUndefinedError): return False - return isinstance(o, qml.Hamiltonian) and len(o.coeffs) > 1 + return isinstance(o, (qml.ops.Hamiltonian, qml.ops.LinearCombination)) and len(o.coeffs) > 1 def enable_new_opmath(): @@ -3007,7 +3016,7 @@ def convert_to_opmath(op): Returns: Operator: An operator using the new arithmetic operations, if relevant """ - if isinstance(op, qml.Hamiltonian): + if isinstance(op, (qml.ops.Hamiltonian, qml.ops.LinearCombination)): c, ops = op.terms() ops = tuple(convert_to_opmath(o) for o in ops) return qml.dot(c, ops) @@ -3016,11 +3025,50 @@ def convert_to_opmath(op): return op +@contextmanager +def disable_new_opmath_cm(): + r"""Allows to use the old operator arithmetic within a + temporary context using the `with` statement.""" + + was_active = qml.operation.active_new_opmath() + try: + if was_active: + disable_new_opmath() + yield + except Exception as e: + raise e + finally: + if was_active: + enable_new_opmath() + else: + disable_new_opmath() + + +@contextmanager +def enable_new_opmath_cm(): + r"""Allows to use the new operator arithmetic within a + temporary context using the `with` statement.""" + + was_active = qml.operation.active_new_opmath() + try: + if not was_active: + enable_new_opmath() + yield + except Exception as e: + raise e + finally: + if was_active: + enable_new_opmath() + else: + disable_new_opmath() + + # pylint: disable=too-many-branches -def convert_to_legacy_H(op): +def convert_to_H(op): """ - Converts arithmetic operators into :class:`~pennylane.Hamiltonian` instance. - Objects of any other type are returned directly. + Converts arithmetic operators into a :class:`~pennylane.ops.Hamiltonian` or + :class:`~pennylane.ops.LinearCombination` instance, depending on whether + new_opmath is enabled. Objects of any other type are returned directly. Arithmetic operators include :class:`~pennylane.ops.op_math.Prod`, :class:`~pennylane.ops.op_math.Sum` and :class:`~pennylane.ops.op_math.SProd`. @@ -3029,7 +3077,8 @@ def convert_to_legacy_H(op): op (Operator): The operator instance to convert. Returns: - Operator: The operator as a :class:`~pennylane.Hamiltonian` instance + Operator: The operator as a :class:`~pennylane.ops.LinearCombination` instance + if `active_new_opmath()`, otherwise a :class:`~pennylane.ops.Hamiltonian` """ if not isinstance(op, (qml.ops.op_math.Prod, qml.ops.op_math.SProd, qml.ops.op_math.Sum)): return op @@ -3038,6 +3087,7 @@ def convert_to_legacy_H(op): ops = [] op = qml.simplify(op) + product = qml.ops.op_math.Prod if active_new_opmath() else Tensor if isinstance(op, Observable): coeffs.append(1.0) @@ -3048,13 +3098,13 @@ def convert_to_legacy_H(op): if isinstance(op.base, Observable): ops.append(op.base) elif isinstance(op.base, qml.ops.op_math.Prod): - ops.append(qml.operation.Tensor(*op.base)) + ops.append(product(*op.base)) else: raise ValueError("The base of scalar product must be an observable or a product.") elif isinstance(op, qml.ops.Prod): coeffs.append(1.0) - ops.append(qml.operation.Tensor(*op)) + ops.append(product(*op)) elif isinstance(op, qml.ops.Sum): for factor in op: @@ -3063,14 +3113,14 @@ def convert_to_legacy_H(op): if isinstance(factor.base, Observable): ops.append(factor.base) elif isinstance(factor.base, qml.ops.op_math.Prod): - ops.append(qml.operation.Tensor(*factor.base)) + ops.append(product(*factor.base)) else: raise ValueError( "The base of scalar product must be an observable or a product." ) elif isinstance(factor, (qml.ops.Prod)): coeffs.append(1.0) - ops.append(qml.operation.Tensor(*factor)) + ops.append(product(*factor)) elif isinstance(factor, Observable): coeffs.append(1.0) ops.append(factor) @@ -3085,6 +3135,25 @@ def convert_to_legacy_H(op): return qml.Hamiltonian(coeffs, ops) +def convert_to_legacy_H(op): + """ + Converts arithmetic operators into a legacy :class:`~pennylane.Hamiltonian` instance. + Objects of any other type are returned directly. + + Arithmetic operators include :class:`~pennylane.ops.op_math.Prod`, + :class:`~pennylane.ops.op_math.Sum` and :class:`~pennylane.ops.op_math.SProd`. + + Args: + op (Operator): The operator instance to convert. + + Returns: + Operator: The operator as a :class:`~pennylane.Hamiltonian` instance + """ + with disable_new_opmath_cm(): + res = convert_to_H(op) + return res + + def __getattr__(name): """To facilitate StatePrep rename""" if name == "StatePrep": diff --git a/pennylane/ops/functions/bind_new_parameters.py b/pennylane/ops/functions/bind_new_parameters.py index 48254895181..130574518ad 100644 --- a/pennylane/ops/functions/bind_new_parameters.py +++ b/pennylane/ops/functions/bind_new_parameters.py @@ -198,8 +198,11 @@ def bind_new_parameters_pow(op: Pow, params: Sequence[TensorLike]): return Pow(bind_new_parameters(op.base, params), op.scalar) -@bind_new_parameters.register -def bind_new_parameters_hamiltonian(op: qml.Hamiltonian, params: Sequence[TensorLike]): +@bind_new_parameters.register(qml.ops.Hamiltonian) +@bind_new_parameters.register(qml.ops.LinearCombination) +def bind_new_parameters_hamiltonian( + op: Union[qml.ops.Hamiltonian, qml.ops.LinearCombination], params: Sequence[TensorLike] +): new_H = qml.Hamiltonian(params, op.ops) if op.grouping_indices is not None: new_H.grouping_indices = op.grouping_indices diff --git a/pennylane/ops/functions/eigvals.py b/pennylane/ops/functions/eigvals.py index 8403683b88f..572f19faeee 100644 --- a/pennylane/ops/functions/eigvals.py +++ b/pennylane/ops/functions/eigvals.py @@ -114,7 +114,8 @@ def circuit(theta): raise TransformError("Input is not an Operator, tape, QNode, or quantum function") return _eigvals_tranform(op, k=k, which=which) - if isinstance(op, qml.Hamiltonian): + if isinstance(op, qml.ops.Hamiltonian): + warnings.warn( "For Hamiltonians, the eigenvalues will be computed numerically. " "This may be computationally intensive for a large number of wires. " diff --git a/pennylane/ops/functions/equal.py b/pennylane/ops/functions/equal.py index 920caed107b..f7ce6911b28 100644 --- a/pennylane/ops/functions/equal.py +++ b/pennylane/ops/functions/equal.py @@ -28,7 +28,16 @@ from pennylane.measurements.counts import CountsMP from pennylane.pulse.parametrized_evolution import ParametrizedEvolution from pennylane.operation import Observable, Operator, Tensor -from pennylane.ops import Hamiltonian, Controlled, Pow, Adjoint, Exp, SProd, CompositeOp +from pennylane.ops import ( + Hamiltonian, + LinearCombination, + Controlled, + Pow, + Adjoint, + Exp, + SProd, + CompositeOp, +) from pennylane.templates.subroutines import ControlledSequence from pennylane.tape import QuantumTape @@ -368,7 +377,7 @@ def _equal_tensor(op1: Tensor, op2: Observable, **kwargs): if not isinstance(op2, Observable): return False - if isinstance(op2, Hamiltonian): + if isinstance(op2, (Hamiltonian, LinearCombination)): return op2.compare(op1) if isinstance(op2, Tensor): diff --git a/pennylane/ops/functions/generator.py b/pennylane/ops/functions/generator.py index 239343a5a5e..b39986731b1 100644 --- a/pennylane/ops/functions/generator.py +++ b/pennylane/ops/functions/generator.py @@ -21,8 +21,8 @@ import numpy as np import pennylane as qml -from pennylane.ops import Hamiltonian, SProd, Prod, Sum -from pennylane.operation import convert_to_legacy_H +from pennylane.ops import Hamiltonian, LinearCombination, SProd, Prod, Sum +from pennylane.operation import convert_to_H # pylint: disable=too-many-branches @@ -30,7 +30,7 @@ def _generator_hamiltonian(gen, op): """Return the generator as type :class:`~.Hamiltonian`.""" wires = op.wires - if isinstance(gen, qml.Hamiltonian): + if isinstance(gen, (Hamiltonian, LinearCombination)): H = gen elif isinstance(gen, (qml.Hermitian, qml.SparseHamiltonian)): @@ -43,10 +43,10 @@ def _generator_hamiltonian(gen, op): H = qml.pauli_decompose(mat, wire_order=wires, hide_identity=True) elif isinstance(gen, qml.operation.Observable): - H = 1.0 * gen + H = qml.Hamiltonian([1.0], [gen]) elif isinstance(gen, (SProd, Prod, Sum)): - H = convert_to_legacy_H(gen) + H = convert_to_H(gen) return H @@ -67,7 +67,7 @@ def _generator_prefactor(gen): if isinstance(gen, Prod): gen = qml.simplify(gen) - if isinstance(gen, Hamiltonian): + if isinstance(gen, (Hamiltonian, LinearCombination)): gen = qml.dot(gen.coeffs, gen.ops) # convert to Sum if isinstance(gen, Sum): diff --git a/pennylane/ops/functions/matrix.py b/pennylane/ops/functions/matrix.py index fe5663e6f90..11764a71962 100644 --- a/pennylane/ops/functions/matrix.py +++ b/pennylane/ops/functions/matrix.py @@ -211,7 +211,8 @@ def circuit(): if isinstance(op, qml.operation.Tensor) and wire_order is not None: op = 1.0 * op # convert to a Hamiltonian - if isinstance(op, qml.Hamiltonian): + if isinstance(op, qml.ops.Hamiltonian): + return op.sparse_matrix(wire_order=wire_order).toarray() try: diff --git a/pennylane/ops/identity.py b/pennylane/ops/identity.py index 98e851df00d..054386c4487 100644 --- a/pennylane/ops/identity.py +++ b/pennylane/ops/identity.py @@ -412,4 +412,6 @@ def pow(self, z): return [GlobalPhase(z * self.data[0], self.wires)] def generator(self): + # needs to return a new_opmath instance regardless of whether new_opmath is enabled, because + # it otherwise can't handle Identity with no wires, see PR #5194 return qml.s_prod(-1, qml.I(self.wires)) diff --git a/pennylane/ops/op_math/__init__.py b/pennylane/ops/op_math/__init__.py index cc0b050ed94..54fc06e30b9 100644 --- a/pennylane/ops/op_math/__init__.py +++ b/pennylane/ops/op_math/__init__.py @@ -46,6 +46,7 @@ ~ControlledOp ~Evolution ~Exp + ~LinearCombination ~Pow ~Prod ~Sum @@ -129,6 +130,7 @@ from .prod import Prod, prod from .sprod import SProd, s_prod from .sum import Sum, sum +from .linear_combination import LinearCombination from .symbolicop import ScalarSymbolicOp, SymbolicOp from .controlled_decompositions import ctrl_decomp_zyz, ctrl_decomp_bisect diff --git a/pennylane/ops/op_math/adjoint.py b/pennylane/ops/op_math/adjoint.py index b7b71cf5bb8..2f896620d53 100644 --- a/pennylane/ops/op_math/adjoint.py +++ b/pennylane/ops/op_math/adjoint.py @@ -297,7 +297,7 @@ def label(self, decimals=None, base_label=None, cache=None): return f"({base_label})†" if self.base.arithmetic_depth > 0 else f"{base_label}†" def matrix(self, wire_order=None): - if isinstance(self.base, qml.Hamiltonian): + if isinstance(self.base, qml.ops.Hamiltonian): base_matrix = qml.matrix(self.base, wire_order=wire_order) else: base_matrix = self.base.matrix(wire_order=wire_order) @@ -401,7 +401,7 @@ def has_generator(self): return self.base.has_generator def generator(self): - return qml.s_prod(-1.0, self.base.generator()) + return -1 * self.base.generator() class AdjointObs(Adjoint, Observable): diff --git a/pennylane/ops/op_math/composite.py b/pennylane/ops/op_math/composite.py index 7b9508c713b..75f0ab38284 100644 --- a/pennylane/ops/op_math/composite.py +++ b/pennylane/ops/op_math/composite.py @@ -57,9 +57,6 @@ def __init__( self.queue_idx = None self._name = self.__class__.__name__ - if len(operands) < 2: - raise ValueError(f"Require at least two operators to combine; got {len(operands)}") - self.operands = operands self._wires = qml.wires.Wires.all_wires([op.wires for op in operands]) self._hash = None @@ -152,7 +149,7 @@ def is_hermitian(self): # pylint: disable=arguments-renamed, invalid-overridden-method @property def has_matrix(self): - return all(op.has_matrix or isinstance(op, qml.Hamiltonian) for op in self) + return all(op.has_matrix or isinstance(op, qml.ops.Hamiltonian) for op in self) def eigvals(self): """Return the eigenvalues of the specified operator. diff --git a/pennylane/ops/op_math/controlled.py b/pennylane/ops/op_math/controlled.py index 1ad08db2ceb..fba2b92cd9f 100644 --- a/pennylane/ops/op_math/controlled.py +++ b/pennylane/ops/op_math/controlled.py @@ -667,6 +667,8 @@ def generator(self): projectors = ( qml.Projector([val], wires=w) for val, w in zip(self.control_values, self.control_wires) ) + # needs to return a new_opmath instance regardless of whether new_opmath is enabled, because + # it otherwise can't handle ControlledGlobalPhase, see PR #5194 return qml.prod(*projectors, sub_gen) @property diff --git a/pennylane/ops/op_math/exp.py b/pennylane/ops/op_math/exp.py index ae78298661a..d3119e274e0 100644 --- a/pennylane/ops/op_math/exp.py +++ b/pennylane/ops/op_math/exp.py @@ -32,12 +32,13 @@ OperatorPropertyUndefined, Tensor, ) -from pennylane.ops.qubit import Hamiltonian from pennylane.wires import Wires from .sprod import SProd from .sum import Sum +from .linear_combination import LinearCombination from .symbolicop import ScalarSymbolicOp +from ..qubit.hamiltonian import Hamiltonian def exp(op, coeff=1, num_steps=None, id=None): @@ -224,7 +225,7 @@ def has_decomposition(self): coeff *= base.scalar base = base.base is_pauli_rot = qml.pauli.is_pauli_word(self.base) and math.real(self.coeff) == 0 - is_hamiltonian = isinstance(base, Hamiltonian) + is_hamiltonian = isinstance(base, (Hamiltonian, LinearCombination)) is_sum_of_pauli_words = isinstance(base, Sum) and all( qml.pauli.is_pauli_word(o) for o in base ) @@ -270,7 +271,7 @@ def _recursive_decomposition(self, base: Operator, coeff: complex): ) # Change base to `Sum`/`Prod` - if isinstance(base, Hamiltonian): + if isinstance(base, (Hamiltonian, LinearCombination)): base = qml.dot(base.coeffs, base.ops) elif isinstance(base, Tensor): base = qml.prod(*base.obs) @@ -278,11 +279,10 @@ def _recursive_decomposition(self, base: Operator, coeff: complex): if isinstance(base, SProd): return self._recursive_decomposition(base.base, base.scalar * coeff) - if self.num_steps is not None and isinstance(base, (Hamiltonian, Sum)): + if self.num_steps is not None and isinstance(base, Sum): # Apply trotter decomposition - coeffs = base.coeffs if isinstance(base, Hamiltonian) else [1] * len(base) + coeffs, ops = [1] * len(base), base.operands coeffs = [c * coeff for c in coeffs] - ops = base.ops if isinstance(base, Hamiltonian) else base.operands return self._trotter_decomposition(ops, coeffs) # Store operator classes with generators @@ -305,7 +305,7 @@ def _recursive_decomposition(self, base: Operator, coeff: complex): # Some generators are not wire-ordered (e.g. OrbitalRotation) mapped_wires_g = qml.map_wires(g, dict(zip(g.wires, base.wires))) - if qml.equal(base, mapped_wires_g) and math.real(coeff) == 0: + if qml.equal(mapped_wires_g, base) and math.real(coeff) == 0: coeff = math.real( -1j / c * coeff ) # cancel the coefficients added by the generator @@ -314,7 +314,7 @@ def _recursive_decomposition(self, base: Operator, coeff: complex): # could have absorbed the coefficient. simplified_g = qml.simplify(qml.s_prod(c, mapped_wires_g)) - if qml.equal(base, simplified_g) and math.real(coeff) == 0: + if qml.equal(simplified_g, base) and math.real(coeff) == 0: coeff = math.real(-1j * coeff) # cancel the coefficients added by the generator return [op_class(coeff, g.wires)] diff --git a/pennylane/ops/op_math/linear_combination.py b/pennylane/ops/op_math/linear_combination.py new file mode 100644 index 00000000000..bb306df30f2 --- /dev/null +++ b/pennylane/ops/op_math/linear_combination.py @@ -0,0 +1,588 @@ +# Copyright 2024 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +LinearCombination class +""" +# pylint: disable=too-many-arguments, protected-access, too-many-instance-attributes + +import itertools +import numbers +from copy import copy +from typing import List + +import pennylane as qml +from pennylane.operation import Observable, Tensor, Operator, convert_to_opmath + +from .sum import Sum + + +class LinearCombination(Sum): + r"""Operator representing a linear combination of operators. + + The ``LinearCombination`` is represented as a linear combination of other operators, e.g., + :math:`\sum_{k=0}^{N-1} c_k O_k`, where the :math:`c_k` are trainable parameters. + + Args: + coeffs (tensor_like): coefficients of the ``LinearCombination`` expression + observables (Iterable[Observable]): observables in the ``LinearCombination`` expression, of same length as ``coeffs`` + simplify (bool): Specifies whether the ``LinearCombination`` is simplified upon initialization + (like-terms are combined). The default value is `False`. Note that ``coeffs`` cannot + be differentiated when using the ``'torch'`` interface and ``simplify=True``. + grouping_type (str): If not ``None``, compute and store information on how to group commuting + observables upon initialization. This information may be accessed when a :class:`~.QNode` containing this + ``LinearCombination`` is executed on devices. The string refers to the type of binary relation between Pauli words. + Can be ``'qwc'`` (qubit-wise commuting), ``'commuting'``, or ``'anticommuting'``. + method (str): The graph coloring heuristic to use in solving minimum clique cover for grouping, which + can be ``'lf'`` (Largest First) or ``'rlf'`` (Recursive Largest First). Ignored if ``grouping_type=None``. + id (str): name to be assigned to this ``LinearCombination`` instance + + **Example:** + + A ``LinearCombination`` can be created by simply passing the list of coefficients + as well as the list of observables: + + >>> coeffs = [0.2, -0.543] + >>> obs = [qml.X(0) @ qml.Z(1), qml.Z(0) @ qml.Hadamard(2)] + >>> H = qml.ops.LinearCombination(coeffs, obs) + >>> print(H) + 0.2 * (X(0) @ Z(1)) + -0.543 * (Z(0) @ Hadamard(wires=[2])) + + + The coefficients can be a trainable tensor, for example: + + >>> coeffs = tf.Variable([0.2, -0.543], dtype=tf.double) + >>> obs = [qml.X(0) @ qml.Z(1), qml.Z(0) @ qml.Hadamard(2)] + >>> H = qml.ops.LinearCombination(coeffs, obs) + >>> print(H) + 0.2 * (X(0) @ Z(1)) + -0.543 * (Z(0) @ Hadamard(wires=[2])) + + + A ``LinearCombination`` can store information on which commuting observables should be measured together in + a circuit: + + >>> obs = [qml.X(0), qml.X(1), qml.Z(0)] + >>> coeffs = np.array([1., 2., 3.]) + >>> H = qml.ops.LinearCombination(coeffs, obs, grouping_type='qwc') + >>> H.grouping_indices + ((0, 1), (2,)) + + This attribute can be used to compute groups of coefficients and observables: + + >>> grouped_coeffs = [coeffs[list(indices)] for indices in H.grouping_indices] + >>> grouped_obs = [[H.ops[i] for i in indices] for indices in H.grouping_indices] + >>> grouped_coeffs + [array([1., 2.]), array([3.])] + >>> grouped_obs + [[X(0), X(1)], [Z(0)]] + + Devices that evaluate a ``LinearCombination`` expectation by splitting it into its local observables can + use this information to reduce the number of circuits evaluated. + + Note that one can compute the ``grouping_indices`` for an already initialized ``LinearCombination`` by + using the :func:`compute_grouping ` method. + """ + + num_wires = qml.operation.AnyWires + grad_method = "A" # supports analytic gradients + batch_size = None + ndim_params = None # could be (0,) * len(coeffs), but it is not needed. Define at class-level + + def _flatten(self): + # note that we are unable to restore grouping type or method without creating new properties + return (self._coeffs, self._ops, self.data), (self.grouping_indices,) + + @classmethod + def _unflatten(cls, data, metadata): + new_op = cls(data[0], data[1]) + new_op._grouping_indices = metadata[0] # pylint: disable=protected-access + new_op.data = data[2] + return new_op + + def __init__( + self, + coeffs, + observables: List[Operator], + simplify=False, + grouping_type=None, + method="rlf", + _pauli_rep=None, + id=None, + ): + if qml.math.shape(coeffs)[0] != len(observables): + raise ValueError( + "Could not create valid LinearCombination; " + "number of coefficients and operators does not match." + ) + if _pauli_rep is None: + _pauli_rep = self._build_pauli_rep_static(coeffs, observables) + + if simplify: + # simplify upon initialization changes ops such that they wouldnt be removed in self.queue() anymore + if qml.QueuingManager.recording(): + for o in observables: + qml.QueuingManager.remove(o) + + coeffs, observables, _pauli_rep = self._simplify_coeffs_ops( + coeffs, observables, _pauli_rep + ) + + self._coeffs = coeffs + + self._ops = [convert_to_opmath(op) for op in observables] + + self._hyperparameters = {"ops": self._ops} + + with qml.QueuingManager.stop_recording(): + operands = tuple(qml.s_prod(c, op) for c, op in zip(coeffs, observables)) + + super().__init__( + *operands, grouping_type=grouping_type, method=method, id=id, _pauli_rep=_pauli_rep + ) + + @staticmethod + def _build_pauli_rep_static(coeffs, observables): + """PauliSentence representation of the Sum of operations.""" + + if all(pauli_reps := [op.pauli_rep for op in observables]): + new_rep = qml.pauli.PauliSentence() + for c, ps in zip(coeffs, pauli_reps): + for pw, coeff in ps.items(): + new_rep[pw] += coeff * c + return new_rep + return None + + def _check_batching(self): + """Override for LinearCombination, batching is not yet supported.""" + + def label(self, decimals=None, base_label=None, cache=None): + decimals = None if (len(self.parameters) > 3) else decimals + return Operator.label(self, decimals=decimals, base_label=base_label or "𝓗", cache=cache) + + @property + def coeffs(self): + """Return the coefficients defining the LinearCombination. + + Returns: + Iterable[float]): coefficients in the LinearCombination expression + """ + return self._coeffs + + @property + def ops(self): + """Return the operators defining the LinearCombination. + + Returns: + Iterable[Observable]): observables in the LinearCombination expression + """ + return self._ops + + def terms(self): + r"""Retrieve the coefficients and operators of the ``LinearCombination``. + + Returns: + tuple[list[tensor_like or float], list[.Operation]]: list of coefficients :math:`c_i` + and list of operations :math:`O_i` + + **Example** + + >>> coeffs = [1., 2., 3.] + >>> ops = [X(0), X(0) @ X(1), X(1) @ X(2)] + >>> op = qml.ops.LinearCombination(coeffs, ops) + >>> op.terms() + ([1.0, 2.0, 3.0], [X(0), X(0) @ X(1), X(1) @ X(2)]) + + """ + return self.coeffs, self.ops + + def compute_grouping(self, grouping_type="qwc", method="rlf"): + """ + Compute groups of operators and coefficients corresponding to commuting + observables of this ``LinearCombination``. + + .. note:: + + If grouping is requested, the computed groupings are stored as a list of list of indices + in ``LinearCombination.grouping_indices``. + + Args: + grouping_type (str): The type of binary relation between Pauli words used to compute + the grouping. Can be ``'qwc'``, ``'commuting'``, or ``'anticommuting'``. + method (str): The graph coloring heuristic to use in solving minimum clique cover for + grouping, which can be ``'lf'`` (Largest First) or ``'rlf'`` (Recursive Largest + First). + + **Example** + + .. code-block:: python + + import pennylane as qml + + a = qml.X(0) + b = qml.prod(qml.X(0), qml.X(1)) + c = qml.Z(0) + obs = [a, b, c] + coeffs = [1.0, 2.0, 3.0] + + op = qml.ops.LinearCombination(coeffs, obs) + + >>> op.grouping_indices is None + True + >>> op.compute_grouping(grouping_type="qwc") + >>> op.grouping_indices + ((2,), (0, 1)) + """ + if not self.pauli_rep: + raise ValueError("Cannot compute grouping for Sums containing non-Pauli operators.") + + _, ops = self.terms() + + with qml.QueuingManager.stop_recording(): + op_groups = qml.pauli.group_observables(ops, grouping_type=grouping_type, method=method) + + ops = copy(ops) + + indices = [] + available_indices = list(range(len(ops))) + for partition in op_groups: # pylint:disable=too-many-nested-blocks + indices_this_group = [] + for pauli_word in partition: + # find index of this pauli word in remaining original observables, + for ind, observable in enumerate(ops): + if qml.pauli.are_identical_pauli_words(pauli_word, observable): + indices_this_group.append(available_indices[ind]) + # delete this observable and its index, so it cannot be found again + ops.pop(ind) + available_indices.pop(ind) + break + indices.append(tuple(indices_this_group)) + + self._grouping_indices = tuple(indices) + + @property + def wires(self): + r"""The sorted union of wires from all operators. + + Returns: + (Wires): Combined wires present in all terms, sorted. + """ + return self._wires + + @property + def name(self): + return "LinearCombination" + + @staticmethod + @qml.QueuingManager.stop_recording() + def _simplify_coeffs_ops(coeffs, ops, pr, cutoff=1.0e-12): + """Simplify coeffs and ops + + Returns: + coeffs, ops, pauli_rep""" + + if len(ops) == 0: + return [], [], pr + + # try using pauli_rep: + if pr is not None: + if len(pr) == 0: + return [], [], pr + + # collect coefficients and ops + new_coeffs = [] + new_ops = [] + + for pw, coeff in pr.items(): + pw_op = pw.operation(wire_order=pr.wires) + new_ops.append(pw_op) + new_coeffs.append(coeff) + + return new_coeffs, new_ops, pr + + if len(ops) == 1: + return coeffs, [ops[0].simplify()], pr + + op_as_sum = qml.dot(coeffs, ops) + op_as_sum = op_as_sum.simplify(cutoff) + new_coeffs, new_ops = op_as_sum.terms() + return new_coeffs, new_ops, pr + + def simplify(self, cutoff=1.0e-12): + coeffs, ops, pr = self._simplify_coeffs_ops(self.coeffs, self.ops, self.pauli_rep, cutoff) + return LinearCombination(coeffs, ops, _pauli_rep=pr) + + def _obs_data(self): + r"""Extracts the data from a ``LinearCombination`` and serializes it in an order-independent fashion. + + This allows for comparison between ``LinearCombination``s that are equivalent, but are defined with terms and tensors + expressed in different orders. For example, `qml.X(0) @ qml.Z(1)` and + `qml.Z(1) @ qml.X(0)` are equivalent observables with different orderings. + + .. Note:: + + In order to store the data from each term of the ``LinearCombination`` in an order-independent serialization, + we make use of sets. Note that all data contained within each term must be immutable, hence the use of + strings and frozensets. + + **Example** + + >>> H = qml.ops.LinearCombination([1, 1], [qml.X(0) @ qml.X(1), qml.Z(0)]) + >>> print(H._obs_data()) + {(1, frozenset({('Prod', , ())})), + (1, frozenset({('PauliZ', , ())}))} + """ + data = set() + + coeffs_arr = qml.math.toarray(self.coeffs) + for co, op in zip(coeffs_arr, self.ops): + obs = op.non_identity_obs if isinstance(op, Tensor) else [op] + tensor = [] + for ob in obs: + parameters = tuple( + str(param) for param in ob.parameters + ) # Converts params into immutable type + if isinstance(ob, qml.GellMann): + parameters += (ob.hyperparameters["index"],) + tensor.append((ob.name, ob.wires, parameters)) + data.add((co, frozenset(tensor))) + + return data + + def compare(self, other): + r"""Determines whether the operator is equivalent to another. + + Currently only supported for :class:`~LinearCombination`, :class:`~.Observable`, or :class:`~.Tensor`. + LinearCombinations/observables are equivalent if they represent the same operator + (their matrix representations are equal), and they are defined on the same wires. + + .. Warning:: + + The compare method does **not** check if the matrix representation + of a :class:`~.Hermitian` observable is equal to an equivalent + observable expressed in terms of Pauli matrices, or as a + linear combination of Hermitians. + To do so would require the matrix form of LinearCombinations and Tensors + be calculated, which would drastically increase runtime. + + Returns: + (bool): True if equivalent. + + **Examples** + + >>> H = qml.ops.LinearCombination( + ... [0.5, 0.5], + ... [qml.PauliZ(0) @ qml.PauliY(1), qml.PauliY(1) @ qml.PauliZ(0) @ qml.Identity("a")] + ... ) + >>> obs = qml.PauliZ(0) @ qml.PauliY(1) + >>> print(H.compare(obs)) + True + + >>> H1 = qml.ops.LinearCombination([1, 1], [qml.PauliX(0), qml.PauliZ(1)]) + >>> H2 = qml.ops.LinearCombination([1, 1], [qml.PauliZ(0), qml.PauliX(1)]) + >>> H1.compare(H2) + False + + >>> ob1 = qml.ops.LinearCombination([1], [qml.PauliX(0)]) + >>> ob2 = qml.Hermitian(np.array([[0, 1], [1, 0]]), 0) + >>> ob1.compare(ob2) + False + """ + + if (pr1 := self.pauli_rep) is not None and (pr2 := other.pauli_rep) is not None: + pr1.simplify() + pr2.simplify() + return pr1 == pr2 + + if isinstance(other, (LinearCombination, qml.ops.Hamiltonian)): + op1 = self.simplify() + op2 = other.simplify() + return op1._obs_data() == op2._obs_data() # pylint: disable=protected-access + + if isinstance(other, (Tensor, Observable)): + op1 = self.simplify() + return op1._obs_data() == { + (1, frozenset(other._obs_data())) # pylint: disable=protected-access + } + + if isinstance(other, (Operator)): + op1 = self.simplify() + op2 = other.simplify() + return qml.equal(op1, op2) + + raise ValueError( + "Can only compare a LinearCombination, and a LinearCombination/Observable/Tensor." + ) + + def __matmul__(self, other): + """The product operation between Operator objects.""" + if isinstance(other, LinearCombination): + coeffs1 = self.coeffs + ops1 = self.ops + shared_wires = qml.wires.Wires.shared_wires([self.wires, other.wires]) + if len(shared_wires) > 0: + raise ValueError( + "LinearCombinations can only be multiplied together if they act on " + "different sets of wires" + ) + + coeffs2 = other.coeffs + ops2 = other.ops + + coeffs = qml.math.kron(coeffs1, coeffs2) + ops_list = itertools.product(ops1, ops2) + terms = [qml.prod(t[0], t[1], lazy=False) for t in ops_list] + return qml.ops.LinearCombination(coeffs, terms) + + if isinstance(other, Operator): + if other.arithmetic_depth == 0: + new_ops = [op @ other for op in self.ops] + + # build new pauli rep using old pauli rep + if (pr1 := self.pauli_rep) is not None and (pr2 := other.pauli_rep) is not None: + new_pr = pr1 @ pr2 + else: + new_pr = None + return LinearCombination(self.coeffs, new_ops, _pauli_rep=new_pr) + return qml.prod(self, other) + + return NotImplemented + + def __add__(self, H): + r"""The addition operation between a LinearCombination and a LinearCombination/Tensor/Observable.""" + ops = copy(self.ops) + self_coeffs = self.coeffs + + if isinstance(H, numbers.Number) and H == 0: + return self + + if isinstance(H, (LinearCombination, qml.ops.Hamiltonian)): + coeffs = qml.math.concatenate([self_coeffs, H.coeffs], axis=0) + ops.extend(H.ops) + if (pr1 := self.pauli_rep) is not None and (pr2 := H.pauli_rep) is not None: + _pauli_rep = pr1 + pr2 + else: + _pauli_rep = None + return qml.ops.LinearCombination(coeffs, ops, _pauli_rep=_pauli_rep) + + if isinstance(H, qml.operation.Operator): + coeffs = qml.math.concatenate( + [self_coeffs, qml.math.cast_like([1.0], self_coeffs)], axis=0 + ) + ops.append(H) + + return qml.ops.LinearCombination(coeffs, ops) + + return NotImplemented + + __radd__ = __add__ + + def __mul__(self, a): + r"""The scalar multiplication operation between a scalar and a LinearCombination.""" + if isinstance(a, (int, float, complex)): + self_coeffs = self.coeffs + coeffs = qml.math.multiply(a, self_coeffs) + return qml.ops.LinearCombination(coeffs, self.ops) + + return NotImplemented + + __rmul__ = __mul__ + + def __sub__(self, H): + r"""The subtraction operation between a LinearCombination and a LinearCombination/Tensor/Observable.""" + if isinstance(H, (LinearCombination, qml.ops.Hamiltonian, Tensor, Observable)): + return self + qml.s_prod(-1.0, H, lazy=False) + return NotImplemented + + def queue(self, context=qml.QueuingManager): + """Queues a ``qml.ops.LinearCombination`` instance""" + if qml.QueuingManager.recording(): + for o in self.ops: + context.remove(o) + context.append(self) + return self + + def eigvals(self): + """Return the eigenvalues of the specified operator. + + This method uses pre-stored eigenvalues for standard observables where + possible and stores the corresponding eigenvectors from the eigendecomposition. + + Returns: + array: array containing the eigenvalues of the operator + """ + eigvals = [] + 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)) + ) + else: + tmp_composite = Sum(*ops) # only change compared to CompositeOp.eigvals() + eigvals.append( + qml.utils.expand_vector( + tmp_composite.eigendecomposition["eigval"], + list(tmp_composite.wires), + list(self.wires), + ) + ) + + return self._math_op( + qml.math.asarray(eigvals, like=qml.math.get_deep_interface(eigvals)), axis=0 + ) + + def diagonalizing_gates(self): + r"""Sequence of gates that diagonalize the operator in the computational basis. + + Given the eigendecomposition :math:`O = U \Sigma U^{\dagger}` where + :math:`\Sigma` is a diagonal matrix containing the eigenvalues, + the sequence of diagonalizing gates implements the unitary :math:`U^{\dagger}`. + + The diagonalizing gates rotate the state into the eigenbasis + of the operator. + + A ``DiagGatesUndefinedError`` is raised if no representation by decomposition is defined. + + .. seealso:: :meth:`~.Operator.compute_diagonalizing_gates`. + + Returns: + list[.Operator] or None: a list of operators + """ + diag_gates = [] + for ops in self.overlapping_ops: + if len(ops) == 1: + diag_gates.extend(ops[0].diagonalizing_gates()) + else: + tmp_sum = Sum(*ops) # only change compared to CompositeOp.diagonalizing_gates() + eigvecs = tmp_sum.eigendecomposition["eigvec"] + diag_gates.append( + qml.QubitUnitary( + qml.math.transpose(qml.math.conj(eigvecs)), wires=tmp_sum.wires + ) + ) + return diag_gates + + def map_wires(self, wire_map: dict): + """Returns a copy of the current ``LinearCombination`` with its wires changed according to the given + wire map. + + Args: + wire_map (dict): dictionary containing the old wires as keys and the new wires as values + + Returns: + .LinearCombination: new ``LinearCombination`` + """ + coeffs, ops = self.terms() + new_ops = tuple(op.map_wires(wire_map) for op in ops) + new_op = LinearCombination(coeffs, new_ops) + new_op.grouping_indices = self._grouping_indices + return new_op diff --git a/pennylane/ops/op_math/pow.py b/pennylane/ops/op_math/pow.py index ac0f313235b..011c4309162 100644 --- a/pennylane/ops/op_math/pow.py +++ b/pennylane/ops/op_math/pow.py @@ -329,7 +329,7 @@ def generator(self): See also :func:`~.generator` """ - return qml.s_prod(self.z, self.base.generator(), lazy=False) + return self.z * self.base.generator() def pow(self, z): return [Pow(base=self.base, z=self.z * z)] diff --git a/pennylane/ops/op_math/prod.py b/pennylane/ops/op_math/prod.py index e19bd65cfe4..94f1df746ec 100644 --- a/pennylane/ops/op_math/prod.py +++ b/pennylane/ops/op_math/prod.py @@ -30,7 +30,6 @@ from pennylane.ops.op_math.pow import Pow from pennylane.ops.op_math.sprod import SProd from pennylane.ops.op_math.sum import Sum -from pennylane.ops.qubit import Hamiltonian from pennylane.ops.qubit.non_parametric_ops import PauliX, PauliY, PauliZ from pennylane.queuing import QueuingManager from pennylane.typing import TensorLike @@ -300,7 +299,10 @@ def matrix(self, wire_order=None): batched: List[bool] = [] # batched[i] tells if mats[i] is batched or not for ops in self.overlapping_ops: gen = ( - (qml.matrix(op) if isinstance(op, Hamiltonian) else op.matrix(), op.wires) + ( + (qml.matrix(op) if isinstance(op, qml.ops.Hamiltonian) else op.matrix()), + op.wires, + ) for op in ops ) diff --git a/pennylane/ops/op_math/sprod.py b/pennylane/ops/op_math/sprod.py index 9470d95beb2..f1da913d94c 100644 --- a/pennylane/ops/op_math/sprod.py +++ b/pennylane/ops/op_math/sprod.py @@ -261,7 +261,7 @@ def sparse_matrix(self, wire_order=None): @property def has_matrix(self): """Bool: Whether or not the Operator returns a defined matrix.""" - return isinstance(self.base, qml.Hamiltonian) or self.base.has_matrix + return isinstance(self.base, qml.ops.Hamiltonian) or self.base.has_matrix @staticmethod def _matrix(scalar, mat): diff --git a/pennylane/ops/op_math/sum.py b/pennylane/ops/op_math/sum.py index 1686f1c8cbf..cb9a580f935 100644 --- a/pennylane/ops/op_math/sum.py +++ b/pennylane/ops/op_math/sum.py @@ -15,15 +15,17 @@ This file contains the implementation of the Sum class which contains logic for computing the sum of operations. """ +# pylint: disable=too-many-arguments,too-many-instance-attributes,protected-access + import warnings import itertools +from collections.abc import Iterable from copy import copy from typing import List import pennylane as qml from pennylane import math from pennylane.operation import Operator, convert_to_opmath -from pennylane.ops.qubit import Hamiltonian from pennylane.queuing import QueuingManager from .composite import CompositeOp @@ -207,6 +209,7 @@ def circuit(weights): _op_symbol = "+" _math_op = math.sum + grad_method = "A" def _flatten(self): return tuple(self.operands), (self.grouping_indices,) @@ -241,6 +244,32 @@ def grouping_indices(self): """ return self._grouping_indices + @grouping_indices.setter + def grouping_indices(self, value): + """Set the grouping indices, if known without explicit computation, or if + computation was done externally. The groups are not verified. + + Args: + value (list[list[int]]): List of lists of indexes of the observables in ``self.ops``. Each sublist + represents a group of commuting observables. + """ + if value is None: + return + + _, ops = self.terms() + + if ( + not isinstance(value, Iterable) + or any(not isinstance(sublist, Iterable) for sublist in value) + or any(i not in range(len(ops)) for sl in value for i in sl) + ): + raise ValueError( + f"The grouped index value needs to be a tuple of tuples of integers between 0 and the " + f"number of observables in the Sum; got {value}" + ) + # make sure all tuples so can be hashable + self._grouping_indices = tuple(tuple(sublist) for sublist in value) + def __str__(self): """String representation of the Sum.""" ops = self.operands @@ -261,6 +290,8 @@ def is_hermitian(self): """If all of the terms in the sum are hermitian, then the Sum is hermitian.""" if self.pauli_rep is not None: coeffs_list = list(self.pauli_rep.values()) + if len(coeffs_list) == 0: + return True if not math.is_abstract(coeffs_list[0]): return not any(math.iscomplex(c) for c in coeffs_list) @@ -288,7 +319,7 @@ def matrix(self, wire_order=None): tensor_like: matrix representation """ gen = ( - (qml.matrix(op) if isinstance(op, Hamiltonian) else op.matrix(), op.wires) + (qml.matrix(op) if isinstance(op, qml.ops.Hamiltonian) else op.matrix(), op.wires) for op in self ) @@ -330,6 +361,7 @@ def adjoint(self): def _build_pauli_rep(self): """PauliSentence representation of the Sum of operations.""" + if all(operand_pauli_reps := [op.pauli_rep for op in self.operands]): new_rep = qml.pauli.PauliSentence() for operand_rep in operand_pauli_reps: diff --git a/pennylane/ops/op_math/symbolicop.py b/pennylane/ops/op_math/symbolicop.py index 8d155f3bff8..ccd90828e5a 100644 --- a/pennylane/ops/op_math/symbolicop.py +++ b/pennylane/ops/op_math/symbolicop.py @@ -196,7 +196,7 @@ def data(self, new_data): @property def has_matrix(self): - return self.base.has_matrix or isinstance(self.base, qml.Hamiltonian) + return self.base.has_matrix or isinstance(self.base, qml.ops.Hamiltonian) @property def hash(self): @@ -243,7 +243,7 @@ def matrix(self, wire_order=None): tensor_like: matrix representation """ # compute base matrix - if isinstance(self.base, qml.Hamiltonian): + if isinstance(self.base, qml.ops.Hamiltonian): base_matrix = qml.matrix(self.base) else: base_matrix = self.base.matrix() diff --git a/pennylane/ops/qubit/hamiltonian.py b/pennylane/ops/qubit/hamiltonian.py index d7c9a95749f..2f6410de261 100644 --- a/pennylane/ops/qubit/hamiltonian.py +++ b/pennylane/ops/qubit/hamiltonian.py @@ -22,6 +22,7 @@ from copy import copy import functools from typing import List +from warnings import warn import numpy as np import scipy @@ -66,6 +67,7 @@ class Hamiltonian(Observable): The Hamiltonian is represented as a linear combination of other operators, e.g., :math:`\sum_{k=0}^{N-1} c_k O_k`, where the :math:`c_k` are trainable parameters. + Args: coeffs (tensor_like): coefficients of the Hamiltonian expression observables (Iterable[Observable]): observables in the Hamiltonian expression, of same length as coeffs @@ -184,6 +186,16 @@ def __init__( method="rlf", id=None, ): + if qml.operation.active_new_opmath(): + warn( + "Using 'qml.ops.Hamiltonian' with new operator arithmetic is deprecated. " + "Instead, use 'qml.Hamiltonian', or use 'qml.operation.disable_new_opmath()' " + "to continue to access the legacy functionality. See " + "https://docs.pennylane.ai/en/stable/development/deprecations.html for more " + "details.", + qml.PennyLaneDeprecationWarning, + ) + if qml.math.shape(coeffs)[0] != len(observables): raise ValueError( "Could not create valid Hamiltonian; " @@ -625,6 +637,13 @@ def compare(self, other): >>> ob1.compare(ob2) False """ + + if isinstance(other, qml.operation.Operator): + if (pr1 := self.pauli_rep) is not None and (pr2 := other.pauli_rep) is not None: + pr1.simplify() + pr2.simplify() + return pr1 == pr2 + if isinstance(other, Hamiltonian): self.simplify() other.simplify() @@ -657,12 +676,12 @@ def __matmul__(self, H): coeffs = qml.math.kron(coeffs1, coeffs2) ops_list = itertools.product(ops1, ops2) terms = [qml.operation.Tensor(t[0], t[1]) for t in ops_list] - return qml.Hamiltonian(coeffs, terms, simplify=True) + return Hamiltonian(coeffs, terms, simplify=True) if isinstance(H, (Tensor, Observable)): terms = [op @ copy(H) for op in ops1] - return qml.Hamiltonian(coeffs1, terms, simplify=True) + return Hamiltonian(coeffs1, terms, simplify=True) return NotImplemented @@ -679,7 +698,7 @@ def __rmatmul__(self, H): if isinstance(H, (Tensor, Observable)): terms = [copy(H) @ op for op in ops1] - return qml.Hamiltonian(coeffs1, terms, simplify=True) + return Hamiltonian(coeffs1, terms, simplify=True) return NotImplemented @@ -694,14 +713,14 @@ def __add__(self, H): if isinstance(H, Hamiltonian): coeffs = qml.math.concatenate([self_coeffs, copy(H.coeffs)], axis=0) ops.extend(H.ops.copy()) - return qml.Hamiltonian(coeffs, ops, simplify=True) + return Hamiltonian(coeffs, ops, simplify=True) if isinstance(H, (Tensor, Observable)): coeffs = qml.math.concatenate( [self_coeffs, qml.math.cast_like([1.0], self_coeffs)], axis=0 ) ops.append(H) - return qml.Hamiltonian(coeffs, ops, simplify=True) + return Hamiltonian(coeffs, ops, simplify=True) return NotImplemented @@ -712,7 +731,7 @@ def __mul__(self, a): if isinstance(a, (int, float)): self_coeffs = copy(self.coeffs) coeffs = qml.math.multiply(a, self_coeffs) - return qml.Hamiltonian(coeffs, self.ops.copy()) + return Hamiltonian(coeffs, self.ops.copy()) return NotImplemented @@ -749,6 +768,8 @@ def __imul__(self, a): r"""The inplace scalar multiplication operation between a scalar and a Hamiltonian.""" if isinstance(a, (int, float)): self._coeffs = qml.math.multiply(a, self._coeffs) + if self.pauli_rep is not None: + self._pauli_rep = qml.math.multiply(a, self._pauli_rep) return self return NotImplemented diff --git a/pennylane/ops/qubit/parametric_ops_multi_qubit.py b/pennylane/ops/qubit/parametric_ops_multi_qubit.py index 1da176c1d6a..e09b29caaae 100644 --- a/pennylane/ops/qubit/parametric_ops_multi_qubit.py +++ b/pennylane/ops/qubit/parametric_ops_multi_qubit.py @@ -115,7 +115,7 @@ def compute_matrix(theta, num_wires): # pylint: disable=arguments-differ ) def generator(self): - return qml.s_prod(-0.5, functools.reduce(matmul, [PauliZ(w) for w in self.wires])) + return qml.Hamiltonian([-0.5], [functools.reduce(matmul, [PauliZ(w) for w in self.wires])]) @staticmethod def compute_eigvals(theta, num_wires): # pylint: disable=arguments-differ @@ -406,7 +406,10 @@ def compute_matrix(theta, pauli_word): # pylint: disable=arguments-differ def generator(self): pauli_word = self.hyperparameters["pauli_word"] wire_map = {w: i for i, w in enumerate(self.wires)} - return qml.s_prod(-0.5, qml.pauli.string_to_pauli_word(pauli_word, wire_map=wire_map)) + + return qml.Hamiltonian( + [-0.5], [qml.pauli.string_to_pauli_word(pauli_word, wire_map=wire_map)] + ) @staticmethod def compute_eigvals(theta, pauli_word): # pylint: disable=arguments-differ @@ -774,7 +777,7 @@ class IsingXX(Operation): parameter_frequencies = [(1,)] def generator(self): - return qml.s_prod(-0.5, qml.prod(PauliX(wires=self.wires[0]), PauliX(wires=self.wires[1]))) + return qml.Hamiltonian([-0.5], [PauliX(wires=self.wires[0]) @ PauliX(wires=self.wires[1])]) def __init__(self, phi, wires, id=None): super().__init__(phi, wires=wires, id=id) @@ -910,7 +913,7 @@ class IsingYY(Operation): parameter_frequencies = [(1,)] def generator(self): - return qml.s_prod(-0.5, qml.prod(PauliY(wires=self.wires[0]), PauliY(wires=self.wires[1]))) + return qml.Hamiltonian([-0.5], [PauliY(wires=self.wires[0]) @ PauliY(wires=self.wires[1])]) def __init__(self, phi, wires, id=None): super().__init__(phi, wires=wires, id=id) @@ -1053,7 +1056,7 @@ class IsingZZ(Operation): parameter_frequencies = [(1,)] def generator(self): - return qml.s_prod(-0.5, qml.prod(PauliZ(wires=self.wires[0]), PauliZ(wires=self.wires[1]))) + return qml.Hamiltonian([-0.5], [PauliZ(wires=self.wires[0]) @ PauliZ(wires=self.wires[1])]) def __init__(self, phi, wires, id=None): super().__init__(phi, wires=wires, id=id) @@ -1236,12 +1239,13 @@ class IsingXY(Operation): parameter_frequencies = [(0.5, 1.0)] def generator(self): - return qml.s_prod( - 0.25, - qml.sum( - qml.prod(PauliX(wires=self.wires[0]), PauliX(wires=self.wires[1])), - qml.prod(PauliY(wires=self.wires[0]), PauliY(wires=self.wires[1])), - ), + + return qml.Hamiltonian( + [0.25, 0.25], + [ + qml.X(wires=self.wires[0]) @ qml.X(wires=self.wires[1]), + qml.Y(wires=self.wires[0]) @ qml.Y(wires=self.wires[1]), + ], ) def __init__(self, phi, wires, id=None): diff --git a/pennylane/ops/qubit/parametric_ops_single_qubit.py b/pennylane/ops/qubit/parametric_ops_single_qubit.py index 02b913cb49b..c38cc550943 100644 --- a/pennylane/ops/qubit/parametric_ops_single_qubit.py +++ b/pennylane/ops/qubit/parametric_ops_single_qubit.py @@ -70,7 +70,7 @@ class RX(Operation): parameter_frequencies = [(1,)] def generator(self): - return qml.s_prod(-0.5, PauliX(wires=self.wires)) + return qml.Hamiltonian([-0.5], [PauliX(wires=self.wires)]) def __init__(self, phi, wires, id=None): super().__init__(phi, wires=wires, id=id) @@ -166,7 +166,7 @@ class RY(Operation): parameter_frequencies = [(1,)] def generator(self): - return qml.s_prod(-0.5, PauliY(wires=self.wires)) + return qml.Hamiltonian([-0.5], [PauliY(wires=self.wires)]) def __init__(self, phi, wires, id=None): super().__init__(phi, wires=wires, id=id) @@ -261,7 +261,7 @@ class RZ(Operation): parameter_frequencies = [(1,)] def generator(self): - return qml.s_prod(-0.5, PauliZ(wires=self.wires)) + return qml.Hamiltonian([-0.5], [PauliZ(wires=self.wires)]) def __init__(self, phi, wires, id=None): super().__init__(phi, wires=wires, id=id) diff --git a/pennylane/ops/qubit/qchem_ops.py b/pennylane/ops/qubit/qchem_ops.py index 498a4125103..a8c2036f00a 100644 --- a/pennylane/ops/qubit/qchem_ops.py +++ b/pennylane/ops/qubit/qchem_ops.py @@ -171,7 +171,7 @@ def circuit(phi): def generator(self): w1, w2 = self.wires - return 0.25 * (qml.X(w1) @ qml.Y(w2) - qml.Y(w1) @ qml.X(w2)) + return qml.Hamiltonian([0.25, -0.25], [qml.X(w1) @ qml.Y(w2), qml.Y(w1) @ qml.X(w2)]) def __init__(self, phi, wires, id=None): super().__init__(phi, wires=wires, id=id) @@ -316,11 +316,9 @@ class SingleExcitationMinus(Operation): def generator(self): w1, w2 = self.wires - return 0.25 * ( - -qml.Identity(w1) - + qml.X(w1) @ qml.Y(w2) - - qml.Y(w1) @ qml.X(w2) - - qml.Z(w1) @ qml.Z(w2) + return qml.Hamiltonian( + [-0.25, 0.25, -0.25, -0.25], + [qml.Identity(w1), qml.X(w1) @ qml.Y(w2), qml.Y(w1) @ qml.X(w2), qml.Z(w1) @ qml.Z(w2)], ) def __init__(self, phi, wires, id=None): @@ -446,8 +444,9 @@ class SingleExcitationPlus(Operation): def generator(self): w1, w2 = self.wires - return 0.25 * ( - qml.Identity(w1) + qml.X(w1) @ qml.Y(w2) - qml.Y(w1) @ qml.X(w2) + qml.Z(w1) @ qml.Z(w2) + return qml.Hamiltonian( + [0.25, 0.25, -0.25, 0.25], + [qml.Identity(w1), qml.X(w1) @ qml.Y(w2), qml.Y(w1) @ qml.X(w2), qml.Z(w1) @ qml.Z(w2)], ) def __init__(self, phi, wires, id=None): @@ -597,16 +596,18 @@ def circuit(phi): def generator(self): w0, w1, w2, w3 = self.wires - # coeffs = [0.0625, 0.0625, -0.0625, 0.0625, -0.0625, 0.0625, -0.0625, -0.0625] - return 0.0625 * ( - qml.X(w0) @ qml.X(w1) @ qml.X(w2) @ qml.Y(w3) - + qml.X(w0) @ qml.X(w1) @ qml.Y(w2) @ qml.X(w3) - - qml.X(w0) @ qml.Y(w1) @ qml.X(w2) @ qml.X(w3) - + qml.X(w0) @ qml.Y(w1) @ qml.Y(w2) @ qml.Y(w3) - - qml.Y(w0) @ qml.X(w1) @ qml.X(w2) @ qml.X(w3) - + qml.Y(w0) @ qml.X(w1) @ qml.Y(w2) @ qml.Y(w3) - - qml.Y(w0) @ qml.Y(w1) @ qml.X(w2) @ qml.Y(w3) - - qml.Y(w0) @ qml.Y(w1) @ qml.Y(w2) @ qml.X(w3) + return qml.Hamiltonian( + [0.0625, 0.0625, -0.0625, 0.0625, -0.0625, 0.0625, -0.0625, -0.0625], + [ + qml.X(w0) @ qml.X(w1) @ qml.X(w2) @ qml.Y(w3), + qml.X(w0) @ qml.X(w1) @ qml.Y(w2) @ qml.X(w3), + qml.X(w0) @ qml.Y(w1) @ qml.X(w2) @ qml.X(w3), + qml.X(w0) @ qml.Y(w1) @ qml.Y(w2) @ qml.Y(w3), + qml.Y(w0) @ qml.X(w1) @ qml.X(w2) @ qml.X(w3), + qml.Y(w0) @ qml.X(w1) @ qml.Y(w2) @ qml.Y(w3), + qml.Y(w0) @ qml.Y(w1) @ qml.X(w2) @ qml.Y(w3), + qml.Y(w0) @ qml.Y(w1) @ qml.Y(w2) @ qml.X(w3), + ], ) def pow(self, z): @@ -967,11 +968,14 @@ class OrbitalRotation(Operation): def generator(self): w0, w1, w2, w3 = self.wires - return 0.25 * ( - qml.X(w0) @ qml.Z(w1) @ qml.Y(w2) - - (qml.Y(w0) @ qml.Z(w1) @ qml.X(w2)) - + (qml.X(w1) @ qml.Z(w2) @ qml.Y(w3)) - - (qml.Y(w1) @ qml.Z(w2) @ qml.X(w3)) + return qml.Hamiltonian( + [0.25, -0.25, 0.25, -0.25], + [ + qml.X(w0) @ qml.Z(w1) @ qml.Y(w2), + (qml.Y(w0) @ qml.Z(w1) @ qml.X(w2)), + (qml.X(w1) @ qml.Z(w2) @ qml.Y(w3)), + (qml.Y(w1) @ qml.Z(w2) @ qml.X(w3)), + ], ) def __init__(self, phi, wires, id=None): @@ -1155,11 +1159,15 @@ class FermionicSWAP(Operation): def generator(self): w1, w2 = self.wires - return 0.5 * qml.Identity(w1) @ qml.Identity(w2) - 0.25 * ( - qml.Identity(w1) @ qml.Z(w2) - + qml.Z(w1) @ qml.Identity(w2) - + qml.X(w1) @ qml.X(w2) - + qml.Y(w1) @ qml.Y(w2) + return qml.Hamiltonian( + [0.5, -0.25, -0.25, -0.25, -0.25], + [ + qml.Identity(w1) @ qml.Identity(w2), + qml.Identity(w1) @ qml.Z(w2), + qml.Z(w1) @ qml.Identity(w2), + qml.X(w1) @ qml.X(w2), + qml.Y(w1) @ qml.Y(w2), + ], ) def __init__(self, phi, wires, id=None): diff --git a/pennylane/ops/qutrit/parametric_ops.py b/pennylane/ops/qutrit/parametric_ops.py index 182d14c7269..00ca48aa4b6 100644 --- a/pennylane/ops/qutrit/parametric_ops.py +++ b/pennylane/ops/qutrit/parametric_ops.py @@ -112,6 +112,8 @@ class TRX(Operation): _index_dict = {(0, 1): 1, (0, 2): 4, (1, 2): 6} def generator(self): + # this generator returns SProd, even with the old op_math, because other options are not suitable + # to qudit operators (for example, they do not have a matrix defined as a Hamiltonian) return qml.s_prod(-0.5, qml.GellMann(self.wires, index=self._index_dict[self.subspace])) def __init__(self, phi, wires, subspace=(0, 1), id=None): @@ -257,6 +259,8 @@ class TRY(Operation): _index_dict = {(0, 1): 2, (0, 2): 5, (1, 2): 7} def generator(self): + # this generator returns SProd, even with the old op_math, because other options are not suitable + # to qudit operators (for example, they do not have a matrix defined as a Hamiltonian) return qml.s_prod(-0.5, qml.GellMann(self.wires, index=self._index_dict[self.subspace])) def __init__(self, phi, wires, subspace=(0, 1), id=None): @@ -395,6 +399,8 @@ class TRZ(Operation): parameter_frequencies = [(0.5, 1)] def generator(self): + # these generators return SProd and Sum, even with the old op_math, because other options are + # not suitable to qudit operators (for example, they do not have a matrix defined as a Hamiltonian) if self.subspace == (0, 1): return qml.s_prod(-0.5, qml.GellMann(wires=self.wires, index=3)) diff --git a/pennylane/optimize/riemannian_gradient.py b/pennylane/optimize/riemannian_gradient.py index 67054d61633..bcc9cd19484 100644 --- a/pennylane/optimize/riemannian_gradient.py +++ b/pennylane/optimize/riemannian_gradient.py @@ -71,7 +71,7 @@ def append_time_evolution( with QueuingManager.stop_recording(): new_operations.append( qml.QubitUnitary( - expm(-1j * t * riemannian_gradient.sparse_matrix().toarray()), + expm(-1j * t * riemannian_gradient.sparse_matrix(tape.wires).toarray()), wires=range(len(riemannian_gradient.wires)), ) ) @@ -267,7 +267,7 @@ def __init__(self, circuit, stepsize=0.01, restriction=None, exact=False, trotte self.circuit = circuit self.circuit.construct([], {}) self.hamiltonian = circuit.func().obs - if not isinstance(self.hamiltonian, qml.Hamiltonian): + if not isinstance(self.hamiltonian, (qml.ops.Hamiltonian, qml.ops.LinearCombination)): raise TypeError( f"circuit must return the expectation value of a Hamiltonian," f"received {type(circuit.func().obs)}" @@ -280,7 +280,9 @@ def __init__(self, circuit, stepsize=0.01, restriction=None, exact=False, trotte f"optimizing a {self.nqubits} qubit circuit may be slow.", UserWarning, ) - if restriction is not None and not isinstance(restriction, qml.Hamiltonian): + if restriction is not None and not isinstance( + restriction, (qml.ops.Hamiltonian, qml.ops.LinearCombination) + ): raise TypeError(f"restriction must be a Hamiltonian, received {type(restriction)}") ( self.lie_algebra_basis_ops, diff --git a/pennylane/optimize/shot_adaptive.py b/pennylane/optimize/shot_adaptive.py index 8faad3753ff..66688d33df1 100644 --- a/pennylane/optimize/shot_adaptive.py +++ b/pennylane/optimize/shot_adaptive.py @@ -311,7 +311,9 @@ def _single_shot_qnode_gradients(self, qnode, args, kwargs): tape = qnode.tape [expval] = tape.measurements coeffs, observables = ( - expval.obs.terms() if isinstance(expval.obs, qml.Hamiltonian) else ([1.0], [expval.obs]) + expval.obs.terms() + if isinstance(expval.obs, (qml.ops.LinearCombination, qml.ops.Hamiltonian)) + else ([1.0], [expval.obs]) ) if self.lipschitz is None: diff --git a/pennylane/pauli/conversion.py b/pennylane/pauli/conversion.py index 436dbeb14e5..e42382df64f 100644 --- a/pennylane/pauli/conversion.py +++ b/pennylane/pauli/conversion.py @@ -21,7 +21,17 @@ import pennylane as qml from pennylane.operation import Tensor -from pennylane.ops import Hamiltonian, Identity, PauliX, PauliY, PauliZ, Prod, SProd, Sum +from pennylane.ops import ( + Hamiltonian, + LinearCombination, + Identity, + PauliX, + PauliY, + PauliZ, + Prod, + SProd, + Sum, +) from pennylane.ops.qubit.matrix_ops import _walsh_hadamard_transform from .pauli_arithmetic import I, PauliSentence, PauliWord, X, Y, Z, op_map @@ -325,7 +335,7 @@ def pauli_decompose( } ) - return Hamiltonian(coeffs, obs) + return qml.Hamiltonian(coeffs, obs) def pauli_sentence(op): @@ -411,7 +421,7 @@ def _(op: SProd): return ps -@_pauli_sentence.register +@_pauli_sentence.register(Hamiltonian) def _(op: Hamiltonian): if not all(is_pauli_word(o) for o in op.ops): raise ValueError(f"Op must be a linear combination of Pauli operators only, got: {op}") @@ -433,6 +443,14 @@ def term_2_pauli_word(term): return ps +@_pauli_sentence.register(LinearCombination) +def _(op: LinearCombination): + if not all(is_pauli_word(o) for o in op.ops): + raise ValueError(f"Op must be a linear combination of Pauli operators only, got: {op}") + + return op._build_pauli_rep() # pylint: disable=protected-access + + @_pauli_sentence.register def _(op: Sum): ps = PauliSentence() diff --git a/pennylane/pauli/pauli_arithmetic.py b/pennylane/pauli/pauli_arithmetic.py index 8b64583c0ed..22e7616ab9f 100644 --- a/pennylane/pauli/pauli_arithmetic.py +++ b/pennylane/pauli/pauli_arithmetic.py @@ -25,7 +25,7 @@ from pennylane.typing import TensorLike from pennylane.wires import Wires from pennylane.operation import Tensor -from pennylane.ops import Hamiltonian, Identity, PauliX, PauliY, PauliZ, Prod, SProd, Sum +from pennylane.ops import Identity, PauliX, PauliY, PauliZ, Prod, SProd, Sum I = "I" @@ -520,10 +520,10 @@ def hamiltonian(self, wire_order=None): if len(self) == 0: if wire_order in (None, [], Wires([])): raise ValueError("Can't get the Hamiltonian for an empty PauliWord.") - return Hamiltonian([1], [Identity(wires=wire_order)]) + return qml.Hamiltonian([1], [Identity(wires=wire_order)]) obs = [_make_operation(op, wire) for wire, op in self.items()] - return Hamiltonian([1], [obs[0] if len(obs) == 1 else Tensor(*obs)]) + return qml.Hamiltonian([1], [obs[0] if len(obs) == 1 else Tensor(*obs)]) def map_wires(self, wire_map: dict) -> "PauliWord": """Return a new PauliWord with the wires mapped.""" @@ -991,12 +991,12 @@ def hamiltonian(self, wire_order=None): if len(self) == 0: if wire_order in (None, [], Wires([])): raise ValueError("Can't get the Hamiltonian for an empty PauliSentence.") - return Hamiltonian([], []) + return qml.Hamiltonian([], []) wire_order = wire_order or self.wires wire_order = list(wire_order) - return Hamiltonian( + return qml.Hamiltonian( list(self.values()), [pw.operation(wire_order=wire_order, get_as_tensor=True) for pw in self], ) @@ -1007,6 +1007,8 @@ def simplify(self, tol=1e-8): for pw, coeff in items: if abs(coeff) <= tol: del self[pw] + if len(self) == 0: + self = PauliSentence({}) # pylint: disable=self-cls-assignment def map_wires(self, wire_map: dict) -> "PauliSentence": """Return a new PauliSentence with the wires mapped.""" diff --git a/pennylane/pauli/pauli_interface.py b/pennylane/pauli/pauli_interface.py index 30c6cfa7393..4402c562ed4 100644 --- a/pennylane/pauli/pauli_interface.py +++ b/pennylane/pauli/pauli_interface.py @@ -17,7 +17,16 @@ from typing import Union from functools import singledispatch -from pennylane.ops import Hamiltonian, Identity, PauliX, PauliY, PauliZ, Prod, SProd +from pennylane.ops import ( + Hamiltonian, + LinearCombination, + Identity, + PauliX, + PauliY, + PauliZ, + Prod, + SProd, +) from pennylane.operation import Tensor from .utils import is_pauli_word @@ -72,8 +81,9 @@ def _pw_prefactor_tensor(observable: Tensor): raise ValueError(f"Expected a valid Pauli word, got {observable}") -@_pauli_word_prefactor.register -def _pw_prefactor_ham(observable: Hamiltonian): +@_pauli_word_prefactor.register(Hamiltonian) +@_pauli_word_prefactor.register(LinearCombination) +def _pw_prefactor_ham(observable: Union[Hamiltonian, LinearCombination]): if is_pauli_word(observable): return observable.coeffs[0] raise ValueError(f"Expected a valid Pauli word, got {observable}") diff --git a/pennylane/pauli/utils.py b/pennylane/pauli/utils.py index 6197fefb7d2..f3246591d62 100644 --- a/pennylane/pauli/utils.py +++ b/pennylane/pauli/utils.py @@ -20,7 +20,7 @@ * `arXiv:1701.08213 `_ * `arXiv:1907.09386 `_ """ -from functools import lru_cache, reduce, singledispatch +from functools import lru_cache, singledispatch from itertools import product from typing import List, Union @@ -28,7 +28,16 @@ import pennylane as qml from pennylane.operation import Tensor -from pennylane.ops import Hamiltonian, Identity, PauliX, PauliY, PauliZ, Prod, SProd +from pennylane.ops import ( + Hamiltonian, + LinearCombination, + Identity, + PauliX, + PauliY, + PauliZ, + Prod, + SProd, +) from pennylane.wires import Wires # To make this quicker later on @@ -119,8 +128,9 @@ def _is_pw_tensor(observable: Tensor): return set(observable.name).issubset(pauli_word_names) -@_is_pauli_word.register -def _is_pw_ham(observable: Hamiltonian): +@_is_pauli_word.register(Hamiltonian) +@_is_pauli_word.register(LinearCombination) +def _is_pw_ham(observable: Union[Hamiltonian, LinearCombination]): return False if len(observable.ops) != 1 else is_pauli_word(observable.ops[0]) @@ -430,13 +440,34 @@ def pauli_word_to_string(pauli_word, wire_map=None): if not is_pauli_word(pauli_word): raise TypeError(f"Expected Pauli word observables, instead got {pauli_word}") - if isinstance(pauli_word, Hamiltonian): + if isinstance(pauli_word, qml.ops.Hamiltonian): # hamiltonian contains only one term - pauli_word = pauli_word.ops[0] - elif isinstance(pauli_word, SProd): - pauli_word = pauli_word.base - if isinstance(pauli_word, Prod): - pauli_word = Tensor(*pauli_word.operands) + return _pauli_word_to_string_legacy(pauli_word, wire_map) + + pr = next(iter(pauli_word.pauli_rep.keys())) + + # If there is no wire map, we must infer from the structure of Paulis + if wire_map is None: + wire_map = {pauli_word.wires.labels[i]: i for i in range(len(pauli_word.wires))} + + n_qubits = len(wire_map) + + # Set default value of all characters to identity + pauli_string = ["I"] * n_qubits + + for wire, op_label in pr.items(): + pauli_string[wire_map[wire]] = op_label + + return "".join(pauli_string) + + +def _pauli_word_to_string_legacy(pauli_word, wire_map): + """Turn a legacy Hamiltonian operator to strings""" + # TODO: Give Hamiltonian a pauli rep to make this branch obsolete + pauli_word = pauli_word.ops[0] + + if wire_map is None: + wire_map = {pauli_word.wires.labels[i]: i for i in range(len(pauli_word.wires))} character_map = {"Identity": "I", "PauliX": "X", "PauliY": "Y", "PauliZ": "Z"} @@ -565,31 +596,7 @@ def pauli_word_to_matrix(pauli_word, wire_map=None): if wire_map is None: wire_map = {pauli_word.wires.labels[i]: i for i in range(len(pauli_word.wires))} - n_qubits = len(wire_map) - - # If there is only a single qubit, we can return the matrix directly - if n_qubits == 1: - return pauli_word.matrix() - - # There may be more than one qubit in the Pauli but still only - # one of them with anything acting on it, so take that into account - pauli_names = [pauli_word.name] if isinstance(pauli_word.name, str) else pauli_word.name - - # Special case: the identity Pauli - if pauli_names == ["Identity"]: - return np.eye(2**n_qubits) - - # If there is more than one qubit, we must go through the wire map wire - # by wire and pick out the relevant matrices - pauli_mats = [ID_MAT for x in range(n_qubits)] - - for wire_label, wire_idx in wire_map.items(): - if wire_label in pauli_word.wires.labels: - op_idx = pauli_word.wires.labels.index(wire_label) - # compute_matrix() only works because we work with Paulis here - pauli_mats[wire_idx] = getattr(qml, pauli_names[op_idx]).compute_matrix() - - return reduce(np.kron, pauli_mats) + return pauli_word.matrix(wire_map) def is_qwc(pauli_vec_1, pauli_vec_2): diff --git a/pennylane/pulse/hardware_hamiltonian.py b/pennylane/pulse/hardware_hamiltonian.py index 3efe5582cf4..6683bcd7133 100644 --- a/pennylane/pulse/hardware_hamiltonian.py +++ b/pennylane/pulse/hardware_hamiltonian.py @@ -21,7 +21,6 @@ import pennylane as qml from pennylane.wires import Wires from pennylane.operation import Operator -from pennylane.ops.qubit.hamiltonian import Hamiltonian from .parametrized_hamiltonian import ParametrizedHamiltonian @@ -223,8 +222,8 @@ def circuit(params): amplitude_and_phase(qml.math.sin, amplitude, phase), ] - drive_x_term = 0.5 * sum(qml.X(wire) for wire in wires) - drive_y_term = -0.5 * sum(qml.Y(wire) for wire in wires) + drive_x_term = qml.Hamiltonian([0.5] * len(wires), [qml.X(wire) for wire in wires]) + drive_y_term = qml.Hamiltonian([-0.5] * len(wires), [qml.Y(wire) for wire in wires]) observables = [drive_x_term, drive_y_term] @@ -353,7 +352,9 @@ def __add__(self, other): # pylint: disable=too-many-return-statements settings = self.settings pulses = self.pulses - if isinstance(other, (Hamiltonian, ParametrizedHamiltonian)): + if isinstance( + other, (qml.ops.Hamiltonian, qml.ops.LinearCombination, ParametrizedHamiltonian) + ): new_coeffs = coeffs + list(other.coeffs.copy()) new_ops = ops + other.ops.copy() return HardwareHamiltonian( diff --git a/pennylane/pulse/parametrized_evolution.py b/pennylane/pulse/parametrized_evolution.py index 2dfdab81d3f..3f2d5ed80ab 100644 --- a/pennylane/pulse/parametrized_evolution.py +++ b/pennylane/pulse/parametrized_evolution.py @@ -378,7 +378,7 @@ def __init__( id=None, **odeint_kwargs, ): - if not all(op.has_matrix or isinstance(op, qml.Hamiltonian) for op in H.ops): + if not all(op.has_matrix or isinstance(op, qml.ops.Hamiltonian) for op in H.ops): raise ValueError( "All operators inside the parametrized hamiltonian must have a matrix defined." ) diff --git a/pennylane/pulse/parametrized_hamiltonian.py b/pennylane/pulse/parametrized_hamiltonian.py index 69a08b77a58..a7ce9bda78b 100644 --- a/pennylane/pulse/parametrized_hamiltonian.py +++ b/pennylane/pulse/parametrized_hamiltonian.py @@ -20,7 +20,6 @@ import pennylane as qml from pennylane.operation import Operator from pennylane.ops import Sum -from pennylane.ops.qubit.hamiltonian import Hamiltonian from pennylane.typing import TensorLike from pennylane.wires import Wires @@ -337,7 +336,7 @@ def __add__(self, H): ops = self.ops.copy() coeffs = self.coeffs.copy() - if isinstance(H, (Hamiltonian, ParametrizedHamiltonian)): + if isinstance(H, (qml.ops.Hamiltonian, qml.ops.LinearCombination, ParametrizedHamiltonian)): # if Hamiltonian, coeffs array must be converted to list new_coeffs = coeffs + list(H.coeffs.copy()) new_ops = ops + H.ops.copy() @@ -362,7 +361,7 @@ def __radd__(self, H): ops = self.ops.copy() coeffs = self.coeffs.copy() - if isinstance(H, (Hamiltonian, ParametrizedHamiltonian)): + if isinstance(H, (qml.ops.Hamiltonian, qml.ops.LinearCombination, ParametrizedHamiltonian)): # if Hamiltonian, coeffs array must be converted to list new_coeffs = list(H.coeffs.copy()) + coeffs new_ops = H.ops.copy() + ops diff --git a/pennylane/qaoa/cycle.py b/pennylane/qaoa/cycle.py index 43f4d74d475..5d6a3de3cad 100644 --- a/pennylane/qaoa/cycle.py +++ b/pennylane/qaoa/cycle.py @@ -29,7 +29,6 @@ import numpy as np import pennylane as qml -from pennylane.ops import Hamiltonian def edges_to_wires(graph: Union[nx.Graph, rx.PyGraph, rx.PyDiGraph]) -> Dict[Tuple, int]: @@ -140,7 +139,7 @@ def wires_to_edges(graph: Union[nx.Graph, rx.PyGraph, rx.PyDiGraph]) -> Dict[int ) -def cycle_mixer(graph: Union[nx.DiGraph, rx.PyDiGraph]) -> Hamiltonian: +def cycle_mixer(graph: Union[nx.DiGraph, rx.PyDiGraph]) -> qml.operation.Operator: r"""Calculates the cycle-mixer Hamiltonian. Following methods outlined `here `__, the @@ -227,7 +226,7 @@ def cycle_mixer(graph: Union[nx.DiGraph, rx.PyDiGraph]) -> Hamiltonian: f"Input graph must be a nx.DiGraph or rx.PyDiGraph, got {type(graph).__name__}" ) - hamiltonian = Hamiltonian([], []) + hamiltonian = qml.Hamiltonian([], []) graph_edges = sorted(graph.edge_list()) if isinstance(graph, rx.PyDiGraph) else graph.edges for edge in graph_edges: @@ -236,7 +235,9 @@ def cycle_mixer(graph: Union[nx.DiGraph, rx.PyDiGraph]) -> Hamiltonian: return hamiltonian -def _partial_cycle_mixer(graph: Union[nx.DiGraph, rx.PyDiGraph], edge: Tuple) -> Hamiltonian: +def _partial_cycle_mixer( + graph: Union[nx.DiGraph, rx.PyDiGraph], edge: Tuple +) -> qml.operation.Operator: r"""Calculates the partial cycle-mixer Hamiltonian for a specific edge. For an edge :math:`(i, j)`, this function returns: @@ -292,10 +293,10 @@ def _partial_cycle_mixer(graph: Union[nx.DiGraph, rx.PyDiGraph], edge: Tuple) -> coeffs.extend([0.25, 0.25, 0.25, -0.25]) - return Hamiltonian(coeffs, ops) + return qml.Hamiltonian(coeffs, ops) -def loss_hamiltonian(graph: Union[nx.Graph, rx.PyGraph, rx.PyDiGraph]) -> Hamiltonian: +def loss_hamiltonian(graph: Union[nx.Graph, rx.PyGraph, rx.PyDiGraph]) -> qml.operation.Operator: r"""Calculates the loss Hamiltonian for the maximum-weighted cycle problem. We consider the problem of selecting a cycle from a graph that has the greatest product of edge @@ -406,7 +407,7 @@ def loss_hamiltonian(graph: Union[nx.Graph, rx.PyGraph, rx.PyDiGraph]) -> Hamilt coeffs.append(np.log(weight)) ops.append(qml.Z(edges_to_qubits[get_nvalues(edge)])) - H = Hamiltonian(coeffs, ops) + H = qml.Hamiltonian(coeffs, ops) # store the valuable information that all observables are in one commuting group H.grouping_indices = [list(range(len(H.ops)))] @@ -448,7 +449,7 @@ def _square_hamiltonian_terms( return squared_coeffs, squared_ops -def out_flow_constraint(graph: Union[nx.DiGraph, rx.PyDiGraph]) -> Hamiltonian: +def out_flow_constraint(graph: Union[nx.DiGraph, rx.PyDiGraph]) -> qml.operation.Operator: r"""Calculates the `out flow constraint `__ Hamiltonian for the maximum-weighted cycle problem. @@ -491,7 +492,7 @@ def out_flow_constraint(graph: Union[nx.DiGraph, rx.PyDiGraph]) -> Hamiltonian: if isinstance(graph, (nx.DiGraph, rx.PyDiGraph)) and not hasattr(graph, "out_edges"): raise ValueError("Input graph must be directed") - hamiltonian = Hamiltonian([], []) + hamiltonian = qml.Hamiltonian([], []) graph_nodes = graph.node_indexes() if isinstance(graph, rx.PyDiGraph) else graph.nodes for node in graph_nodes: @@ -500,7 +501,7 @@ def out_flow_constraint(graph: Union[nx.DiGraph, rx.PyDiGraph]) -> Hamiltonian: return hamiltonian -def net_flow_constraint(graph: Union[nx.DiGraph, rx.PyDiGraph]) -> Hamiltonian: +def net_flow_constraint(graph: Union[nx.DiGraph, rx.PyDiGraph]) -> qml.operation.Operator: r"""Calculates the `net flow constraint `__ Hamiltonian for the maximum-weighted cycle problem. @@ -544,7 +545,7 @@ def net_flow_constraint(graph: Union[nx.DiGraph, rx.PyDiGraph]) -> Hamiltonian: f"Input graph must be a nx.DiGraph or rx.PyDiGraph, got {type(graph).__name__}" ) - hamiltonian = Hamiltonian([], []) + hamiltonian = qml.Hamiltonian([], []) graph_nodes = graph.node_indexes() if isinstance(graph, rx.PyDiGraph) else graph.nodes for node in graph_nodes: @@ -555,7 +556,7 @@ def net_flow_constraint(graph: Union[nx.DiGraph, rx.PyDiGraph]) -> Hamiltonian: def _inner_out_flow_constraint_hamiltonian( graph: Union[nx.DiGraph, rx.PyDiGraph], node: int -) -> Hamiltonian: +) -> qml.operation.Operator: r"""Calculates the inner portion of the Hamiltonian in :func:`out_flow_constraint`. For a given :math:`i`, this function returns: @@ -614,7 +615,7 @@ def _inner_out_flow_constraint_hamiltonian( coeffs.append(d * (d - 2)) ops.append(qml.Identity(0)) - H = Hamiltonian(coeffs, ops) + H = qml.Hamiltonian(coeffs, ops) H.simplify() # store the valuable information that all observables are in one commuting group H.grouping_indices = [list(range(len(H.ops)))] @@ -624,7 +625,7 @@ def _inner_out_flow_constraint_hamiltonian( def _inner_net_flow_constraint_hamiltonian( graph: Union[nx.DiGraph, rx.PyDiGraph], node: int -) -> Hamiltonian: +) -> qml.operation.Operator: r"""Calculates the squared inner portion of the Hamiltonian in :func:`net_flow_constraint`. @@ -685,8 +686,8 @@ def _inner_net_flow_constraint_hamiltonian( ops.append(qml.Z(wires)) coeffs, ops = _square_hamiltonian_terms(coeffs, ops) - H = Hamiltonian(coeffs, ops) - H.simplify() + H = qml.Hamiltonian(coeffs, ops) + H = H.simplify() # store the valuable information that all observables are in one commuting group H.grouping_indices = [list(range(len(H.ops)))] return H diff --git a/pennylane/qaoa/layers.py b/pennylane/qaoa/layers.py index b9bc8483866..25712aa4d04 100644 --- a/pennylane/qaoa/layers.py +++ b/pennylane/qaoa/layers.py @@ -31,7 +31,12 @@ def _diagonal_terms(hamiltonian): val = True for i in hamiltonian.ops: - obs = i.obs if isinstance(i, Tensor) else [i] + if isinstance(i, Tensor): + obs = i.obs + elif isinstance(i, qml.ops.Prod): + obs = i.operands + else: + obs = [i] for j in obs: if j.name not in ("PauliZ", "Identity"): val = False @@ -94,7 +99,7 @@ def circuit(gamma): 1: ──H───────────╰RZZ(1.00)── """ - if not isinstance(hamiltonian, qml.Hamiltonian): + if not isinstance(hamiltonian, (qml.ops.Hamiltonian, qml.ops.LinearCombination)): raise ValueError( f"hamiltonian must be of type pennylane.Hamiltonian, got {type(hamiltonian).__name__}" ) @@ -156,7 +161,7 @@ def circuit(alpha): 1: ──H───────────╰RXX(1.00)── """ - if not isinstance(hamiltonian, qml.Hamiltonian): + if not isinstance(hamiltonian, (qml.ops.Hamiltonian, qml.ops.LinearCombination)): raise ValueError( f"hamiltonian must be of type pennylane.Hamiltonian, got {type(hamiltonian).__name__}" ) diff --git a/pennylane/qchem/factorization.py b/pennylane/qchem/factorization.py index 311c2db8911..c5f1083fe53 100644 --- a/pennylane/qchem/factorization.py +++ b/pennylane/qchem/factorization.py @@ -317,8 +317,13 @@ def basis_rotation(one_electron, two_electron, tol_factor=1.0e-5): ops_l.append(ops_l_) ops = [ops_t] + ops_l - c_group = [op.coeffs for op in ops] - o_group = [op.ops for op in ops] + + c_group, o_group = [], [] + for op in ops: + c_g, o_g = op.simplify().terms() + c_group.append(c_g) + o_group.append(o_g) + u_transform = list([t_eigvecs] + list(v_unitaries)) # Inverse of diagonalizing unitaries return c_group, o_group, u_transform diff --git a/pennylane/qchem/tapering.py b/pennylane/qchem/tapering.py index 27e7ddea32e..0f3f94692d3 100644 --- a/pennylane/qchem/tapering.py +++ b/pennylane/qchem/tapering.py @@ -598,9 +598,9 @@ def _build_generator(operation, wire_order, op_gen=None): f"Generator for {operation} is not implemented, please provide it with 'op_gen' args." ) from exc else: # check that user-provided generator is correct - if not isinstance(op_gen, (qml.Hamiltonian, PauliSentence)) and not isinstance( - getattr(op_gen, "pauli_rep", None), PauliSentence - ): + if not isinstance( + op_gen, (qml.ops.Hamiltonian, qml.ops.LinearCombination, PauliSentence) + ) and not isinstance(getattr(op_gen, "pauli_rep", None), PauliSentence): raise ValueError( f"Generator for the operation needs to be a valid operator, but got {type(op_gen)}." ) diff --git a/pennylane/qcut/cutcircuit.py b/pennylane/qcut/cutcircuit.py index eec00693173..0237f4c17a5 100644 --- a/pennylane/qcut/cutcircuit.py +++ b/pennylane/qcut/cutcircuit.py @@ -50,7 +50,9 @@ def processing_fn(res): # Expand the tapes for handling Hamiltonian with two or more terms tape_meas_ops = tape.measurements - if tape_meas_ops and isinstance(tape_meas_ops[0].obs, qml.Hamiltonian): + if tape_meas_ops and isinstance( + tape_meas_ops[0].obs, (qml.ops.Hamiltonian, qml.ops.LinearCombination) + ): if len(tape_meas_ops) > 1: raise NotImplementedError( "Hamiltonian expansion is supported only with a single Hamiltonian" diff --git a/pennylane/qcut/tapes.py b/pennylane/qcut/tapes.py index d00ba77b236..9765d088430 100644 --- a/pennylane/qcut/tapes.py +++ b/pennylane/qcut/tapes.py @@ -80,16 +80,17 @@ def tape_to_graph(tape: QuantumTape) -> MultiDiGraph: order += 1 # pylint: disable=undefined-loop-variable for m in tape.measurements: obs = getattr(m, "obs", None) - if obs is not None and isinstance(obs, Tensor): + if obs is not None and isinstance(obs, (Tensor, qml.ops.Prod)): if isinstance(m, SampleMP): raise ValueError( "Sampling from tensor products of observables " "is not supported in circuit cutting" ) - for o in obs.obs: - m_ = m.__class__(obs=o) + for o in obs.operands if isinstance(obs, qml.ops.op_math.Prod) else obs.obs: + m_ = m.__class__(obs=o) _add_operator_node(graph, m_, order, wire_latest_node) + elif isinstance(m, SampleMP) and obs is None: for w in m.wires: s_ = qml.sample(qml.Projector([1], wires=w)) @@ -202,7 +203,8 @@ def graph_to_tape(graph: MultiDiGraph) -> QuantumTape: if measurement_type is ExpectationMP: if len(observables) > 1: - measurements_from_graph.append(qml.expval(Tensor(*observables))) + prod_type = qml.prod if qml.operation.active_new_opmath() else Tensor + measurements_from_graph.append(qml.expval(prod_type(*observables))) else: measurements_from_graph.append(qml.expval(obs)) diff --git a/pennylane/shadows/classical_shadow.py b/pennylane/shadows/classical_shadow.py index 00e21428dda..b1eb24111b5 100644 --- a/pennylane/shadows/classical_shadow.py +++ b/pennylane/shadows/classical_shadow.py @@ -228,11 +228,27 @@ def qnode(): (T, 2**n, 2**n), ) + def _convert_to_pauli_words_with_pauli_rep(self, pr, num_wires): + """Convert to recipe using pauli representation""" + pr_to_recipe_map = {"X": 0, "Y": 1, "Z": 2, "I": -1} + + coeffs_and_words = [] + for pw, c in pr.items(): + word = [-1] * num_wires + for i, s in pw.items(): + word[self.wire_map.index(i)] = pr_to_recipe_map[s] + + coeffs_and_words.append((c, word)) + + return coeffs_and_words + def _convert_to_pauli_words(self, observable): """Given an observable, obtain a list of coefficients and Pauli words, the sum of which is equal to the observable""" num_wires = self.bits.shape[1] + + # Legacy support for old opmath obs_to_recipe_map = {"PauliX": 0, "PauliY": 1, "PauliZ": 2, "Identity": -1} def pauli_list_to_word(obs): @@ -253,9 +269,7 @@ def pauli_list_to_word(obs): word = pauli_list_to_word(observable.obs) return [(1, word)] - # TODO: cases for new operator arithmetic - - if isinstance(observable, qml.Hamiltonian): + if isinstance(observable, qml.ops.Hamiltonian): coeffs_and_words = [] for coeff, op in zip(observable.data, observable.ops): coeffs_and_words.extend( @@ -263,6 +277,14 @@ def pauli_list_to_word(obs): ) return coeffs_and_words + # Support for all operators with a valid pauli_rep + if (pr := observable.pauli_rep) is not None: + return self._convert_to_pauli_words_with_pauli_rep(pr, num_wires) + + raise ValueError( + "Observable must have a valid pauli representation. Recevied {observable} with observable.pauli_rep = {pr}" + ) + def expval(self, H, k=1): r"""Compute expectation value of an observable :math:`H`. @@ -306,7 +328,7 @@ def qnode(x): >>> shadow.expval(H, k=1) array(1.9980000000000002) """ - if not isinstance(H, Iterable): + if not isinstance(H, (list, tuple)): H = [H] coeffs_and_words = [self._convert_to_pauli_words(h) for h in H] diff --git a/pennylane/templates/subroutines/qdrift.py b/pennylane/templates/subroutines/qdrift.py index f6f6be983b7..35299e3a7af 100644 --- a/pennylane/templates/subroutines/qdrift.py +++ b/pennylane/templates/subroutines/qdrift.py @@ -16,7 +16,7 @@ import pennylane as qml from pennylane.operation import Operation from pennylane.math import requires_grad, unwrap -from pennylane.ops import Sum, SProd, Hamiltonian +from pennylane.ops import Sum, SProd, Hamiltonian, LinearCombination @qml.QueuingManager.stop_recording() @@ -149,7 +149,7 @@ def __init__( # pylint: disable=too-many-arguments ): r"""Initialize the QDrift class""" - if isinstance(hamiltonian, Hamiltonian): + if isinstance(hamiltonian, (Hamiltonian, LinearCombination)): coeffs, ops = hamiltonian.terms() elif isinstance(hamiltonian, Sum): @@ -273,7 +273,7 @@ def error(hamiltonian, time, n=1): Returns: float: upper bound on the precision achievable using the QDrift protocol """ - if isinstance(hamiltonian, Hamiltonian): + if isinstance(hamiltonian, (Hamiltonian, LinearCombination)): lmbda = qml.math.sum(qml.math.abs(hamiltonian.coeffs)) elif isinstance(hamiltonian, Sum): diff --git a/pennylane/templates/subroutines/trotter.py b/pennylane/templates/subroutines/trotter.py index a51062399e4..52a56a0c05d 100644 --- a/pennylane/templates/subroutines/trotter.py +++ b/pennylane/templates/subroutines/trotter.py @@ -183,13 +183,12 @@ def __init__( # pylint: disable=too-many-arguments f"The order of a TrotterProduct must be 1 or a positive even integer, got {order}." ) - if isinstance(hamiltonian, (qml.Hamiltonian)): + if isinstance(hamiltonian, (qml.ops.Hamiltonian, qml.ops.LinearCombination)): coeffs, ops = hamiltonian.terms() if len(coeffs) < 2: raise ValueError( "There should be at least 2 terms in the Hamiltonian. Otherwise use `qml.exp`" ) - hamiltonian = qml.dot(coeffs, ops) if isinstance(hamiltonian, SProd): diff --git a/pennylane/transforms/hamiltonian_expand.py b/pennylane/transforms/hamiltonian_expand.py index e30f285b729..43c5794d5f6 100644 --- a/pennylane/transforms/hamiltonian_expand.py +++ b/pennylane/transforms/hamiltonian_expand.py @@ -113,7 +113,10 @@ def hamiltonian_expand(tape: QuantumTape, group: bool = True) -> (Sequence[Quant if ( len(tape.measurements) != 1 - or not isinstance(hamiltonian := tape.measurements[0].obs, qml.Hamiltonian) + or not isinstance( + hamiltonian := tape.measurements[0].obs, + (qml.ops.Hamiltonian, qml.ops.LinearCombination), + ) or not isinstance(tape.measurements[0], ExpectationMP) ): raise ValueError( diff --git a/pennylane/transforms/sign_expand/sign_expand.py b/pennylane/transforms/sign_expand/sign_expand.py index 28b6c9aa916..ea5d6dc872e 100644 --- a/pennylane/transforms/sign_expand/sign_expand.py +++ b/pennylane/transforms/sign_expand/sign_expand.py @@ -311,7 +311,7 @@ def circuit(): # TODO qml.utils.sparse_hamiltonian at the moment does not allow autograd to push gradients through if ( - not isinstance(hamiltonian, qml.Hamiltonian) + not isinstance(hamiltonian, (qml.ops.Hamiltonian, qml.ops.LinearCombination)) or len(tape.measurements) > 1 or tape.measurements[0].return_type not in [qml.measurements.Expectation, qml.measurements.Variance] diff --git a/pennylane/transforms/transpile.py b/pennylane/transforms/transpile.py index 020c4d0e462..5010203252a 100644 --- a/pennylane/transforms/transpile.py +++ b/pennylane/transforms/transpile.py @@ -9,7 +9,8 @@ import pennylane as qml from pennylane.transforms import transform -from pennylane import Hamiltonian +from pennylane.ops import Hamiltonian +from pennylane.ops import LinearCombination from pennylane.operation import Tensor from pennylane.ops import __all__ as all_ops from pennylane.ops.qubit import SWAP @@ -143,9 +144,12 @@ def circuit(): f"Not all wires present in coupling map! wires: {wires}, coupling map: {coupling_graph.nodes}" ) - if any(isinstance(m.obs, (Hamiltonian, Tensor)) for m in tape.measurements): + if any( + isinstance(m.obs, (Hamiltonian, LinearCombination, Tensor, qml.ops.Prod)) + for m in tape.measurements + ): raise NotImplementedError( - "Measuring expectation values of tensor products or Hamiltonians is not yet supported" + "Measuring expectation values of tensor products, Prods, or Hamiltonians is not yet supported" ) if any(len(op.wires) > 2 for op in tape.operations): diff --git a/pennylane/utils.py b/pennylane/utils.py index dee38af069e..bbea184fe00 100644 --- a/pennylane/utils.py +++ b/pennylane/utils.py @@ -175,26 +175,26 @@ def expand_vector(vector, original_wires, expanded_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) != (2**N,): - raise ValueError("Vector parameter must be of length 2**len(original_wires)") + if qml.math.shape(vector) != (qudit_order**N,): + raise ValueError(f"Vector parameter must be of length {qudit_order}**len(original_wires)") - dims = [2] * N + dims = [qudit_order] * N tensor = qml.math.reshape(vector, dims) if D > 0: - extra_dims = [2] * D - ones = qml.math.ones(2**D).reshape(extra_dims) + 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 = [] - for wire in original_wires: - wire_indices.append(expanded_wires.index(wire)) - + wire_indices = [expanded_wires.index(wire) for wire in original_wires] wire_indices = np.array(wire_indices) # Order tensor factors according to wires @@ -203,4 +203,4 @@ def expand_vector(vector, original_wires, expanded_wires): expanded_tensor, tuple(original_indices), tuple(wire_indices) ) - return qml.math.reshape(expanded_tensor, 2**M) + return qml.math.reshape(expanded_tensor, qudit_order**M) diff --git a/tests/circuit_graph/test_circuit_graph_hash.py b/tests/circuit_graph/test_circuit_graph_hash.py index d4269cf47ab..91e5f2e7a6a 100644 --- a/tests/circuit_graph/test_circuit_graph_hash.py +++ b/tests/circuit_graph/test_circuit_graph_hash.py @@ -23,21 +23,23 @@ from pennylane.wires import Wires +@pytest.mark.usefixtures("use_legacy_opmath") class TestCircuitGraphHash: """Test the creation of a hash on a CircuitGraph""" - numeric_queues = [ - ([qml.RX(0.3, wires=[0])], [], "RX!0.3![0]|||"), - ( - [ - qml.RX(0.3, wires=[0]), - qml.RX(0.4, wires=[1]), - qml.RX(0.5, wires=[2]), - ], - [], - "RX!0.3![0]RX!0.4![1]RX!0.5![2]|||", - ), - ] + with qml.operation.disable_new_opmath_cm(): + numeric_queues = [ + ([qml.RX(0.3, wires=[0])], [], "RX!0.3![0]|||"), + ( + [ + qml.RX(0.3, wires=[0]), + qml.RX(0.4, wires=[1]), + qml.RX(0.5, wires=[2]), + ], + [], + "RX!0.3![0]RX!0.4![1]RX!0.5![2]|||", + ), + ] @pytest.mark.parametrize("queue, observable_queue, expected_string", numeric_queues) def test_serialize_numeric_arguments(self, queue, observable_queue, expected_string): @@ -48,33 +50,38 @@ def test_serialize_numeric_arguments(self, queue, observable_queue, expected_str assert circuit_graph_1.serialize() == circuit_graph_2.serialize() assert expected_string == circuit_graph_1.serialize() - returntype1 = qml.expval - returntype2 = qml.var - - observable1 = qml.PauliZ(wires=[0]) - observable2 = qml.Hermitian(np.array([[1, 0], [0, -1]]), wires=[0]) - observable3 = Tensor(qml.PauliZ(0) @ qml.PauliZ(1)) - - numeric_observable_queue = [ - (returntype1, observable1, "|||ObservableReturnTypes.Expectation!PauliZ[0]"), - ( - returntype1, - observable2, - "|||ObservableReturnTypes.Expectation!Hermitian![[ 1 0]\n [ 0 -1]]![0]", - ), - ( - returntype1, - observable3, - "|||ObservableReturnTypes.Expectation!['PauliZ', 'PauliZ'][0, 1]", - ), - (returntype2, observable1, "|||ObservableReturnTypes.Variance!PauliZ[0]"), - ( - returntype2, - observable2, - "|||ObservableReturnTypes.Variance!Hermitian![[ 1 0]\n [ 0 -1]]![0]", - ), - (returntype2, observable3, "|||ObservableReturnTypes.Variance!['PauliZ', 'PauliZ'][0, 1]"), - ] + with qml.operation.disable_new_opmath_cm(): + returntype1 = qml.expval + returntype2 = qml.var + + observable1 = qml.PauliZ(wires=[0]) + observable2 = qml.Hermitian(np.array([[1, 0], [0, -1]]), wires=[0]) + observable3 = Tensor(qml.PauliZ(0) @ qml.PauliZ(1)) + + numeric_observable_queue = [ + (returntype1, observable1, "|||ObservableReturnTypes.Expectation!PauliZ[0]"), + ( + returntype1, + observable2, + "|||ObservableReturnTypes.Expectation!Hermitian![[ 1 0]\n [ 0 -1]]![0]", + ), + ( + returntype1, + observable3, + "|||ObservableReturnTypes.Expectation!['PauliZ', 'PauliZ'][0, 1]", + ), + (returntype2, observable1, "|||ObservableReturnTypes.Variance!PauliZ[0]"), + ( + returntype2, + observable2, + "|||ObservableReturnTypes.Variance!Hermitian![[ 1 0]\n [ 0 -1]]![0]", + ), + ( + returntype2, + observable3, + "|||ObservableReturnTypes.Variance!['PauliZ', 'PauliZ'][0, 1]", + ), + ] @pytest.mark.parametrize("obs, op, expected_string", numeric_observable_queue) def test_serialize_numeric_arguments_observables_expval_var(self, obs, op, expected_string): diff --git a/tests/conftest.py b/tests/conftest.py index cf99dc15238..824ecdadd66 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,6 +23,7 @@ import pennylane as qml from pennylane.devices import DefaultGaussian +from pennylane.operation import disable_new_opmath_cm, enable_new_opmath_cm # defaults TOL = 1e-3 @@ -173,6 +174,34 @@ def tear_down_thermitian(): qml.THermitian._eigs = {} +####################################################################### +# Fixtures for testing under new and old opmath + + +@pytest.fixture(scope="function") +def use_legacy_opmath(): + with disable_new_opmath_cm() as cm: + yield cm + + +# @pytest.fixture(scope="function") +# def use_legacy_opmath(): +# with disable_new_opmath_cm(): +# yield + + +@pytest.fixture(scope="function") +def use_new_opmath(): + with enable_new_opmath_cm() as cm: + yield cm + + +@pytest.fixture(params=[disable_new_opmath_cm, enable_new_opmath_cm], scope="function") +def use_legacy_and_new_opmath(request): + with request.param() as cm: + yield cm + + ####################################################################### try: diff --git a/tests/data/attributes/operator/test_operator.py b/tests/data/attributes/operator/test_operator.py index 62ac21ed417..e21a9be02e8 100644 --- a/tests/data/attributes/operator/test_operator.py +++ b/tests/data/attributes/operator/test_operator.py @@ -89,7 +89,7 @@ def test_value_init(self, obs_in): assert dset_op.info["py_type"] == get_type_str(type(obs_in)) obs_out = dset_op.get_value() - assert repr(obs_out) == repr(obs_in) + assert qml.equal(obs_out, obs_in) assert obs_in.compare(obs_out) def test_bind_init(self, obs_in): @@ -103,10 +103,49 @@ def test_bind_init(self, obs_in): assert dset_op.info["py_type"] == get_type_str(type(obs_in)) obs_out = dset_op.get_value() - assert repr(obs_out) == repr(obs_in) + assert qml.equal(obs_out, obs_in) assert obs_in.compare(obs_out) +@pytest.mark.parametrize( + "obs_in", + [ + qml.ops.LinearCombination([1.0, 2.0], [qml.X(0) @ qml.Z(1), qml.Y(1) @ qml.Z(2)]), + qml.ops.sum(qml.X(0), qml.Y(0)), + qml.ops.sum(qml.X(0) @ qml.Z(1), 3 * qml.Y(2)), + qml.ops.prod(qml.X(0), qml.Y(1)), + qml.ops.s_prod(1.2j, qml.X(1) @ qml.Y(2)), + ], +) +class TestDatasetArithmeticOperators: + """Tests serializing Observable operators using the ``qml.equal()`` method.""" + + def test_value_init(self, obs_in): + """Test that a DatasetOperator can be value-initialized + from an observable, and that the deserialized operator + is equivalent.""" + dset_op = DatasetOperator(obs_in) + + assert dset_op.info["type_id"] == "operator" + assert dset_op.info["py_type"] == get_type_str(type(obs_in)) + + obs_out = dset_op.get_value() + assert qml.equal(obs_out, obs_in) + + def test_bind_init(self, obs_in): + """Test that DatasetOperator can be initialized from a HDF5 group + that contains an operator attribute.""" + bind = DatasetOperator(obs_in).bind + + dset_op = DatasetOperator(bind=bind) + + assert dset_op.info["type_id"] == "operator" + assert dset_op.info["py_type"] == get_type_str(type(obs_in)) + + obs_out = dset_op.get_value() + assert qml.equal(obs_out, obs_in) + + class TestDatasetOperator: @pytest.mark.parametrize( "op_in", diff --git a/tests/devices/default_qubit/test_default_qubit_preprocessing.py b/tests/devices/default_qubit/test_default_qubit_preprocessing.py index cf7e1f7524a..1c67fbd0b11 100644 --- a/tests/devices/default_qubit/test_default_qubit_preprocessing.py +++ b/tests/devices/default_qubit/test_default_qubit_preprocessing.py @@ -510,9 +510,8 @@ def test_preprocess_check_validity_fail(self): with pytest.raises(qml.DeviceError, match="Operator NoMatNoDecompOp"): program(tapes) - @pytest.mark.parametrize( - "ops, measurement, message", - [ + with qml.operation.disable_new_opmath_cm(): + invalid_tape_adjoint_test_cases = [ ( [qml.RX(0.1, wires=0)], [qml.probs(op=qml.PauliX(0))], @@ -523,6 +522,31 @@ def test_preprocess_check_validity_fail(self): [qml.expval(qml.Hamiltonian([1], [qml.PauliZ(0)]))], "not supported on adjoint", ), + ] + + @pytest.mark.usefixtures("use_legacy_opmath") + @pytest.mark.parametrize( + "ops, measurement, message", + invalid_tape_adjoint_test_cases, + ) + @pytest.mark.filterwarnings("ignore:Differentiating with respect to") + def test_preprocess_invalid_tape_adjoint_legacy_opmath(self, ops, measurement, message): + """Test that preprocessing fails if adjoint differentiation is requested and an + invalid tape is used""" + qs = qml.tape.QuantumScript(ops, measurement) + execution_config = qml.devices.ExecutionConfig(gradient_method="adjoint") + program, _ = qml.device("default.qubit").preprocess(execution_config) + with pytest.raises(qml.DeviceError, match=message): + program([qs]) + + @pytest.mark.parametrize( + "ops, measurement, message", + [ + ( + [qml.RX(0.1, wires=0)], + [qml.probs(op=qml.PauliX(0))], + "adjoint diff supports either all expectation values or", + ) ], ) @pytest.mark.filterwarnings("ignore:Differentiating with respect to") @@ -790,7 +814,10 @@ def test_u3_non_trainable_params(self): assert len(res.operations) == 5 assert res.trainable_params == [0, 1, 2, 3, 4] - def test_unsupported_obs(self): + @pytest.mark.usefixtures( + "use_legacy_opmath" + ) # this is only an issue for legacy Hamiltonian that does not define a matrix method + def test_unsupported_obs_legacy_opmath(self): """Test that the correct error is raised if a Hamiltonian measurement is differentiated""" obs = qml.Hamiltonian([2, 0.5], [qml.PauliZ(0), qml.PauliY(1)]) qs = qml.tape.QuantumScript([qml.RX(0.5, wires=1)], [qml.expval(obs)]) diff --git a/tests/devices/default_qubit/test_default_qubit_tracking.py b/tests/devices/default_qubit/test_default_qubit_tracking.py index d2efce1e15c..3975a42145c 100644 --- a/tests/devices/default_qubit/test_default_qubit_tracking.py +++ b/tests/devices/default_qubit/test_default_qubit_tracking.py @@ -194,7 +194,6 @@ def circuit_3(y): shot_testing_combos = [ # expval combinations ([qml.expval(qml.PauliX(0))], 1, 10), - ([qml.expval(qml.PauliX(0)), qml.expval(qml.PauliX(0) @ qml.PauliY(1))], 1, 10), ([qml.expval(qml.PauliX(0)), qml.expval(qml.PauliY(0))], 2, 20), # Hamiltonian test cases ([qml.expval(qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.PauliX(1)]))], 2, 20), @@ -251,3 +250,39 @@ def test_single_expval(mps, expected_exec, expected_shots): assert dev.tracker.totals["executions"] == 3 * expected_exec assert dev.tracker.totals["simulations"] == 1 assert dev.tracker.totals["shots"] == 3 * expected_shots + + +@pytest.mark.xfail # TODO Prod instances are not automatically +def test_multiple_expval_with_prods(): + mps, expected_exec, expected_shots = ( + [qml.expval(qml.PauliX(0)), qml.expval(qml.PauliX(0) @ qml.PauliY(1))], + 1, + 10, + ) + dev = qml.device("default.qubit") + tape = qml.tape.QuantumScript([], mps, shots=10) + + with dev.tracker: + dev.execute(tape) + + assert dev.tracker.totals["executions"] == expected_exec + assert dev.tracker.totals["simulations"] == 1 + assert dev.tracker.totals["shots"] == expected_shots + + +@pytest.mark.usefixtures("use_legacy_opmath") +def test_multiple_expval_with_Tensors_legacy_opmath(): + mps, expected_exec, expected_shots = ( + [qml.expval(qml.PauliX(0)), qml.expval(qml.operation.Tensor(qml.PauliX(0), qml.PauliY(1)))], + 1, + 10, + ) + dev = qml.device("default.qubit") + tape = qml.tape.QuantumScript([], mps, shots=10) + + with dev.tracker: + dev.execute(tape) + + assert dev.tracker.totals["executions"] == expected_exec + assert dev.tracker.totals["simulations"] == 1 + assert dev.tracker.totals["shots"] == expected_shots diff --git a/tests/devices/qutrit_mixed/test_qutrit_mixed_sampling.py b/tests/devices/qutrit_mixed/test_qutrit_mixed_sampling.py index 4f15954fa5d..00e98c5de5a 100644 --- a/tests/devices/qutrit_mixed/test_qutrit_mixed_sampling.py +++ b/tests/devices/qutrit_mixed/test_qutrit_mixed_sampling.py @@ -29,7 +29,7 @@ from pennylane.measurements import Shots -APPROX_ATOL = 0.01 +APPROX_ATOL = 0.05 QUDIT_DIM = 3 ONE_QUTRIT = 1 TWO_QUTRITS = 2 @@ -365,7 +365,7 @@ def test_sample_observables(self): qml.sample(qml.GellMann(0, 1) @ qml.GellMann(1, 1)), state, shots=shots ) assert results_gel_1s.shape == (shots.total_shots,) - assert results_gel_1s.dtype == np.int64 + assert results_gel_1s.dtype == np.float64 assert sorted(np.unique(results_gel_1s)) == [-1, 0, 1] @flaky @@ -652,10 +652,14 @@ class TestHamiltonianSamples: """Test that the measure_with_samples function works as expected for Hamiltonian and Sum observables""" + @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_hamiltonian_expval(self, obs): """Test that sampling works well for Hamiltonian and Sum observables""" - shots = qml.measurements.Shots(10000) + if not qml.operation.active_new_opmath(): + obs = qml.operation.convert_to_legacy_H(obs) + + shots = qml.measurements.Shots(10000) x, y = np.array(0.67), np.array(0.95) ops = [qml.TRY(x, wires=0), qml.TRZ(y, wires=0)] state = create_initial_state((0,)) @@ -668,10 +672,14 @@ def test_hamiltonian_expval(self, obs): assert isinstance(res, np.float64) assert np.allclose(res, expected, atol=APPROX_ATOL) + @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_hamiltonian_expval_shot_vector(self, obs): """Test that sampling works well for Hamiltonian and Sum observables with a shot vector""" - shots = qml.measurements.Shots((10000, 100000)) + if not qml.operation.active_new_opmath(): + obs = qml.operation.convert_to_legacy_H(obs) + + shots = qml.measurements.Shots((10000, 100000)) x, y = np.array(0.67), np.array(0.95) ops = [qml.TRY(x, wires=0), qml.TRZ(y, wires=0)] state = create_initial_state((0,)) diff --git a/tests/devices/test_default_clifford.py b/tests/devices/test_default_clifford.py index a09fade867d..9ceb1fdb492 100644 --- a/tests/devices/test_default_clifford.py +++ b/tests/devices/test_default_clifford.py @@ -254,6 +254,7 @@ def circuit_fn(): assert qml.math.shape(samples[2]) == (shots,) +@pytest.mark.usefixtures("use_legacy_and_new_opmath") @pytest.mark.parametrize("tableau", [True, False]) @pytest.mark.parametrize("shots", [None, 50000]) @pytest.mark.parametrize( @@ -284,7 +285,7 @@ def circuit_fn(): gotten_probs, target_probs = qnode_clfrd(), qnode_qubit() - assert qml.math.allclose(gotten_probs, target_probs, atol=1e-2 if shots else 1e-8) + assert qml.math.allclose(gotten_probs, target_probs, atol=5e-2 if shots else 1e-8) def test_meas_probs_large(): diff --git a/tests/devices/test_default_qubit_jax.py b/tests/devices/test_default_qubit_jax.py index b90b67a0baa..9df88d9a12f 100644 --- a/tests/devices/test_default_qubit_jax.py +++ b/tests/devices/test_default_qubit_jax.py @@ -1059,7 +1059,22 @@ def circuit(): # evaluated one expval altogether assert spy.call_count == 1 - def test_direct_eval_hamiltonian_broadcasted_error_jax(self): + def test_direct_eval_linear_combination_broadcasted_jax(self): + """Tests that the correct result is returned when attempting to evaluate a Hamiltonian with + broadcasting and shots=None directly via its sparse representation with Jax.""" + dev = qml.device("default.qubit.jax", wires=2) + H = qml.ops.LinearCombination(jnp.array([0.1, 0.2]), [qml.PauliX(0), qml.PauliZ(1)]) + + @qml.qnode(dev, diff_method="backprop", interface="jax") + def circuit(): + qml.RX(jnp.zeros(5), 0) + return qml.expval(H) + + res = circuit() + assert qml.math.allclose(res, 0.2) + + @pytest.mark.usefixtures("use_legacy_opmath") + def test_direct_eval_hamiltonian_broadcasted_error_jax_legacy_opmath(self): """Tests that an error is raised when attempting to evaluate a Hamiltonian with broadcasting and shots=None directly via its sparse representation with Jax.""" dev = qml.device("default.qubit.jax", wires=2) diff --git a/tests/devices/test_default_qubit_legacy.py b/tests/devices/test_default_qubit_legacy.py index 49163d8bde6..d6c067400ad 100644 --- a/tests/devices/test_default_qubit_legacy.py +++ b/tests/devices/test_default_qubit_legacy.py @@ -2377,7 +2377,8 @@ def circuit(): # evaluated one expval per Pauli observable assert spy.call_count == 2 - def test_error_hamiltonian_expval_finite_shots(self): + @pytest.mark.usefixtures("use_legacy_opmath") # only a problem for legacy opmath + def test_error_hamiltonian_expval_finite_shots_legacy_opmath(self): """Tests that the Hamiltonian is split for finite shots.""" dev = qml.device("default.qubit.legacy", wires=2, shots=10) H = qml.Hamiltonian([0.1, 0.2], [qml.PauliX(0), qml.PauliZ(1)]) @@ -2385,7 +2386,8 @@ def test_error_hamiltonian_expval_finite_shots(self): with pytest.raises(AssertionError, match="Hamiltonian must be used with shots=None"): dev.expval(H) - def test_error_hamiltonian_expval_wrong_wires(self): + @pytest.mark.usefixtures("use_legacy_opmath") + def test_error_hamiltonian_expval_wrong_wires_legacy_opmath(self): """Tests that expval fails if Hamiltonian uses non-device wires.""" dev = qml.device("default.qubit.legacy", wires=2, shots=None) H = qml.Hamiltonian([0.1, 0.2, 0.3], [qml.PauliX(0), qml.PauliZ(1), qml.PauliY(2)]) diff --git a/tests/devices/test_default_qubit_tf.py b/tests/devices/test_default_qubit_tf.py index 5fbfcb1820d..ac81f6cf36c 100644 --- a/tests/devices/test_default_qubit_tf.py +++ b/tests/devices/test_default_qubit_tf.py @@ -927,7 +927,22 @@ def test_three_qubit_no_parameters_broadcasted(self, broadcasted_init_state, op, expected = np.einsum("ij,lj->li", mat, state) assert np.allclose(res, expected, atol=tol, rtol=0) - def test_direct_eval_hamiltonian_broadcasted_error_tf(self): + def test_direct_eval_hamiltonian_broadcasted_tf(self): + """Tests that the correct result is returned when attempting to evaluate a Hamiltonian with + broadcasting and shots=None directly via its sparse representation with TF.""" + dev = qml.device("default.qubit.tf", wires=2) + ham = qml.Hamiltonian(tf.Variable([0.1, 0.2]), [qml.PauliX(0), qml.PauliZ(1)]) + + @qml.qnode(dev, diff_method="backprop", interface="tf") + def circuit(): + qml.RX(np.zeros(5), 0) # Broadcast the state by applying a broadcasted identity + return qml.expval(ham) + + res = circuit() + assert qml.math.allclose(res, 0.2) + + @pytest.mark.usefixtures("use_legacy_opmath") + def test_direct_eval_hamiltonian_broadcasted_error_tf_legacy_opmath(self): """Tests that an error is raised when attempting to evaluate a Hamiltonian with broadcasting and shots=None directly via its sparse representation with TF.""" dev = qml.device("default.qubit.tf", wires=2) diff --git a/tests/devices/test_default_qubit_torch.py b/tests/devices/test_default_qubit_torch.py index 52f80820318..0f8f5200e42 100644 --- a/tests/devices/test_default_qubit_torch.py +++ b/tests/devices/test_default_qubit_torch.py @@ -914,7 +914,27 @@ def test_three_qubit_no_parameters_broadcasted( expected = qml.math.einsum("ij,lj->li", op_mat, state) assert torch.allclose(res, expected, atol=tol, rtol=0) - def test_direct_eval_hamiltonian_broadcasted_error_torch(self, device, torch_device, mocker): + def test_direct_eval_hamiltonian_broadcasted_torch(self, device, torch_device, mocker): + """Tests that the correct result is returned when attempting to evaluate a Hamiltonian with + broadcasting and shots=None directly via its sparse representation with torch.""" + + dev = device(wires=2, torch_device=torch_device) + ham = qml.Hamiltonian( + torch.tensor([0.1, 0.2], requires_grad=True), [qml.PauliX(0), qml.PauliZ(1)] + ) + + @qml.qnode(dev, diff_method="backprop", interface="torch") + def circuit(): + qml.RX(np.zeros(5), 0) # Broadcast the state by applying a broadcasted identity + return qml.expval(ham) + + res = circuit() + assert qml.math.allclose(res, 0.2) + + @pytest.mark.usefixtures("use_legacy_opmath") + def test_direct_eval_hamiltonian_broadcasted_error_torch_legacy_opmath( + self, device, torch_device, mocker + ): """Tests that an error is raised when attempting to evaluate a Hamiltonian with broadcasting and shots=None directly via its sparse representation with torch.""" diff --git a/tests/devices/test_preprocess.py b/tests/devices/test_preprocess.py index b593e64c3d1..d02f57df595 100644 --- a/tests/devices/test_preprocess.py +++ b/tests/devices/test_preprocess.py @@ -230,7 +230,8 @@ def test_invalid_tensor_observable(self): with pytest.raises(DeviceError, match="not supported on device"): validate_observables(tape, lambda obj: obj.name == "PauliX") - def test_valid_tensor_observable(self): + @pytest.mark.usefixtures("use_legacy_opmath") # only required for legacy observables + def test_valid_tensor_observable_legacy_opmath(self): """Test that a valid tensor ovservable passes without error.""" tape = QuantumScript([], [qml.expval(qml.PauliZ(0) @ qml.PauliY(1))]) assert ( diff --git a/tests/drawer/test_tape_text.py b/tests/drawer/test_tape_text.py index c4d644a691f..806c8594bd8 100644 --- a/tests/drawer/test_tape_text.py +++ b/tests/drawer/test_tape_text.py @@ -576,12 +576,15 @@ def test_setting_max_length(self, ml): (qml.probs(op=qml.PauliZ(0)), "0: ──── Probs[Z]"), (qml.sample(wires=0), "0: ──── Sample"), (qml.sample(op=qml.PauliX(0)), "0: ──── Sample[X]"), - (qml.expval(0.1 * qml.PauliX(0) @ qml.PauliY(1)), "0: ──── β•­<𝓗(0.10)>\n1: ──── β•°<𝓗(0.10)>"), + ( + qml.expval(0.1 * qml.PauliX(0) @ qml.PauliY(1)), + "0: ──── β•­<(0.10*X)@Y>\n1: ──── β•°<(0.10*X)@Y>", + ), ( qml.expval( 0.1 * qml.PauliX(0) + 0.2 * qml.PauliY(1) + 0.3 * qml.PauliZ(0) + 0.4 * qml.PauliZ(1) ), - "0: ──── β•­<𝓗>\n1: ──── β•°<𝓗>", + "0: ──── β•­<(((0.10*X)+(0.20*Y))+(0.30*Z))+(0.40*Z)>\n1: ──── β•°<(((0.10*X)+(0.20*Y))+(0.30*Z))+(0.40*Z)>", ), # Operations (both regular and controlled) and nested multi-valued controls (qml.ctrl(qml.PauliX(wires=2), control=[0, 1]), "0: ─╭●── \n1: β”€β”œβ—β”€β”€ \n2: ─╰X── "), diff --git a/tests/gradients/core/test_adjoint_diff.py b/tests/gradients/core/test_adjoint_diff.py index 65e726303fa..2cbc0c63216 100644 --- a/tests/gradients/core/test_adjoint_diff.py +++ b/tests/gradients/core/test_adjoint_diff.py @@ -54,7 +54,8 @@ def test_finite_shots_warns(self): ): dev.adjoint_jacobian(tape) - def test_hamiltonian_error(self, dev): + @pytest.mark.usefixtures("use_legacy_opmath") + def test_hamiltonian_error_legacy_opmath(self, dev): """Test that error is raised for qml.Hamiltonian""" with qml.queuing.AnnotatedQueue() as q: @@ -72,6 +73,24 @@ def test_hamiltonian_error(self, dev): ): dev.adjoint_jacobian(tape) + def test_linear_combination_adjoint_warning(self, dev): + """Test that error is raised for qml.Hamiltonian""" + + with qml.queuing.AnnotatedQueue() as q: + qml.expval( + qml.ops.LinearCombination( + [np.array(-0.05), np.array(0.17)], + [qml.PauliX(0), qml.PauliZ(0)], + ) + ) + + tape = qml.tape.QuantumScript.from_queue(q) + with pytest.warns( + UserWarning, + match="Differentiating with respect to the input parameters of LinearCombination", + ): + dev.adjoint_jacobian(tape) + def test_unsupported_op(self, dev): """Test if a QuantumFunctionError is raised for an unsupported operation, i.e., multi-parameter operations that are not qml.Rot""" diff --git a/tests/gradients/core/test_hadamard_gradient.py b/tests/gradients/core/test_hadamard_gradient.py index d8bb7a882ef..e04c6abead2 100644 --- a/tests/gradients/core/test_hadamard_gradient.py +++ b/tests/gradients/core/test_hadamard_gradient.py @@ -779,7 +779,7 @@ def circuit(weights): qml.gradients.hadamard_grad(circuit)(weights) @pytest.mark.autograd - def test_no_trainable_params_qnode_autograd_legacy(self): + def test_no_trainable_params_qnode_autograd_legacy_opmath(self): """Test that the correct ouput and warning is generated in the absence of any trainable parameters""" dev = qml.device("default.qubit.autograd", wires=2) @@ -795,7 +795,7 @@ def circuit(weights): qml.gradients.hadamard_grad(circuit)(weights) @pytest.mark.torch - def test_no_trainable_params_qnode_torch_legacy(self): + def test_no_trainable_params_qnode_torch_legacy_opmath(self): """Test that the correct ouput and warning is generated in the absence of any trainable parameters""" dev = qml.device("default.qubit.torch", wires=2) @@ -811,7 +811,7 @@ def circuit(weights): qml.gradients.hadamard_grad(circuit)(weights) @pytest.mark.tf - def test_no_trainable_params_qnode_tf_legacy(self): + def test_no_trainable_params_qnode_tf_legacy_opmath(self): """Test that the correct ouput and warning is generated in the absence of any trainable parameters""" dev = qml.device("default.qubit.tf", wires=2) @@ -827,7 +827,7 @@ def circuit(weights): qml.gradients.hadamard_grad(circuit)(weights) @pytest.mark.jax - def test_no_trainable_params_qnode_jax_legacy(self): + def test_no_trainable_params_qnode_jax_legacy_opmath(self): """Test that the correct ouput and warning is generated in the absence of any trainable parameters""" dev = qml.device("default.qubit.jax", wires=2) diff --git a/tests/gradients/core/test_pulse_gradient.py b/tests/gradients/core/test_pulse_gradient.py index 73824e3ecce..7518ebcadbb 100644 --- a/tests/gradients/core/test_pulse_gradient.py +++ b/tests/gradients/core/test_pulse_gradient.py @@ -181,7 +181,8 @@ def test_with_general_ob(self, ham, params, time, ob): # Check that the inserted exponential is correct assert qml.equal(qml.exp(qml.dot([-1j * exp_shift], [ob])), _ops[1]) - def test_warnings(self): + @pytest.mark.usefixtures("use_legacy_opmath") # this is only an issue with legacy Hamiltonian + def test_warnings_legacy_opmath(self): """Test that a warning is raised for computing eigenvalues of a Hamiltonian for more than four wires but not for fewer wires.""" import jax diff --git a/tests/gradients/core/test_pulse_odegen.py b/tests/gradients/core/test_pulse_odegen.py index bc2d51d6e8e..f13fddefff4 100644 --- a/tests/gradients/core/test_pulse_odegen.py +++ b/tests/gradients/core/test_pulse_odegen.py @@ -14,7 +14,7 @@ """ Tests for the gradients.pulse_odegen module. """ -# pylint:disable=import-outside-toplevel +# pylint:disable=import-outside-toplevel, use-implicit-booleaness-not-comparison import copy import pytest @@ -111,7 +111,7 @@ def test_with_commuting_const_terms_ham(self, terms, t): jax.config.update("jax_enable_x64", True) num_terms = len(terms) - H = qml.math.dot([qml.pulse.constant for _ in range(num_terms)], terms) + H = qml.dot([qml.pulse.constant for _ in range(num_terms)], terms) params = [jnp.array(0.4), jnp.array(0.9), jnp.array(-0.5)][:num_terms] T = t[1] - t[0] @@ -161,7 +161,7 @@ def manual_matrix(params): return jax.scipy.linalg.expm(-1j * T * exp) num_terms = len(terms) - H = qml.math.dot([qml.pulse.constant for _ in range(num_terms)], terms) + H = qml.dot([qml.pulse.constant for _ in range(num_terms)], terms) params = [jnp.array(0.4), jnp.array(0.9), jnp.array(-0.5), jnp.array(0.28)][:num_terms] T = t[1] - t[0] @@ -236,7 +236,7 @@ def test_with_commuting_timedep_terms_ham(self, terms, t): jax.config.update("jax_enable_x64", True) num_terms = len(terms) - H = qml.math.dot([jnp.polyval for _ in range(num_terms)], terms) + H = qml.dot([jnp.polyval for _ in range(num_terms)], terms) params = [jnp.array([0.4, 0.1, 0.2]), jnp.array([0.9, -0.2, 0.5]), jnp.array([-0.5, 0.2])] params = params[:num_terms] # Jacobian functions of the effective rotation parameter (all polyval) @@ -286,7 +286,7 @@ def test_with_noncommuting_timedep_terms_ham(self, terms, t): jax.config.update("jax_enable_x64", True) num_terms = len(terms) - H = qml.math.dot([jnp.polyval for _ in range(num_terms)], terms) + H = qml.dot([jnp.polyval for _ in range(num_terms)], terms) mats = [expand_matrix(term.matrix(), term.wires, H.wires) for term in terms] t = jnp.array(t) diff --git a/tests/gradients/parameter_shift/test_parameter_shift_shot_vec.py b/tests/gradients/parameter_shift/test_parameter_shift_shot_vec.py index d0b03af34d4..34c731cebc9 100644 --- a/tests/gradients/parameter_shift/test_parameter_shift_shot_vec.py +++ b/tests/gradients/parameter_shift/test_parameter_shift_shot_vec.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """Tests for the gradients.parameter_shift module using the new return types and devices that define a shot vector.""" +# pylint:disable=use-implicit-booleaness-not-comparison from functools import partial import pytest from flaky import flaky @@ -2028,7 +2029,7 @@ def test_not_expval_error(self, broadcast): qml.CNOT(wires=[0, 1]) obs = [qml.PauliZ(0), qml.PauliZ(0) @ qml.PauliX(1), qml.PauliY(0)] coeffs = np.array([0.1, 0.2, 0.3]) - H = np.dot(obs, coeffs) + H = qml.dot(coeffs, obs) qml.var(H) tape = qml.tape.QuantumScript.from_queue(q, shots=shot_vec) diff --git a/tests/interfaces/test_autograd.py b/tests/interfaces/test_autograd.py index f914c902a52..94b5ce5392f 100644 --- a/tests/interfaces/test_autograd.py +++ b/tests/interfaces/test_autograd.py @@ -200,8 +200,8 @@ def test_no_batch_transform(self, mocker): tape = qml.tape.QuantumScript.from_queue(q) spy = mocker.spy(dev, "batch_transform") - with pytest.raises(AssertionError, match="Hamiltonian must be used with shots=None"): - qml.execute([tape], dev, None, device_batch_transform=False) + res = qml.execute([tape], dev, None, device_batch_transform=False) + assert np.allclose(res[0], np.cos(y), atol=0.1) spy.assert_not_called() diff --git a/tests/measurements/legacy/test_classical_shadow_legacy.py b/tests/measurements/legacy/test_classical_shadow_legacy.py index 7066f26f068..506a28a7d2e 100644 --- a/tests/measurements/legacy/test_classical_shadow_legacy.py +++ b/tests/measurements/legacy/test_classical_shadow_legacy.py @@ -420,7 +420,7 @@ def test_non_pauli_error(self): """Test that an error is raised when a non-Pauli observable is passed""" circuit = hadamard_circuit(3) - msg = "Observable must be a linear combination of Pauli observables" + msg = "Observable must have a valid pauli representation." with pytest.raises(ValueError, match=msg): circuit(qml.Hadamard(0) @ qml.Hadamard(2)) diff --git a/tests/measurements/test_classical_shadow.py b/tests/measurements/test_classical_shadow.py index b156c9ea5b6..45384c43267 100644 --- a/tests/measurements/test_classical_shadow.py +++ b/tests/measurements/test_classical_shadow.py @@ -573,7 +573,7 @@ def test_non_pauli_error(self): """Test that an error is raised when a non-Pauli observable is passed""" circuit = hadamard_circuit(3) - msg = "Observable must be a linear combination of Pauli observables" + msg = "Observable must have a valid pauli representation." with pytest.raises(ValueError, match=msg): circuit(qml.Hadamard(0) @ qml.Hadamard(2)) diff --git a/tests/measurements/test_sample.py b/tests/measurements/test_sample.py index fcb1c8430ab..89ed97c3ef7 100644 --- a/tests/measurements/test_sample.py +++ b/tests/measurements/test_sample.py @@ -320,38 +320,43 @@ def circuit(): circuit() @pytest.mark.parametrize( - "obs,exp", + "obs", [ # Single observables - (None, int), # comp basis samples - (qml.PauliX(0), int), - (qml.PauliY(0), int), - (qml.PauliZ(0), int), - (qml.Hadamard(0), int), - (qml.Identity(0), int), - (qml.Hermitian(np.diag([1, 2]), 0), float), - (qml.Hermitian(np.diag([1.0, 2.0]), 0), float), + (None), # comp basis samples, expected to be int + (qml.PauliX(0)), + (qml.PauliY(0)), + (qml.PauliZ(0)), + (qml.Hadamard(0)), + # (qml.Identity(0)), + (qml.Hermitian(np.diag([1, 2]), 0)), + (qml.Hermitian(np.diag([1.0, 2.0]), 0)), # Tensor product observables ( qml.PauliX("c") @ qml.PauliY("a") @ qml.PauliZ(1) @ qml.Hadamard("wire1") - @ qml.Identity("b"), - int, - ), - (qml.Projector([0, 1], wires=[0, 1]) @ qml.PauliZ(2), float), - (qml.Hermitian(np.array(np.eye(2)), wires=[0]) @ qml.PauliZ(2), float), - ( - qml.Projector([0, 1], wires=[0, 1]) @ qml.Hermitian(np.array(np.eye(2)), wires=[2]), - float, + @ qml.Identity("b") ), + (qml.Projector([0, 1], wires=[0, 1]) @ qml.PauliZ(2)), + (qml.Hermitian(np.array(np.eye(2)), wires=[0]) @ qml.PauliZ(2)), + (qml.Projector([0, 1], wires=[0, 1]) @ qml.Hermitian(np.array(np.eye(2)), wires=[2])), ], ) - def test_numeric_type(self, obs, exp): + def test_numeric_type(self, obs): """Test that the numeric type is correct.""" + eigval_type = type(obs.eigvals()[0]) if obs is not None else np.int64 + res = qml.sample(obs) if obs is not None else qml.sample() - assert res.numeric_type is exp + if res.numeric_type == int: + expected_type = np.int64 + elif res.numeric_type == float: + expected_type = np.float64 + elif res.numeric_type == complex: + expected_type = np.complex64 + + assert expected_type == eigval_type def test_shape_no_shots_error(self): """Test that the appropriate error is raised with no shots are specified""" diff --git a/tests/ops/functions/conftest.py b/tests/ops/functions/conftest.py index 8b881947847..b8aa17ebc82 100644 --- a/tests/ops/functions/conftest.py +++ b/tests/ops/functions/conftest.py @@ -43,7 +43,8 @@ qml.adjoint(qml.PauliX(0)), qml.adjoint(qml.RX(1.1, 0)), Tensor(qml.PauliX(0), qml.PauliX(1)), - qml.Hamiltonian([1.1, 2.2], [qml.PauliX(0), qml.PauliZ(0)]), + qml.operation.convert_to_legacy_H(qml.Hamiltonian([1.1, 2.2], [qml.PauliX(0), qml.PauliZ(0)])), + qml.ops.LinearCombination([1.1, 2.2], [qml.PauliX(0), qml.PauliZ(0)]), qml.s_prod(1.1, qml.RX(1.1, 0)), qml.prod(qml.PauliX(0), qml.PauliY(1), qml.PauliZ(0)), qml.ctrl(qml.RX(1.1, 0), 1), diff --git a/tests/ops/functions/test_assert_valid.py b/tests/ops/functions/test_assert_valid.py index 7a72910979a..d083762552a 100644 --- a/tests/ops/functions/test_assert_valid.py +++ b/tests/ops/functions/test_assert_valid.py @@ -373,7 +373,11 @@ def test_generated_list_of_ops(class_to_validate): def test_explicit_list_of_ops(valid_instance): """Test the validity of operators that could not be auto-generated.""" - assert_valid(valid_instance) + if valid_instance.name == "Hamiltonian": + with qml.operation.disable_new_opmath_cm(): + assert_valid(valid_instance) + else: + assert_valid(valid_instance) def test_explicit_list_of_failing_ops(invalid_instance_and_error): diff --git a/tests/ops/functions/test_dot.py b/tests/ops/functions/test_dot.py index 6cc0b783f16..61ea6ec46dd 100644 --- a/tests/ops/functions/test_dot.py +++ b/tests/ops/functions/test_dot.py @@ -17,7 +17,7 @@ import pytest import pennylane as qml -from pennylane.ops import Hamiltonian, Prod, SProd, Sum +from pennylane.ops import Prod, SProd, Sum from pennylane.pauli.pauli_arithmetic import PauliWord, PauliSentence, I, X, Y, Z pw1 = PauliWord({0: I, 1: X, 2: Y}) @@ -307,7 +307,7 @@ def test_dot_returns_hamiltonian_simplified(self): to the simplified hamiltonian.""" ps = qml.dot(coeffs0, ops0, pauli=True) h_ps = ps.hamiltonian() - h = Hamiltonian(coeffs0, ops0) + h = qml.Hamiltonian(coeffs0, ops0) h.simplify() assert qml.equal(h_ps, h) diff --git a/tests/ops/functions/test_eigvals.py b/tests/ops/functions/test_eigvals.py index 7397c5bc070..49612c9688d 100644 --- a/tests/ops/functions/test_eigvals.py +++ b/tests/ops/functions/test_eigvals.py @@ -117,16 +117,33 @@ def test_ctrl(self): expected = np.linalg.eigvals(qml.matrix(qml.CNOT(wires=[0, 1]))) assert np.allclose(np.sort(res), np.sort(expected)) - def test_tensor_product(self): + @pytest.mark.usefixtures("use_legacy_opmath") + def test_tensor_product_legacy_opmath(self): """Test a tensor product""" res = qml.eigvals(qml.PauliX(0) @ qml.Identity(1) @ qml.PauliZ(1)) expected = reduce(np.kron, [[1, -1], [1, 1], [1, -1]]) assert np.allclose(res, expected) + def test_tensor_product(self): + """Test a tensor product""" + res = qml.eigvals(qml.prod(qml.PauliX(0), qml.Identity(1), qml.PauliZ(1), lazy=False)) + expected = [1.0, -1.0, -1.0, 1.0] + assert np.allclose(res, expected) + def test_hamiltonian(self): """Test that the matrix of a Hamiltonian is correctly returned""" ham = qml.PauliZ(0) @ qml.PauliY(1) - 0.5 * qml.PauliX(1) + res = qml.eigvals(ham) + + expected = np.linalg.eigvalsh(reduce(np.kron, [Z, Y]) - 0.5 * reduce(np.kron, [I, X])) + assert np.allclose(res, expected) + + @pytest.mark.usefixtures("use_legacy_opmath") + def test_hamiltonian_legacy_opmath(self): + """Test that the matrix of a Hamiltonian is correctly returned""" + ham = qml.PauliZ(0) @ qml.PauliY(1) - 0.5 * qml.PauliX(1) + with pytest.warns(UserWarning, match="the eigenvalues will be computed numerically"): res = qml.eigvals(ham) diff --git a/tests/ops/functions/test_equal.py b/tests/ops/functions/test_equal.py index 0fb184dfe57..2fbe6107400 100644 --- a/tests/ops/functions/test_equal.py +++ b/tests/ops/functions/test_equal.py @@ -113,7 +113,6 @@ ) ) - PARAMETRIZED_MEASUREMENTS = [ qml.sample(qml.PauliY(0)), qml.sample(wires=0), @@ -190,7 +189,6 @@ ) ) - equal_hamiltonians = [ ( qml.Hamiltonian([1, 1], [qml.PauliX(0) @ qml.Identity(1), qml.PauliZ(0)]), @@ -1310,6 +1308,7 @@ def test_mv_arithmetic_as_op(self, mp_fn): assert not qml.equal(mp1, mp4) +@pytest.mark.usefixtures("use_legacy_opmath") # TODO update qml.equal with new opmath class TestObservablesComparisons: """Tests comparisons between Hamiltonians, Tensors and PauliX/Y/Z operators""" @@ -1330,6 +1329,10 @@ def test_identity_equal(self): @pytest.mark.parametrize(("H1", "H2", "res"), equal_hamiltonians) def test_hamiltonian_equal(self, H1, H2, res): """Tests that equality can be checked between Hamiltonians""" + if not qml.operation.active_new_opmath(): + H1 = qml.operation.convert_to_legacy_H(H1) + H2 = qml.operation.convert_to_legacy_H(H2) + assert qml.equal(H1, H2) == qml.equal(H2, H1) assert qml.equal(H1, H2) == res @@ -1342,12 +1345,20 @@ def test_tensors_equal(self, T1, T2, res): @pytest.mark.parametrize(("H", "T", "res"), equal_hamiltonians_and_tensors) def test_hamiltonians_and_tensors_equal(self, H, T, res): """Tests that equality can be checked between a Hamiltonian and a Tensor""" + if not qml.operation.active_new_opmath(): + H = qml.operation.convert_to_legacy_H(H) + T = qml.operation.Tensor(*T.operands) + assert qml.equal(H, T) == qml.equal(T, H) assert qml.equal(H, T) == res @pytest.mark.parametrize(("op1", "op2", "res"), equal_pauli_operators) def test_pauli_operator_equals(self, op1, op2, res): """Tests that equality can be checked between PauliX/Y/Z operators, and between Pauli operators and Hamiltonians""" + if not qml.operation.active_new_opmath(): + op1 = qml.operation.convert_to_legacy_H(op1) + op2 = qml.operation.convert_to_legacy_H(op2) + assert qml.equal(op1, op2) == qml.equal(op2, op1) assert qml.equal(op1, op2) == res @@ -1719,11 +1730,10 @@ def test_prod_with_multi_wire_bases(self, base_list1, base_list2, res): def test_prod_of_prods(self): """Test that prod of prods and just an equivalent Prod get compared correctly""" X = qml.PauliX - qml.operation.enable_new_opmath() + op1 = (0.5 * X(0)) @ (0.5 * X(1)) @ (0.5 * X(2)) @ (0.5 * X(3)) @ (0.5 * X(4)) op2 = qml.prod(*[0.5 * X(i) for i in range(5)]) assert qml.equal(op1, op2) - qml.operation.disable_new_opmath() class TestSumComparisons: @@ -1821,7 +1831,6 @@ def test_sum_equal_order_invarient(self): def test_sum_of_sums(self): """Test that sum of sums and just an equivalent sum get compared correctly""" X = qml.PauliX - qml.operation.enable_new_opmath() op1 = ( 0.5 * X(0) + 0.5 * X(1) @@ -1836,7 +1845,6 @@ def test_sum_of_sums(self): ) op2 = qml.sum(*[0.5 * X(i) for i in range(10)]) assert qml.equal(op1, op2) - qml.operation.disable_new_opmath() def f1(p, t): diff --git a/tests/ops/functions/test_generator.py b/tests/ops/functions/test_generator.py index 0f4813160c9..935eddb0b90 100644 --- a/tests/ops/functions/test_generator.py +++ b/tests/ops/functions/test_generator.py @@ -341,22 +341,36 @@ class TestObservableReturn: """Tests for format="observable". This format preserves the initial generator encoded in the operator.""" + @pytest.mark.usefixtures("use_legacy_opmath") def test_observable(self): """Test a generator that returns a single observable is correct""" gen = qml.generator(ObservableOp, format="observable")(0.5, wires=0) assert gen.name == "Hamiltonian" assert gen.compare(ObservableOp(0.5, wires=0).generator()) + def test_observable_opmath(self): + """Test a generator that returns a single observable is correct with opmath enabled""" + gen = qml.generator(ObservableOp, format="observable")(0.5, wires=0) + assert gen.name == "SProd" + assert qml.equal(gen, ObservableOp(0.5, wires=0).generator()) + + @pytest.mark.usefixtures("use_legacy_opmath") def test_tensor_observable(self): """Test a generator that returns a tensor observable is correct""" gen = qml.generator(TensorOp, format="observable")(0.5, wires=[0, 1]) assert gen.name == "Hamiltonian" assert gen.compare(TensorOp(0.5, wires=[0, 1]).generator()) + def test_tensor_observable_opmath(self): + """Test a generator that returns a tensor observable is correct with opmath enabled""" + gen = qml.generator(TensorOp, format="observable")(0.5, wires=[0, 1]) + assert gen.name == "Prod" + assert qml.equal(gen, TensorOp(0.5, wires=[0, 1]).generator()) + def test_hamiltonian(self): """Test a generator that returns a Hamiltonian""" gen = qml.generator(HamiltonianOp, format="observable")(0.5, wires=[0, 1]) - assert gen.name == "Hamiltonian" + assert isinstance(gen, type(qml.Hamiltonian([], []))) assert gen.compare(HamiltonianOp(0.5, wires=[0, 1]).generator()) def test_hermitian(self): @@ -374,39 +388,43 @@ def test_sparse_hamiltonian(self): assert np.all(gen.parameters[0].toarray() == SparseOp.H.toarray()) +@pytest.mark.usefixtures("use_legacy_and_new_opmath") class TestHamiltonianReturn: """Tests for format="hamiltonian". This format always returns the generator - as a Hamiltonian.""" + as a Hamiltonian (either a qml.ops.Hamiltonian or a qml.ops.LinearCombination + depending on whether new_opmath is enabled.)""" def test_observable_no_coeff(self): """Test a generator that returns an observable with no coefficient is correct""" gen = qml.generator(qml.PhaseShift, format="hamiltonian")(0.5, wires=0) - assert gen.name == "Hamiltonian" - assert gen.compare(1.0 * qml.PhaseShift(0.5, wires=0).generator()) + assert isinstance(gen, qml.Hamiltonian) + assert gen.compare(qml.Hamiltonian([1.0], [qml.PhaseShift(0.5, wires=0).generator()])) + @pytest.mark.usefixtures("use_legacy_opmath") def test_observable(self): """Test a generator that returns a single observable is correct""" gen = qml.generator(ObservableOp, format="hamiltonian")(0.5, wires=0) - assert gen.name == "Hamiltonian" + assert isinstance(gen, qml.Hamiltonian) assert gen.compare(ObservableOp(0.5, wires=0).generator()) + @pytest.mark.usefixtures("use_legacy_opmath") def test_tensor_observable(self): """Test a generator that returns a tensor observable is correct""" gen = qml.generator(TensorOp, format="hamiltonian")(0.5, wires=[0, 1]) - assert gen.name == "Hamiltonian" + assert isinstance(gen, qml.Hamiltonian) assert gen.compare(TensorOp(0.5, wires=[0, 1]).generator()) def test_hamiltonian(self): """Test a generator that returns a Hamiltonian""" gen = qml.generator(HamiltonianOp, format="hamiltonian")(0.5, wires=[0, 1]) - assert gen.name == "Hamiltonian" + assert isinstance(gen, qml.Hamiltonian) assert gen.compare(HamiltonianOp(0.5, wires=[0, 1]).generator()) def test_hermitian(self): """Test a generator that returns a Hermitian observable is correct""" gen = qml.generator(HermitianOp, format="hamiltonian")(0.5, wires=0) - assert gen.name == "Hamiltonian" + assert isinstance(gen, qml.Hamiltonian) expected = qml.pauli_decompose(HermitianOp.H, hide_identity=True) assert gen.compare(expected) @@ -415,11 +433,22 @@ def test_sparse_hamiltonian(self): """Test a generator that returns a SparseHamiltonian observable is correct""" gen = qml.generator(SparseOp, format="hamiltonian")(0.5, wires=0) - assert gen.name == "Hamiltonian" + assert isinstance(gen, qml.Hamiltonian) expected = qml.pauli_decompose(SparseOp.H.toarray(), hide_identity=True) assert gen.compare(expected) + def test_sum(self): + """Test a generator that returns a Sum is correct""" + gen = qml.generator(SumOp, format="hamiltonian")(0.5, wires=[0, 1]) + assert isinstance(gen, qml.Hamiltonian) + + expected = qml.Hamiltonian( + [1.0, 0.5], [qml.PauliX(0) @ qml.Identity(1), qml.PauliX(0) @ qml.PauliY(1)] + ) + + assert gen.compare(expected) + class TestArithmeticReturn: """Tests for format="arithmetic". This format always returns the generator as an Arithmetic Operator.""" diff --git a/tests/ops/op_math/test_adjoint.py b/tests/ops/op_math/test_adjoint.py index dee5f630864..73627b09484 100644 --- a/tests/ops/op_math/test_adjoint.py +++ b/tests/ops/op_math/test_adjoint.py @@ -76,6 +76,7 @@ class CustomOp(qml.operation.Operation): assert "grad_recipe" in dir(op) assert "control_wires" in dir(op) + @pytest.mark.usefixtures("use_legacy_opmath") def test_observable(self): """Test that when the base is an Observable, Adjoint will also inherit from Observable.""" @@ -177,6 +178,7 @@ def test_template_base(self): assert op.wires == qml.wires.Wires((0, 1)) + @pytest.mark.usefixtures("use_legacy_opmath") def test_hamiltonian_base(self): """Test adjoint initialization for a hamiltonian.""" base = 2.0 * qml.PauliX(0) @ qml.PauliY(0) + qml.PauliZ("b") @@ -315,6 +317,7 @@ def test_queue_category(self): op = Adjoint(qml.PauliX(0)) assert op._queue_category == "_ops" # pylint: disable=protected-access + @pytest.mark.usefixtures("use_legacy_opmath") def test_queue_category_None(self): """Test that the queue category `None` for some observables carries over.""" op = Adjoint(qml.PauliX(0) @ qml.PauliY(1)) @@ -486,12 +489,13 @@ def test_has_generator_false(self): assert op.has_generator is False + @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_generator(self): """Assert that the generator of an Adjoint is -1.0 times the base generator.""" base = qml.RX(1.23, wires=0) op = Adjoint(base) - assert qml.equal(base.generator(), qml.s_prod(-1.0, op.generator())) + assert qml.equal(base.generator(), -1.0 * op.generator()) def test_no_generator(self): """Test that an adjointed non-Operation raises a GeneratorUndefinedError.""" @@ -860,6 +864,7 @@ def test_single_op_defined_outside_queue_eager(self): assert len(q) == 1 assert q.queue[0] is out + @pytest.mark.usefixtures("use_legacy_opmath") def test_single_observable(self): """Test passing a single preconstructed observable in a queuing context.""" @@ -1043,6 +1048,7 @@ def test_single_op_eager(self): assert isinstance(out, qml.RX) assert out.data == (-x,) + @pytest.mark.xfail # TODO not sure what the expected behavior here is with new opmath def test_observable(self): """Test providing a preconstructed Observable outside of a queuing context.""" diff --git a/tests/ops/op_math/test_composite.py b/tests/ops/op_math/test_composite.py index 8ce1908b283..e3eeec70879 100644 --- a/tests/ops/op_math/test_composite.py +++ b/tests/ops/op_math/test_composite.py @@ -75,6 +75,7 @@ def test_direct_initialization_fails(self): with pytest.raises(TypeError, match="Can't instantiate abstract class CompositeOp"): _ = CompositeOp(*self.simple_operands) # pylint:disable=abstract-class-instantiated + @pytest.mark.xfail def test_raise_error_fewer_than_2_operands(self): """Test that initializing a composite operator with less than 2 operands raises a ValueError.""" with pytest.raises(ValueError, match="Require at least two operators to combine;"): diff --git a/tests/ops/op_math/test_controlled.py b/tests/ops/op_math/test_controlled.py index b84a783ba85..f4e6b7d8d81 100644 --- a/tests/ops/op_math/test_controlled.py +++ b/tests/ops/op_math/test_controlled.py @@ -462,13 +462,9 @@ def test_has_generator_false(self): assert op.has_generator is False - @pytest.mark.parametrize("use_new_op_math", [True, False]) - def test_generator(self, use_new_op_math): + def test_generator(self): """Test that the generator is a tensor product of projectors and the base's generator.""" - if use_new_op_math: - qml.operation.enable_new_opmath() - base = qml.RZ(-0.123, wires="a") control_values = [0, 1] op = Controlled(base, ("b", "c"), control_values=control_values) @@ -492,8 +488,32 @@ def test_generator(self, use_new_op_math): expected.matrix(wire_order=["a", "b", "c"]), op.matrix(wire_order=["a", "b", "c"]) ) - if use_new_op_math: - qml.operation.disable_new_opmath() + @pytest.mark.usefixtures("use_legacy_opmath") + def test_generator_legacy_opmath(self): + """Test that the generator is a tensor product of projectors and the base's generator.""" + + base = qml.RZ(-0.123, wires="a") + control_values = [0, 1] + op = Controlled(base, ("b", "c"), control_values=control_values) + + base_gen, base_gen_coeff = qml.generator(base, format="prefactor") + gen_tensor, gen_coeff = qml.generator(op, format="prefactor") + + assert base_gen_coeff == gen_coeff + + for wire, val in zip(op.control_wires, control_values): + ob = list(op for op in gen_tensor.operands if op.wires == qml.wires.Wires(wire)) + assert len(ob) == 1 + assert ob[0].data == ([val],) + + ob = list(op for op in gen_tensor.operands if op.wires == base.wires) + assert len(ob) == 1 + assert ob[0].__class__ is base_gen.__class__ + + expected = qml.exp(op.generator(), 1j * op.data[0]) + assert qml.math.allclose( + expected.matrix(wire_order=["a", "b", "c"]), op.matrix(wire_order=["a", "b", "c"]) + ) def test_diagonalizing_gates(self): """Test that the Controlled diagonalizing gates is the same as the base diagonalizing gates.""" diff --git a/tests/ops/op_math/test_evolution.py b/tests/ops/op_math/test_evolution.py index 7bdd7656dde..852b557593b 100644 --- a/tests/ops/op_math/test_evolution.py +++ b/tests/ops/op_math/test_evolution.py @@ -71,13 +71,21 @@ def test_generator(self): U = Evolution(qml.PauliX(0), 3) assert U.base == U.generator() - def test_num_params_for_parametric_base(self): + @pytest.mark.usefixtures("use_legacy_opmath") + def test_num_params_for_parametric_bas_legacy_opmath(self): base_op = 0.5 * qml.PauliY(0) + qml.PauliZ(0) @ qml.PauliX(1) op = Evolution(base_op, 1.23) assert base_op.num_params == 2 assert op.num_params == 1 + def test_num_params_for_parametric_base(self): + base_op = 0.5 * qml.PauliY(0) + qml.PauliZ(0) @ qml.PauliX(1) + op = Evolution(base_op, 1.23) + + assert base_op.num_params == 1 + assert op.num_params == 1 + def test_data(self): """Test initializing and accessing the data property.""" diff --git a/tests/ops/op_math/test_exp.py b/tests/ops/op_math/test_exp.py index 3fa88cc939a..3d1e62096af 100644 --- a/tests/ops/op_math/test_exp.py +++ b/tests/ops/op_math/test_exp.py @@ -432,6 +432,7 @@ def test_non_pauli_word_base_no_decomposition(self): ): op.decomposition() + @pytest.mark.usefixtures("use_legacy_opmath") def test_nontensor_tensor_no_decomposition(self): """Checks that accessing the decomposition throws an error if the base is a Tensor object that is not a mathematical tensor""" @@ -444,8 +445,8 @@ def test_nontensor_tensor_no_decomposition(self): @pytest.mark.parametrize( "base, base_string", ( - (qml.PauliZ(0) @ qml.PauliY(1), "ZY"), - (qml.PauliY(0) @ qml.Identity(1) @ qml.PauliZ(2), "YIZ"), + (qml.prod(qml.PauliZ(0), qml.PauliY(1)), "ZY"), + (qml.prod(qml.PauliY(0), qml.Identity(1), qml.PauliZ(2)), "YIZ"), ), ) def test_decomposition_into_pauli_rot(self, base, base_string): @@ -459,9 +460,11 @@ def test_decomposition_into_pauli_rot(self, base, base_string): @pytest.mark.parametrize("op_name", all_qubit_operators) @pytest.mark.parametrize("str_wires", (True, False)) + @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_generator_decomposition(self, op_name, str_wires): """Check that Exp decomposes into a specific operator if ``base`` corresponds to the generator of that operator.""" + op_class = getattr(qml.ops.qubit, op_name) # pylint:disable=no-member if not op_class.has_generator: diff --git a/tests/ops/op_math/test_linear_combination.py b/tests/ops/op_math/test_linear_combination.py new file mode 100644 index 00000000000..da746bafb46 --- /dev/null +++ b/tests/ops/op_math/test_linear_combination.py @@ -0,0 +1,2021 @@ +# 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. +""" +Tests for the LinearCombination class. +""" +# pylint: disable=too-many-public-methods, too-few-public-methods +from collections.abc import Iterable +from copy import copy + +import numpy as np +import pytest +import scipy + +import pennylane as qml +from pennylane import numpy as pnp, X, Y, Z +from pennylane.wires import Wires +from pennylane.pauli import PauliWord, PauliSentence +from pennylane.ops import LinearCombination + +from pennylane.operation import enable_new_opmath_cm + + +@pytest.mark.usefixtures("use_legacy_opmath") +def test_switching(): + """Test that switching to new from old opmath changes the dispatch of qml.Hamiltonian""" + Ham = qml.Hamiltonian([1.0, 2.0, 3.0], [X(0), X(0) @ X(1), X(2)]) + assert isinstance(Ham, qml.Hamiltonian) + assert not isinstance(Ham, qml.ops.LinearCombination) + + with enable_new_opmath_cm(): + LC = qml.Hamiltonian([1.0, 2.0, 3.0], [X(0), X(0) @ X(1), X(2)]) + assert isinstance(LC, qml.Hamiltonian) + assert isinstance(LC, qml.ops.LinearCombination) + + +@pytest.mark.usefixtures("use_legacy_and_new_opmath") +class TestParityWithHamiltonian: + """Test that Hamiltonian and LinearCombination can be used interchangeably when new opmath is disabled or enabled""" + + def test_isinstance_Hamiltonian(self): + H = qml.Hamiltonian([1.0, 2.0, 3.0], [X(0), X(0) @ X(1), X(2)]) + assert isinstance(H, qml.Hamiltonian) + + +# Make test data in different interfaces, if installed +COEFFS_PARAM_INTERFACE = [ + ([-0.05, 0.17], 1.7, "autograd"), + (np.array([-0.05, 0.17]), np.array(1.7), "autograd"), + (pnp.array([-0.05, 0.17], requires_grad=True), pnp.array(1.7, requires_grad=True), "autograd"), +] + +try: + import jax + from jax import numpy as jnp + + COEFFS_PARAM_INTERFACE.append((jnp.array([-0.05, 0.17]), jnp.array(1.7), "jax")) +except ImportError: + pass + +try: + import tensorflow as tf + + COEFFS_PARAM_INTERFACE.append( + (tf.Variable([-0.05, 0.17], dtype=tf.double), tf.Variable(1.7, dtype=tf.double), "tf") + ) +except ImportError: + pass + +try: + import torch + + COEFFS_PARAM_INTERFACE.append((torch.tensor([-0.05, 0.17]), torch.tensor(1.7), "torch")) +except ImportError: + pass + +H_ONE_QUBIT = np.array([[1.0, 0.5j], [-0.5j, 2.5]]) + +H_TWO_QUBITS = np.array( + [[0.5, 1.0j, 0.0, -3j], [-1.0j, -1.1, 0.0, -0.1], [0.0, 0.0, -0.9, 12.0], [3j, -0.1, 12.0, 0.0]] +) + +COEFFS = [(0.5, 1.2, -0.7), (2.2, -0.2, 0.0), (0.33,)] + +OBSERVABLES = [ + (Z(0), Y(0), Z(1)), + (X(0) @ Z(1), Y(0) @ Z(1), Z(1)), + (qml.Hermitian(H_TWO_QUBITS, [0, 1]),), +] + +valid_LinearCombinations = [ + ((1.0,), (qml.Hermitian(H_TWO_QUBITS, [0, 1]),)), + ((-0.8,), (Z(0),)), + ((0.6,), (X(0) @ X(1),)), + ((0.5, -1.6), (X(0), Y(1))), + ((0.5, -1.6), (X(1), Y(1))), + ((0.5, -1.6), (X("a"), Y("b"))), + ((1.1, -0.4, 0.333), (X(0), qml.Hermitian(H_ONE_QUBIT, 2), Z(2))), + ((-0.4, 0.15), (qml.Hermitian(H_TWO_QUBITS, [0, 2]), Z(1))), + ([1.5, 2.0], [Z(0), Y(2)]), + (np.array([-0.1, 0.5]), [qml.Hermitian(H_TWO_QUBITS, [0, 1]), Y(0)]), + ((0.5, 1.2), (X(0), X(0) @ X(1))), + ((0.5 + 1.2j, 1.2 + 0.5j), (X(0), Y(1))), + ((0.7 + 0j, 0 + 1.3j), (X(0), Y(1))), +] + +invalid_LinearCombinations = [ + ((), (Z(0),)), + ((), (Z(0), Y(1))), + ((3.5,), ()), + ((1.2, -0.4), ()), + ((0.5, 1.2), (Z(0),)), + ((1.0,), (Z(0), Y(0))), +] + +simplify_LinearCombinations = [ + ( + qml.ops.LinearCombination([], []), + qml.ops.LinearCombination([], []), + ), + ( + qml.ops.LinearCombination([1, 1, 1], [X(0) @ qml.Identity(1), X(0), X(1)]), + qml.ops.LinearCombination([2, 1], [X(0), X(1)]), + ), + ( + qml.ops.LinearCombination([-1, 1, 1], [X(0) @ qml.Identity(1), X(0), X(1)]), + qml.ops.LinearCombination([1], [X(1)]), + ), + ( + qml.ops.LinearCombination( + [1, 0.5], + [X(0) @ Y(1), Y(1) @ qml.Identity(2) @ X(0)], + ), + qml.ops.LinearCombination([1.5], [X(0) @ Y(1)]), + ), + ( + qml.ops.LinearCombination( + [1, 1, 0.5], + [ + qml.Hermitian(np.array([[1, 0], [0, -1]]), "a"), + X("b") @ Y(1.3), + Y(1.3) @ qml.Identity(-0.9) @ X("b"), + ], + ), + qml.ops.LinearCombination( + [1, 1.5], + [qml.Hermitian(np.array([[1, 0], [0, -1]]), "a"), X("b") @ Y(1.3)], + ), + ), + # Simplifies to zero LinearCombination + ( + qml.ops.LinearCombination([1, -0.5, -0.5], [X(0) @ qml.Identity(1), X(0), X(0)]), + qml.ops.LinearCombination([], []), + ), + ( + qml.ops.LinearCombination( + [1, -1], + [X(4) @ qml.Identity(0) @ X(1), X(4) @ X(1)], + ), + qml.ops.LinearCombination([], []), + ), + ( + qml.ops.LinearCombination([0], [qml.Identity(0)]), + qml.ops.LinearCombination([], []), + ), +] + +equal_LinearCombinations = [ + ( + qml.ops.LinearCombination([1, 1], [X(0) @ qml.Identity(1), Z(0)]), + qml.ops.LinearCombination([1, 1], [X(0), Z(0)]), + True, + ), + ( + qml.ops.LinearCombination([1, 1], [X(0) @ qml.Identity(1), Y(2) @ Z(0)]), + qml.ops.LinearCombination([1, 1], [X(0), Z(0) @ Y(2) @ qml.Identity(1)]), + True, + ), + ( + qml.ops.LinearCombination([1, 1, 1], [X(0) @ qml.Identity(1), Z(0), qml.Identity(1)]), + qml.ops.LinearCombination([1, 1], [X(0), Z(0)]), + False, + ), + ( + qml.ops.LinearCombination([1], [Z(0) @ X(1)]), + Z(0) @ X(1), + True, + ), + (qml.ops.LinearCombination([1], [Z(0)]), Z(0), True), + ( + qml.ops.LinearCombination( + [1, 1, 1], + [ + qml.Hermitian(np.array([[1, 0], [0, -1]]), "b") @ qml.Identity(7), + Z(3), + qml.Identity(1.2), + ], + ), + qml.ops.LinearCombination( + [1, 1, 1], + [qml.Hermitian(np.array([[1, 0], [0, -1]]), "b"), Z(3), qml.Identity(1.2)], + ), + True, + ), + ( + qml.ops.LinearCombination([1, 1], [Z(3) @ qml.Identity(1.2), Z(3)]), + qml.ops.LinearCombination([2], [Z(3)]), + True, + ), +] + +add_LinearCombinations = [ + ( + qml.ops.LinearCombination([1, 1.2, 0.1], [X(0), Z(1), X(2)]), + qml.ops.LinearCombination([0.5, 0.3, 1], [X(0), X(1), X(2)]), + qml.ops.LinearCombination([1.5, 1.2, 1.1, 0.3], [X(0), Z(1), X(2), X(1)]), + ), + ( + qml.ops.LinearCombination([1.3, 0.2, 0.7], [X(0) @ X(1), qml.Hadamard(1), X(2)]), + qml.ops.LinearCombination([0.5, 0.3, 1.6], [X(0), X(1) @ X(0), X(2)]), + qml.ops.LinearCombination( + [1.6, 0.2, 2.3, 0.5], + [X(0) @ X(1), qml.Hadamard(1), X(2), X(0)], + ), + ), + ( + qml.ops.LinearCombination([1, 1], [X(0), qml.Hermitian(np.array([[1, 0], [0, -1]]), 0)]), + qml.ops.LinearCombination( + [0.5, 0.5], [X(0), qml.Hermitian(np.array([[1, 0], [0, -1]]), 0)] + ), + qml.ops.LinearCombination( + [1.5, 1.5], [X(0), qml.Hermitian(np.array([[1, 0], [0, -1]]), 0)] + ), + ), + ( + qml.ops.LinearCombination([1, 1.2, 0.1], [X(0), Z(1), X(2)]), + X(0) @ qml.Identity(1), + qml.ops.LinearCombination([2, 1.2, 0.1], [X(0), Z(1), X(2)]), + ), + ( + qml.ops.LinearCombination([1.3, 0.2, 0.7], [X(0) @ X(1), qml.Hadamard(1), X(2)]), + qml.Hadamard(1), + qml.ops.LinearCombination([1.3, 1.2, 0.7], [X(0) @ X(1), qml.Hadamard(1), X(2)]), + ), + ( + qml.ops.LinearCombination([1, 1.2, 0.1], [X("b"), Z(3.1), X(1.6)]), + X("b") @ qml.Identity(5), + qml.ops.LinearCombination([2, 1.2, 0.1], [X("b"), Z(3.1), X(1.6)]), + ), + # Case where arguments coeffs and ops to the LinearCombination are iterables other than lists + ( + qml.ops.LinearCombination((1, 1.2, 0.1), (X(0), Z(1), X(2))), + qml.ops.LinearCombination(np.array([0.5, 0.3, 1]), np.array([X(0), X(1), X(2)])), + qml.ops.LinearCombination( + (1.5, 1.2, 1.1, 0.3), + np.array([X(0), Z(1), X(2), X(1)]), + ), + ), + # Case where the 1st LinearCombination doesn't contain all wires + ( + qml.ops.LinearCombination([1.23, -3.45], [X(0), Y(1)]), + qml.ops.LinearCombination([6.78], [Z(2)]), + qml.ops.LinearCombination([1.23, -3.45, 6.78], [X(0), Y(1), Z(2)]), + ), +] + +add_zero_LinearCombinations = [ + qml.ops.LinearCombination([1, 1.2, 0.1], [X(0), Z(1), X(2)]), + qml.ops.LinearCombination([1, 1], [X(0), qml.Hermitian(np.array([[1, 0], [0, -1]]), 0)]), + qml.ops.LinearCombination([1.5, 1.2, 1.1, 0.3], [X(0), Z(1), X(2), X(1)]), +] + +iadd_zero_LinearCombinations = [ + # identical LinearCombinations + ( + qml.ops.LinearCombination([1, 1.2, 0.1], [X(0), Z(1), X(2)]), + qml.ops.LinearCombination([1, 1.2, 0.1], [X(0), Z(1), X(2)]), + ), + ( + qml.ops.LinearCombination([1, 1], [X(0), qml.Hermitian(np.array([[1, 0], [0, -1]]), 0)]), + qml.ops.LinearCombination([1, 1], [X(0), qml.Hermitian(np.array([[1, 0], [0, -1]]), 0)]), + ), + ( + qml.ops.LinearCombination([1.5, 1.2, 1.1, 0.3], [X(0), Z(1), X(2), X(1)]), + qml.ops.LinearCombination([1.5, 1.2, 1.1, 0.3], [X(0), Z(1), X(2), X(1)]), + ), +] + +sub_LinearCombinations = [ + ( + qml.ops.LinearCombination([1, 1.2, 0.1], [X(0), Z(1), X(2)]), + qml.ops.LinearCombination([0.5, 0.3, 1.6], [X(0), X(1), X(2)]), + qml.ops.LinearCombination([0.5, 1.2, -1.5, -0.3], [X(0), Z(1), X(2), X(1)]), + ), + ( + qml.ops.LinearCombination([1, 1.2, 0.1], [X(0), Z(1), X(2)]), + X(0) @ qml.Identity(1), + qml.ops.LinearCombination([1.2, 0.1], [Z(1), X(2)]), + ), + ( + qml.ops.LinearCombination([1, 1.2, 0.1], [X("b"), Z(3.1), X(1.6)]), + X("b") @ qml.Identity(1), + qml.ops.LinearCombination([1.2, 0.1], [Z(3.1), X(1.6)]), + ), + # The result is the zero LinearCombination + ( + qml.ops.LinearCombination([1, 1.2, 0.1], [X(0), Z(1), X(2)]), + qml.ops.LinearCombination([1, 1.2, 0.1], [X(0), Z(1), X(2)]), + qml.ops.LinearCombination([], []), + ), + ( + qml.ops.LinearCombination([1.0, 2.0], [X(4), Z(2)]), + qml.ops.LinearCombination([1.0, 2.0], [X(4), Z(2)]), + qml.ops.LinearCombination([], []), + ), + # Case where arguments coeffs and ops to the LinearCombination are iterables other than lists + ( + qml.ops.LinearCombination((1, 1.2, 0.1), (X(0), Z(1), X(2))), + qml.ops.LinearCombination(np.array([0.5, 0.3, 1.6]), np.array([X(0), X(1), X(2)])), + qml.ops.LinearCombination( + (0.5, 1.2, -1.5, -0.3), + np.array([X(0), Z(1), X(2), X(1)]), + ), + ), + # Case where the 1st LinearCombination doesn't contain all wires + ( + qml.ops.LinearCombination([1.23, -3.45], [X(0), Y(1)]), + qml.ops.LinearCombination([6.78], [Z(2)]), + qml.ops.LinearCombination([1.23, -3.45, -6.78], [X(0), Y(1), Z(2)]), + ), +] + +mul_LinearCombinations = [ + ( + 0.5, + qml.ops.LinearCombination( + [1, 2], [X(0), Z(1)] + ), # Case where the types of the coefficient and the scalar differ + qml.ops.LinearCombination([0.5, 1.0], [X(0), Z(1)]), + ), + ( + 3.0, + qml.ops.LinearCombination([1.5, 0.5], [X(0), Z(1)]), + qml.ops.LinearCombination([4.5, 1.5], [X(0), Z(1)]), + ), + ( + -1.3, + qml.ops.LinearCombination([1, -0.3], [X(0), Z(1) @ Z(2)]), + qml.ops.LinearCombination([-1.3, 0.39], [X(0), Z(1) @ Z(2)]), + ), + ( + -1.3, + qml.ops.LinearCombination( + [1, -0.3], + [qml.Hermitian(np.array([[1, 0], [0, -1]]), "b"), Z(23) @ Z(0)], + ), + qml.ops.LinearCombination( + [-1.3, 0.39], + [qml.Hermitian(np.array([[1, 0], [0, -1]]), "b"), Z(23) @ Z(0)], + ), + ), + # The result is the zero LinearCombination + ( + 0.0, + qml.ops.LinearCombination([1], [X(0)]), + qml.ops.LinearCombination([0], [X(0)]), + ), + ( + 0.0, + qml.ops.LinearCombination([1.0, 1.2, 0.1], [X(0), Z(1), X(2)]), + qml.ops.LinearCombination([0.0, 0.0, 0.0], [X(0), Z(1), X(2)]), + ), + # Case where arguments coeffs and ops to the LinearCombination are iterables other than lists + ( + 3.0, + qml.ops.LinearCombination((1.5, 0.5), (X(0), Z(1))), + qml.ops.LinearCombination(np.array([4.5, 1.5]), np.array([X(0), Z(1)])), + ), +] + +matmul_LinearCombinations = [ + ( + qml.ops.LinearCombination([1, 1], [X(0), Z(1)]), + qml.ops.LinearCombination([0.5, 0.5], [Z(2), Z(3)]), + qml.ops.LinearCombination( + [0.5, 0.5, 0.5, 0.5], + [ + X(0) @ Z(2), + X(0) @ Z(3), + Z(1) @ Z(2), + Z(1) @ Z(3), + ], + ), + ), + ( + qml.ops.LinearCombination([0.5, 0.25], [X(0) @ X(1), Z(0)]), + qml.ops.LinearCombination([1, 1], [X(3) @ Z(2), Z(2)]), + qml.ops.LinearCombination( + [0.5, 0.5, 0.25, 0.25], + [ + X(0) @ X(1) @ X(3) @ Z(2), + X(0) @ X(1) @ Z(2), + Z(0) @ X(3) @ Z(2), + Z(0) @ Z(2), + ], + ), + ), + ( + qml.ops.LinearCombination([1, 1], [X(0), Z(1)]), + X(2), + qml.ops.LinearCombination([1, 1], [X(0) @ X(2), Z(1) @ X(2)]), + ), +] + +rmatmul_LinearCombinations = [ + ( + qml.ops.LinearCombination([0.5, 0.5], [Z(2), Z(3)]), + qml.ops.LinearCombination([1, 1], [X(0), Z(1)]), + qml.ops.LinearCombination( + [0.5, 0.5, 0.5, 0.5], + [ + X(0) @ Z(2), + X(0) @ Z(3), + Z(1) @ Z(2), + Z(1) @ Z(3), + ], + ), + ), + ( + qml.ops.LinearCombination([1, 1], [X(3) @ Z(2), Z(2)]), + qml.ops.LinearCombination([0.5, 0.25], [X(0) @ X(1), Z(0)]), + qml.ops.LinearCombination( + [0.5, 0.5, 0.25, 0.25], + [ + X(0) @ X(1) @ X(3) @ Z(2), + X(0) @ X(1) @ Z(2), + Z(0) @ X(3) @ Z(2), + Z(0) @ Z(2), + ], + ), + ), + ( + qml.ops.LinearCombination([1, 1], [X(0), Z(1)]), + X(2), + qml.ops.LinearCombination([1, 1], [X(2) @ X(0), X(2) @ Z(1)]), + ), +] + +big_LinearCombination_coeffs = np.array( + [ + -0.04207898, + 0.17771287, + 0.17771287, + -0.24274281, + -0.24274281, + 0.17059738, + 0.04475014, + -0.04475014, + -0.04475014, + 0.04475014, + 0.12293305, + 0.16768319, + 0.16768319, + 0.12293305, + 0.17627641, + ] +) + +big_LinearCombination_ops = [ + qml.Identity(wires=[0]), + Z(wires=[0]), + Z(wires=[1]), + Z(wires=[2]), + Z(wires=[3]), + Z(wires=[0]) @ Z(wires=[1]), + Y(wires=[0]) @ X(wires=[1]) @ X(wires=[2]) @ Y(wires=[3]), + Y(wires=[0]) @ Y(wires=[1]) @ X(wires=[2]) @ X(wires=[3]), + X(wires=[0]) @ X(wires=[1]) @ Y(wires=[2]) @ Y(wires=[3]), + X(wires=[0]) @ Y(wires=[1]) @ Y(wires=[2]) @ X(wires=[3]), + Z(wires=[0]) @ Z(wires=[2]), + Z(wires=[0]) @ Z(wires=[3]), + Z(wires=[1]) @ Z(wires=[2]), + Z(wires=[1]) @ Z(wires=[3]), + Z(wires=[2]) @ Z(wires=[3]), +] + +big_LinearCombination = qml.ops.LinearCombination( + big_LinearCombination_coeffs, big_LinearCombination_ops +) + +big_LinearCombination_grad = ( + np.array( + [ + [ + [6.52084595e-18, -2.11464420e-02, -1.16576858e-02], + [-8.22589330e-18, -5.20597922e-02, -1.85365365e-02], + [-2.73850768e-17, 1.14202988e-01, -5.45041403e-03], + [-1.27514307e-17, -1.10465531e-01, 5.19489457e-02], + ], + [ + [-2.45428288e-02, 8.38921555e-02, -2.00641818e-17], + [-2.21085973e-02, 7.39332741e-04, -1.25580654e-17], + [9.62058625e-03, -1.51398765e-01, 2.02129847e-03], + [1.10020832e-03, -3.49066271e-01, 2.13669117e-03], + ], + ] + ), +) + + +def circuit1(param): + """First Pauli subcircuit""" + qml.RX(param, wires=0) + qml.RY(param, wires=0) + return qml.expval(X(0)) + + +def circuit2(param): + """Second Pauli subcircuit""" + qml.RX(param, wires=0) + qml.RY(param, wires=0) + return qml.expval(Z(0)) + + +dev = qml.device("default.qubit", wires=2) + + +class TestLinearCombination: + """Test the LinearCombination class""" + + PAULI_REPS = ( + ( + list(range(3)), + [X(i) for i in range(3)], + PauliSentence({PauliWord({i: "X"}): 1.0 * i for i in range(3)}), + ), + ( + list(range(3)), + [qml.s_prod(i, X(i)) for i in range(3)], + PauliSentence({PauliWord({i: "X"}): 1.0 * i * i for i in range(3)}), + ), + ) + + @pytest.mark.parametrize("simplify", [None, True]) + @pytest.mark.parametrize("coeffs, ops, true_pauli", PAULI_REPS) + def test_pauli_rep(self, coeffs, ops, true_pauli, simplify): + """Test the pauli rep is correctly constructed""" + H = qml.ops.LinearCombination(coeffs, ops, simplify=simplify) + pr = H.pauli_rep + if simplify: + pr.simplify() + true_pauli.simplify() + assert pr is not None + assert pr == true_pauli + + def test_is_hermitian_trivial(self): + """Test that an empty LinearCombination is trivially hermitian""" + op = qml.ops.LinearCombination([], []) + assert op.is_hermitian + + IS_HERMITIAN_TEST = ( + (qml.ops.LinearCombination([0.5, 0.5], [X(0), X(1) @ X(2)]), True), + (qml.ops.LinearCombination([0.5, 0.5j], [X(0), X(1) @ X(2)]), False), + (qml.ops.LinearCombination([0.5, 0.5], [X(0), qml.Hadamard(0)]), True), + ) + + @pytest.mark.parametrize("op, res", IS_HERMITIAN_TEST) + def test_is_hermitian(self, op, res): + assert op.is_hermitian is res + + @pytest.mark.parametrize("coeffs, ops", valid_LinearCombinations) + def test_LinearCombination_valid_init(self, coeffs, ops): + """Tests that the LinearCombination object is created with + the correct attributes""" + H = qml.ops.LinearCombination(coeffs, ops) + assert np.allclose(H.terms()[0], coeffs) + assert H.terms()[1] == list(ops) + + @pytest.mark.parametrize("coeffs, ops", invalid_LinearCombinations) + def test_LinearCombination_invalid_init_exception(self, coeffs, ops): + """Tests that an exception is raised when giving an invalid + combination of coefficients and ops""" + with pytest.raises(ValueError, match="number of coefficients and operators does not match"): + qml.ops.LinearCombination(coeffs, ops) + + def test_integer_coefficients(self): + """Test that handling integers is not a problem""" + H1, H2, true_res = ( + qml.ops.LinearCombination([1, 2], [X(4), Z(2)]), # not failing with float coeffs + qml.ops.LinearCombination([1, 2], [X(4), Z(2)]), + qml.ops.LinearCombination([], []), + ) + res = H1 - H2 + assert res.compare(true_res) + + # pylint: disable=protected-access + @pytest.mark.parametrize("coeffs, ops", valid_LinearCombinations) + @pytest.mark.parametrize("grouping_type", (None, "qwc")) + def test_flatten_unflatten(self, coeffs, ops, grouping_type): + """Test the flatten and unflatten methods for LinearCombinations""" + + if any(not qml.pauli.is_pauli_word(t) for t in ops) and grouping_type: + pytest.skip("grouping type must be none if a term is not a pauli word.") + + H = LinearCombination(coeffs, ops, grouping_type=grouping_type) + data, metadata = H._flatten() + assert metadata[0] == H.grouping_indices + assert hash(metadata) + assert len(data) == 3 + assert qml.math.allequal( + data[0], H._coeffs + ) # Previously checking "is" instead of "==", problem? + assert data[1] == H._ops + assert data[2] == H.data + + new_H = LinearCombination._unflatten(*H._flatten()) + assert qml.equal(H, new_H) + assert new_H.grouping_indices == H.grouping_indices + + @pytest.mark.parametrize("coeffs, ops", valid_LinearCombinations) + def test_LinearCombination_wires(self, coeffs, ops): + """Tests that the LinearCombination object has correct wires.""" + H = qml.ops.LinearCombination(coeffs, ops) + assert set(H.wires) == set(w for op in H.ops for w in op.wires) + + def test_label(self): + """Tests the label method of LinearCombination when <=3 coefficients.""" + H = qml.ops.LinearCombination((-0.8,), (Z(0),)) + assert H.label() == "𝓗" + assert H.label(decimals=2) == "𝓗\n(-0.80)" + + def test_label_many_coefficients(self): + """Tests the label method of LinearCombination when >3 coefficients.""" + H = LinearCombination([0.1] * 5, [X(i) for i in range(5)]) + assert H.label() == "𝓗" + assert H.label(decimals=2) == "𝓗" + + LINEARCOMBINATION_STR = ( + (qml.ops.LinearCombination([0.5, 0.5], [X(0), X(1)]), "0.5 * X(0) + 0.5 * X(1)"), + ( + qml.ops.LinearCombination([0.5, 0.5], [qml.prod(X(0), X(1)), qml.prod(X(1), X(2))]), + "0.5 * (X(0) @ X(1)) + 0.5 * (X(1) @ X(2))", + ), + ) + + @pytest.mark.parametrize("op, string", LINEARCOMBINATION_STR) + def test_LinearCombination_str(self, op, string): + """Tests that the __str__ function for printing is correct""" + assert str(op) == string + + LINEARCOMBINATION_REPR = ( + (qml.ops.LinearCombination([0.5, 0.5], [X(0), X(1)]), "0.5 * X(0) + 0.5 * X(1)"), + ( + qml.ops.LinearCombination([0.5, 0.5], [qml.prod(X(0), X(1)), qml.prod(X(1), X(2))]), + "0.5 * (X(0) @ X(1)) + 0.5 * (X(1) @ X(2))", + ), + ( + qml.ops.LinearCombination(range(15), [qml.prod(X(i), X(i + 1)) for i in range(15)]), + "(\n 0 * (X(0) @ X(1))\n + 1 * (X(1) @ X(2))\n + 2 * (X(2) @ X(3))\n + 3 * (X(3) @ X(4))\n + 4 * (X(4) @ X(5))\n + 5 * (X(5) @ X(6))\n + 6 * (X(6) @ X(7))\n + 7 * (X(7) @ X(8))\n + 8 * (X(8) @ X(9))\n + 9 * (X(9) @ X(10))\n + 10 * (X(10) @ X(11))\n + 11 * (X(11) @ X(12))\n + 12 * (X(12) @ X(13))\n + 13 * (X(13) @ X(14))\n + 14 * (X(14) @ X(15))\n)", + ), + ) + + @pytest.mark.parametrize("op, string", LINEARCOMBINATION_REPR) + def test_LinearCombination_repr(self, op, string): + """Tests that the __repr__ function for printing is correct""" + assert repr(op) == string + + def test_LinearCombination_name(self): + """Tests the name property of the LinearCombination class""" + H = qml.ops.LinearCombination([], []) + assert H.name == "LinearCombination" + + @pytest.mark.parametrize(("old_H", "new_H"), simplify_LinearCombinations) + def test_simplify(self, old_H, new_H): + """Tests the simplify method""" + old_H = old_H.simplify() + assert old_H.compare(new_H) + + def test_simplify_while_queueing(self): + """Tests that simplifying a LinearCombination in a tape context + queues the simplified LinearCombination.""" + + with qml.queuing.AnnotatedQueue() as q: + a = X(wires=0) + b = Y(wires=1) + c = qml.Identity(wires=2) + d = b @ c + H = qml.ops.LinearCombination([1.0, 2.0], [a, d]) + H = H.simplify() + + # check that H is simplified + assert H.ops == [a, b] + # check that the simplified LinearCombination is in the queue + assert q.get_info(H) is not None + + def test_data(self): + """Tests the obs_data method""" + # pylint: disable=protected-access + + H = qml.ops.LinearCombination( + [1, 1, 0.5], + [Z(0), Z(0) @ X(1), X(2) @ qml.Identity(1)], + ) + data = H._obs_data() + + expected = { + (0.5, frozenset({("Prod", qml.wires.Wires([2, 1]), ())})), + (1.0, frozenset({("PauliZ", qml.wires.Wires(0), ())})), + (1.0, frozenset({("Prod", qml.wires.Wires([0, 1]), ())})), + } + + assert data == expected + + def test_data_gell_mann(self): + """Tests that the obs_data method for LinearCombinations with qml.GellMann + observables includes the Gell-Mann index.""" + H = qml.ops.LinearCombination( + [1, -1, 0.5], + [ + qml.GellMann(wires=0, index=3), + qml.GellMann(wires=0, index=3) @ qml.GellMann(wires=1, index=1), + qml.GellMann(wires=2, index=2), + ], + ) + data = H._obs_data() + + expected = { + (-1.0, frozenset({("Prod", qml.wires.Wires([0, 1]), ())})), + (0.5, frozenset({("GellMann", qml.wires.Wires(2), (2,))})), + (1.0, frozenset({("GellMann", qml.wires.Wires(0), (3,))})), + } + + assert data == expected + + COMPARE_WITH_OPS = ( + (qml.ops.LinearCombination([0.5], [X(0) @ X(1)]), qml.s_prod(0.5, X(0) @ X(1))), + (qml.ops.LinearCombination([0.5], [X(0) + X(1)]), qml.s_prod(0.5, qml.sum(X(0), X(1)))), + (qml.ops.LinearCombination([1.0], [X(0)]), X(0)), + (qml.ops.LinearCombination([1.0], [qml.Hadamard(0)]), qml.Hadamard(0)), + (qml.ops.LinearCombination([1.0], [X(0) @ X(1)]), X(0) @ X(1)), + ) + + @pytest.mark.parametrize("H, op", COMPARE_WITH_OPS) + def test_compare_to_simple_ops(self, H, op): + assert H.compare(op) + + @pytest.mark.xfail + def test_compare_gell_mann(self): + """Tests that the compare method returns the correct result for LinearCombinations + with qml.GellMann present.""" + H1 = qml.ops.LinearCombination([1], [qml.GellMann(wires=2, index=2)]) + H2 = qml.ops.LinearCombination( + [1], [qml.GellMann(wires=2, index=1) @ qml.GellMann(wires=1, index=2)] + ) + H3 = qml.ops.LinearCombination([1], [qml.GellMann(wires=2, index=1)]) + H4 = qml.ops.LinearCombination( + [1], [qml.GellMann(wires=2, index=1) @ qml.GellMann(wires=1, index=3)] + ) + + assert H1.compare(qml.GellMann(wires=2, index=2)) is True + assert H1.compare(qml.GellMann(wires=2, index=1)) is False + assert H1.compare(H3) is False + assert H2.compare(qml.GellMann(wires=2, index=1) @ qml.GellMann(wires=1, index=2)) is True + assert H2.compare(qml.GellMann(wires=2, index=2) @ qml.GellMann(wires=1, index=2)) is False + assert H2.compare(H4) is False + + @pytest.mark.xfail # TODO: decide whether we want to continue to have this legacy behavior + def test_LinearCombination_equal_error(self): + """Tests that the correct error is raised when compare() is called on invalid type""" + + H = qml.ops.LinearCombination([1], [Z(0)]) + with pytest.raises( + ValueError, + match=r"Can only compare a LinearCombination, and a LinearCombination/Observable/Tensor.", + ): + H.compare([[1, 0], [0, -1]]) + + @pytest.mark.parametrize(("H1", "H2", "res"), equal_LinearCombinations) + def test_LinearCombination_equal(self, H1, H2, res): + """Tests that equality can be checked between LinearCombinations""" + assert H1.compare(H2) == res + + @pytest.mark.parametrize(("H1", "H2", "H"), add_LinearCombinations) + def test_LinearCombination_add(self, H1, H2, H): + """Tests that LinearCombinations are added correctly""" + res = H1 + H2 + assert isinstance(res, LinearCombination) + assert H.compare(res) + + @pytest.mark.parametrize("H", add_zero_LinearCombinations) + def test_LinearCombination_add_zero(self, H): + """Tests that LinearCombinations can be added to zero""" + assert H.compare(H + 0) + assert H.compare(0 + H) + assert H.compare(H + 0.0) + assert H.compare(0.0 + H) + assert H.compare(H + 0e1) + assert H.compare(0e1 + H) + + @pytest.mark.parametrize(("coeff", "H", "res"), mul_LinearCombinations) + def test_LinearCombination_mul(self, coeff, H, res): + """Tests that scalars and LinearCombinations are multiplied correctly""" + assert res.compare(coeff * H) + assert res.compare(H * coeff) + + def test_LinearCombination_mul_coeff_cast(self): + """Test that the coefficients are correct when the type of the existing + and the new coefficients differ.""" + h = qml.ops.LinearCombination([0.5, 0.5], [X(0) @ X(0), Y(0) @ Y(1)]) + assert np.all(h.coeffs == np.array([0.5, 0.5])) + + @pytest.mark.parametrize(("H1", "H2", "H"), sub_LinearCombinations) + def test_LinearCombination_sub(self, H1, H2, H): + """Tests that LinearCombinations are subtracted correctly""" + assert H.compare(H1 - H2) + + def test_LinearCombination_tensor_matmul(self): + """Tests that a LinearCombination can be multiplied by a tensor.""" + H = qml.ops.LinearCombination([1.0, 1.0], [X(0), Y(0)]) + t = Z(1) @ Z(2) + out = H @ t + + expected = qml.ops.LinearCombination( + [1, 1], + [ + X(0) @ Z(1) @ Z(2), + Y(0) @ Z(1) @ Z(2), + ], + ) + assert expected.compare(out) + + @pytest.mark.parametrize(("H1", "H2", "H"), matmul_LinearCombinations) + def test_LinearCombination_matmul(self, H1, H2, H): + """Tests that LinearCombinations are tensored correctly""" + assert H.compare(H1 @ H2) + + @pytest.mark.parametrize(("H1", "H2", "H"), rmatmul_LinearCombinations) + def test_LinearCombination_rmatmul(self, H1, H2, H): + """Tests that LinearCombinations are tensored correctly when using __rmatmul__""" + assert H.compare(H1 @ H2) + + def test_arithmetic_errors(self): + """Tests that the arithmetic operations thrown the correct errors""" + H = qml.ops.LinearCombination([1], [Z(0)]) + A = [[1, 0], [0, -1]] + with pytest.raises(TypeError, match="unsupported operand type"): + _ = H @ A + with pytest.raises(TypeError, match="unsupported operand type"): + _ = A @ H + with pytest.raises(TypeError, match="unsupported operand type"): + _ = H + A + with pytest.raises(TypeError, match="can't multiply sequence by non-int"): + _ = H * A + with pytest.raises(TypeError, match="unsupported operand type"): + _ = H - A + with pytest.raises(TypeError, match="unsupported operand type"): + H += A + with pytest.raises(TypeError, match="unsupported operand type"): + H *= A + with pytest.raises(TypeError, match="unsupported operand type"): + H -= A + + def test_LinearCombination_queue_outside(self): + """Tests that LinearCombination are queued correctly when components are defined outside the recording context.""" + + H = X(1) + 3 * Z(0) @ Z(2) + Z(1) + + with qml.queuing.AnnotatedQueue() as q: + qml.Hadamard(wires=1) + X(wires=0) + qml.expval(H) + + assert len(q.queue) == 3 + assert isinstance(q.queue[0], qml.Hadamard) + assert isinstance(q.queue[1], qml.PauliX) + assert isinstance(q.queue[2], qml.measurements.MeasurementProcess) + queue_op = q.queue[2].obs + assert H.pauli_rep == queue_op.pauli_rep + + def test_LinearCombination_queue_inside(self): + """Tests that LinearCombination are queued correctly when components are instantiated inside the recording context.""" + assert qml.operation.active_new_opmath() + with qml.queuing.AnnotatedQueue() as q: + m = qml.expval(qml.ops.LinearCombination([1, 3, 1], [X(1), Z(0) @ Z(2), Z(1)])) + + assert len(q.queue) == 1 + assert q.queue[0] is m + + def test_terms(self): + """Tests that the terms representation is returned correctly.""" + coeffs = pnp.array([1.0, 2.0], requires_grad=True) + ops = [X(0), Z(1)] + h = qml.ops.LinearCombination(coeffs, ops) + c, o = h.terms() + assert isinstance(c, Iterable) + assert isinstance(o, list) + assert all(isinstance(item, np.ndarray) for item in c) + assert all(item.requires_grad for item in c) + assert all(isinstance(item, qml.operation.Operator) for item in o) + + def test_LinearCombination_no_empty_wire_list_error(self): + """Test that empty LinearCombination does not raise an empty wire error.""" + lincomb = qml.ops.LinearCombination([], []) + assert isinstance(lincomb, qml.ops.LinearCombination) + + def test_map_wires_no_grouping(self): + """Test the map_wires method.""" + coeffs = pnp.array([1.0, 2.0, -3.0], requires_grad=True) + ops = [X(0), Z(1), Y(2)] + h = qml.ops.LinearCombination(coeffs, ops) + wire_map = {0: 10, 1: 11, 2: 12} + mapped_h = h.map_wires(wire_map=wire_map) + final_obs = [X(10), Z(11), Y(12)] + assert h is not mapped_h + assert h.wires == Wires([0, 1, 2]) + assert mapped_h.wires == Wires([10, 11, 12]) + for obs1, obs2 in zip(mapped_h.ops, final_obs): + assert qml.equal(obs1, obs2) + for coeff1, coeff2 in zip(mapped_h.coeffs, h.coeffs): + assert coeff1 == coeff2 + + def test_map_wires_grouping(self): + """Test the map_wires method.""" + coeffs = pnp.array([1.0, 2.0, -3.0], requires_grad=True) + ops = [X(0), Z(1), Y(2)] + h = qml.ops.LinearCombination(coeffs, ops, grouping_type="qwc") + group_indices_before = copy(h.grouping_indices) + wire_map = {0: 10, 1: 11, 2: 12} + mapped_h = h.map_wires(wire_map=wire_map) + final_obs = [X(10), Z(11), Y(12)] + assert h is not mapped_h + assert h.wires == Wires([0, 1, 2]) + assert mapped_h.wires == Wires([10, 11, 12]) + for obs1, obs2 in zip(mapped_h.ops, final_obs): + assert qml.equal(obs1, obs2) + for coeff1, coeff2 in zip(mapped_h.coeffs, h.coeffs): + assert coeff1 == coeff2 + assert group_indices_before == mapped_h.grouping_indices + + def test_hermitian_tensor_prod(self): + """Test that the tensor product of a LinearCombination with Hermitian observable works.""" + tensor = X(0) @ X(1) + herm = qml.Hermitian([[1, 0], [0, 1]], wires=4) + + ham = qml.ops.LinearCombination([1.0, 1.0], [tensor, X(2)]) @ qml.ops.LinearCombination( + [1.0], [herm] + ) + assert isinstance(ham, qml.ops.LinearCombination) + + def test_diagonalizing_gates(self): + """Test that LinearCombination has valid diagonalizing gates""" + LC = qml.ops.LinearCombination([1.1, 2.2], [qml.X(0), qml.Z(0)]) + SUM = qml.sum(qml.s_prod(1.1, qml.X(0)), qml.s_prod(2.2, qml.Z(0))) + + assert LC.diagonalizing_gates() == SUM.diagonalizing_gates() + + def test_eigvals(self): + """Test that LinearCombination has valid eigvals""" + LC = qml.ops.LinearCombination([1.1, 2.2, 3.3], [qml.X(0), qml.Z(0), qml.Y(1)]) + + assert len(LC.overlapping_ops[0]) > 1 # will use one branch + assert len(LC.overlapping_ops[1]) == 1 # will use the other branch + + SUM = qml.sum( + qml.s_prod(1.1, qml.X(0)), qml.s_prod(2.2, qml.Z(0)), qml.s_prod(3.3, qml.Y(1)) + ) + + assert np.all(LC.eigvals() == SUM.eigvals()) + + +class TestLinearCombinationCoefficients: + """Test the creation of a LinearCombination""" + + @pytest.mark.parametrize("coeffs", [el[0] for el in COEFFS_PARAM_INTERFACE]) + def test_creation_different_coeff_types(self, coeffs): + """Check that LinearCombination's coefficients and data attributes are set correctly.""" + H = qml.ops.LinearCombination(coeffs, [X(0), Z(0)]) + assert np.allclose(coeffs, H.coeffs) + assert np.allclose([coeffs[i] for i in range(qml.math.shape(coeffs)[0])], H.data) + + @pytest.mark.parametrize("coeffs", [el[0] for el in COEFFS_PARAM_INTERFACE]) + def test_simplify(self, coeffs): + """Test that simplify works with different coefficient types.""" + H1 = qml.ops.LinearCombination(coeffs, [X(0), Z(1)]) + H2 = qml.ops.LinearCombination(coeffs, [X(0), qml.Identity(0) @ Z(1)]) + H2 = H2.simplify() + assert H1.compare(H2) + assert qml.math.allclose(H1.data, H2.data) + + # TODO: increase coverage + def test_operands(self): + op = qml.ops.LinearCombination([1.1, 2.2], [X(0), Z(0)]) + assert op.operands == (qml.s_prod(1.1, X(0)), qml.s_prod(2.2, Z(0))) + + +@pytest.mark.tf +class TestLinearCombinationArithmeticTF: + """Tests creation of LinearCombinations using arithmetic + operations with TensorFlow tensor coefficients.""" + + def test_LinearCombination_equal(self): + """Tests equality""" + coeffs = tf.Variable([0.5, -1.6]) + obs = [X(0), Y(1)] + H1 = qml.ops.LinearCombination(coeffs, obs) + + coeffs2 = tf.Variable([-1.6, 0.5]) + obs2 = [Y(1), X(0)] + H2 = qml.ops.LinearCombination(coeffs2, obs2) + + assert H1.compare(H2) + + def test_LinearCombination_add(self): + """Tests that LinearCombinations are added correctly""" + coeffs = tf.Variable([0.5, -1.5]) + obs = [X(0), Y(1)] + H1 = qml.ops.LinearCombination(coeffs, obs) + + coeffs2 = tf.Variable([0.5, -0.5]) + H2 = qml.ops.LinearCombination(coeffs2, obs) + + coeffs_expected = tf.Variable([1.0, -2.0]) + H = qml.ops.LinearCombination(coeffs_expected, obs) + + assert H.compare(H1 + H2) + + def test_LinearCombination_sub(self): + """Tests that LinearCombinations are subtracted correctly""" + coeffs = tf.constant([1.0, -2.0]) + obs = [X(0), Y(1)] + H1 = qml.ops.LinearCombination(coeffs, obs) + + coeffs2 = tf.constant([0.5, -0.5]) + H2 = qml.ops.LinearCombination(coeffs2, obs) + + coeffs_expected = tf.constant([0.5, -1.5]) + H = qml.ops.LinearCombination(coeffs_expected, obs) + + assert H.compare(H1 - H2) + + def test_LinearCombination_matmul(self): + """Tests that LinearCombinations are tensored correctly""" + + coeffs = tf.Variable([1.0, 2.0]) + obs = [X(0), Y(1)] + H1 = qml.ops.LinearCombination(coeffs, obs) + + coeffs2 = tf.Variable([-1.0, -2.0]) + obs2 = [X(2), Y(3)] + H2 = qml.ops.LinearCombination(coeffs2, obs2) + + coeffs_expected = tf.Variable([-4.0, -2.0, -2.0, -1.0]) + obs_expected = [ + qml.prod(Y(1), Y(3)), + qml.prod(X(0), Y(3)), + qml.prod(X(2), Y(1)), + qml.prod(X(0), X(2)), + ] + H = qml.ops.LinearCombination(coeffs_expected, obs_expected) + + assert H.compare(H1 @ H2) + + +@pytest.mark.torch +class TestLinearCombinationArithmeticTorch: + """Tests creation of LinearCombinations using arithmetic + operations with torch tensor coefficients.""" + + def test_LinearCombination_equal(self): + """Tests equality""" + coeffs = torch.tensor([0.5, -1.6]) + obs = [X(0), Y(1)] + H1 = qml.ops.LinearCombination(coeffs, obs) + + coeffs2 = torch.tensor([-1.6, 0.5]) + obs2 = [Y(1), X(0)] + H2 = qml.ops.LinearCombination(coeffs2, obs2) + + assert H1.compare(H2) + + def test_LinearCombination_add(self): + """Tests that LinearCombinations are added correctly""" + coeffs = torch.tensor([0.5, -1.6]) + obs = [X(0), Y(1)] + H1 = qml.ops.LinearCombination(coeffs, obs) + + coeffs2 = torch.tensor([0.5, -0.4]) + H2 = qml.ops.LinearCombination(coeffs2, obs) + + coeffs_expected = torch.tensor([1.0, -2.0]) + H = qml.ops.LinearCombination(coeffs_expected, obs) + + assert H.compare(H1 + H2) + + def test_LinearCombination_sub(self): + """Tests that LinearCombinations are subtracted correctly""" + coeffs = torch.tensor([1.0, -2.0]) + obs = [X(0), Y(1)] + H1 = qml.ops.LinearCombination(coeffs, obs) + + coeffs2 = torch.tensor([0.5, -0.4]) + H2 = qml.ops.LinearCombination(coeffs2, obs) + + coeffs_expected = torch.tensor([0.5, -1.6]) + H = qml.ops.LinearCombination(coeffs_expected, obs) + + assert H.compare(H1 - H2) + + H1 -= H2 + assert H.compare(H1) + + def test_LinearCombination_matmul(self): + """Tests that LinearCombinations are tensored correctly""" + + coeffs = torch.tensor([1.0, 2.0]) + obs = [X(0), Y(1)] + H1 = qml.ops.LinearCombination(coeffs, obs) + + coeffs2 = torch.tensor([-1.0, -2.0]) + obs2 = [X(2), Y(3)] + H2 = qml.ops.LinearCombination(coeffs2, obs2) + + coeffs_expected = torch.tensor([-4.0, -2.0, -2.0, -1.0]) + obs_expected = [ + qml.prod(Y(1), Y(3)), + qml.prod(X(0), Y(3)), + qml.prod(X(2), Y(1)), + qml.prod(X(0), X(2)), + ] + H = qml.ops.LinearCombination(coeffs_expected, obs_expected) + + assert H.compare(H1 @ H2) + + +@pytest.mark.autograd +class TestLinearCombinationArithmeticAutograd: + """Tests creation of LinearCombinations using arithmetic + operations with autograd tensor coefficients.""" + + def test_LinearCombination_equal(self): + """Tests equality""" + coeffs = pnp.array([0.5, -1.6]) + obs = [X(0), Y(1)] + H1 = qml.ops.LinearCombination(coeffs, obs) + + coeffs2 = pnp.array([-1.6, 0.5]) + obs2 = [Y(1), X(0)] + H2 = qml.ops.LinearCombination(coeffs2, obs2) + + assert H1.compare(H2) + + def test_LinearCombination_add(self): + """Tests that LinearCombinations are added correctly""" + coeffs = pnp.array([0.5, -1.5]) + obs = [X(0), Y(1)] + H1 = qml.ops.LinearCombination(coeffs, obs) + + coeffs2 = pnp.array([0.5, -0.5]) + H2 = qml.ops.LinearCombination(coeffs2, obs) + + coeffs_expected = pnp.array([1.0, -2.0]) + H = qml.ops.LinearCombination(coeffs_expected, obs) + + assert H.compare(H1 + H2) + + def test_LinearCombination_sub(self): + """Tests that LinearCombinations are subtracted correctly""" + coeffs = pnp.array([1.0, -2.0]) + obs = [X(0), Y(1)] + H1 = qml.ops.LinearCombination(coeffs, obs) + + coeffs2 = pnp.array([0.5, -0.5]) + H2 = qml.ops.LinearCombination(coeffs2, obs) + + coeffs_expected = pnp.array([0.5, -1.5]) + H = qml.ops.LinearCombination(coeffs_expected, obs) + + assert H.compare(H1 - H2) + + def test_LinearCombination_matmul(self): + """Tests that LinearCombinations are tensored correctly""" + coeffs = pnp.array([1.0, 2.0]) + obs = [X(0), Y(1)] + H1 = qml.ops.LinearCombination(coeffs, obs) + + coeffs2 = pnp.array([-1.0, -2.0]) + obs2 = [X(2), Y(3)] + H2 = qml.ops.LinearCombination(coeffs2, obs2) + + coeffs_expected = pnp.array([-4.0, -2.0, -2.0, -1.0]) + obs_expected = [ + qml.prod(Y(1), Y(3)), + qml.prod(X(0), Y(3)), + qml.prod(X(2), Y(1)), + qml.prod(X(0), X(2)), + ] + H = qml.ops.LinearCombination(coeffs_expected, obs_expected) + + assert H.compare(H1 @ H2) + + +class TestLinearCombinationSparseMatrix: + """Tests for sparse matrix representation.""" + + @pytest.mark.parametrize( + ("coeffs", "obs", "wires", "ref_matrix"), + [ + ( + [1, -0.45], + [qml.prod(Z(0), Z(1)), qml.prod(Y(0), Z(1))], + None, + np.array( + [ + [1.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.45j, 0.0 + 0.0j], + [0.0 + 0.0j, -1.0 + 0.0j, 0.0 + 0.0j, 0.0 - 0.45j], + [0.0 - 0.45j, 0.0 + 0.0j, -1.0 + 0.0j, 0.0 + 0.0j], + [0.0 + 0.0j, 0.0 + 0.45j, 0.0 + 0.0j, 1.0 + 0.0j], + ] + ), + ), + ( + [0.1], + [qml.prod(Z("b"), X("a"))], + ["a", "c", "b"], + np.array( + [ + [ + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.1 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + ], + [ + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + -0.1 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + ], + [ + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.1 + 0.0j, + 0.0 + 0.0j, + ], + [ + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + -0.1 + 0.0j, + ], + [ + 0.1 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + ], + [ + 0.0 + 0.0j, + -0.1 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + ], + [ + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.1 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + ], + [ + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + -0.1 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + ], + ] + ), + ), + ( + [0.21, -0.78, 0.52], + [ + qml.prod(Z(0), Z(1)), + qml.prod(X(0), Z(1)), + qml.prod(Y(0), Z(1)), + ], + None, + np.array( + [ + [0.21 + 0.0j, 0.0 + 0.0j, -0.78 - 0.52j, 0.0 + 0.0j], + [0.0 + 0.0j, -0.21 + 0.0j, 0.0 + 0.0j, 0.78 + 0.52j], + [-0.78 + 0.52j, 0.0 + 0.0j, -0.21 + 0.0j, 0.0 + 0.0j], + [0.0 + 0.0j, 0.78 - 0.52j, 0.0 + 0.0j, 0.21 + 0.0j], + ] + ), + ), + ], + ) + def test_sparse_matrix(self, coeffs, obs, wires, ref_matrix): + """Tests that sparse_LinearCombination returns a correct sparse matrix""" + H = qml.ops.LinearCombination(coeffs, obs) + + sparse_matrix = H.sparse_matrix(wire_order=wires) + + assert np.allclose(sparse_matrix.toarray(), ref_matrix) + + def test_sparse_format(self): + """Tests that sparse_LinearCombination returns a scipy.sparse.csr_matrix object""" + + coeffs = [-0.25, 0.75] + obs = [ + X(wires=[0]) @ Z(wires=[1]), + Y(wires=[0]) @ Z(wires=[1]), + ] + H = qml.ops.LinearCombination(coeffs, obs) + + sparse_matrix = H.sparse_matrix() + + assert isinstance(sparse_matrix, scipy.sparse.csr_matrix) + + +@pytest.mark.jax +class TestLinearCombinationArithmeticJax: + """Tests creation of LinearCombinations using arithmetic + operations with jax tensor coefficients.""" + + def test_LinearCombination_equal(self): + """Tests equality""" + coeffs = jnp.array([0.5, -1.6]) + obs = [X(0), Y(1)] + H1 = qml.ops.LinearCombination(coeffs, obs) + + coeffs2 = jnp.array([-1.6, 0.5]) + obs2 = [Y(1), X(0)] + H2 = qml.ops.LinearCombination(coeffs2, obs2) + + assert H1.compare(H2) + + def test_LinearCombination_add(self): + """Tests that LinearCombinations are added correctly""" + coeffs = jnp.array([0.5, -1.5]) + obs = [X(0), Y(1)] + H1 = qml.ops.LinearCombination(coeffs, obs) + + coeffs2 = jnp.array([0.5, -0.5]) + H2 = qml.ops.LinearCombination(coeffs2, obs) + + coeffs_expected = jnp.array([1.0, -2.0]) + H = qml.ops.LinearCombination(coeffs_expected, obs) + + assert H.compare(H1 + H2) + + def test_LinearCombination_sub(self): + """Tests that LinearCombinations are subtracted correctly""" + + coeffs = jnp.array([1.0, -2.0]) + obs = [X(0), Y(1)] + H1 = qml.ops.LinearCombination(coeffs, obs) + + coeffs2 = jnp.array([0.5, -0.4]) + H2 = qml.ops.LinearCombination(coeffs2, obs) + + coeffs_expected = jnp.array([0.5, -1.6]) + H = qml.ops.LinearCombination(coeffs_expected, obs) + + assert H.compare(H1 - H2) + + H1 -= H2 + assert H.compare(H1) + + def test_LinearCombination_matmul(self): + """Tests that LinearCombinations are tensored correctly""" + + coeffs = jnp.array([1.0, 2.0]) + obs = [X(0), Y(1)] + H1 = qml.ops.LinearCombination(coeffs, obs) + + coeffs2 = jnp.array([-1.0, -2.0]) + obs2 = [X(2), Y(3)] + H2 = qml.ops.LinearCombination(coeffs2, obs2) + + coeffs_expected = jnp.array([-4.0, -2.0, -2.0, -1.0]) + obs_expected = [ + qml.prod(Y(1), Y(3)), + qml.prod(X(0), Y(3)), + qml.prod(X(2), Y(1)), + qml.prod(X(0), X(2)), + ] + H = qml.ops.LinearCombination(coeffs_expected, obs_expected) + + assert H.compare(H1 @ H2) + + +class TestGrouping: + """Tests for the grouping functionality""" + + def test_indentities_preserved(self): + """Tests that the grouping indices do not drop identity terms when the wire order is nonstandard.""" + + obs = [Z(1), Z(0), qml.Identity(0)] + + H = qml.ops.LinearCombination([1.0, 1.0, 1.0], obs, grouping_type="qwc") + assert H.grouping_indices == ((0, 1, 2),) + + def test_grouping_is_correct_kwarg(self): + """Basic test checking that grouping with a kwarg works as expected""" + a = X(0) + b = X(1) + c = Z(0) + obs = [a, b, c] + coeffs = [1.0, 2.0, 3.0] + + H = qml.ops.LinearCombination(coeffs, obs, grouping_type="qwc") + assert H.grouping_indices == ((0, 1), (2,)) + + def test_grouping_is_correct_compute_grouping(self): + """Basic test checking that grouping with compute_grouping works as expected""" + a = X(0) + b = X(1) + c = Z(0) + obs = [a, b, c] + coeffs = [1.0, 2.0, 3.0] + + H = qml.ops.LinearCombination(coeffs, obs, grouping_type="qwc") + H.compute_grouping() + assert H.grouping_indices == ((0, 1), (2,)) + + def test_set_grouping(self): + """Test that we can set grouping indices.""" + H = qml.ops.LinearCombination([1.0, 2.0, 3.0], [X(0), X(1), Z(0)]) + H.grouping_indices = [[0, 1], [2]] + + assert H.grouping_indices == ((0, 1), (2,)) + + def test_set_grouping_error(self): + """Test that grouping indices are validated.""" + H = qml.ops.LinearCombination([1.0, 2.0, 3.0], [X(0), X(1), Z(0)]) + + with pytest.raises(ValueError, match="The grouped index value"): + H.grouping_indices = [[3, 1], [2]] + + with pytest.raises(ValueError, match="The grouped index value"): + H.grouping_indices = "a" + + def test_grouping_for_non_groupable_LinearCombinations(self): + """Test that grouping is computed correctly, even if no observables commute""" + a = X(0) + b = Y(0) + c = Z(0) + obs = [a, b, c] + coeffs = [1.0, 2.0, 3.0] + + H = qml.ops.LinearCombination(coeffs, obs, grouping_type="qwc") + assert H.grouping_indices == ((0,), (1,), (2,)) + + def test_grouping_is_reset_when_simplifying(self): + """Tests that calling simplify() resets the grouping""" + obs = [X(0), X(1), Z(0)] + coeffs = [1.0, 2.0, 3.0] + + H = qml.ops.LinearCombination(coeffs, obs, grouping_type="qwc") + assert H.grouping_indices is not None + + H = H.simplify() + assert H.grouping_indices is None + + def test_grouping_does_not_alter_queue(self): + """Tests that grouping is invisible to the queue.""" + a = X(0) + b = X(1) + c = Z(0) + obs = [a, b, c] + coeffs = [1.0, 2.0, 3.0] + + with qml.queuing.AnnotatedQueue() as q: + H = qml.ops.LinearCombination(coeffs, obs, grouping_type="qwc") + + assert q.queue == [H] + + def test_grouping_method_can_be_set(self): + r"""Tests that the grouping method can be controlled by kwargs. + This is done by changing from default to 'rlf' and checking the result.""" + a = X(0) + b = X(1) + c = Z(0) + obs = [a, b, c] + coeffs = [1.0, 2.0, 3.0] + + # compute grouping during construction + H2 = qml.ops.LinearCombination(coeffs, obs, grouping_type="qwc", method="lf") + assert H2.grouping_indices == ((2, 1), (0,)) + + # compute grouping separately + H3 = qml.ops.LinearCombination(coeffs, obs, grouping_type=None) + H3.compute_grouping(method="lf") + assert H3.grouping_indices == ((2, 1), (0,)) + + def test_grouping_with_duplicate_terms(self): + """Test that the grouping indices are correct when the LinearCombination has duplicate + operators.""" + a = X(0) + b = X(1) + c = Z(0) + d = X(0) + e = Z(0) + obs = [a, b, c, d, e] + coeffs = [1.0, 2.0, 3.0, 4.0, 5.0] + + # compute grouping during construction + H2 = qml.ops.LinearCombination(coeffs, obs, grouping_type="qwc") + + assert H2.grouping_indices == ((0, 1, 3), (2, 4)) + # Following assertions are to check that grouping does not mutate the list of ops/coeffs + assert H2.coeffs == coeffs + assert H2.ops == obs + + +class TestLinearCombinationEvaluation: + """Test the usage of a LinearCombination as an observable""" + + @pytest.mark.parametrize("coeffs, param, interface", COEFFS_PARAM_INTERFACE) + def test_vqe_forward_different_coeff_types(self, coeffs, param, interface): + """Check that manually splitting a LinearCombination expectation has the same + result as passing the LinearCombination as an observable""" + device = qml.device("default.qubit", wires=2) + H = qml.ops.LinearCombination(coeffs, [X(0), Z(0)]) + + @qml.qnode(device, interface=interface) + def circuit(): + qml.RX(param, wires=0) + qml.RY(param, wires=0) + return qml.expval(H) + + @qml.qnode(device, interface=interface) + def node1(): + qml.RX(param, wires=0) + qml.RY(param, wires=0) + return qml.expval(X(0)) + + @qml.qnode(device, interface=interface) + def node2(): + qml.RX(param, wires=0) + qml.RY(param, wires=0) + return qml.expval(Z(0)) + + res = circuit() + res_expected = coeffs[0] * node1() + coeffs[1] * node2() + assert np.isclose(res, res_expected) + + def test_simplify_reduces_tape_parameters(self): + """Test that simplifying a LinearCombination reduces the number of parameters on a tape""" + device = qml.device("default.qubit", wires=2) + + @qml.qnode(device) + def circuit(): + qml.RY(0.1, wires=0) + return qml.expval(qml.ops.LinearCombination([1.0, 2.0], [X(1), X(1)], simplify=True)) + + circuit() + pars = circuit.qtape.get_parameters(trainable_only=False) + # simplify worked and added 1. and 2. + assert pars == [0.1, 3.0] + + +class TestLinearCombinationDifferentiation: + """Test that the LinearCombination coefficients are differentiable""" + + @pytest.mark.parametrize("simplify", [True, False]) + @pytest.mark.parametrize("group", [None, "qwc"]) + def test_trainable_coeffs_paramshift(self, simplify, group): + """Test the parameter-shift method by comparing the differentiation of linearly combined subcircuits + with the differentiation of a LinearCombination expectation""" + coeffs = pnp.array([-0.05, 0.17], requires_grad=True) + param = pnp.array(1.7, requires_grad=True) + + # differentiating a circuit with measurement expval(H) + @qml.qnode(dev, diff_method="parameter-shift") + def circuit(coeffs, param): + qml.RX(param, wires=0) + qml.RY(param, wires=0) + return qml.expval( + qml.ops.LinearCombination( + coeffs, + [X(0), Z(0)], + simplify=simplify, + grouping_type=group, + ) + ) + + grad_fn = qml.grad(circuit) + grad = grad_fn(coeffs, param) + + # differentiating a cost that combines circuits with + # measurements expval(Pauli) + half1 = qml.QNode(circuit1, dev, diff_method="parameter-shift") + half2 = qml.QNode(circuit2, dev, diff_method="parameter-shift") + + def combine(coeffs, param): + return coeffs[0] * half1(param) + coeffs[1] * half2(param) + + grad_fn_expected = qml.grad(combine) + grad_expected = grad_fn_expected(coeffs, param) + + assert np.allclose(grad[0], grad_expected[0]) + assert np.allclose(grad[1], grad_expected[1]) + + def test_nontrainable_coeffs_paramshift(self): + """Test the parameter-shift method if the coefficients are explicitly set non-trainable + by not passing them to the qnode.""" + coeffs = np.array([-0.05, 0.17]) + param = pnp.array(1.7, requires_grad=True) + + # differentiating a circuit with measurement expval(H) + @qml.qnode(dev, diff_method="parameter-shift") + def circuit(param): + qml.RX(param, wires=0) + qml.RY(param, wires=0) + return qml.expval( + qml.ops.LinearCombination( + coeffs, + [X(0), Z(0)], + ) + ) + + grad_fn = qml.grad(circuit) + grad = grad_fn(param) + + # differentiating a cost that combines circuits with + # measurements expval(Pauli) + half1 = qml.QNode(circuit1, dev, diff_method="parameter-shift") + half2 = qml.QNode(circuit2, dev, diff_method="parameter-shift") + + def combine(param): + return coeffs[0] * half1(param) + coeffs[1] * half2(param) + + grad_fn_expected = qml.grad(combine) + grad_expected = grad_fn_expected(param) + + assert np.allclose(grad, grad_expected) + + @pytest.mark.autograd + @pytest.mark.parametrize("simplify", [True, False]) + @pytest.mark.parametrize("group", [None, "qwc"]) + def test_trainable_coeffs_autograd(self, simplify, group): + """Test the autograd interface by comparing the differentiation of linearly combined subcircuits + with the differentiation of a LinearCombination expectation""" + coeffs = pnp.array([-0.05, 0.17], requires_grad=True) + param = pnp.array(1.7, requires_grad=True) + + # differentiating a circuit with measurement expval(H) + @qml.qnode(dev, interface="autograd") + def circuit(coeffs, param): + qml.RX(param, wires=0) + qml.RY(param, wires=0) + return qml.expval( + qml.ops.LinearCombination( + coeffs, + [X(0), Z(0)], + simplify=simplify, + grouping_type=group, + ) + ) + + grad_fn = qml.grad(circuit) + grad = grad_fn(coeffs, param) + + # differentiating a cost that combines circuits with + # measurements expval(Pauli) + half1 = qml.QNode(circuit1, dev, interface="autograd") + half2 = qml.QNode(circuit2, dev, interface="autograd") + + def combine(coeffs, param): + return coeffs[0] * half1(param) + coeffs[1] * half2(param) + + grad_fn_expected = qml.grad(combine) + grad_expected = grad_fn_expected(coeffs, param) + + assert np.allclose(grad[0], grad_expected[0]) + assert np.allclose(grad[1], grad_expected[1]) + + @pytest.mark.autograd + def test_nontrainable_coeffs_autograd(self): + """Test the autograd interface if the coefficients are explicitly set non-trainable""" + coeffs = pnp.array([-0.05, 0.17], requires_grad=False) + param = pnp.array(1.7, requires_grad=True) + + # differentiating a circuit with measurement expval(H) + @qml.qnode(dev, interface="autograd") + def circuit(coeffs, param): + qml.RX(param, wires=0) + qml.RY(param, wires=0) + return qml.expval(qml.ops.LinearCombination(coeffs, [X(0), Z(0)])) + + grad_fn = qml.grad(circuit) + grad = grad_fn(coeffs, param) + + # differentiating a cost that combines circuits with + # measurements expval(Pauli) + half1 = qml.QNode(circuit1, dev, interface="autograd") + half2 = qml.QNode(circuit2, dev, interface="autograd") + + def combine(coeffs, param): + return coeffs[0] * half1(param) + coeffs[1] * half2(param) + + grad_fn_expected = qml.grad(combine) + grad_expected = grad_fn_expected(coeffs, param) + + assert np.allclose(grad, grad_expected) + + @pytest.mark.jax + @pytest.mark.parametrize("simplify", [True, False]) + @pytest.mark.parametrize("group", [None, "qwc"]) + def test_trainable_coeffs_jax(self, simplify, group): + """Test the jax interface by comparing the differentiation of linearly + combined subcircuits with the differentiation of a LinearCombination expectation""" + + coeffs = jnp.array([-0.05, 0.17]) + param = jnp.array(1.7) + + # differentiating a circuit with measurement expval(H) + @qml.qnode(dev, interface="jax", diff_method="backprop") + def circuit(coeffs, param): + qml.RX(param, wires=0) + qml.RY(param, wires=0) + return qml.expval( + qml.ops.LinearCombination( + coeffs, + [X(0), Z(0)], + simplify=simplify, + grouping_type=group, + ) + ) + + grad_fn = jax.grad(circuit) + grad = grad_fn(coeffs, param) + + # differentiating a cost that combines circuits with + # measurements expval(Pauli) + half1 = qml.QNode(circuit1, dev, interface="jax", diff_method="backprop") + half2 = qml.QNode(circuit2, dev, interface="jax", diff_method="backprop") + + def combine(coeffs, param): + return coeffs[0] * half1(param) + coeffs[1] * half2(param) + + grad_fn_expected = jax.grad(combine) + grad_expected = grad_fn_expected(coeffs, param) + + assert np.allclose(grad[0], grad_expected[0]) + assert np.allclose(grad[1], grad_expected[1]) + + # pylint: disable=superfluous-parens + @pytest.mark.jax + def test_nontrainable_coeffs_jax(self): + """Test the jax interface if the coefficients are explicitly set non-trainable""" + coeffs = np.array([-0.05, 0.17]) + param = jnp.array(1.7) + + # differentiating a circuit with measurement expval(H) + @qml.qnode(dev, interface="jax", diff_method="backprop") + def circuit(coeffs, param): + qml.RX(param, wires=0) + qml.RY(param, wires=0) + return qml.expval(qml.ops.LinearCombination(coeffs, [X(0), Z(0)])) + + grad_fn = jax.grad(circuit, argnums=1) + grad = grad_fn(coeffs, param) + + # differentiating a cost that combines circuits with + # measurements expval(Pauli) + half1 = qml.QNode(circuit1, dev, interface="jax", diff_method="backprop") + half2 = qml.QNode(circuit2, dev, interface="jax", diff_method="backprop") + + def combine(coeffs, param): + return coeffs[0] * half1(param) + coeffs[1] * half2(param) + + grad_fn_expected = jax.grad(combine, argnums=1) + grad_expected = grad_fn_expected(coeffs, param) + + assert np.allclose(grad, grad_expected) + + @pytest.mark.torch + @pytest.mark.parametrize("simplify", [True, False]) + @pytest.mark.parametrize("group", [None, "qwc"]) + def test_trainable_coeffs_torch_simplify(self, group, simplify): + """Test the torch interface by comparing the differentiation of linearly combined subcircuits + with the differentiation of a LinearCombination expectation""" + coeffs = torch.tensor([-0.05, 0.17], requires_grad=True) + param = torch.tensor(1.7, requires_grad=True) + + # differentiating a circuit with measurement expval(H) + @qml.qnode(dev, interface="torch", diff_method="backprop") + def circuit(coeffs, param): + qml.RX(param, wires=0) + qml.RY(param, wires=0) + return qml.expval( + qml.ops.LinearCombination( + coeffs, + [X(0), Z(0)], + simplify=simplify, + grouping_type=group, + ) + ) + + res = circuit(coeffs, param) + res.backward() # pylint:disable=no-member + grad = (coeffs.grad, param.grad) + + # differentiating a cost that combines circuits with + # measurements expval(Pauli) + + # we need to create new tensors here + coeffs2 = torch.tensor([-0.05, 0.17], requires_grad=True) + param2 = torch.tensor(1.7, requires_grad=True) + + half1 = qml.QNode(circuit1, dev, interface="torch", diff_method="backprop") + half2 = qml.QNode(circuit2, dev, interface="torch", diff_method="backprop") + + def combine(coeffs, param): + return coeffs[0] * half1(param) + coeffs[1] * half2(param) + + res_expected = combine(coeffs2, param2) + res_expected.backward() + grad_expected = (coeffs2.grad, param2.grad) + + assert qml.math.allclose(grad[0], grad_expected[0]) + assert qml.math.allclose(grad[1], grad_expected[1]) + + @pytest.mark.torch + def test_nontrainable_coeffs_torch(self): + """Test the torch interface if the coefficients are explicitly set non-trainable""" + coeffs = torch.tensor([-0.05, 0.17], requires_grad=False) + param = torch.tensor(1.7, requires_grad=True) + + # differentiating a circuit with measurement expval(H) + @qml.qnode(dev, interface="torch", diff_method="backprop") + def circuit(coeffs, param): + qml.RX(param, wires=0) + qml.RY(param, wires=0) + return qml.expval( + qml.ops.LinearCombination( + coeffs, + [X(0), Z(0)], + ) + ) + + res = circuit(coeffs, param) + res.backward() # pylint:disable=no-member + + # differentiating a cost that combines circuits with + # measurements expval(Pauli) + + # we need to create new tensors here + coeffs2 = torch.tensor([-0.05, 0.17], requires_grad=False) + param2 = torch.tensor(1.7, requires_grad=True) + + half1 = qml.QNode(circuit1, dev, interface="torch", diff_method="backprop") + half2 = qml.QNode(circuit2, dev, interface="torch", diff_method="backprop") + + def combine(coeffs, param): + return coeffs[0] * half1(param) + coeffs[1] * half2(param) + + res_expected = combine(coeffs2, param2) + res_expected.backward() + + assert coeffs.grad is None + assert np.allclose(param.grad, param2.grad) + + @pytest.mark.tf + @pytest.mark.parametrize("simplify", [True, False]) + @pytest.mark.parametrize("group", [None, "qwc"]) + def test_trainable_coeffs_tf(self, simplify, group): + """Test the tf interface by comparing the differentiation of linearly combined subcircuits + with the differentiation of a LinearCombination expectation""" + coeffs = tf.Variable([-0.05, 0.17], dtype=tf.double) + param = tf.Variable(1.7, dtype=tf.double) + + # differentiating a circuit with measurement expval(H) + @qml.qnode(dev, interface="tf", diff_method="backprop") + def circuit(coeffs, param): + qml.RX(param, wires=0) + qml.RY(param, wires=0) + return qml.expval( + qml.ops.LinearCombination( + coeffs, + [X(0), Z(0)], + simplify=simplify, + grouping_type=group, + ) + ) + + with tf.GradientTape() as tape: + res = circuit(coeffs, param) + grad = tape.gradient(res, [coeffs, param]) + + # differentiating a cost that combines circuits with + # measurements expval(Pauli) + + # we need to create new tensors here + coeffs2 = tf.Variable([-0.05, 0.17], dtype=tf.double) + param2 = tf.Variable(1.7, dtype=tf.double) + half1 = qml.QNode(circuit1, dev, interface="tf", diff_method="backprop") + half2 = qml.QNode(circuit2, dev, interface="tf", diff_method="backprop") + + def combine(coeffs, param): + return coeffs[0] * half1(param) + coeffs[1] * half2(param) + + with tf.GradientTape() as tape2: + res_expected = combine(coeffs2, param2) + grad_expected = tape2.gradient(res_expected, [coeffs2, param2]) + + assert np.allclose(grad[0], grad_expected[0]) + assert np.allclose(grad[1], grad_expected[1]) + + @pytest.mark.tf + def test_nontrainable_coeffs_tf(self): + """Test the tf interface if the coefficients are explicitly set non-trainable""" + + coeffs = tf.constant([-0.05, 0.17], dtype=tf.double) + param = tf.Variable(1.7, dtype=tf.double) + + # differentiating a circuit with measurement expval(H) + @qml.qnode(dev, interface="tf", diff_method="backprop") + def circuit(coeffs, param): + qml.RX(param, wires=0) + qml.RY(param, wires=0) + return qml.expval( + qml.ops.LinearCombination( + coeffs, + [X(0), Z(0)], + ) + ) + + with tf.GradientTape() as tape: + res = circuit(coeffs, param) + grad = tape.gradient(res, [coeffs, param]) + + # differentiating a cost that combines circuits with + # measurements expval(Pauli) + + # we need to create new tensors here + coeffs2 = tf.constant([-0.05, 0.17], dtype=tf.double) + param2 = tf.Variable(1.7, dtype=tf.double) + half1 = qml.QNode(circuit1, dev, interface="tf", diff_method="backprop") + half2 = qml.QNode(circuit2, dev, interface="tf", diff_method="backprop") + + def combine(coeffs, param): + return coeffs[0] * half1(param) + coeffs[1] * half2(param) + + with tf.GradientTape() as tape2: + res_expected = combine(coeffs2, param2) + grad_expected = tape2.gradient(res_expected, [coeffs2, param2]) + + assert grad[0] is None + assert np.allclose(grad[1], grad_expected[1]) + + # TODO: update logic of adjoint differentiation to catch attempt to differentiate lincomb coeffs + @pytest.mark.xfail + def test_not_supported_by_adjoint_differentiation(self): + """Test that error is raised when attempting the adjoint differentiation method.""" + device = qml.device("default.qubit", wires=2) + + coeffs = pnp.array([-0.05, 0.17], requires_grad=True) + param = pnp.array(1.7, requires_grad=True) + + @qml.qnode(device, diff_method="adjoint") + def circuit(coeffs, param): + qml.RX(param, wires=0) + qml.RY(param, wires=0) + return qml.expval( + qml.ops.LinearCombination( + coeffs, + [X(0), Z(0)], + ) + ) + + grad_fn = qml.grad(circuit) + with pytest.raises( + qml.DeviceError, + match="not supported on adjoint", + ): + grad_fn(coeffs, param) diff --git a/tests/ops/op_math/test_pow_op.py b/tests/ops/op_math/test_pow_op.py index 934380030f3..37f99d61cbe 100644 --- a/tests/ops/op_math/test_pow_op.py +++ b/tests/ops/op_math/test_pow_op.py @@ -153,6 +153,28 @@ class CustomObs(qml.operation.Observable): base = CustomObs(wires=0) ob: Pow = power_method(base=base, z=-1.2) + assert isinstance(ob, Pow) + assert isinstance(ob, qml.operation.Operator) + assert not isinstance(ob, qml.operation.Operation) + assert not isinstance(ob, PowOperation) + + # Check some basic observable functionality + assert ob.compare(ob) + + # check the dir + assert "grad_recipe" not in dir(ob) + + @pytest.mark.usefixtures("use_legacy_opmath") + def test_observable_legacy_opmath(self, power_method): + """Test that when the base is an Observable, Adjoint will also inherit from Observable.""" + + class CustomObs(qml.operation.Observable): + num_wires = 1 + num_params = 0 + + base = CustomObs(wires=0) + ob: Pow = power_method(base=base, z=-1.2) + assert isinstance(ob, Pow) assert isinstance(ob, qml.operation.Operator) assert not isinstance(ob, qml.operation.Operation) @@ -236,9 +258,10 @@ def test_template_base(self, power_method): assert op.wires == qml.wires.Wires((0, 1)) assert op.num_wires == 2 + @pytest.mark.usefixtures("use_legacy_opmath") def test_hamiltonian_base(self, power_method): """Test pow initialization for a hamiltonian.""" - base = 2.0 * qml.PauliX(0) @ qml.PauliY(0) + qml.PauliZ("b") + base = qml.Hamiltonian([2.0, 1.0], [qml.PauliX(0) @ qml.PauliY(0), qml.PauliZ("b")]) op: Pow = power_method(base=base, z=3.4) @@ -349,6 +372,7 @@ def test_queue_category(self, power_method): op: Pow = power_method(base=qml.PauliX(0), z=3.5) assert op._queue_category == "_ops" # pylint: disable=protected-access + @pytest.mark.usefixtures("use_legacy_opmath") def test_queue_category_None(self, power_method): """Test that the queue category `None` for some observables carries over.""" op: Pow = power_method(base=qml.PauliX(0) @ qml.PauliY(1), z=-1.1) diff --git a/tests/ops/op_math/test_prod.py b/tests/ops/op_math/test_prod.py index 3fa5dcad38d..08b2be3a907 100644 --- a/tests/ops/op_math/test_prod.py +++ b/tests/ops/op_math/test_prod.py @@ -505,6 +505,7 @@ def qfunc(): assert qml.equal(prod_op, qml.PauliX(0)) assert not isinstance(prod_op, Prod) + @pytest.mark.xfail # this requirement has been lifted def test_prod_accepts_single_operator_but_Prod_does_not(self): """Tests that the prod wrapper can accept a single operator, and return it.""" @@ -958,6 +959,26 @@ def test_eigendecompostion(self): assert np.allclose(eig_vals, true_eigvals) assert np.allclose(eig_vecs, true_eigvecs) + def test_qutrit_eigvals(self): + """Test that the eigvals can be computed with qutrit observables.""" + + op1 = qml.GellMann(wires=0) + op2 = qml.GellMann(index=8, wires=1) + + prod_op = qml.prod(op1, op2) + eigs = prod_op.eigvals() + + mat_eigs = np.linalg.eigvals(prod_op.matrix()) + + sorted_eigs = np.sort(eigs) + sorted_mat_eigs = np.sort(mat_eigs) + assert qml.math.allclose(sorted_eigs, sorted_mat_eigs) + + # pylint: disable=import-outside-top-level + from pennylane.ops.functions.assert_valid import _check_eigendecomposition + + _check_eigendecomposition(prod_op) + def test_eigen_caching(self): """Test that the eigendecomposition is stored in cache.""" diag_prod_op = Prod(qml.PauliZ(wires=0), qml.PauliZ(wires=1)) diff --git a/tests/ops/op_math/test_sum.py b/tests/ops/op_math/test_sum.py index f503b5a8048..5ed97b879d6 100644 --- a/tests/ops/op_math/test_sum.py +++ b/tests/ops/op_math/test_sum.py @@ -299,7 +299,6 @@ def test_eigen_caching(self): assert np.allclose(eig_vals, cached_vals) assert np.allclose(eig_vecs, cached_vecs) - qml.operation.enable_new_opmath() SUM_REPR = ( (qml.sum(X(0), Y(1), Z(2)), "X(0) + Y(1) + Z(2)"), (X(0) + X(1) + X(2), "X(0) + X(1) + X(2)"), @@ -314,14 +313,12 @@ def test_eigen_caching(self): "(\n 0.5 * (X(0) @ (0.5 * X(1)))\n + 0.7 * X(1)\n + 0.8 * ((X(0) @ Y(1)) @ Z(1))\n)", ), ) - qml.operation.disable_new_opmath() @pytest.mark.parametrize("op, repr_true", SUM_REPR) def test_repr(self, op, repr_true): """Test the string representation of Sum instances""" assert repr(op) == repr_true - qml.operation.enable_new_opmath() SUM_REPR_EVAL = ( X(0) + Y(1) + Z(2), # single line output 0.5 * X(0) + 3.5 * Y(1) + 10 * Z(2), # single line output @@ -331,14 +328,11 @@ def test_repr(self, op, repr_true): + 1000000000 * Z(2), # multiline output # qml.sum(*[0.5 * X(i) for i in range(10)]) # multiline output needs fixing of https://github.com/PennyLaneAI/pennylane/issues/5162 before working ) - qml.operation.disable_new_opmath() @pytest.mark.parametrize("op", SUM_REPR_EVAL) def test_eval_sum(self, op): """Test that string representations of Sum can be evaluated and yield the same operator""" - qml.operation.enable_new_opmath() assert qml.equal(eval(repr(op)), op) - qml.operation.disable_new_opmath() class TestMatrix: @@ -846,6 +840,25 @@ def test_flatten_unflatten_with_groups(self, grouping_type, method): assert old_coeffs == new_coeffs assert old_ops == new_ops + def test_grouping_indices_setter(self): + """Test that grouping indices can be set""" + H = qml.sum(*[qml.X("a"), qml.X("b"), qml.Y("b")]) + + H.grouping_indices = [[0, 1], [2]] + + assert isinstance(H.grouping_indices, tuple) + assert H.grouping_indices == ((0, 1), (2,)) + + def test_grouping_indices_setter_error(self): + """Test that setting incompatible indices raises an error""" + H = qml.sum(*[qml.X("a"), qml.X("b"), qml.Y("b")]) + + with pytest.raises( + ValueError, + match="The grouped index value needs to be a tuple of tuples of integers between 0", + ): + H.grouping_indices = [[0, 1, 3], [2]] + class TestSimplify: """Test Sum simplify method and depth property.""" diff --git a/tests/ops/qubit/test_hamiltonian.py b/tests/ops/qubit/test_hamiltonian.py index 4a72974a90c..0a3ab5104d9 100644 --- a/tests/ops/qubit/test_hamiltonian.py +++ b/tests/ops/qubit/test_hamiltonian.py @@ -24,9 +24,10 @@ import pennylane as qml from pennylane import numpy as pnp -from pennylane.ops.qubit.hamiltonian import Hamiltonian + from pennylane.wires import Wires + # Make test data in different interfaces, if installed COEFFS_PARAM_INTERFACE = [ ([-0.05, 0.17], 1.7, "autograd"), @@ -66,559 +67,598 @@ COEFFS = [(0.5, 1.2, -0.7), (2.2, -0.2, 0.0), (0.33,)] -OBSERVABLES = [ - (qml.PauliZ(0), qml.PauliY(0), qml.PauliZ(1)), - (qml.PauliX(0) @ qml.PauliZ(1), qml.PauliY(0) @ qml.PauliZ(1), qml.PauliZ(1)), - (qml.Hermitian(H_TWO_QUBITS, [0, 1]),), -] +with qml.operation.disable_new_opmath_cm(): -valid_hamiltonians = [ - ((1.0,), (qml.Hermitian(H_TWO_QUBITS, [0, 1]),)), - ((-0.8,), (qml.PauliZ(0),)), - ((0.6,), (qml.PauliX(0) @ qml.PauliX(1),)), - ((0.5, -1.6), (qml.PauliX(0), qml.PauliY(1))), - ((0.5, -1.6), (qml.PauliX(1), qml.PauliY(1))), - ((0.5, -1.6), (qml.PauliX("a"), qml.PauliY("b"))), - ((1.1, -0.4, 0.333), (qml.PauliX(0), qml.Hermitian(H_ONE_QUBIT, 2), qml.PauliZ(2))), - ((-0.4, 0.15), (qml.Hermitian(H_TWO_QUBITS, [0, 2]), qml.PauliZ(1))), - ([1.5, 2.0], [qml.PauliZ(0), qml.PauliY(2)]), - (np.array([-0.1, 0.5]), [qml.Hermitian(H_TWO_QUBITS, [0, 1]), qml.PauliY(0)]), - ((0.5, 1.2), (qml.PauliX(0), qml.PauliX(0) @ qml.PauliX(1))), - ((0.5 + 1.2j, 1.2 + 0.5j), (qml.PauliX(0), qml.PauliY(1))), - ((0.7 + 0j, 0 + 1.3j), (qml.PauliX(0), qml.PauliY(1))), -] + OBSERVABLES = [ + (qml.PauliZ(0), qml.PauliY(0), qml.PauliZ(1)), + (qml.PauliX(0) @ qml.PauliZ(1), qml.PauliY(0) @ qml.PauliZ(1), qml.PauliZ(1)), + (qml.Hermitian(H_TWO_QUBITS, [0, 1]),), + ] -valid_hamiltonians_str = [ - " (1.0) [Hermitian0,1]", - " (-0.8) [Z0]", - " (0.6) [X0 X1]", - " (-1.6) [Y1]\n+ (0.5) [X0]", - " (-1.6) [Y1]\n+ (0.5) [X1]", - " (-1.6) [Yb]\n+ (0.5) [Xa]", - " (-0.4) [Hermitian2]\n+ (0.333) [Z2]\n+ (1.1) [X0]", - " (0.15) [Z1]\n+ (-0.4) [Hermitian0,2]", - " (1.5) [Z0]\n+ (2.0) [Y2]", - " (0.5) [Y0]\n+ (-0.1) [Hermitian0,1]", - " (0.5) [X0]\n+ (1.2) [X0 X1]", - " ((0.5+1.2j)) [X0]\n+ ((1.2+0.5j)) [Y1]", - " (1.3j) [Y1]\n+ ((0.7+0j)) [X0]", -] + valid_hamiltonians = [ + ((1.0,), (qml.Hermitian(H_TWO_QUBITS, [0, 1]),)), + ((-0.8,), (qml.PauliZ(0),)), + ((0.6,), (qml.PauliX(0) @ qml.PauliX(1),)), + ((0.5, -1.6), (qml.PauliX(0), qml.PauliY(1))), + ((0.5, -1.6), (qml.PauliX(1), qml.PauliY(1))), + ((0.5, -1.6), (qml.PauliX("a"), qml.PauliY("b"))), + ((1.1, -0.4, 0.333), (qml.PauliX(0), qml.Hermitian(H_ONE_QUBIT, 2), qml.PauliZ(2))), + ((-0.4, 0.15), (qml.Hermitian(H_TWO_QUBITS, [0, 2]), qml.PauliZ(1))), + ([1.5, 2.0], [qml.PauliZ(0), qml.PauliY(2)]), + (np.array([-0.1, 0.5]), [qml.Hermitian(H_TWO_QUBITS, [0, 1]), qml.PauliY(0)]), + ((0.5, 1.2), (qml.PauliX(0), qml.PauliX(0) @ qml.PauliX(1))), + ((0.5 + 1.2j, 1.2 + 0.5j), (qml.PauliX(0), qml.PauliY(1))), + ((0.7 + 0j, 0 + 1.3j), (qml.PauliX(0), qml.PauliY(1))), + ] -valid_hamiltonians_repr = [ - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", -] + valid_hamiltonians_str = [ + " (1.0) [Hermitian0,1]", + " (-0.8) [Z0]", + " (0.6) [X0 X1]", + " (-1.6) [Y1]\n+ (0.5) [X0]", + " (-1.6) [Y1]\n+ (0.5) [X1]", + " (-1.6) [Yb]\n+ (0.5) [Xa]", + " (-0.4) [Hermitian2]\n+ (0.333) [Z2]\n+ (1.1) [X0]", + " (0.15) [Z1]\n+ (-0.4) [Hermitian0,2]", + " (1.5) [Z0]\n+ (2.0) [Y2]", + " (0.5) [Y0]\n+ (-0.1) [Hermitian0,1]", + " (0.5) [X0]\n+ (1.2) [X0 X1]", + " ((0.5+1.2j)) [X0]\n+ ((1.2+0.5j)) [Y1]", + " (1.3j) [Y1]\n+ ((0.7+0j)) [X0]", + ] -invalid_hamiltonians = [ - ((), (qml.PauliZ(0),)), - ((), (qml.PauliZ(0), qml.PauliY(1))), - ((3.5,), ()), - ((1.2, -0.4), ()), - ((0.5, 1.2), (qml.PauliZ(0),)), - ((1.0,), (qml.PauliZ(0), qml.PauliY(0))), -] + valid_hamiltonians_repr = [ + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + ] -simplify_hamiltonians = [ - ( - qml.Hamiltonian([1, 1, 1], [qml.PauliX(0) @ qml.Identity(1), qml.PauliX(0), qml.PauliX(1)]), - qml.Hamiltonian([2, 1], [qml.PauliX(0), qml.PauliX(1)]), - ), - ( - qml.Hamiltonian( - [-1, 1, 1], [qml.PauliX(0) @ qml.Identity(1), qml.PauliX(0), qml.PauliX(1)] + invalid_hamiltonians = [ + ((), (qml.PauliZ(0),)), + ((), (qml.PauliZ(0), qml.PauliY(1))), + ((3.5,), ()), + ((1.2, -0.4), ()), + ((0.5, 1.2), (qml.PauliZ(0),)), + ((1.0,), (qml.PauliZ(0), qml.PauliY(0))), + ] + + simplify_hamiltonians = [ + ( + qml.Hamiltonian( + [1, 1, 1], [qml.PauliX(0) @ qml.Identity(1), qml.PauliX(0), qml.PauliX(1)] + ), + qml.Hamiltonian([2, 1], [qml.PauliX(0), qml.PauliX(1)]), ), - qml.Hamiltonian([1], [qml.PauliX(1)]), - ), - ( - qml.Hamiltonian( - [1, 0.5], - [qml.PauliX(0) @ qml.PauliY(1), qml.PauliY(1) @ qml.Identity(2) @ qml.PauliX(0)], + ( + qml.Hamiltonian( + [-1, 1, 1], [qml.PauliX(0) @ qml.Identity(1), qml.PauliX(0), qml.PauliX(1)] + ), + qml.Hamiltonian([1], [qml.PauliX(1)]), ), - qml.Hamiltonian([1.5], [qml.PauliX(0) @ qml.PauliY(1)]), - ), - ( - qml.Hamiltonian( - [1, 1, 0.5], - [ - qml.Hermitian(np.array([[1, 0], [0, -1]]), "a"), - qml.PauliX("b") @ qml.PauliY(1.3), - qml.PauliY(1.3) @ qml.Identity(-0.9) @ qml.PauliX("b"), - ], + ( + qml.Hamiltonian( + [1, 0.5], + [qml.PauliX(0) @ qml.PauliY(1), qml.PauliY(1) @ qml.Identity(2) @ qml.PauliX(0)], + ), + qml.Hamiltonian([1.5], [qml.PauliX(0) @ qml.PauliY(1)]), ), - qml.Hamiltonian( - [1, 1.5], - [qml.Hermitian(np.array([[1, 0], [0, -1]]), "a"), qml.PauliX("b") @ qml.PauliY(1.3)], + ( + qml.Hamiltonian( + [1, 1, 0.5], + [ + qml.Hermitian(np.array([[1, 0], [0, -1]]), "a"), + qml.PauliX("b") @ qml.PauliY(1.3), + qml.PauliY(1.3) @ qml.Identity(-0.9) @ qml.PauliX("b"), + ], + ), + qml.Hamiltonian( + [1, 1.5], + [ + qml.Hermitian(np.array([[1, 0], [0, -1]]), "a"), + qml.PauliX("b") @ qml.PauliY(1.3), + ], + ), ), - ), - # Simplifies to zero Hamiltonian - ( - qml.Hamiltonian( - [1, -0.5, -0.5], [qml.PauliX(0) @ qml.Identity(1), qml.PauliX(0), qml.PauliX(0)] + # Simplifies to zero Hamiltonian + ( + qml.Hamiltonian( + [1, -0.5, -0.5], [qml.PauliX(0) @ qml.Identity(1), qml.PauliX(0), qml.PauliX(0)] + ), + qml.Hamiltonian([], []), ), - qml.Hamiltonian([], []), - ), - ( - qml.Hamiltonian( - [1, -1], - [qml.PauliX(4) @ qml.Identity(0) @ qml.PauliX(1), qml.PauliX(4) @ qml.PauliX(1)], + ( + qml.Hamiltonian( + [1, -1], + [qml.PauliX(4) @ qml.Identity(0) @ qml.PauliX(1), qml.PauliX(4) @ qml.PauliX(1)], + ), + qml.Hamiltonian([], []), ), - qml.Hamiltonian([], []), - ), - ( - qml.Hamiltonian([0], [qml.Identity(0)]), - qml.Hamiltonian([0], [qml.Identity(0)]), - ), -] + ( + qml.Hamiltonian([0], [qml.Identity(0)]), + qml.Hamiltonian([0], [qml.Identity(0)]), + ), + ] -equal_hamiltonians = [ - ( - qml.Hamiltonian([1, 1], [qml.PauliX(0) @ qml.Identity(1), qml.PauliZ(0)]), - qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.PauliZ(0)]), - True, - ), - ( - qml.Hamiltonian([1, 1], [qml.PauliX(0) @ qml.Identity(1), qml.PauliY(2) @ qml.PauliZ(0)]), - qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.PauliZ(0) @ qml.PauliY(2) @ qml.Identity(1)]), - True, - ), - ( - qml.Hamiltonian( - [1, 1, 1], [qml.PauliX(0) @ qml.Identity(1), qml.PauliZ(0), qml.Identity(1)] + equal_hamiltonians = [ + ( + qml.Hamiltonian([1, 1], [qml.PauliX(0) @ qml.Identity(1), qml.PauliZ(0)]), + qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.PauliZ(0)]), + True, ), - qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.PauliZ(0)]), - False, - ), - (qml.Hamiltonian([1], [qml.PauliZ(0) @ qml.PauliX(1)]), qml.PauliZ(0) @ qml.PauliX(1), True), - (qml.Hamiltonian([1], [qml.PauliZ(0)]), qml.PauliZ(0), True), - ( - qml.Hamiltonian( - [1, 1, 1], - [ - qml.Hermitian(np.array([[1, 0], [0, -1]]), "b") @ qml.Identity(7), - qml.PauliZ(3), - qml.Identity(1.2), - ], + ( + qml.Hamiltonian( + [1, 1], [qml.PauliX(0) @ qml.Identity(1), qml.PauliY(2) @ qml.PauliZ(0)] + ), + qml.Hamiltonian( + [1, 1], [qml.PauliX(0), qml.PauliZ(0) @ qml.PauliY(2) @ qml.Identity(1)] + ), + True, ), - qml.Hamiltonian( - [1, 1, 1], - [qml.Hermitian(np.array([[1, 0], [0, -1]]), "b"), qml.PauliZ(3), qml.Identity(1.2)], + ( + qml.Hamiltonian( + [1, 1, 1], [qml.PauliX(0) @ qml.Identity(1), qml.PauliZ(0), qml.Identity(1)] + ), + qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.PauliZ(0)]), + False, ), - True, - ), - ( - qml.Hamiltonian([1, 1], [qml.PauliZ(3) @ qml.Identity(1.2), qml.PauliZ(3)]), - qml.Hamiltonian([2], [qml.PauliZ(3)]), - True, - ), -] + ( + qml.Hamiltonian([1], [qml.PauliZ(0) @ qml.PauliX(1)]), + qml.PauliZ(0) @ qml.PauliX(1), + True, + ), + (qml.Hamiltonian([1], [qml.PauliZ(0)]), qml.PauliZ(0), True), + ( + qml.Hamiltonian( + [1, 1, 1], + [ + qml.Hermitian(np.array([[1, 0], [0, -1]]), "b") @ qml.Identity(7), + qml.PauliZ(3), + qml.Identity(1.2), + ], + ), + qml.Hamiltonian( + [1, 1, 1], + [qml.Hermitian(np.array([[1, 0], [0, -1]]), "b"), qml.PauliZ(3), qml.Identity(1.2)], + ), + True, + ), + ( + qml.Hamiltonian([1, 1], [qml.PauliZ(3) @ qml.Identity(1.2), qml.PauliZ(3)]), + qml.Hamiltonian([2], [qml.PauliZ(3)]), + True, + ), + ] -add_hamiltonians = [ - ( - qml.Hamiltonian([1, 1.2, 0.1], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]), - qml.Hamiltonian([0.5, 0.3, 1], [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)]), - qml.Hamiltonian( - [1.5, 1.2, 1.1, 0.3], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2), qml.PauliX(1)] + add_hamiltonians = [ + ( + qml.Hamiltonian([1, 1.2, 0.1], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]), + qml.Hamiltonian([0.5, 0.3, 1], [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)]), + qml.Hamiltonian( + [1.5, 1.2, 1.1, 0.3], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2), qml.PauliX(1)] + ), ), - ), - ( - qml.Hamiltonian( - [1.3, 0.2, 0.7], [qml.PauliX(0) @ qml.PauliX(1), qml.Hadamard(1), qml.PauliX(2)] + ( + qml.Hamiltonian( + [1.3, 0.2, 0.7], [qml.PauliX(0) @ qml.PauliX(1), qml.Hadamard(1), qml.PauliX(2)] + ), + qml.Hamiltonian( + [0.5, 0.3, 1.6], [qml.PauliX(0), qml.PauliX(1) @ qml.PauliX(0), qml.PauliX(2)] + ), + qml.Hamiltonian( + [1.6, 0.2, 2.3, 0.5], + [qml.PauliX(0) @ qml.PauliX(1), qml.Hadamard(1), qml.PauliX(2), qml.PauliX(0)], + ), ), - qml.Hamiltonian( - [0.5, 0.3, 1.6], [qml.PauliX(0), qml.PauliX(1) @ qml.PauliX(0), qml.PauliX(2)] + ( + qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.Hermitian(np.array([[1, 0], [0, -1]]), 0)]), + qml.Hamiltonian( + [0.5, 0.5], [qml.PauliX(0), qml.Hermitian(np.array([[1, 0], [0, -1]]), 0)] + ), + qml.Hamiltonian( + [1.5, 1.5], [qml.PauliX(0), qml.Hermitian(np.array([[1, 0], [0, -1]]), 0)] + ), ), - qml.Hamiltonian( - [1.6, 0.2, 2.3, 0.5], - [qml.PauliX(0) @ qml.PauliX(1), qml.Hadamard(1), qml.PauliX(2), qml.PauliX(0)], + ( + qml.Hamiltonian([1, 1.2, 0.1], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]), + qml.PauliX(0) @ qml.Identity(1), + qml.Hamiltonian([2, 1.2, 0.1], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]), ), - ), - ( - qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.Hermitian(np.array([[1, 0], [0, -1]]), 0)]), - qml.Hamiltonian([0.5, 0.5], [qml.PauliX(0), qml.Hermitian(np.array([[1, 0], [0, -1]]), 0)]), - qml.Hamiltonian([1.5, 1.5], [qml.PauliX(0), qml.Hermitian(np.array([[1, 0], [0, -1]]), 0)]), - ), - ( - qml.Hamiltonian([1, 1.2, 0.1], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]), - qml.PauliX(0) @ qml.Identity(1), - qml.Hamiltonian([2, 1.2, 0.1], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]), - ), - ( - qml.Hamiltonian( - [1.3, 0.2, 0.7], [qml.PauliX(0) @ qml.PauliX(1), qml.Hadamard(1), qml.PauliX(2)] + ( + qml.Hamiltonian( + [1.3, 0.2, 0.7], [qml.PauliX(0) @ qml.PauliX(1), qml.Hadamard(1), qml.PauliX(2)] + ), + qml.Hadamard(1), + qml.Hamiltonian( + [1.3, 1.2, 0.7], [qml.PauliX(0) @ qml.PauliX(1), qml.Hadamard(1), qml.PauliX(2)] + ), ), - qml.Hadamard(1), - qml.Hamiltonian( - [1.3, 1.2, 0.7], [qml.PauliX(0) @ qml.PauliX(1), qml.Hadamard(1), qml.PauliX(2)] + ( + qml.Hamiltonian([1, 1.2, 0.1], [qml.PauliX("b"), qml.PauliZ(3.1), qml.PauliX(1.6)]), + qml.PauliX("b") @ qml.Identity(5), + qml.Hamiltonian([2, 1.2, 0.1], [qml.PauliX("b"), qml.PauliZ(3.1), qml.PauliX(1.6)]), ), - ), - ( - qml.Hamiltonian([1, 1.2, 0.1], [qml.PauliX("b"), qml.PauliZ(3.1), qml.PauliX(1.6)]), - qml.PauliX("b") @ qml.Identity(5), - qml.Hamiltonian([2, 1.2, 0.1], [qml.PauliX("b"), qml.PauliZ(3.1), qml.PauliX(1.6)]), - ), - # Case where arguments coeffs and ops to the Hamiltonian are iterables other than lists - ( - qml.Hamiltonian((1, 1.2, 0.1), (qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2))), - qml.Hamiltonian( - np.array([0.5, 0.3, 1]), np.array([qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)]) + # Case where arguments coeffs and ops to the Hamiltonian are iterables other than lists + ( + qml.Hamiltonian((1, 1.2, 0.1), (qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2))), + qml.Hamiltonian( + np.array([0.5, 0.3, 1]), np.array([qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)]) + ), + qml.Hamiltonian( + (1.5, 1.2, 1.1, 0.3), + np.array([qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2), qml.PauliX(1)]), + ), ), - qml.Hamiltonian( - (1.5, 1.2, 1.1, 0.3), - np.array([qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2), qml.PauliX(1)]), + # Case where the 1st hamiltonian doesn't contain all wires + ( + qml.Hamiltonian([1.23, -3.45], [qml.PauliX(0), qml.PauliY(1)]), + qml.Hamiltonian([6.78], [qml.PauliZ(2)]), + qml.Hamiltonian([1.23, -3.45, 6.78], [qml.PauliX(0), qml.PauliY(1), qml.PauliZ(2)]), ), - ), - # Case where the 1st hamiltonian doesn't contain all wires - ( - qml.Hamiltonian([1.23, -3.45], [qml.PauliX(0), qml.PauliY(1)]), - qml.Hamiltonian([6.78], [qml.PauliZ(2)]), - qml.Hamiltonian([1.23, -3.45, 6.78], [qml.PauliX(0), qml.PauliY(1), qml.PauliZ(2)]), - ), -] - -add_zero_hamiltonians = [ - qml.Hamiltonian([1, 1.2, 0.1], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]), - qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.Hermitian(np.array([[1, 0], [0, -1]]), 0)]), - qml.Hamiltonian( - [1.5, 1.2, 1.1, 0.3], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2), qml.PauliX(1)] - ), -] + ] -iadd_zero_hamiltonians = [ - # identical hamiltonians - ( + add_zero_hamiltonians = [ qml.Hamiltonian([1, 1.2, 0.1], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]), - qml.Hamiltonian([1, 1.2, 0.1], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]), - ), - ( - qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.Hermitian(np.array([[1, 0], [0, -1]]), 0)]), qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.Hermitian(np.array([[1, 0], [0, -1]]), 0)]), - ), - ( qml.Hamiltonian( [1.5, 1.2, 1.1, 0.3], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2), qml.PauliX(1)] ), - qml.Hamiltonian( - [1.5, 1.2, 1.1, 0.3], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2), qml.PauliX(1)] + ] + + iadd_zero_hamiltonians = [ + # identical hamiltonians + ( + qml.Hamiltonian([1, 1.2, 0.1], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]), + qml.Hamiltonian([1, 1.2, 0.1], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]), ), - ), -] + ( + qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.Hermitian(np.array([[1, 0], [0, -1]]), 0)]), + qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.Hermitian(np.array([[1, 0], [0, -1]]), 0)]), + ), + ( + qml.Hamiltonian( + [1.5, 1.2, 1.1, 0.3], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2), qml.PauliX(1)] + ), + qml.Hamiltonian( + [1.5, 1.2, 1.1, 0.3], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2), qml.PauliX(1)] + ), + ), + ] -sub_hamiltonians = [ - ( - qml.Hamiltonian([1, 1.2, 0.1], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]), - qml.Hamiltonian([0.5, 0.3, 1.6], [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)]), - qml.Hamiltonian( - [0.5, 1.2, -1.5, -0.3], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2), qml.PauliX(1)] + sub_hamiltonians = [ + ( + qml.Hamiltonian([1, 1.2, 0.1], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]), + qml.Hamiltonian([0.5, 0.3, 1.6], [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)]), + qml.Hamiltonian( + [0.5, 1.2, -1.5, -0.3], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2), qml.PauliX(1)] + ), ), - ), - ( - qml.Hamiltonian( - [1.3, 0.2, 1], [qml.PauliX(0) @ qml.PauliX(1), qml.Hadamard(1), qml.PauliX(2)] + ( + qml.Hamiltonian( + [1.3, 0.2, 1], [qml.PauliX(0) @ qml.PauliX(1), qml.Hadamard(1), qml.PauliX(2)] + ), + qml.Hamiltonian( + [0.5, 0.3, 1], [qml.PauliX(0), qml.PauliX(1) @ qml.PauliX(0), qml.PauliX(2)] + ), + qml.Hamiltonian( + [1, 0.2, -0.5], [qml.PauliX(0) @ qml.PauliX(1), qml.Hadamard(1), qml.PauliX(0)] + ), ), - qml.Hamiltonian( - [0.5, 0.3, 1], [qml.PauliX(0), qml.PauliX(1) @ qml.PauliX(0), qml.PauliX(2)] + ( + qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.Hermitian(np.array([[1, 0], [0, -1]]), 0)]), + qml.Hamiltonian( + [0.5, 0.5], [qml.PauliX(0), qml.Hermitian(np.array([[1, 0], [0, -1]]), 0)] + ), + qml.Hamiltonian( + [0.5, 0.5], [qml.PauliX(0), qml.Hermitian(np.array([[1, 0], [0, -1]]), 0)] + ), ), - qml.Hamiltonian( - [1, 0.2, -0.5], [qml.PauliX(0) @ qml.PauliX(1), qml.Hadamard(1), qml.PauliX(0)] + ( + qml.Hamiltonian([1, 1.2, 0.1], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]), + qml.PauliX(0) @ qml.Identity(1), + qml.Hamiltonian([1.2, 0.1], [qml.PauliZ(1), qml.PauliX(2)]), ), - ), - ( - qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.Hermitian(np.array([[1, 0], [0, -1]]), 0)]), - qml.Hamiltonian([0.5, 0.5], [qml.PauliX(0), qml.Hermitian(np.array([[1, 0], [0, -1]]), 0)]), - qml.Hamiltonian([0.5, 0.5], [qml.PauliX(0), qml.Hermitian(np.array([[1, 0], [0, -1]]), 0)]), - ), - ( - qml.Hamiltonian([1, 1.2, 0.1], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]), - qml.PauliX(0) @ qml.Identity(1), - qml.Hamiltonian([1.2, 0.1], [qml.PauliZ(1), qml.PauliX(2)]), - ), - ( - qml.Hamiltonian( - [1.3, 0.2, 0.7], [qml.PauliX(0) @ qml.PauliX(1), qml.Hadamard(1), qml.PauliX(2)] + ( + qml.Hamiltonian( + [1.3, 0.2, 0.7], [qml.PauliX(0) @ qml.PauliX(1), qml.Hadamard(1), qml.PauliX(2)] + ), + qml.Hadamard(1), + qml.Hamiltonian( + [1.3, -0.8, 0.7], [qml.PauliX(0) @ qml.PauliX(1), qml.Hadamard(1), qml.PauliX(2)] + ), ), - qml.Hadamard(1), - qml.Hamiltonian( - [1.3, -0.8, 0.7], [qml.PauliX(0) @ qml.PauliX(1), qml.Hadamard(1), qml.PauliX(2)] + ( + qml.Hamiltonian([1, 1.2, 0.1], [qml.PauliX("b"), qml.PauliZ(3.1), qml.PauliX(1.6)]), + qml.PauliX("b") @ qml.Identity(1), + qml.Hamiltonian([1.2, 0.1], [qml.PauliZ(3.1), qml.PauliX(1.6)]), ), - ), - ( - qml.Hamiltonian([1, 1.2, 0.1], [qml.PauliX("b"), qml.PauliZ(3.1), qml.PauliX(1.6)]), - qml.PauliX("b") @ qml.Identity(1), - qml.Hamiltonian([1.2, 0.1], [qml.PauliZ(3.1), qml.PauliX(1.6)]), - ), - # The result is the zero Hamiltonian - ( - qml.Hamiltonian([1, 1.2, 0.1], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]), - qml.Hamiltonian([1, 1.2, 0.1], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]), - qml.Hamiltonian([], []), - ), - ( - qml.Hamiltonian([1, 2], [qml.PauliX(4), qml.PauliZ(2)]), - qml.Hamiltonian([1, 2], [qml.PauliX(4), qml.PauliZ(2)]), - qml.Hamiltonian([], []), - ), - # Case where arguments coeffs and ops to the Hamiltonian are iterables other than lists - ( - qml.Hamiltonian((1, 1.2, 0.1), (qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2))), - qml.Hamiltonian( - np.array([0.5, 0.3, 1.6]), np.array([qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)]) + # The result is the zero Hamiltonian + ( + qml.Hamiltonian([1, 1.2, 0.1], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]), + qml.Hamiltonian([1, 1.2, 0.1], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]), + qml.Hamiltonian([], []), ), - qml.Hamiltonian( - (0.5, 1.2, -1.5, -0.3), - np.array([qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2), qml.PauliX(1)]), + ( + qml.Hamiltonian([1, 2], [qml.PauliX(4), qml.PauliZ(2)]), + qml.Hamiltonian([1, 2], [qml.PauliX(4), qml.PauliZ(2)]), + qml.Hamiltonian([], []), ), - ), - # Case where the 1st hamiltonian doesn't contain all wires - ( - qml.Hamiltonian([1.23, -3.45], [qml.PauliX(0), qml.PauliY(1)]), - qml.Hamiltonian([6.78], [qml.PauliZ(2)]), - qml.Hamiltonian([1.23, -3.45, -6.78], [qml.PauliX(0), qml.PauliY(1), qml.PauliZ(2)]), - ), -] - -mul_hamiltonians = [ - ( - 0.5, - qml.Hamiltonian( - [1, 2], [qml.PauliX(0), qml.PauliZ(1)] - ), # Case where the types of the coefficient and the scalar differ - qml.Hamiltonian([0.5, 1.0], [qml.PauliX(0), qml.PauliZ(1)]), - ), - ( - 3, - qml.Hamiltonian([1.5, 0.5], [qml.PauliX(0), qml.PauliZ(1)]), - qml.Hamiltonian([4.5, 1.5], [qml.PauliX(0), qml.PauliZ(1)]), - ), - ( - -1.3, - qml.Hamiltonian([1, -0.3], [qml.PauliX(0), qml.PauliZ(1) @ qml.PauliZ(2)]), - qml.Hamiltonian([-1.3, 0.39], [qml.PauliX(0), qml.PauliZ(1) @ qml.PauliZ(2)]), - ), - ( - -1.3, - qml.Hamiltonian( - [1, -0.3], - [qml.Hermitian(np.array([[1, 0], [0, -1]]), "b"), qml.PauliZ(23) @ qml.PauliZ(0)], + # Case where arguments coeffs and ops to the Hamiltonian are iterables other than lists + ( + qml.Hamiltonian((1, 1.2, 0.1), (qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2))), + qml.Hamiltonian( + np.array([0.5, 0.3, 1.6]), np.array([qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)]) + ), + qml.Hamiltonian( + (0.5, 1.2, -1.5, -0.3), + np.array([qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2), qml.PauliX(1)]), + ), ), - qml.Hamiltonian( - [-1.3, 0.39], - [qml.Hermitian(np.array([[1, 0], [0, -1]]), "b"), qml.PauliZ(23) @ qml.PauliZ(0)], + # Case where the 1st hamiltonian doesn't contain all wires + ( + qml.Hamiltonian([1.23, -3.45], [qml.PauliX(0), qml.PauliY(1)]), + qml.Hamiltonian([6.78], [qml.PauliZ(2)]), + qml.Hamiltonian([1.23, -3.45, -6.78], [qml.PauliX(0), qml.PauliY(1), qml.PauliZ(2)]), ), - ), - # The result is the zero Hamiltonian - ( - 0, - qml.Hamiltonian([1], [qml.PauliX(0)]), - qml.Hamiltonian([0], [qml.PauliX(0)]), - ), - ( - 0, - qml.Hamiltonian([1, 1.2, 0.1], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]), - qml.Hamiltonian([0, 0, 0], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]), - ), - # Case where arguments coeffs and ops to the Hamiltonian are iterables other than lists - ( - 3, - qml.Hamiltonian((1.5, 0.5), (qml.PauliX(0), qml.PauliZ(1))), - qml.Hamiltonian(np.array([4.5, 1.5]), np.array([qml.PauliX(0), qml.PauliZ(1)])), - ), -] + ] -matmul_hamiltonians = [ - ( - qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.PauliZ(1)]), - qml.Hamiltonian([0.5, 0.5], [qml.PauliZ(2), qml.PauliZ(3)]), - qml.Hamiltonian( - [0.5, 0.5, 0.5, 0.5], - [ - qml.PauliX(0) @ qml.PauliZ(2), - qml.PauliX(0) @ qml.PauliZ(3), - qml.PauliZ(1) @ qml.PauliZ(2), - qml.PauliZ(1) @ qml.PauliZ(3), - ], + mul_hamiltonians = [ + ( + 0.5, + qml.Hamiltonian( + [1, 2], [qml.PauliX(0), qml.PauliZ(1)] + ), # Case where the types of the coefficient and the scalar differ + qml.Hamiltonian([0.5, 1.0], [qml.PauliX(0), qml.PauliZ(1)]), ), - ), - ( - qml.Hamiltonian([0.5, 0.25], [qml.PauliX(0) @ qml.PauliX(1), qml.PauliZ(0)]), - qml.Hamiltonian([1, 1], [qml.PauliX(3) @ qml.PauliZ(2), qml.PauliZ(2)]), - qml.Hamiltonian( - [0.5, 0.5, 0.25, 0.25], - [ - qml.PauliX(0) @ qml.PauliX(1) @ qml.PauliX(3) @ qml.PauliZ(2), - qml.PauliX(0) @ qml.PauliX(1) @ qml.PauliZ(2), - qml.PauliZ(0) @ qml.PauliX(3) @ qml.PauliZ(2), - qml.PauliZ(0) @ qml.PauliZ(2), - ], + ( + 3, + qml.Hamiltonian([1.5, 0.5], [qml.PauliX(0), qml.PauliZ(1)]), + qml.Hamiltonian([4.5, 1.5], [qml.PauliX(0), qml.PauliZ(1)]), ), - ), - ( - qml.Hamiltonian([1, 1], [qml.PauliX("b"), qml.Hermitian(np.array([[1, 0], [0, -1]]), 0)]), - qml.Hamiltonian([2, 2], [qml.PauliZ(1.2), qml.PauliY("c")]), - qml.Hamiltonian( - [2, 2, 2, 2], - [ - qml.PauliX("b") @ qml.PauliZ(1.2), - qml.PauliX("b") @ qml.PauliY("c"), - qml.Hermitian(np.array([[1, 0], [0, -1]]), 0) @ qml.PauliZ(1.2), - qml.Hermitian(np.array([[1, 0], [0, -1]]), 0) @ qml.PauliY("c"), - ], + ( + -1.3, + qml.Hamiltonian([1, -0.3], [qml.PauliX(0), qml.PauliZ(1) @ qml.PauliZ(2)]), + qml.Hamiltonian([-1.3, 0.39], [qml.PauliX(0), qml.PauliZ(1) @ qml.PauliZ(2)]), ), - ), - ( - qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.PauliZ(1)]), - qml.PauliX(2), - qml.Hamiltonian([1, 1], [qml.PauliX(0) @ qml.PauliX(2), qml.PauliZ(1) @ qml.PauliX(2)]), - ), - # Case where arguments coeffs and ops to the Hamiltonian are iterables other than lists - ( - qml.Hamiltonian((1, 1), (qml.PauliX(0), qml.PauliZ(1))), - qml.Hamiltonian(np.array([0.5, 0.5]), np.array([qml.PauliZ(2), qml.PauliZ(3)])), - qml.Hamiltonian( - (0.5, 0.5, 0.5, 0.5), - np.array( + ( + -1.3, + qml.Hamiltonian( + [1, -0.3], + [qml.Hermitian(np.array([[1, 0], [0, -1]]), "b"), qml.PauliZ(23) @ qml.PauliZ(0)], + ), + qml.Hamiltonian( + [-1.3, 0.39], + [qml.Hermitian(np.array([[1, 0], [0, -1]]), "b"), qml.PauliZ(23) @ qml.PauliZ(0)], + ), + ), + # The result is the zero Hamiltonian + ( + 0, + qml.Hamiltonian([1], [qml.PauliX(0)]), + qml.Hamiltonian([0], [qml.PauliX(0)]), + ), + ( + 0, + qml.Hamiltonian([1, 1.2, 0.1], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]), + qml.Hamiltonian([0, 0, 0], [qml.PauliX(0), qml.PauliZ(1), qml.PauliX(2)]), + ), + # Case where arguments coeffs and ops to the Hamiltonian are iterables other than lists + ( + 3, + qml.Hamiltonian((1.5, 0.5), (qml.PauliX(0), qml.PauliZ(1))), + qml.Hamiltonian(np.array([4.5, 1.5]), np.array([qml.PauliX(0), qml.PauliZ(1)])), + ), + ] + + matmul_hamiltonians = [ + ( + qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.PauliZ(1)]), + qml.Hamiltonian([0.5, 0.5], [qml.PauliZ(2), qml.PauliZ(3)]), + qml.Hamiltonian( + [0.5, 0.5, 0.5, 0.5], [ qml.PauliX(0) @ qml.PauliZ(2), qml.PauliX(0) @ qml.PauliZ(3), qml.PauliZ(1) @ qml.PauliZ(2), qml.PauliZ(1) @ qml.PauliZ(3), - ] + ], ), ), - ), -] - -rmatmul_hamiltonians = [ - ( - qml.Hamiltonian([0.5, 0.5], [qml.PauliZ(2), qml.PauliZ(3)]), - qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.PauliZ(1)]), - qml.Hamiltonian( - [0.5, 0.5, 0.5, 0.5], - [ - qml.PauliX(0) @ qml.PauliZ(2), - qml.PauliX(0) @ qml.PauliZ(3), - qml.PauliZ(1) @ qml.PauliZ(2), - qml.PauliZ(1) @ qml.PauliZ(3), - ], + ( + qml.Hamiltonian([0.5, 0.25], [qml.PauliX(0) @ qml.PauliX(1), qml.PauliZ(0)]), + qml.Hamiltonian([1, 1], [qml.PauliX(3) @ qml.PauliZ(2), qml.PauliZ(2)]), + qml.Hamiltonian( + [0.5, 0.5, 0.25, 0.25], + [ + qml.PauliX(0) @ qml.PauliX(1) @ qml.PauliX(3) @ qml.PauliZ(2), + qml.PauliX(0) @ qml.PauliX(1) @ qml.PauliZ(2), + qml.PauliZ(0) @ qml.PauliX(3) @ qml.PauliZ(2), + qml.PauliZ(0) @ qml.PauliZ(2), + ], + ), ), - ), - ( - qml.Hamiltonian([1, 1], [qml.PauliX(3) @ qml.PauliZ(2), qml.PauliZ(2)]), - qml.Hamiltonian([0.5, 0.25], [qml.PauliX(0) @ qml.PauliX(1), qml.PauliZ(0)]), - qml.Hamiltonian( - [0.5, 0.5, 0.25, 0.25], - [ - qml.PauliX(0) @ qml.PauliX(1) @ qml.PauliX(3) @ qml.PauliZ(2), - qml.PauliX(0) @ qml.PauliX(1) @ qml.PauliZ(2), - qml.PauliZ(0) @ qml.PauliX(3) @ qml.PauliZ(2), - qml.PauliZ(0) @ qml.PauliZ(2), - ], + ( + qml.Hamiltonian( + [1, 1], [qml.PauliX("b"), qml.Hermitian(np.array([[1, 0], [0, -1]]), 0)] + ), + qml.Hamiltonian([2, 2], [qml.PauliZ(1.2), qml.PauliY("c")]), + qml.Hamiltonian( + [2, 2, 2, 2], + [ + qml.PauliX("b") @ qml.PauliZ(1.2), + qml.PauliX("b") @ qml.PauliY("c"), + qml.Hermitian(np.array([[1, 0], [0, -1]]), 0) @ qml.PauliZ(1.2), + qml.Hermitian(np.array([[1, 0], [0, -1]]), 0) @ qml.PauliY("c"), + ], + ), ), - ), - ( - qml.Hamiltonian([2, 2], [qml.PauliZ(1.2), qml.PauliY("c")]), - qml.Hamiltonian([1, 1], [qml.PauliX("b"), qml.Hermitian(np.array([[1, 0], [0, -1]]), 0)]), - qml.Hamiltonian( - [2, 2, 2, 2], - [ - qml.PauliX("b") @ qml.PauliZ(1.2), - qml.PauliX("b") @ qml.PauliY("c"), - qml.Hermitian(np.array([[1, 0], [0, -1]]), 0) @ qml.PauliZ(1.2), - qml.Hermitian(np.array([[1, 0], [0, -1]]), 0) @ qml.PauliY("c"), - ], + ( + qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.PauliZ(1)]), + qml.PauliX(2), + qml.Hamiltonian([1, 1], [qml.PauliX(0) @ qml.PauliX(2), qml.PauliZ(1) @ qml.PauliX(2)]), ), - ), - ( - qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.PauliZ(1)]), - qml.PauliX(2), - qml.Hamiltonian([1, 1], [qml.PauliX(2) @ qml.PauliX(0), qml.PauliX(2) @ qml.PauliZ(1)]), - ), - # Case where arguments coeffs and ops to the Hamiltonian are iterables other than lists - ( - qml.Hamiltonian(np.array([0.5, 0.5]), np.array([qml.PauliZ(2), qml.PauliZ(3)])), - qml.Hamiltonian((1, 1), (qml.PauliX(0), qml.PauliZ(1))), - qml.Hamiltonian( - (0.5, 0.5, 0.5, 0.5), - np.array( + # Case where arguments coeffs and ops to the Hamiltonian are iterables other than lists + ( + qml.Hamiltonian((1, 1), (qml.PauliX(0), qml.PauliZ(1))), + qml.Hamiltonian(np.array([0.5, 0.5]), np.array([qml.PauliZ(2), qml.PauliZ(3)])), + qml.Hamiltonian( + (0.5, 0.5, 0.5, 0.5), + np.array( + [ + qml.PauliX(0) @ qml.PauliZ(2), + qml.PauliX(0) @ qml.PauliZ(3), + qml.PauliZ(1) @ qml.PauliZ(2), + qml.PauliZ(1) @ qml.PauliZ(3), + ] + ), + ), + ), + ] + + rmatmul_hamiltonians = [ + ( + qml.Hamiltonian([0.5, 0.5], [qml.PauliZ(2), qml.PauliZ(3)]), + qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.PauliZ(1)]), + qml.Hamiltonian( + [0.5, 0.5, 0.5, 0.5], [ qml.PauliX(0) @ qml.PauliZ(2), qml.PauliX(0) @ qml.PauliZ(3), qml.PauliZ(1) @ qml.PauliZ(2), qml.PauliZ(1) @ qml.PauliZ(3), - ] + ], + ), + ), + ( + qml.Hamiltonian([1, 1], [qml.PauliX(3) @ qml.PauliZ(2), qml.PauliZ(2)]), + qml.Hamiltonian([0.5, 0.25], [qml.PauliX(0) @ qml.PauliX(1), qml.PauliZ(0)]), + qml.Hamiltonian( + [0.5, 0.5, 0.25, 0.25], + [ + qml.PauliX(0) @ qml.PauliX(1) @ qml.PauliX(3) @ qml.PauliZ(2), + qml.PauliX(0) @ qml.PauliX(1) @ qml.PauliZ(2), + qml.PauliZ(0) @ qml.PauliX(3) @ qml.PauliZ(2), + qml.PauliZ(0) @ qml.PauliZ(2), + ], + ), + ), + ( + qml.Hamiltonian([2, 2], [qml.PauliZ(1.2), qml.PauliY("c")]), + qml.Hamiltonian( + [1, 1], [qml.PauliX("b"), qml.Hermitian(np.array([[1, 0], [0, -1]]), 0)] + ), + qml.Hamiltonian( + [2, 2, 2, 2], + [ + qml.PauliX("b") @ qml.PauliZ(1.2), + qml.PauliX("b") @ qml.PauliY("c"), + qml.Hermitian(np.array([[1, 0], [0, -1]]), 0) @ qml.PauliZ(1.2), + qml.Hermitian(np.array([[1, 0], [0, -1]]), 0) @ qml.PauliY("c"), + ], + ), + ), + ( + qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.PauliZ(1)]), + qml.PauliX(2), + qml.Hamiltonian([1, 1], [qml.PauliX(2) @ qml.PauliX(0), qml.PauliX(2) @ qml.PauliZ(1)]), + ), + # Case where arguments coeffs and ops to the Hamiltonian are iterables other than lists + ( + qml.Hamiltonian(np.array([0.5, 0.5]), np.array([qml.PauliZ(2), qml.PauliZ(3)])), + qml.Hamiltonian((1, 1), (qml.PauliX(0), qml.PauliZ(1))), + qml.Hamiltonian( + (0.5, 0.5, 0.5, 0.5), + np.array( + [ + qml.PauliX(0) @ qml.PauliZ(2), + qml.PauliX(0) @ qml.PauliZ(3), + qml.PauliZ(1) @ qml.PauliZ(2), + qml.PauliZ(1) @ qml.PauliZ(3), + ] + ), ), ), - ), -] - -big_hamiltonian_coeffs = np.array( - [ - -0.04207898, - 0.17771287, - 0.17771287, - -0.24274281, - -0.24274281, - 0.17059738, - 0.04475014, - -0.04475014, - -0.04475014, - 0.04475014, - 0.12293305, - 0.16768319, - 0.16768319, - 0.12293305, - 0.17627641, ] -) -big_hamiltonian_ops = [ - qml.Identity(wires=[0]), - qml.PauliZ(wires=[0]), - qml.PauliZ(wires=[1]), - qml.PauliZ(wires=[2]), - qml.PauliZ(wires=[3]), - qml.PauliZ(wires=[0]) @ qml.PauliZ(wires=[1]), - qml.PauliY(wires=[0]) @ qml.PauliX(wires=[1]) @ qml.PauliX(wires=[2]) @ qml.PauliY(wires=[3]), - qml.PauliY(wires=[0]) @ qml.PauliY(wires=[1]) @ qml.PauliX(wires=[2]) @ qml.PauliX(wires=[3]), - qml.PauliX(wires=[0]) @ qml.PauliX(wires=[1]) @ qml.PauliY(wires=[2]) @ qml.PauliY(wires=[3]), - qml.PauliX(wires=[0]) @ qml.PauliY(wires=[1]) @ qml.PauliY(wires=[2]) @ qml.PauliX(wires=[3]), - qml.PauliZ(wires=[0]) @ qml.PauliZ(wires=[2]), - qml.PauliZ(wires=[0]) @ qml.PauliZ(wires=[3]), - qml.PauliZ(wires=[1]) @ qml.PauliZ(wires=[2]), - qml.PauliZ(wires=[1]) @ qml.PauliZ(wires=[3]), - qml.PauliZ(wires=[2]) @ qml.PauliZ(wires=[3]), -] + big_hamiltonian_coeffs = np.array( + [ + -0.04207898, + 0.17771287, + 0.17771287, + -0.24274281, + -0.24274281, + 0.17059738, + 0.04475014, + -0.04475014, + -0.04475014, + 0.04475014, + 0.12293305, + 0.16768319, + 0.16768319, + 0.12293305, + 0.17627641, + ] + ) -big_hamiltonian = qml.Hamiltonian(big_hamiltonian_coeffs, big_hamiltonian_ops) + big_hamiltonian_ops = [ + qml.Identity(wires=[0]), + qml.PauliZ(wires=[0]), + qml.PauliZ(wires=[1]), + qml.PauliZ(wires=[2]), + qml.PauliZ(wires=[3]), + qml.PauliZ(wires=[0]) @ qml.PauliZ(wires=[1]), + qml.PauliY(wires=[0]) + @ qml.PauliX(wires=[1]) + @ qml.PauliX(wires=[2]) + @ qml.PauliY(wires=[3]), + qml.PauliY(wires=[0]) + @ qml.PauliY(wires=[1]) + @ qml.PauliX(wires=[2]) + @ qml.PauliX(wires=[3]), + qml.PauliX(wires=[0]) + @ qml.PauliX(wires=[1]) + @ qml.PauliY(wires=[2]) + @ qml.PauliY(wires=[3]), + qml.PauliX(wires=[0]) + @ qml.PauliY(wires=[1]) + @ qml.PauliY(wires=[2]) + @ qml.PauliX(wires=[3]), + qml.PauliZ(wires=[0]) @ qml.PauliZ(wires=[2]), + qml.PauliZ(wires=[0]) @ qml.PauliZ(wires=[3]), + qml.PauliZ(wires=[1]) @ qml.PauliZ(wires=[2]), + qml.PauliZ(wires=[1]) @ qml.PauliZ(wires=[3]), + qml.PauliZ(wires=[2]) @ qml.PauliZ(wires=[3]), + ] -big_hamiltonian_grad = ( - np.array( - [ - [ - [6.52084595e-18, -2.11464420e-02, -1.16576858e-02], - [-8.22589330e-18, -5.20597922e-02, -1.85365365e-02], - [-2.73850768e-17, 1.14202988e-01, -5.45041403e-03], - [-1.27514307e-17, -1.10465531e-01, 5.19489457e-02], - ], + big_hamiltonian = qml.Hamiltonian(big_hamiltonian_coeffs, big_hamiltonian_ops) + + big_hamiltonian_grad = ( + np.array( [ - [-2.45428288e-02, 8.38921555e-02, -2.00641818e-17], - [-2.21085973e-02, 7.39332741e-04, -1.25580654e-17], - [9.62058625e-03, -1.51398765e-01, 2.02129847e-03], - [1.10020832e-03, -3.49066271e-01, 2.13669117e-03], - ], - ] - ), -) + [ + [6.52084595e-18, -2.11464420e-02, -1.16576858e-02], + [-8.22589330e-18, -5.20597922e-02, -1.85365365e-02], + [-2.73850768e-17, 1.14202988e-01, -5.45041403e-03], + [-1.27514307e-17, -1.10465531e-01, 5.19489457e-02], + ], + [ + [-2.45428288e-02, 8.38921555e-02, -2.00641818e-17], + [-2.21085973e-02, 7.39332741e-04, -1.25580654e-17], + [9.62058625e-03, -1.51398765e-01, 2.02129847e-03], + [1.10020832e-03, -3.49066271e-01, 2.13669117e-03], + ], + ] + ), + ) def circuit1(param): @@ -638,6 +678,23 @@ def circuit2(param): dev = qml.device("default.qubit", wires=2) +@pytest.mark.usefixtures("use_legacy_and_new_opmath") +def test_deprecation_with_new_opmath(recwarn): + """Test that a warning is raised if attempting to create a Hamiltonian with new operator + arithmetic enabled.""" + if qml.operation.active_new_opmath(): + with pytest.warns( + qml.PennyLaneDeprecationWarning, + match="Using 'qml.ops.Hamiltonian' with new operator arithmetic is deprecated", + ): + _ = qml.ops.Hamiltonian([1.0], [qml.X(0)]) + + else: + _ = qml.Hamiltonian([1.0], [qml.X(0)]) + assert len(recwarn) == 0 + + +@pytest.mark.usefixtures("use_legacy_opmath") class TestHamiltonian: """Test the Hamiltonian class""" @@ -645,6 +702,7 @@ class TestHamiltonian: def test_hamiltonian_valid_init(self, coeffs, ops): """Tests that the Hamiltonian object is created with the correct attributes""" + H = qml.Hamiltonian(coeffs, ops) assert np.allclose(H.terms()[0], coeffs) assert H.terms()[1] == list(ops) @@ -672,11 +730,11 @@ def test_hamiltonian_invalid_observables(self, obs): @pytest.mark.parametrize("grouping_type", (None, "qwc")) def test_flatten_unflatten(self, coeffs, ops, grouping_type): """Test the flatten and unflatten methods for hamiltonians""" - + assert not qml.operation.active_new_opmath() if any(not qml.pauli.is_pauli_word(t) for t in ops) and grouping_type: pytest.skip("grouping type must be none if a term is not a pauli word.") - H = Hamiltonian(coeffs, ops, grouping_type=grouping_type) + H = qml.Hamiltonian(coeffs, ops, grouping_type=grouping_type) data, metadata = H._flatten() assert metadata[0] == H.grouping_indices assert hash(metadata) @@ -684,7 +742,7 @@ def test_flatten_unflatten(self, coeffs, ops, grouping_type): assert data[0] is H.data assert data[1] is H._ops - new_H = Hamiltonian._unflatten(*H._flatten()) + new_H = qml.Hamiltonian._unflatten(*H._flatten()) assert qml.equal(H, new_H) assert new_H.grouping_indices == H.grouping_indices @@ -1041,6 +1099,7 @@ def test_hamiltonian_no_pauli_rep(self): assert h.pauli_rep is None +@pytest.mark.usefixtures("use_legacy_opmath") class TestHamiltonianCoefficients: """Test the creation of a Hamiltonian""" @@ -1062,6 +1121,7 @@ def test_simplify(self, coeffs): @pytest.mark.tf +@pytest.mark.usefixtures("use_legacy_opmath") class TestHamiltonianArithmeticTF: """Tests creation of Hamiltonians using arithmetic operations with TensorFlow tensor coefficients.""" @@ -1134,6 +1194,7 @@ def test_hamiltonian_matmul(self): assert H.compare(H1 @ H2) +@pytest.mark.usefixtures("use_legacy_opmath") class TestHamiltonianArithmeticTorch: """Tests creation of Hamiltonians using arithmetic operations with torch tensor coefficients.""" @@ -1210,6 +1271,7 @@ def test_hamiltonian_matmul(self): assert H.compare(H1 @ H2) +@pytest.mark.usefixtures("use_legacy_opmath") class TestHamiltonianArithmeticAutograd: """Tests creation of Hamiltonians using arithmetic operations with autograd tensor coefficients.""" @@ -1286,133 +1348,135 @@ def test_hamiltonian_matmul(self): assert H.compare(H1 @ H2) -class TestHamiltonianSparseMatrix: - """Tests for sparse matrix representation.""" - - @pytest.mark.parametrize( - ("coeffs", "obs", "wires", "ref_matrix"), - [ - ( - [1, -0.45], - [qml.PauliZ(0) @ qml.PauliZ(1), qml.PauliY(0) @ qml.PauliZ(1)], - None, - np.array( - [ - [1.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.45j, 0.0 + 0.0j], - [0.0 + 0.0j, -1.0 + 0.0j, 0.0 + 0.0j, 0.0 - 0.45j], - [0.0 - 0.45j, 0.0 + 0.0j, -1.0 + 0.0j, 0.0 + 0.0j], - [0.0 + 0.0j, 0.0 + 0.45j, 0.0 + 0.0j, 1.0 + 0.0j], - ] - ), +with qml.operation.disable_new_opmath_cm(): + TEST_SPARSE_MATRIX = [ + ( + [1, -0.45], + [qml.PauliZ(0) @ qml.PauliZ(1), qml.PauliY(0) @ qml.PauliZ(1)], + None, + np.array( + [ + [1.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.45j, 0.0 + 0.0j], + [0.0 + 0.0j, -1.0 + 0.0j, 0.0 + 0.0j, 0.0 - 0.45j], + [0.0 - 0.45j, 0.0 + 0.0j, -1.0 + 0.0j, 0.0 + 0.0j], + [0.0 + 0.0j, 0.0 + 0.45j, 0.0 + 0.0j, 1.0 + 0.0j], + ] ), - ( - [0.1], - [qml.PauliZ("b") @ qml.PauliX("a")], - ["a", "c", "b"], - np.array( + ), + ( + [0.1], + [qml.PauliZ("b") @ qml.PauliX("a")], + ["a", "c", "b"], + np.array( + [ [ - [ - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.1 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - ], - [ - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - -0.1 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - ], - [ - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.1 + 0.0j, - 0.0 + 0.0j, - ], - [ - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - -0.1 + 0.0j, - ], - [ - 0.1 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - ], - [ - 0.0 + 0.0j, - -0.1 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - ], - [ - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.1 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - ], - [ - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - -0.1 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - 0.0 + 0.0j, - ], - ] - ), + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.1 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + ], + [ + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + -0.1 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + ], + [ + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.1 + 0.0j, + 0.0 + 0.0j, + ], + [ + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + -0.1 + 0.0j, + ], + [ + 0.1 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + ], + [ + 0.0 + 0.0j, + -0.1 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + ], + [ + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.1 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + ], + [ + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + -0.1 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + ], + ] ), - ( - [0.21, -0.78, 0.52], + ), + ( + [0.21, -0.78, 0.52], + [ + qml.PauliZ(0) @ qml.PauliZ(1), + qml.PauliX(0) @ qml.PauliZ(1), + qml.PauliY(0) @ qml.PauliZ(1), + ], + None, + np.array( [ - qml.PauliZ(0) @ qml.PauliZ(1), - qml.PauliX(0) @ qml.PauliZ(1), - qml.PauliY(0) @ qml.PauliZ(1), - ], - None, - np.array( - [ - [0.21 + 0.0j, 0.0 + 0.0j, -0.78 - 0.52j, 0.0 + 0.0j], - [0.0 + 0.0j, -0.21 + 0.0j, 0.0 + 0.0j, 0.78 + 0.52j], - [-0.78 + 0.52j, 0.0 + 0.0j, -0.21 + 0.0j, 0.0 + 0.0j], - [0.0 + 0.0j, 0.78 - 0.52j, 0.0 + 0.0j, 0.21 + 0.0j], - ] - ), + [0.21 + 0.0j, 0.0 + 0.0j, -0.78 - 0.52j, 0.0 + 0.0j], + [0.0 + 0.0j, -0.21 + 0.0j, 0.0 + 0.0j, 0.78 + 0.52j], + [-0.78 + 0.52j, 0.0 + 0.0j, -0.21 + 0.0j, 0.0 + 0.0j], + [0.0 + 0.0j, 0.78 - 0.52j, 0.0 + 0.0j, 0.21 + 0.0j], + ] ), - ], - ) + ), + ] + + +@pytest.mark.usefixtures("use_legacy_opmath") +class TestHamiltonianSparseMatrix: + """Tests for sparse matrix representation.""" + + @pytest.mark.parametrize(["coeffs", "obs", "wires", "ref_matrix"], TEST_SPARSE_MATRIX) def test_sparse_matrix(self, coeffs, obs, wires, ref_matrix): """Tests that sparse_hamiltonian returns a correct sparse matrix""" H = qml.Hamiltonian(coeffs, obs) @@ -1446,6 +1510,7 @@ def test_observable_error(self): @pytest.mark.jax +@pytest.mark.usefixtures("use_legacy_opmath") class TestHamiltonianArithmeticJax: """Tests creation of Hamiltonians using arithmetic operations with jax tensor coefficients.""" @@ -1519,6 +1584,7 @@ def test_hamiltonian_matmul(self): assert H.compare(H1 @ H2) +@pytest.mark.usefixtures("use_legacy_opmath") class TestGrouping: """Tests for the grouping functionality""" @@ -1624,6 +1690,7 @@ def test_grouping_method_can_be_set(self): assert H3.grouping_indices == ((2, 1), (0,)) +@pytest.mark.usefixtures("use_legacy_opmath") class TestHamiltonianEvaluation: """Test the usage of a Hamiltonian as an observable""" @@ -1673,6 +1740,7 @@ def circuit(): assert pars == [0.1, 3.0] +@pytest.mark.usefixtures("use_legacy_opmath") class TestHamiltonianDifferentiation: """Test that the Hamiltonian coefficients are differentiable""" diff --git a/tests/ops/qubit/test_parametric_ops.py b/tests/ops/qubit/test_parametric_ops.py index cd7e9949c02..88ead3da971 100644 --- a/tests/ops/qubit/test_parametric_ops.py +++ b/tests/ops/qubit/test_parametric_ops.py @@ -30,8 +30,6 @@ MultiRZ as old_loc_MultiRZ, ) -from pennylane.ops.op_math.sprod import SProd - from pennylane.wires import Wires PARAMETRIZED_OPERATIONS = [ @@ -236,6 +234,7 @@ def test_pcphase_raises_error(self): class TestParameterFrequencies: + @pytest.mark.usefixtures("use_legacy_and_new_opmath") @pytest.mark.parametrize("op", PARAMETRIZED_OPERATIONS) def test_parameter_frequencies_match_generator(self, op, tol): if not qml.operation.has_gen(op): @@ -2974,12 +2973,13 @@ def test_init_incorrect_pauli_word_length_error(self, pauli_word, wires): ("IIIXYZ"), ], ) + @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_multirz_generator(self, pauli_word): """Test that the generator of the MultiRZ gate is correct.""" op = qml.PauliRot(0.3, pauli_word, wires=range(len(pauli_word))) gen = op.generator() - assert isinstance(gen, SProd) + assert isinstance(gen, qml.Hamiltonian) if pauli_word[0] == "I": # this is the identity @@ -2994,7 +2994,7 @@ def test_multirz_generator(self, pauli_word): else: expected_gen = expected_gen @ getattr(qml, f"Pauli{pauli}")(wires=i) - assert qml.equal(gen, qml.s_prod(-0.5, expected_gen)) + assert qml.equal(gen, qml.Hamiltonian([-0.5], [expected_gen])) @pytest.mark.torch @pytest.mark.gpu @@ -3016,7 +3016,8 @@ def test_pauli_rot_identity_torch(self, torch_device, theta): exp = torch.tensor(np.diag([val, val]), device=torch_device) assert qml.math.allclose(mat, exp) - def test_pauli_rot_generator(self): + @pytest.mark.usefixtures("use_legacy_opmath") + def test_pauli_rot_generator_legacy_opmath(self): """Test that the generator of the PauliRot operation is correctly returned.""" op = qml.PauliRot(0.65, "ZY", wires=["a", 7]) @@ -3027,6 +3028,16 @@ def test_pauli_rot_generator(self): assert gen.operands[0].name == expected.obs[0].name assert gen.operands[1].wires == expected.obs[1].wires + def test_pauli_rot_generator(self): + """Test that the generator of the PauliRot operation + is correctly returned.""" + op = qml.PauliRot(0.65, "ZY", wires=["a", 7]) + gen, coeff = qml.generator(op) + expected = qml.PauliZ("a") @ qml.PauliY(7) + + assert coeff == -0.5 + assert gen == expected + class TestMultiRZ: """Test the MultiRZ operation.""" @@ -3178,18 +3189,19 @@ def decomp_circuit(theta): assert np.allclose(qml.jacobian(circuit)(angle), qml.jacobian(decomp_circuit)(angle)) @pytest.mark.parametrize("qubits", range(3, 6)) + @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_multirz_generator(self, qubits, mocker): """Test that the generator of the MultiRZ gate is correct.""" op = qml.MultiRZ(0.3, wires=range(qubits)) gen = op.generator() - assert isinstance(gen, SProd) + assert isinstance(gen, qml.Hamiltonian) expected_gen = qml.PauliZ(wires=0) for i in range(1, qubits): expected_gen = expected_gen @ qml.PauliZ(wires=i) - assert qml.equal(gen, qml.s_prod(-0.5, expected_gen)) + assert qml.equal(gen, qml.Hamiltonian([-0.5], [expected_gen])) spy = mocker.spy(qml.utils, "pauli_eigs") diff --git a/tests/ops/qubit/test_qchem_ops.py b/tests/ops/qubit/test_qchem_ops.py index e2d2035fa64..7d31f842386 100644 --- a/tests/ops/qubit/test_qchem_ops.py +++ b/tests/ops/qubit/test_qchem_ops.py @@ -14,7 +14,7 @@ """ Unit tests for the available qubit operations for quantum chemistry purposes. """ -# pylint: disable=too-few-public-methods +# pylint: disable=too-few-public-methods, unnecessary-lambda-assignment import pytest import numpy as np from scipy.linalg import expm, fractional_matrix_power @@ -46,6 +46,7 @@ class TestParameterFrequencies: + @pytest.mark.usefixtures("use_legacy_and_new_opmath") @pytest.mark.parametrize("op", PARAMETRIZED_QCHEM_OPERATIONS) def test_parameter_frequencies_match_generator(self, op, tol): if not qml.operation.has_gen(op): @@ -1227,3 +1228,15 @@ def test_label_method(op, label1, label2, label3): assert op.label() == label1 assert op.label(decimals=2) == label2 assert op.label(decimals=0) == label3 + + +@pytest.mark.usefixtures("use_legacy_and_new_opmath") +@pytest.mark.parametrize("op", PARAMETRIZED_QCHEM_OPERATIONS) +def test_generators(op): + """Check that the type of the generator returned by the qchem ops is + the same as the type pointed to by qml.Hamiltonian (either Hamiltonian + or LinearCombiantion) for both legacy and new opmath""" + if isinstance(op, (qml.ops.DoubleExcitationPlus, qml.ops.DoubleExcitationMinus)): + pytest.skip(reason="Operator has SparseHamiltonian generator instead") + + assert isinstance(op.generator(), qml.Hamiltonian) diff --git a/tests/ops/qutrit/test_qutrit_observables.py b/tests/ops/qutrit/test_qutrit_observables.py index 9a8c8afa24c..7c793b7ce5b 100644 --- a/tests/ops/qutrit/test_qutrit_observables.py +++ b/tests/ops/qutrit/test_qutrit_observables.py @@ -371,6 +371,7 @@ def test_matrix(self, index, mat, eigs, tol): assert np.allclose(res_static, mat) assert np.allclose(res_dynamic, mat) + @pytest.mark.usefixtures("use_legacy_opmath") def test_obs_data(self): """Test that the _obs_data() method of qml.GellMann returns the correct observable data.""" diff --git a/tests/optimize/test_qng.py b/tests/optimize/test_qng.py index 6eddb607ccb..6651bbccbeb 100644 --- a/tests/optimize/test_qng.py +++ b/tests/optimize/test_qng.py @@ -73,6 +73,32 @@ def circuit(params): assert np.allclose(step1, expected_step) assert np.allclose(step2, expected_step) + @pytest.mark.usefixtures("use_legacy_opmath") + def test_step_and_cost_autograd_with_gen_hamiltonian_legacy_opmath(self): + """Test that the correct cost and step is returned via the + step_and_cost method for the QNG optimizer when the generator + of an operator is a Hamiltonian""" + + dev = qml.device("default.qubit", wires=4) + + @qml.qnode(dev) + def circuit(params): + qml.DoubleExcitation(params[0], wires=[0, 1, 2, 3]) + qml.RY(params[1], wires=0) + return qml.expval(qml.PauliZ(0)) + + var = np.array([0.011, 0.012]) + opt = qml.QNGOptimizer(stepsize=0.01) + + step1, res = opt.step_and_cost(circuit, var) + step2 = opt.step(circuit, var) + + expected = circuit(var) + expected_step = var - opt.stepsize * 4 * qml.grad(circuit)(var) + assert np.all(res == expected) + assert np.allclose(step1, expected_step) + assert np.allclose(step2, expected_step) + def test_step_and_cost_autograd_with_gen_hamiltonian(self): """Test that the correct cost and step is returned via the step_and_cost method for the QNG optimizer when the generator diff --git a/tests/optimize/test_spsa.py b/tests/optimize/test_spsa.py index 9d116cd359f..910d9881c2b 100644 --- a/tests/optimize/test_spsa.py +++ b/tests/optimize/test_spsa.py @@ -442,6 +442,43 @@ def cost(params): ): opt.step(cost, params) + @pytest.mark.usefixtures("use_legacy_opmath") + @pytest.mark.slow + def test_lighting_device_legacy_opmath(self): + """Test SPSAOptimizer implementation with lightning.qubit device.""" + coeffs = [0.2, -0.543, 0.4514] + obs = [ + qml.PauliX(0) @ qml.PauliZ(1), + qml.PauliZ(0) @ qml.Hadamard(2), + qml.PauliX(3) @ qml.PauliZ(1), + ] + H = qml.Hamiltonian(coeffs, obs) + num_qubits = 4 + dev = qml.device("lightning.qubit", wires=num_qubits) + + @qml.qnode(dev) + def cost_fun(params, num_qubits=1): + qml.BasisState([1, 1, 0, 0], wires=range(num_qubits)) + for i in range(num_qubits): + qml.Rot(*params[i], wires=0) + qml.CNOT(wires=[2, 3]) + qml.CNOT(wires=[2, 0]) + qml.CNOT(wires=[3, 1]) + return qml.expval(H) + + init_params = np.random.normal(0, np.pi, (num_qubits, 3), requires_grad=True) + params = init_params + + init_energy = cost_fun(init_params, num_qubits) + + max_iterations = 100 + opt = qml.SPSAOptimizer(maxiter=max_iterations) + for _ in range(max_iterations): + params, energy = opt.step_and_cost(cost_fun, params, num_qubits=num_qubits) + + assert np.all(params != init_params) + assert energy < init_energy + @pytest.mark.xfail(reason="Lightning cannot use adjoint with state prep ops yet") @pytest.mark.slow def test_lighting_device(self): @@ -458,7 +495,7 @@ def test_lighting_device(self): @qml.qnode(dev) def cost_fun(params, num_qubits=1): - qml.BasisState(np.array([1, 1, 0, 0]), wires=range(num_qubits)) + qml.BasisState([1, 1, 0, 0], wires=range(num_qubits)) for i in range(num_qubits): qml.Rot(*params[i], wires=0) qml.CNOT(wires=[2, 3]) diff --git a/tests/pauli/grouping/test_pauli_group_observables.py b/tests/pauli/grouping/test_pauli_group_observables.py index a2d7c3baf73..7863fbde567 100644 --- a/tests/pauli/grouping/test_pauli_group_observables.py +++ b/tests/pauli/grouping/test_pauli_group_observables.py @@ -396,11 +396,6 @@ def test_return_list_coefficients(self): def test_return_new_opmath(self): """Test that using new opmath causes grouped observables to have Prods instead of Tensors""" - old_observables = [ - Tensor(PauliX(0), PauliZ(1)), - Tensor(PauliY(2), PauliZ(1)), - Tensor(PauliZ(1), PauliZ(2)), - ] new_observables = [ qml.prod(PauliX(0), PauliZ(1)), qml.prod(PauliY(2), PauliZ(1)), @@ -412,14 +407,26 @@ def test_return_new_opmath(self): qml.s_prod(1.5, qml.prod(PauliZ(1), PauliZ(2))), ] - old_groups = group_observables(old_observables) new_groups = group_observables(new_observables) mixed_groups = group_observables(mixed_observables) - assert all(isinstance(o, Tensor) for g in old_groups for o in g) assert all(isinstance(o, qml.ops.Prod) for g in new_groups for o in g) assert all(isinstance(o, qml.ops.Prod) for g in mixed_groups for o in g) + @pytest.mark.usefixtures("use_legacy_opmath") + def test_return_new_opmath_legacy_opmath(self): + """Test that using new opmath causes grouped observables to have Prods instead of + Tensors""" + old_observables = [ + Tensor(PauliX(0), PauliZ(1)), + Tensor(PauliY(2), PauliZ(1)), + Tensor(PauliZ(1), PauliZ(2)), + ] + + old_groups = group_observables(old_observables) + + assert all(isinstance(o, Tensor) for g in old_groups for o in g) + class TestDifferentiable: """Tests that grouping observables is differentiable with respect to the coefficients.""" diff --git a/tests/pauli/test_conversion.py b/tests/pauli/test_conversion.py index ff15fa4d57d..46301c4e527 100644 --- a/tests/pauli/test_conversion.py +++ b/tests/pauli/test_conversion.py @@ -86,15 +86,25 @@ def test_hide_identity_true_all_identities(self): for tensor in tensors: assert all(isinstance(o, Identity) for o in tensor.obs) + @pytest.mark.usefixtures("use_legacy_opmath") @pytest.mark.parametrize("hide_identity", [True, False]) @pytest.mark.parametrize("hamiltonian", test_hamiltonians) - def test_observable_types(self, hamiltonian, hide_identity): + def test_observable_types_legacy_opmath(self, hamiltonian, hide_identity): """Tests that the Hamiltonian decomposes into a linear combination of Pauli words.""" allowed_obs = (Tensor, Identity, PauliX, PauliY, PauliZ) _, decomposed_obs = qml.pauli_decompose(hamiltonian, hide_identity).terms() assert all((isinstance(o, allowed_obs) for o in decomposed_obs)) + @pytest.mark.parametrize("hide_identity", [True, False]) + @pytest.mark.parametrize("hamiltonian", test_hamiltonians) + def test_observable_types(self, hamiltonian, hide_identity): + """Tests that the Hamiltonian decomposes into a linear combination of Pauli words.""" + allowed_obs = (qml.ops.Prod, Identity, PauliX, PauliY, PauliZ) + + _, decomposed_obs = qml.pauli_decompose(hamiltonian, hide_identity).terms() + assert all((isinstance(o, allowed_obs) for o in decomposed_obs)) + @pytest.mark.parametrize("hamiltonian", test_hamiltonians) def test_result_length(self, hamiltonian): """Tests that tensors are composed of a number of terms equal to the number @@ -196,9 +206,10 @@ def test_hide_identity_true_all_identities(self): for tensor in tensors: assert all(isinstance(o, Identity) for o in tensor.obs) + @pytest.mark.usefixtures("use_legacy_opmath") @pytest.mark.parametrize("hide_identity", [True, False]) @pytest.mark.parametrize("hamiltonian", test_hamiltonians) - def test_observable_types(self, hamiltonian, hide_identity): + def test_observable_types_legacy_opmath(self, hamiltonian, hide_identity): """Tests that the Hamiltonian decomposes into a linear combination of tensors, the identity matrix, and Pauli matrices.""" allowed_obs = (Tensor, Identity, PauliX, PauliY, PauliZ) @@ -208,6 +219,18 @@ def test_observable_types(self, hamiltonian, hide_identity): ).terms() assert all((isinstance(o, allowed_obs) for o in decomposed_obs)) + @pytest.mark.parametrize("hide_identity", [True, False]) + @pytest.mark.parametrize("hamiltonian", test_hamiltonians) + def test_observable_types(self, hamiltonian, hide_identity): + """Tests that the Hamiltonian decomposes into a linear combination of tensors, + the identity matrix, and Pauli matrices.""" + allowed_obs = (qml.ops.Prod, Identity, PauliX, PauliY, PauliZ) + + _, decomposed_obs = qml.pauli_decompose( + hamiltonian, hide_identity, check_hermitian=False + ).terms() + assert all((isinstance(o, allowed_obs) for o in decomposed_obs)) + @pytest.mark.parametrize("hamiltonian", test_hamiltonians) def test_result_length(self, hamiltonian): """Tests that tensors are composed of a number of terms equal to the number @@ -244,9 +267,10 @@ def test_to_paulisentence(self, hamiltonian): assert isinstance(ps, qml.pauli.PauliSentence) assert np.allclose(hamiltonian, ps.to_mat(range(num_qubits))) + @pytest.mark.usefixtures("use_legacy_opmath") @pytest.mark.parametrize("hide_identity", [True, False]) @pytest.mark.parametrize("matrix", test_general_matrix) - def test_observable_types_general(self, matrix, hide_identity): + def test_observable_types_general_legacy_opmath(self, matrix, hide_identity): """Tests that the matrix decomposes into a linear combination of tensors, the identity matrix, and Pauli matrices.""" shape = matrix.shape @@ -271,6 +295,33 @@ def test_observable_types_general(self, matrix, hide_identity): tensors = filter(lambda obs: isinstance(obs, Tensor), decomposed_obs) assert all(len(tensor.obs) == num_qubits for tensor in tensors) + @pytest.mark.parametrize("hide_identity", [True, False]) + @pytest.mark.parametrize("matrix", test_general_matrix) + def test_observable_types_general(self, matrix, hide_identity): + """Tests that the matrix decomposes into a linear combination of tensors, + the identity matrix, and Pauli matrices.""" + shape = matrix.shape + num_qubits = int(np.ceil(np.log2(max(shape)))) + allowed_obs = (qml.ops.Prod, Identity, PauliX, PauliY, PauliZ) + + decomposed_coeff, decomposed_obs = qml.pauli_decompose( + matrix, hide_identity, check_hermitian=False + ).terms() + + assert all((isinstance(o, allowed_obs) for o in decomposed_obs)) + + linear_comb = sum( + [ + decomposed_coeff[i] * qml.matrix(o, wire_order=range(num_qubits)) + for i, o in enumerate(decomposed_obs) + ] + ) + assert np.allclose(matrix, linear_comb[: shape[0], : shape[1]]) + + if not hide_identity: + tensors = filter(lambda obs: isinstance(obs, qml.ops.Prod), decomposed_obs) + assert all(len(tensor.wires) == num_qubits for tensor in tensors) + @pytest.mark.parametrize("matrix", test_general_matrix) def test_to_paulisentence_general(self, matrix): """Test that a PauliSentence is returned if the kwarg paulis is set to True""" diff --git a/tests/pauli/test_pauli_arithmetic.py b/tests/pauli/test_pauli_arithmetic.py index 019436e9d78..ca81edbe0d0 100644 --- a/tests/pauli/test_pauli_arithmetic.py +++ b/tests/pauli/test_pauli_arithmetic.py @@ -448,18 +448,22 @@ def test_operation_empty_nowires(self): ), ) + @pytest.mark.usefixtures("use_legacy_opmath") @pytest.mark.parametrize("pw, h", tup_pw_hamiltonian) def test_hamiltonian(self, pw, h): """Test that a PauliWord can be cast to a Hamiltonian.""" pw_h = pw.hamiltonian() + h = qml.operation.convert_to_legacy_H(h) assert pw_h.compare(h) + @pytest.mark.usefixtures("use_legacy_opmath") def test_hamiltonian_empty(self): """Test that an empty PauliWord with wire_order returns Identity Hamiltonian.""" op = PauliWord({}).hamiltonian(wire_order=[0, 1]) id = qml.Hamiltonian([1], [qml.Identity(wires=[0, 1])]) assert op.compare(id) + @pytest.mark.usefixtures("use_legacy_opmath") def test_hamiltonian_empty_error(self): """Test that a ValueError is raised if an empty PauliWord is cast to a Hamiltonian.""" @@ -1104,18 +1108,22 @@ def test_operation_wire_order(self): ), ) + @pytest.mark.usefixtures("use_legacy_opmath") @pytest.mark.parametrize("ps, h", tup_ps_hamiltonian) def test_hamiltonian(self, ps, h): """Test that a PauliSentence can be cast to a Hamiltonian.""" ps_h = ps.hamiltonian() + h = qml.operation.convert_to_legacy_H(h) assert ps_h.compare(h) + @pytest.mark.usefixtures("use_legacy_opmath") def test_hamiltonian_empty(self): """Test that an empty PauliSentence with wire_order returns Identity.""" op = ps5.hamiltonian(wire_order=[0, 1]) id = qml.Hamiltonian([], []) assert op.compare(id) + @pytest.mark.usefixtures("use_legacy_opmath") def test_hamiltonian_empty_error(self): """Test that a ValueError is raised if an empty PauliSentence is cast to a Hamiltonian.""" @@ -1124,6 +1132,7 @@ def test_hamiltonian_empty_error(self): ): ps5.hamiltonian() + @pytest.mark.usefixtures("use_legacy_opmath") def test_hamiltonian_wire_order(self): """Test that the wire_order parameter is used when the pauli representation is empty""" op = ps5.hamiltonian(wire_order=["a", "b"]) diff --git a/tests/pauli/test_pauli_utils.py b/tests/pauli/test_pauli_utils.py index 80a456698ca..a6bfe748f1c 100644 --- a/tests/pauli/test_pauli_utils.py +++ b/tests/pauli/test_pauli_utils.py @@ -61,6 +61,25 @@ ] +def _make_pauli_word_strings(): + return [ + (PauliX(0), {0: 0}, "X"), + (Identity(0), {0: 0}, "I"), + (PauliZ(0) @ PauliY(1), {0: 0, 1: 1}, "ZY"), + (PauliX(1), {0: 0, 1: 1}, "IX"), + (PauliX(1), None, "X"), + (PauliX(1), {1: 0, 0: 1}, "XI"), + (PauliZ("a") @ PauliY("b") @ PauliZ("d"), {"a": 0, "b": 1, "c": 2, "d": 3}, "ZYIZ"), + (PauliZ("a") @ PauliY("b") @ PauliZ("d"), None, "ZYZ"), + (PauliX("a") @ PauliY("b") @ PauliZ("d"), {"d": 0, "c": 1, "b": 2, "a": 3}, "ZIYX"), + (4.5 * PauliX(0), {0: 0}, "X"), + (qml.prod(PauliX(0), PauliY(1)), {0: 0, 1: 1}, "XY"), + (PauliX(0) @ PauliZ(0), {0: 0}, "Y"), + (3 * PauliZ(0) @ PauliY(3), {0: 0, 3: 1}, "ZY"), + (qml.s_prod(8, qml.PauliX(0) @ qml.PauliZ(1)), {0: 0, 1: 1}, "XZ"), + ] + + class TestGroupingUtils: """Basic usage and edge-case tests for the measurement optimization utility functions.""" @@ -204,6 +223,7 @@ def test_observables_to_binary_matrix_n_qubits_arg(self): ValueError, observables_to_binary_matrix, observables, n_qubits_invalid ) + @pytest.mark.usefixtures("use_legacy_opmath") def test_is_qwc(self): """Determining if two Pauli words are qubit-wise commuting.""" @@ -349,6 +369,7 @@ def test_are_identical_pauli_words(self): assert not are_identical_pauli_words(pauli_word_7, pauli_word_4) assert not are_identical_pauli_words(pauli_word_6, pauli_word_4) + @pytest.mark.usefixtures("use_legacy_opmath") def test_are_identical_pauli_words_hamiltonian_unsupported(self): """Test that using Hamiltonians that are valid Pauli words with are_identical_pauli_words always returns False""" @@ -416,31 +437,24 @@ def test_qwc_complement_adj_matrix_exception(self): with pytest.raises(ValueError, match="Expected a binary array, instead got"): qwc_complement_adj_matrix(not_binary_observables) - @pytest.mark.parametrize( - "pauli_word,wire_map,expected_string", - [ - (PauliX(0), {0: 0}, "X"), - (Identity(0), {0: 0}, "I"), - (PauliZ(0) @ PauliY(1), {0: 0, 1: 1}, "ZY"), - (PauliX(1), {0: 0, 1: 1}, "IX"), - (PauliX(1), None, "X"), - (PauliX(1), {1: 0, 0: 1}, "XI"), - (PauliZ("a") @ PauliY("b") @ PauliZ("d"), {"a": 0, "b": 1, "c": 2, "d": 3}, "ZYIZ"), - (PauliZ("a") @ PauliY("b") @ PauliZ("d"), None, "ZYZ"), - (PauliX("a") @ PauliY("b") @ PauliZ("d"), {"d": 0, "c": 1, "b": 2, "a": 3}, "ZIYX"), - (4.5 * PauliX(0), {0: 0}, "X"), - (qml.prod(PauliX(0), PauliY(1)), {0: 0, 1: 1}, "XY"), - (PauliX(0) @ PauliZ(0), {0: 0}, "X"), # second operator is ignored!! - (3 * PauliZ(0) @ PauliY(3), {0: 0, 3: 1}, "ZY"), - (qml.s_prod(8, qml.PauliX(0) @ qml.PauliZ(1)), {0: 0, 1: 1}, "XZ"), - (qml.Hamiltonian([4], [qml.PauliX(0) @ qml.PauliZ(1)]), {0: 0, 1: 1}, "XZ"), - ], - ) + PAULI_WORD_STRINGS = _make_pauli_word_strings() + + @pytest.mark.parametrize("pauli_word,wire_map,expected_string", PAULI_WORD_STRINGS) def test_pauli_word_to_string(self, pauli_word, wire_map, expected_string): """Test that Pauli words are correctly converted into strings.""" obtained_string = pauli_word_to_string(pauli_word, wire_map) assert obtained_string == expected_string + with qml.operation.disable_new_opmath_cm(): + PAULI_WORD_STRINGS_LEGACY = _make_pauli_word_strings() + + @pytest.mark.usefixtures("use_legacy_opmath") + @pytest.mark.parametrize("pauli_word,wire_map,expected_string", PAULI_WORD_STRINGS_LEGACY) + def test_pauli_word_to_string_legacy_opmath(self, pauli_word, wire_map, expected_string): + """Test that Pauli words are correctly converted into strings.""" + obtained_string = pauli_word_to_string(pauli_word, wire_map) + assert obtained_string == expected_string + @pytest.mark.parametrize("non_pauli_word", non_pauli_words) def test_pauli_word_to_string_invalid_input(self, non_pauli_word): """Ensure invalid inputs are handled properly when converting Pauli words to strings.""" @@ -462,7 +476,7 @@ def test_pauli_word_to_string_invalid_input(self, non_pauli_word): def test_string_to_pauli_word(self, pauli_string, wire_map, expected_pauli): """Test that valid strings are correctly converted into Pauli words.""" obtained_pauli = string_to_pauli_word(pauli_string, wire_map) - assert obtained_pauli.compare(expected_pauli) + assert qml.equal(obtained_pauli, expected_pauli) @pytest.mark.parametrize( "non_pauli_string,wire_map,error_type,error_message", @@ -483,7 +497,11 @@ def test_string_to_pauli_word_invalid_input( "pauli_word,wire_map,expected_matrix", [ (PauliX(0), {0: 0}, PauliX(0).matrix()), - (Identity(0), {0: 0}, np.eye(2)), + # ( + # Identity(0), + # {0: 0}, + # np.eye(2), + # ), # TODO update PauliSentence.to_mat to handle Identities better https://github.com/PennyLaneAI/pennylane/issues/5354 ( PauliZ(0) @ PauliY(1), {0: 0, 1: 1}, @@ -499,7 +517,7 @@ def test_string_to_pauli_word_invalid_input( {1: 0, 0: 1}, np.array([[0, 0, -1j, 0], [0, 0, 0, 1j], [1j, 0, 0, 0], [0, -1j, 0, 0]]), ), - (Identity(0), {0: 0, 1: 1}, np.eye(4)), + # (Identity(0), {0: 0, 1: 1}, np.eye(4)), # TODO update PauliSentence.to_mat to handle Identities better https://github.com/PennyLaneAI/pennylane/issues/5354 (PauliX(2), None, PauliX(2).matrix()), ( PauliX(2), @@ -553,6 +571,7 @@ def test_string_to_pauli_word_invalid_input( ) def test_pauli_word_to_matrix(self, pauli_word, wire_map, expected_matrix): """Test that Pauli words are correctly converted into matrices.""" + obtained_matrix = pauli_word_to_matrix(pauli_word, wire_map) assert np.allclose(obtained_matrix, expected_matrix) @@ -635,7 +654,7 @@ def test_two_qubit_pauli_group(self): ] pg_2 = list(pauli_group(2, wire_map=wire_map)) - assert all(expected.compare(obtained) for expected, obtained in zip(expected_pg_2, pg_2)) + assert all(qml.equal(expected, obtained) for expected, obtained in zip(expected_pg_2, pg_2)) @pytest.mark.parametrize( "pauli_word_1,pauli_word_2,expected_product", @@ -762,7 +781,21 @@ def test_scaling(self, n): """Test if the number of groups is equal to 3**n""" assert len(partition_pauli_group(n)) == 3**n + @pytest.mark.usefixtures("use_legacy_opmath") @pytest.mark.parametrize("n", range(1, 6)) + def test_is_qwc_legacy_opmath(self, n): + """Test if each group contains only qubit-wise commuting terms""" + for group in partition_pauli_group(n): + size = len(group) + for i in range(size): + for j in range(i, size): + s1 = group[i] + s2 = group[j] + w1 = string_to_pauli_word(s1) + w2 = string_to_pauli_word(s2) + assert is_commuting(w1, w2) + + @pytest.mark.parametrize("n", range(2, 6)) def test_is_qwc(self, n): """Test if each group contains only qubit-wise commuting terms""" for group in partition_pauli_group(n): @@ -956,6 +989,9 @@ def test_diagonalize_qwc_pauli_words_catch_when_not_qwc(self, not_qwc_grouping): assert pytest.raises(ValueError, diagonalize_qwc_pauli_words, not_qwc_grouping) + @pytest.mark.usefixtures( + "use_legacy_opmath" + ) # Handling a LinearCombination is not a problem under new opmath anymore def test_diagonalize_qwc_pauli_words_catch_invalid_type(self): """Test for ValueError raise when diagonalize_qwc_pauli_words is given a list containing invalid operator types.""" @@ -967,9 +1003,8 @@ def test_diagonalize_qwc_pauli_words_catch_invalid_type(self): class TestObservableHF: - @pytest.mark.parametrize( - ("hamiltonian", "result"), - [ + with qml.operation.disable_new_opmath_cm(): + HAMILTONIAN_SIMPLIFY = [ ( qml.Hamiltonian( np.array([0.5, 0.5]), @@ -1006,8 +1041,10 @@ class TestObservableHF: [qml.PauliX(0) @ qml.PauliY(1), qml.PauliX(0) @ qml.PauliZ(1)], ), ), - ], - ) + ] + + @pytest.mark.usefixtures("use_legacy_opmath") + @pytest.mark.parametrize(("hamiltonian", "result"), HAMILTONIAN_SIMPLIFY) def test_simplify(self, hamiltonian, result): r"""Test that simplify returns the correct hamiltonian.""" h = simplify(hamiltonian) @@ -1015,71 +1052,73 @@ def test_simplify(self, hamiltonian, result): class TestTapering: - terms_bin_mat_data = [ - ( - [ - qml.Identity(wires=[0]), - qml.PauliZ(wires=[0]), - qml.PauliZ(wires=[1]), - qml.PauliZ(wires=[2]), - qml.PauliZ(wires=[3]), - qml.PauliZ(wires=[0]) @ qml.PauliZ(wires=[1]), - qml.PauliY(wires=[0]) - @ qml.PauliX(wires=[1]) - @ qml.PauliX(wires=[2]) - @ qml.PauliY(wires=[3]), - qml.PauliY(wires=[0]) - @ qml.PauliY(wires=[1]) - @ qml.PauliX(wires=[2]) - @ qml.PauliX(wires=[3]), - qml.PauliX(wires=[0]) - @ qml.PauliX(wires=[1]) - @ qml.PauliY(wires=[2]) - @ qml.PauliY(wires=[3]), - qml.PauliX(wires=[0]) - @ qml.PauliY(wires=[1]) - @ qml.PauliY(wires=[2]) - @ qml.PauliX(wires=[3]), - qml.PauliZ(wires=[0]) @ qml.PauliZ(wires=[2]), - qml.PauliZ(wires=[0]) @ qml.PauliZ(wires=[3]), - qml.PauliZ(wires=[1]) @ qml.PauliZ(wires=[2]), - qml.PauliZ(wires=[1]) @ qml.PauliZ(wires=[3]), - qml.PauliZ(wires=[2]) @ qml.PauliZ(wires=[3]), - ], - 4, - np.array( + with qml.operation.disable_new_opmath_cm(): + terms_bin_mat_data = [ + ( [ - [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, 1, 0, 0, 0, 0], - [1, 1, 0, 0, 0, 0, 0, 0], - [1, 0, 0, 1, 1, 1, 1, 1], - [1, 1, 0, 0, 1, 1, 1, 1], - [0, 0, 1, 1, 1, 1, 1, 1], - [0, 1, 1, 0, 1, 1, 1, 1], - [1, 0, 1, 0, 0, 0, 0, 0], - [1, 0, 0, 1, 0, 0, 0, 0], - [0, 1, 1, 0, 0, 0, 0, 0], - [0, 1, 0, 1, 0, 0, 0, 0], - [0, 0, 1, 1, 0, 0, 0, 0], - ] + qml.Identity(wires=[0]), + qml.PauliZ(wires=[0]), + qml.PauliZ(wires=[1]), + qml.PauliZ(wires=[2]), + qml.PauliZ(wires=[3]), + qml.PauliZ(wires=[0]) @ qml.PauliZ(wires=[1]), + qml.PauliY(wires=[0]) + @ qml.PauliX(wires=[1]) + @ qml.PauliX(wires=[2]) + @ qml.PauliY(wires=[3]), + qml.PauliY(wires=[0]) + @ qml.PauliY(wires=[1]) + @ qml.PauliX(wires=[2]) + @ qml.PauliX(wires=[3]), + qml.PauliX(wires=[0]) + @ qml.PauliX(wires=[1]) + @ qml.PauliY(wires=[2]) + @ qml.PauliY(wires=[3]), + qml.PauliX(wires=[0]) + @ qml.PauliY(wires=[1]) + @ qml.PauliY(wires=[2]) + @ qml.PauliX(wires=[3]), + qml.PauliZ(wires=[0]) @ qml.PauliZ(wires=[2]), + qml.PauliZ(wires=[0]) @ qml.PauliZ(wires=[3]), + qml.PauliZ(wires=[1]) @ qml.PauliZ(wires=[2]), + qml.PauliZ(wires=[1]) @ qml.PauliZ(wires=[3]), + qml.PauliZ(wires=[2]) @ qml.PauliZ(wires=[3]), + ], + 4, + np.array( + [ + [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, 1, 0, 0, 0, 0], + [1, 1, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 1, 1, 1, 1, 1], + [1, 1, 0, 0, 1, 1, 1, 1], + [0, 0, 1, 1, 1, 1, 1, 1], + [0, 1, 1, 0, 1, 1, 1, 1], + [1, 0, 1, 0, 0, 0, 0, 0], + [1, 0, 0, 1, 0, 0, 0, 0], + [0, 1, 1, 0, 0, 0, 0, 0], + [0, 1, 0, 1, 0, 0, 0, 0], + [0, 0, 1, 1, 0, 0, 0, 0], + ] + ), ), - ), - ( - [ - qml.PauliZ(wires=["a"]) @ qml.PauliX(wires=["b"]), - qml.PauliZ(wires=["a"]) @ qml.PauliY(wires=["c"]), - qml.PauliX(wires=["a"]) @ qml.PauliY(wires=["d"]), - ], - 4, - np.array( - [[1, 0, 0, 0, 0, 1, 0, 0], [1, 0, 1, 0, 0, 0, 1, 0], [0, 0, 0, 1, 1, 0, 0, 1]] + ( + [ + qml.PauliZ(wires=["a"]) @ qml.PauliX(wires=["b"]), + qml.PauliZ(wires=["a"]) @ qml.PauliY(wires=["c"]), + qml.PauliX(wires=["a"]) @ qml.PauliY(wires=["d"]), + ], + 4, + np.array( + [[1, 0, 0, 0, 0, 1, 0, 0], [1, 0, 1, 0, 0, 0, 1, 0], [0, 0, 0, 1, 1, 0, 0, 1]] + ), ), - ), - ] + ] + @pytest.mark.usefixtures("use_legacy_opmath") @pytest.mark.parametrize(("terms", "num_qubits", "result"), terms_bin_mat_data) def test_binary_matrix_from_pws(self, terms, num_qubits, result): r"""Test that _binary_matrix_from_pws returns the correct result.""" diff --git a/tests/pulse/test_rydberg.py b/tests/pulse/test_rydberg.py index dc05f5db527..cddc733e1cf 100644 --- a/tests/pulse/test_rydberg.py +++ b/tests/pulse/test_rydberg.py @@ -206,6 +206,7 @@ def f(p, t): assert len(Hd.ops) == 1 assert qml.equal(Hd.ops[0], ops_expected[0]) + @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_no_detuning(self): """Test that when detuning not specified, the drive term is correctly defined.""" diff --git a/tests/qchem/of_tests/test_convert.py b/tests/qchem/of_tests/test_convert.py index cf7233e69a4..99f143c9720 100644 --- a/tests/qchem/of_tests/test_convert.py +++ b/tests/qchem/of_tests/test_convert.py @@ -24,7 +24,7 @@ import pennylane as qml from pennylane import numpy as np from pennylane import qchem -from pennylane.operation import disable_new_opmath, enable_new_opmath +from pennylane.operation import active_new_opmath openfermion = pytest.importorskip("openfermion") openfermionpyscf = pytest.importorskip("openfermionpyscf") @@ -394,6 +394,7 @@ def test_observable_conversion(_, terms_ref, custom_wires, monkeypatch): ) +@pytest.mark.usefixtures("use_legacy_and_new_opmath") @pytest.mark.parametrize("pl_op, of_op, wire_order", ops_wires) def test_operation_conversion(pl_op, of_op, wire_order): """Assert the conversion between pennylane and openfermion operators""" @@ -402,19 +403,11 @@ def test_operation_conversion(pl_op, of_op, wire_order): converted_of_op = qml.qchem.convert._openfermion_to_pennylane(of_op) _, converted_of_op_terms = converted_of_op - assert all(isinstance(term, pauli_ops_and_tensor) for term in converted_of_op_terms) - assert np.allclose( - qml.matrix(qml.dot(*pl_op), wire_order=wire_order), - qml.matrix(qml.dot(*converted_of_op), wire_order=wire_order), - ) - - # test arithmetic types - enable_new_opmath() - converted_of_op = qml.qchem.convert._openfermion_to_pennylane(of_op) - disable_new_opmath() - _, converted_of_op_terms = converted_of_op - assert all(isinstance(term, pauli_ops_and_prod) for term in converted_of_op_terms) + assert all( + isinstance(term, pauli_ops_and_prod if active_new_opmath() else pauli_ops_and_tensor) + for term in converted_of_op_terms + ) assert np.allclose( qml.matrix(qml.dot(*pl_op), wire_order=wire_order), @@ -490,8 +483,8 @@ def test_types_consistency(): # Build PL operator using 'import_operator' pl = qml.qchem.convert.import_operator(of, "openfermion") - ops = pl.ops - ops_ref = pl_ref.ops + ops = pl.terms()[1] + ops_ref = pl_ref.terms()[1] for i, op in enumerate(ops): assert op.name == ops_ref[i].name @@ -536,25 +529,21 @@ def test_types_consistency(): ) +@pytest.mark.usefixtures("use_legacy_and_new_opmath") @pytest.mark.parametrize("of_op, pl_h, pl_op, wires", of_pl_ops) def test_import_operator(of_op, pl_h, pl_op, wires): """Test the import_operator function correctly imports an OpenFermion operator into a PL one.""" of_h = qml.qchem.convert.import_operator(of_op, "openfermion", wires=wires) - assert qml.equal(of_h, pl_h) + assert qml.pauli.pauli_sentence(pl_h) == qml.pauli.pauli_sentence(of_h) - enable_new_opmath() - of_arithmetic_op = qml.qchem.convert.import_operator(of_op, "openfermion", wires=wires) - disable_new_opmath() + assert isinstance(of_h, type(pl_op) if active_new_opmath() else qml.Hamiltonian) - assert isinstance(of_arithmetic_op, type(pl_op)) - if isinstance(of_arithmetic_op, qml.ops.Sum): + if isinstance(of_h, qml.ops.Sum): assert all( isinstance(term, qml.ops.SProd) and isinstance(term.base, pauli_ops_and_prod) - for term in of_arithmetic_op.operands + for term in of_h.operands ) - assert np.allclose( - qml.matrix(of_arithmetic_op, wire_order=wires), qml.matrix(pl_op, wire_order=wires) - ) + assert np.allclose(qml.matrix(of_h, wire_order=wires), qml.matrix(pl_op, wire_order=wires)) op_1 = ( @@ -781,7 +770,7 @@ def test_integration_mol_file_to_vqe_cost( vqe_hamiltonian = qml.qchem.convert.import_operator( qubit_hamiltonian, wires=custom_wires, format="openfermion" ) - assert len(vqe_hamiltonian.ops) > 1 # just to check if this runs + assert len(vqe_hamiltonian.terms()[1]) > 1 # just to check if this runs num_qubits = len(vqe_hamiltonian.wires) assert num_qubits == 2 * len(active) @@ -1966,7 +1955,8 @@ def test_rcisd_state_energy(molecule, basis, symm, charge, spin, tol): h_ferm = qchem.fermionic_observable(core_constant, one_mo, two_mo) H = qchem.qubit_observable(h_ferm) - energy_pl = np.conj(wf_cisd.T).dot(qml.matrix(H).dot(wf_cisd)) + H_mat = H.sparse_matrix().toarray() + energy_pl = np.conj(wf_cisd.T).dot(H_mat.dot(wf_cisd)) assert np.allclose(energy_pl, myci.e_tot, atol=1e-6) @@ -2002,7 +1992,8 @@ def test_ucisd_state_energy(molecule, basis, symm, charge, spin, tol): h_ferm = qchem.fermionic_observable(core_constant, one_mo, two_mo) H = qchem.qubit_observable(h_ferm) - energy_pl = np.conj(wf_cisd.T).dot(qml.matrix(H).dot(wf_cisd)) + H_mat = H.sparse_matrix().toarray() + energy_pl = np.conj(wf_cisd.T).dot(H_mat.dot(wf_cisd)) assert np.allclose(energy_pl, myci.e_tot, atol=1e-6) @@ -2039,7 +2030,8 @@ def test_rccsd_state_energy(molecule, basis, symm, charge, spin, tol): h_ferm = qchem.fermionic_observable(core_constant, one_mo, two_mo) H = qchem.qubit_observable(h_ferm) - energy_pl = np.conj(wf_ccsd.T).dot(qml.matrix(H).dot(wf_ccsd)) + H_mat = H.sparse_matrix().toarray() + energy_pl = np.conj(wf_ccsd.T).dot(H_mat.dot(wf_ccsd)) assert np.allclose(energy_pl, mycc.e_tot, atol=1e-4) @@ -2075,7 +2067,8 @@ def test_uccsd_state_energy(molecule, basis, symm, charge, spin, tol): h_ferm = qchem.fermionic_observable(core_constant, one_mo, two_mo) H = qchem.qubit_observable(h_ferm) - energy_pl = np.conj(wf_ccsd.T).dot(qml.matrix(H).dot(wf_ccsd)) + H_mat = H.sparse_matrix().toarray() + energy_pl = np.conj(wf_ccsd.T).dot(H_mat.dot(wf_ccsd)) assert np.allclose(energy_pl, mycc.e_tot, atol=1e-2) @@ -2112,8 +2105,8 @@ def test_dmrg_state_energy(molecule, basis, charge, spin, dmrg_dets_coeffs, dmrg h_ferm = qchem.fermionic_observable(core_constant, one_mo, two_mo) H = qchem.qubit_observable(h_ferm) - - energy_pl = np.conj(wf_dmrg.T).dot(qml.matrix(H).dot(wf_dmrg)) + H_mat = H.sparse_matrix().toarray() + energy_pl = np.conj(wf_dmrg.T).dot(H_mat.dot(wf_dmrg)) assert np.allclose(energy_pl, dmrg_e, atol=1e-6) @@ -2151,8 +2144,8 @@ def test_shci_state_energy(molecule, basis, charge, spin, shci_dets_coeffs, shci h_ferm = qchem.fermionic_observable(core_constant, one_mo, two_mo) H = qchem.qubit_observable(h_ferm) - - energy_pl = np.conj(wf_shci.T).dot(qml.matrix(H).dot(wf_shci)) + H_mat = H.sparse_matrix().toarray() + energy_pl = np.conj(wf_shci.T).dot(H_mat.dot(wf_shci)) assert np.allclose(energy_pl, shci_e, atol=1e-6) diff --git a/tests/qchem/of_tests/test_dipole_of.py b/tests/qchem/of_tests/test_dipole_of.py index aee3b527db5..e3e9acdb9c8 100644 --- a/tests/qchem/of_tests/test_dipole_of.py +++ b/tests/qchem/of_tests/test_dipole_of.py @@ -207,7 +207,7 @@ (h2o, x_h2o, 0, range(4), [4, 5], "bravyi_kitaev", coeffs_h2o, ops_h2o), ], ) -@pytest.mark.usefixtures("skip_if_no_openfermion_support") +@pytest.mark.usefixtures("skip_if_no_openfermion_support", "use_legacy_and_new_opmath") def test_dipole_obs(symbols, coords, charge, core, active, mapping, coeffs, ops, tol, tmpdir): r"""Tests the correctness of the dipole observable computed by the ``dipole`` function.""" @@ -224,11 +224,24 @@ def test_dipole_obs(symbols, coords, charge, core, active, mapping, coeffs, ops, assert len(dip) == len(ops) for i, _dip in enumerate(dip): - calc_coeffs = np.array(_dip.coeffs) + d_coeffs, d_ops = _dip.terms() + calc_coeffs = np.array(d_coeffs) exp_coeffs = np.array(coeffs[i]) assert np.allclose(calc_coeffs, exp_coeffs, **tol) - assert all(isinstance(o1, o2.__class__) for o1, o2 in zip(_dip.ops, ops[i])) - assert all(o1.wires == o2.wires for o1, o2 in zip(_dip.ops, ops[i])) + + r_ops = ops[i] + if not qml.operation.active_new_opmath(): + r_ops = [ + ( + qml.operation.Tensor(*obs.simplify()) + if isinstance(obs.simplify(), (qml.ops.op_math.Prod)) + else obs.simplify() + ) + for obs in ops[i] + ] + + assert all(isinstance(o1, o2.__class__) for o1, o2 in zip(d_ops, r_ops)) + assert all(qml.equal(o1, o2) for o1, o2 in zip(d_ops, r_ops)) @pytest.mark.parametrize( diff --git a/tests/qchem/of_tests/test_molecular_hamiltonian.py b/tests/qchem/of_tests/test_molecular_hamiltonian.py index 460f6aaa850..fc449b3a2c7 100644 --- a/tests/qchem/of_tests/test_molecular_hamiltonian.py +++ b/tests/qchem/of_tests/test_molecular_hamiltonian.py @@ -17,13 +17,11 @@ # pylint: disable=too-many-arguments, protected-access import pytest +import pennylane as qml from pennylane import Identity, PauliX, PauliY, PauliZ from pennylane import numpy as np from pennylane import qchem -from pennylane.ops import Hamiltonian -from pennylane.ops.functions import dot -from pennylane.pauli import pauli_sentence -from pennylane.operation import enable_new_opmath, disable_new_opmath +from pennylane.operation import active_new_opmath test_symbols = ["C", "C", "N", "H", "H", "H", "H", "H"] test_coordinates = np.array( @@ -56,7 +54,6 @@ ) -@pytest.mark.parametrize("op_arithmetic", [False, True]) @pytest.mark.parametrize( ( "charge", @@ -73,7 +70,7 @@ (2, 1, "pyscf", 2, 2, "BRAVYI_kitaev"), ], ) -@pytest.mark.usefixtures("skip_if_no_openfermion_support") +@pytest.mark.usefixtures("skip_if_no_openfermion_support", "use_legacy_and_new_opmath") def test_building_hamiltonian( charge, mult, @@ -82,7 +79,6 @@ def test_building_hamiltonian( nact_orbs, mapping, tmpdir, - op_arithmetic, ): r"""Test that the generated Hamiltonian `built_hamiltonian` is an instance of the PennyLane Hamiltonian class and the correctness of the total number of qubits required to run the @@ -99,20 +95,16 @@ def test_building_hamiltonian( "mapping": mapping, "outpath": tmpdir.strpath, } - if op_arithmetic: - enable_new_opmath() built_hamiltonian, qubits = qchem.molecular_hamiltonian(*args, **kwargs) - if op_arithmetic: - disable_new_opmath() - assert not isinstance(built_hamiltonian, Hamiltonian) + if active_new_opmath(): + assert not isinstance(built_hamiltonian, qml.Hamiltonian) else: - assert isinstance(built_hamiltonian, Hamiltonian) + assert isinstance(built_hamiltonian, qml.Hamiltonian) assert qubits == 2 * nact_orbs -@pytest.mark.parametrize("op_arithmetic", [False, True]) @pytest.mark.parametrize( ("symbols", "geometry", "h_ref_data"), [ @@ -212,11 +204,10 @@ def test_building_hamiltonian( ), ], ) -def test_differentiable_hamiltonian(symbols, geometry, h_ref_data, op_arithmetic): +@pytest.mark.usefixtures("use_legacy_and_new_opmath") +def test_differentiable_hamiltonian(symbols, geometry, h_ref_data): r"""Test that molecular_hamiltonian returns the correct Hamiltonian with the differentiable backend.""" - if op_arithmetic: - enable_new_opmath() geometry.requires_grad = True args = [geometry.reshape(2, 3)] @@ -225,43 +216,31 @@ def test_differentiable_hamiltonian(symbols, geometry, h_ref_data, op_arithmetic geometry.requires_grad = False h_noargs = qchem.molecular_hamiltonian(symbols, geometry, method="dhf")[0] - h_ref = ( - dot(h_ref_data[0], h_ref_data[1], pauli=True) - if op_arithmetic - else Hamiltonian(h_ref_data[0], h_ref_data[1]) - ) - - if op_arithmetic: - disable_new_opmath() - h_args_ps = pauli_sentence(h_args) - h_noargs_ps = pauli_sentence(h_noargs) - h_ref_sorted_coeffs = np.sort(list(h_ref.values())) - - assert set(h_args_ps) == set(h_ref) - assert set(h_noargs_ps) == set(h_ref) - - assert np.allclose(np.sort(list(h_args_ps.values())), h_ref_sorted_coeffs) - assert np.allclose(np.sort(list(h_noargs_ps.values())), h_ref_sorted_coeffs) + ops = [ + qml.operation.Tensor(*op) if isinstance(op, qml.ops.Prod) else op + for op in map(qml.simplify, h_ref_data[1]) + ] + h_ref = qml.Hamiltonian(h_ref_data[0], ops) - assert all(val.requires_grad is True for val in h_args_ps.values()) - assert all(val.requires_grad is False for val in h_noargs_ps.values()) + h_ref_coeffs, h_ref_ops = h_ref.terms() + h_args_coeffs, h_args_ops = h_args.terms() + h_noargs_coeffs, h_noargs_ops = h_noargs.terms() - else: - assert np.allclose(np.sort(h_args.coeffs), np.sort(h_ref.coeffs)) - assert Hamiltonian(np.ones(len(h_args.coeffs)), h_args.ops).compare( - Hamiltonian(np.ones(len(h_ref.coeffs)), h_ref.ops) - ) + assert all(coeff.requires_grad is True for coeff in h_args_coeffs) + assert all(coeff.requires_grad is False for coeff in h_noargs_coeffs) - assert np.allclose(np.sort(h_noargs.coeffs), np.sort(h_ref.coeffs)) - assert Hamiltonian(np.ones(len(h_noargs.coeffs)), h_noargs.ops).compare( - Hamiltonian(np.ones(len(h_ref.coeffs)), h_ref.ops) - ) + assert np.allclose(np.sort(h_args_coeffs), np.sort(h_ref_coeffs)) + assert qml.Hamiltonian(np.ones(len(h_args_coeffs)), h_args_ops).compare( + qml.Hamiltonian(np.ones(len(h_ref_coeffs)), h_ref_ops) + ) - assert h_args.coeffs.requires_grad is True - assert h_noargs.coeffs.requires_grad is False + assert np.allclose(np.sort(h_noargs_coeffs), np.sort(h_ref_coeffs)) + assert qml.Hamiltonian(np.ones(len(h_noargs_coeffs)), h_noargs_ops).compare( + qml.Hamiltonian(np.ones(len(h_ref_coeffs)), h_ref_ops) + ) -@pytest.mark.parametrize("op_arithmetic", [False, True]) +@pytest.mark.usefixtures("use_legacy_and_new_opmath") @pytest.mark.parametrize( ("symbols", "geometry", "method", "wiremap"), [ @@ -280,12 +259,8 @@ def test_differentiable_hamiltonian(symbols, geometry, h_ref_data, op_arithmetic ], ) @pytest.mark.usefixtures("skip_if_no_openfermion_support") -def test_custom_wiremap_hamiltonian_pyscf( - symbols, geometry, method, wiremap, tmpdir, op_arithmetic -): +def test_custom_wiremap_hamiltonian_pyscf(symbols, geometry, method, wiremap, tmpdir): r"""Test that the generated Hamiltonian has the correct wire labels given by a custom wiremap.""" - if op_arithmetic: - enable_new_opmath() hamiltonian, _ = qchem.molecular_hamiltonian( symbols=symbols, @@ -297,11 +272,8 @@ def test_custom_wiremap_hamiltonian_pyscf( assert set(hamiltonian.wires) == set(wiremap) - if op_arithmetic: - disable_new_opmath() - -@pytest.mark.parametrize("op_arithmetic", [False, True]) +@pytest.mark.usefixtures("use_legacy_and_new_opmath") @pytest.mark.parametrize( ("symbols", "geometry", "wiremap", "args"), [ @@ -319,14 +291,11 @@ def test_custom_wiremap_hamiltonian_pyscf( ), ], ) -def test_custom_wiremap_hamiltonian_dhf(symbols, geometry, wiremap, args, tmpdir, op_arithmetic): +def test_custom_wiremap_hamiltonian_dhf(symbols, geometry, wiremap, args, tmpdir): r"""Test that the generated Hamiltonian has the correct wire labels given by a custom wiremap.""" wiremap_dict = dict(zip(range(len(wiremap)), wiremap)) - if op_arithmetic: - enable_new_opmath() - hamiltonian_ref, _ = qchem.molecular_hamiltonian( symbols=symbols, coordinates=geometry, @@ -346,9 +315,6 @@ def test_custom_wiremap_hamiltonian_dhf(symbols, geometry, wiremap, args, tmpdir assert wiremap_calc == wiremap_dict - if op_arithmetic: - disable_new_opmath() - file_content = """\ 2 @@ -397,7 +363,6 @@ def test_diff_hamiltonian_error(symbols, geometry): qchem.molecular_hamiltonian(symbols, geometry, mult=3) -@pytest.mark.parametrize("op_arithmetic", [False, True]) @pytest.mark.parametrize( ("symbols", "geometry", "method", "args"), [ @@ -421,11 +386,9 @@ def test_diff_hamiltonian_error(symbols, geometry): ), ], ) -@pytest.mark.usefixtures("skip_if_no_openfermion_support") -def test_real_hamiltonian(symbols, geometry, method, args, tmpdir, op_arithmetic): +@pytest.mark.usefixtures("skip_if_no_openfermion_support", "use_legacy_and_new_opmath") +def test_real_hamiltonian(symbols, geometry, method, args, tmpdir): r"""Test that the generated Hamiltonian has real coefficients.""" - if op_arithmetic: - enable_new_opmath() hamiltonian, _ = qchem.molecular_hamiltonian( symbols=symbols, @@ -435,13 +398,7 @@ def test_real_hamiltonian(symbols, geometry, method, args, tmpdir, op_arithmetic outpath=tmpdir.strpath, ) - if op_arithmetic: - disable_new_opmath() - h_as_ps = pauli_sentence(hamiltonian) - assert np.isrealobj(np.array(h_as_ps.values())) - - else: - assert np.isrealobj(hamiltonian.coeffs) + assert np.isrealobj(hamiltonian.terms()[0]) @pytest.mark.parametrize( diff --git a/tests/qchem/test_dipole.py b/tests/qchem/test_dipole.py index e19db8fe3f3..5856ad5087a 100644 --- a/tests/qchem/test_dipole.py +++ b/tests/qchem/test_dipole.py @@ -22,7 +22,7 @@ from pennylane import numpy as np from pennylane import qchem from pennylane.fermi import from_string -from pennylane.operation import disable_new_opmath, enable_new_opmath +from pennylane.operation import Tensor @pytest.mark.parametrize( @@ -183,26 +183,25 @@ def test_fermionic_dipole(symbols, geometry, core, charge, active, f_ref): ), ], ) +@pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_dipole_moment(symbols, geometry, core, charge, active, coeffs, ops): r"""Test that dipole_moment returns the correct result.""" mol = qchem.Molecule(symbols, geometry, charge=charge) args = [p for p in [geometry] if p.requires_grad] d = qchem.dipole_moment(mol, core=core, active=active, cutoff=1.0e-8)(*args)[0] - d_ref = qml.Hamiltonian(coeffs, ops) + dops = [Tensor(*op) if isinstance(op, qml.ops.Prod) else op for op in map(qml.simplify, ops)] + d_ref = qml.Hamiltonian(coeffs, dops) - assert np.allclose(sorted(d.coeffs), sorted(d_ref.coeffs)) - assert qml.Hamiltonian(np.ones(len(d.coeffs)), d.ops).compare( - qml.Hamiltonian(np.ones(len(d_ref.coeffs)), d_ref.ops) - ) - - enable_new_opmath() - d_op_math = qchem.dipole_moment(mol, core=core, active=active, cutoff=1.0e-8)(*args)[0] - disable_new_opmath() - d_ref_op_math = qml.dot(coeffs, ops) + d_coeff, d_ops = d.terms() + dref_coeff, dref_ops = d_ref.terms() + assert np.allclose(sorted(d_coeff), sorted(dref_coeff)) + assert qml.Hamiltonian(np.ones(len(d_coeff)), d_ops).compare( + qml.Hamiltonian(np.ones(len(dref_coeff)), dref_ops) + ) assert np.allclose( - qml.matrix(d_op_math, wire_order=[0, 1, 2, 3]), - qml.matrix(d_ref_op_math, wire_order=[0, 1, 2, 3]), + qml.matrix(d, wire_order=[0, 1, 2, 3]), + qml.matrix(d_ref, wire_order=[0, 1, 2, 3]), ) @@ -217,6 +216,7 @@ def test_dipole_moment(symbols, geometry, core, charge, active, coeffs, ops): ), ], ) +@pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_dipole_moment_631g_basis(symbols, geometry, core, active): r"""Test that the dipole moment is constructed properly with basis sets having different numbers of primitive Gaussian functions.""" @@ -229,8 +229,7 @@ def test_dipole_moment_631g_basis(symbols, geometry, core, active): mol = qml.qchem.Molecule(symbols, geometry, alpha=alpha, basis_name="6-31g") args = [alpha] d = qchem.dipole_moment(mol, core=core, active=active, cutoff=1.0e-8)(*args)[0] - - assert isinstance(d, qml.Hamiltonian) + assert isinstance(d, (qml.Hamiltonian, qml.ops.Sum)) @pytest.mark.parametrize( diff --git a/tests/qchem/test_factorization.py b/tests/qchem/test_factorization.py index ea68984e9b8..bea18d3c09a 100644 --- a/tests/qchem/test_factorization.py +++ b/tests/qchem/test_factorization.py @@ -316,6 +316,7 @@ def test_empty_error(two_tensor): ), ], ) +@pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_basis_rotation_output( one_matrix, two_tensor, tol_factor, coeffs_ref, ops_ref, eigvecs_ref ): @@ -328,7 +329,7 @@ def test_basis_rotation_output( for j, op in enumerate(ops): ops_ref_str = [qml.pauli.pauli_word_to_string(t) for t in ops_ref[j]] for o in op: - assert qml.pauli.pauli_word_to_string(o) in ops_ref_str + assert (qml.pauli.pauli_word_to_string(o) or "I") in ops_ref_str for i, vecs in enumerate(eigvecs): checks = [] @@ -363,6 +364,7 @@ def test_basis_rotation_output( ) ], ) +@pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_basis_rotation_utransform(core, one_electron, two_electron): r"""Test that basis_rotation function returns the correct transformation matrices. This test constructs the matrix representation of a factorized Hamiltonian and then applies the diff --git a/tests/qchem/test_hamiltonians.py b/tests/qchem/test_hamiltonians.py index c6c9f3c091a..27d6bce45d5 100644 --- a/tests/qchem/test_hamiltonians.py +++ b/tests/qchem/test_hamiltonians.py @@ -21,7 +21,7 @@ from pennylane import Identity, PauliX, PauliY, PauliZ from pennylane import numpy as np from pennylane import qchem -from pennylane.operation import disable_new_opmath, enable_new_opmath +from pennylane.operation import active_new_opmath from pennylane.fermi import from_string @@ -224,31 +224,35 @@ def test_fermionic_hamiltonian(symbols, geometry, alpha, h_ref): ) ], ) +@pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_diff_hamiltonian(symbols, geometry, h_ref_data): r"""Test that diff_hamiltonian returns the correct Hamiltonian.""" mol = qchem.Molecule(symbols, geometry) args = [] h = qchem.diff_hamiltonian(mol)(*args) - h_ref = qml.Hamiltonian(h_ref_data[0], h_ref_data[1]) + + ops = [ + qml.operation.Tensor(*op) if isinstance(op, qml.ops.Prod) else op + for op in map(qml.simplify, h_ref_data[1]) + ] + h_ref = qml.Hamiltonian(h_ref_data[0], ops) assert np.allclose(np.sort(h.terms()[0]), np.sort(h_ref.terms()[0])) assert qml.Hamiltonian(np.ones(len(h.terms()[0])), h.terms()[1]).compare( qml.Hamiltonian(np.ones(len(h_ref.terms()[0])), h_ref.terms()[1]) ) - enable_new_opmath() - h_pl_op = qchem.diff_hamiltonian(mol)(*args) - disable_new_opmath() + assert isinstance(h, qml.ops.Sum if active_new_opmath() else qml.Hamiltonian) wire_order = h_ref.wires - assert not isinstance(h_pl_op, qml.Hamiltonian) assert np.allclose( - qml.matrix(h_pl_op, wire_order=wire_order), + qml.matrix(h, wire_order=wire_order), qml.matrix(h_ref, wire_order=wire_order), ) +@pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_diff_hamiltonian_active_space(): r"""Test that diff_hamiltonian works when an active space is defined.""" @@ -260,13 +264,7 @@ def test_diff_hamiltonian_active_space(): h = qchem.diff_hamiltonian(mol, core=[0], active=[1, 2])(*args) - assert isinstance(h, qml.Hamiltonian) - - enable_new_opmath() - h_op = qchem.diff_hamiltonian(mol, core=[0], active=[1, 2])(*args) - disable_new_opmath() - - assert not isinstance(h_op, qml.Hamiltonian) + assert isinstance(h, qml.ops.Sum if active_new_opmath() else qml.Hamiltonian) def test_gradient_expvalH(): @@ -317,6 +315,7 @@ def circuit(*args): assert np.allclose(grad_qml[0][0], grad_finitediff) +@pytest.mark.usefixtures("use_legacy_and_new_opmath") class TestJax: @pytest.mark.jax def test_gradient_expvalH(self): @@ -344,9 +343,7 @@ def circuit(*args): qml.PauliX(0) qml.PauliX(1) qml.DoubleExcitation(0.22350048111151138, wires=[0, 1, 2, 3]) - enable_new_opmath() h_qubit = qchem.diff_hamiltonian(mol)(*args) - disable_new_opmath() return qml.expval(h_qubit) return circuit diff --git a/tests/qchem/test_observable_hf.py b/tests/qchem/test_observable_hf.py index 41eb15a644a..f6cbd120ec9 100644 --- a/tests/qchem/test_observable_hf.py +++ b/tests/qchem/test_observable_hf.py @@ -19,8 +19,6 @@ import pennylane as qml from pennylane import numpy as np from pennylane import qchem -from pennylane.pauli import pauli_sentence -from pennylane.operation import enable_new_opmath, disable_new_opmath from pennylane.fermi import from_string @@ -168,23 +166,19 @@ def test_fermionic_observable(core_constant, integral_one, integral_two, f_ref): (1.23 * from_string(""), [[1.23], [qml.Identity(0)]]), ], ) +@pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_qubit_observable(f_observable, q_observable): r"""Test that qubit_observable returns the correct operator.""" - h_as_hamiltonian = qchem.qubit_observable(f_observable) - h_ref = qml.Hamiltonian(q_observable[0], q_observable[1]) - - enable_new_opmath() - h_as_op = qchem.qubit_observable(f_observable) - h_ref_as_op = pauli_sentence(h_ref).operation( - h_ref.wires - ) # easy conversion from ham to operation + ops = [ + qml.operation.Tensor(*op) if isinstance(op, qml.ops.Prod) else op + for op in map(qml.simplify, q_observable[1]) + ] + h_ref = qml.Hamiltonian(q_observable[0], ops) - disable_new_opmath() - - assert h_as_hamiltonian.compare(h_ref) + assert h_ref.compare(h_as_op) assert np.allclose( - qml.matrix(h_as_op, wire_order=[0, 1, 2]), qml.matrix(h_ref_as_op, wire_order=[0, 1, 2]) + qml.matrix(h_as_op, wire_order=[0, 1, 2]), qml.matrix(h_ref, wire_order=[0, 1, 2]) ) @@ -201,17 +195,13 @@ def test_qubit_observable(f_observable, q_observable): ), ], ) +@pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_qubit_observable_cutoff(f_observable, cut_off): """Test that qubit_observable returns the correct operator when a cutoff is provided.""" h_ref, h_ref_op = (qml.Hamiltonian([], []), qml.s_prod(0, qml.Identity(0))) - h_as_hamiltonian = qchem.qubit_observable(f_observable, cutoff=cut_off) - - enable_new_opmath() h_as_op = qchem.qubit_observable(f_observable, cutoff=cut_off) - disable_new_opmath() - assert h_as_hamiltonian.compare(h_ref) - assert qml.equal(h_as_op, h_ref_op) + assert h_ref.compare(h_as_op) assert np.allclose( - qml.matrix(h_as_op, wire_order=[0, 1, 2]), qml.matrix(h_ref_op, wire_order=[0, 1, 2]) + qml.matrix(h_ref_op, wire_order=[0, 1, 2]), qml.matrix(h_as_op, wire_order=[0, 1, 2]) ) diff --git a/tests/qchem/test_particle_number.py b/tests/qchem/test_particle_number.py index 101c5a73910..2c244f78882 100644 --- a/tests/qchem/test_particle_number.py +++ b/tests/qchem/test_particle_number.py @@ -20,9 +20,10 @@ from pennylane import Identity, PauliZ from pennylane import numpy as np from pennylane import qchem -from pennylane.operation import enable_new_opmath, disable_new_opmath +from pennylane.operation import active_new_opmath +@pytest.mark.usefixtures("use_legacy_and_new_opmath") @pytest.mark.parametrize( ("orbitals", "coeffs_ref", "ops_ref"), [ @@ -58,16 +59,12 @@ def test_particle_number(orbitals, coeffs_ref, ops_ref): """ n = qchem.particle_number(orbitals) n_ref = qml.Hamiltonian(coeffs_ref, ops_ref) - assert n.compare(n_ref) - - enable_new_opmath() - n_pl_op = qchem.particle_number(orbitals) - disable_new_opmath() + assert n_ref.compare(n) + assert isinstance(n, qml.ops.Sum if active_new_opmath() else qml.Hamiltonian) wire_order = n_ref.wires - assert not isinstance(n_pl_op, qml.Hamiltonian) assert np.allclose( - qml.matrix(n_pl_op, wire_order=wire_order), + qml.matrix(n, wire_order=wire_order), qml.matrix(n_ref, wire_order=wire_order), ) diff --git a/tests/qchem/test_spin.py b/tests/qchem/test_spin.py index 43bf636424b..dbe7fb39d50 100644 --- a/tests/qchem/test_spin.py +++ b/tests/qchem/test_spin.py @@ -19,8 +19,8 @@ import pennylane as qml from pennylane import Identity, PauliX, PauliY, PauliZ from pennylane import numpy as np -from pennylane import qchem -from pennylane.operation import enable_new_opmath, disable_new_opmath +from pennylane import qchem, simplify +from pennylane.operation import Tensor, active_new_opmath @pytest.mark.parametrize( @@ -116,6 +116,7 @@ def test_spin2_matrix_elements(n_spin_orbs, matrix_ref): assert np.allclose(s2_me_result, matrix_ref) +@pytest.mark.usefixtures("use_legacy_and_new_opmath") @pytest.mark.parametrize( ("electrons", "orbitals", "coeffs_ref", "ops_ref"), [ @@ -174,17 +175,14 @@ def test_spin2(electrons, orbitals, coeffs_ref, ops_ref): built by the function `'spin2'`. """ s2 = qchem.spin.spin2(electrons, orbitals) - s2_ref = qml.Hamiltonian(coeffs_ref, ops_ref) - assert s2.compare(s2_ref) - - enable_new_opmath() - s2_pl_op = qchem.spin.spin2(electrons, orbitals) - disable_new_opmath() + sops = [Tensor(*op) if isinstance(op, qml.ops.Prod) else op for op in map(simplify, ops_ref)] + s2_ref = qml.Hamiltonian(coeffs_ref, sops) + assert s2_ref.compare(s2) + assert isinstance(s2, qml.ops.Sum if active_new_opmath() else qml.Hamiltonian) wire_order = s2_ref.wires - assert not isinstance(s2_pl_op, qml.Hamiltonian) assert np.allclose( - qml.matrix(s2_pl_op, wire_order=wire_order), + qml.matrix(s2, wire_order=wire_order), qml.matrix(s2_ref, wire_order=wire_order), ) @@ -206,6 +204,7 @@ def test_exception_spin2(electrons, orbitals, msg_match): qchem.spin.spin2(electrons, orbitals) +@pytest.mark.usefixtures("use_legacy_and_new_opmath") @pytest.mark.parametrize( ("orbitals", "coeffs_ref", "ops_ref"), [ @@ -234,18 +233,13 @@ def test_spinz(orbitals, coeffs_ref, ops_ref): """ sz = qchem.spin.spinz(orbitals) sz_ref = qml.Hamiltonian(coeffs_ref, ops_ref) - assert sz.compare(sz_ref) - - enable_new_opmath() - sz_pl_op = qchem.spin.spinz(orbitals) - disable_new_opmath() - sz_ref_pl_op = qml.dot(coeffs_ref, ops_ref) + assert sz_ref.compare(sz) + assert isinstance(sz, qml.ops.Sum if active_new_opmath() else qml.Hamiltonian) wire_order = sz_ref.wires - assert not isinstance(sz_pl_op, qml.Hamiltonian) assert np.allclose( - qml.matrix(sz_pl_op, wire_order=wire_order), - qml.matrix(sz_ref_pl_op, wire_order=wire_order), + qml.matrix(sz, wire_order=wire_order), + qml.matrix(sz_ref, wire_order=wire_order), ) diff --git a/tests/qchem/test_tapering.py b/tests/qchem/test_tapering.py index c7be05bcc56..11f52c56302 100644 --- a/tests/qchem/test_tapering.py +++ b/tests/qchem/test_tapering.py @@ -33,7 +33,6 @@ _split_pauli_sentence, _taper_pauli_sentence, ) -from pennylane.operation import enable_new_opmath, disable_new_opmath, active_new_opmath @pytest.mark.parametrize( @@ -197,6 +196,7 @@ def test_kernel(binary_matrix, result): ), ], ) +@pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_generate_paulis(generators, num_qubits, result): r"""Test that generate_paulis returns the correct result.""" pauli_ops = qml.paulix_ops(generators, num_qubits) @@ -207,10 +207,6 @@ def test_generate_paulis(generators, num_qubits, result): generators_as_ops = [pauli_sentence(g).operation() for g in generators] assert not any(isinstance(g, qml.Hamiltonian) for g in generators_as_ops) - enable_new_opmath() - pauli_ops = qml.paulix_ops(generators_as_ops, num_qubits) - disable_new_opmath() - for p1, p2 in zip(pauli_ops, result): assert qml.equal(p1, p2) @@ -229,28 +225,14 @@ def test_generate_paulis(generators, num_qubits, result): ), ], ) +@pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_symmetry_generators(symbols, geometry, res_generators): r"""Test that symmetry_generators returns the correct result.""" mol = qml.qchem.Molecule(symbols, geometry) hamiltonian = qml.qchem.diff_hamiltonian(mol)() generators = qml.symmetry_generators(hamiltonian) - for g1, g2 in zip(generators, res_generators): - assert g1.compare(g2) - - # test arithmetic op compatibility: - hamiltonian_as_op = pauli_sentence(hamiltonian).operation() - assert not isinstance(hamiltonian_as_op, qml.Hamiltonian) - - enable_new_opmath() - generators = qml.symmetry_generators(hamiltonian_as_op) - disable_new_opmath() - - for g1, g2 in zip(generators, res_generators): - assert not isinstance( - g1, qml.Hamiltonian - ) # just confirming we are not relying on Hamiltonians any more assert pauli_sentence(g1) == pauli_sentence(g2) @@ -282,18 +264,14 @@ def test_symmetry_generators(symbols, geometry, res_generators): ), ], ) +@pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_clifford(generator, paulixops, result): r"""Test that clifford returns the correct operator.""" u = clifford(generator, paulixops) - assert u.compare(result) + assert pauli_sentence(u) == pauli_sentence(result) # test arithmetic op compatibility: result_as_op = pauli_sentence(result).operation() - generators_as_ops = [pauli_sentence(g).operation() for g in generator] - - enable_new_opmath() - u = clifford(generators_as_ops, paulixops) - disable_new_opmath() assert pauli_sentence(result_as_op) == pauli_sentence(u) @@ -318,6 +296,7 @@ def test_clifford(generator, paulixops, result): ), ], ) +@pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_transform_hamiltonian(symbols, geometry, generator, paulixops, paulix_sector, ham_ref): r"""Test that transform_hamiltonian returns the correct hamiltonian.""" mol = qml.qchem.Molecule(symbols, geometry) @@ -325,26 +304,11 @@ def test_transform_hamiltonian(symbols, geometry, generator, paulixops, paulix_s ham_calc = qml.taper(h, generator, paulixops, paulix_sector) # sort Hamiltonian terms and then compare with reference sorted_terms = list(sorted(zip(ham_calc.terms()[0], ham_calc.terms()[1]))) - for i, term in enumerate(sorted_terms): - assert np.allclose(term[0], ham_ref.terms()[0][i]) - assert term[1].compare(ham_ref.terms()[1][i]) - - # test arithmetic op compatibility: - h_as_op = pauli_sentence(h).operation() - generators_as_ops = [pauli_sentence(g).operation() for g in generator] - - enable_new_opmath() - ham_calc = qml.taper(h_as_op, generators_as_ops, paulixops, paulix_sector) - disable_new_opmath() + hamref_terms = list(zip(*ham_ref.terms())) - assert not isinstance(ham_calc, qml.Hamiltonian) - ham_cal_as_hamiltonian = pauli_sentence(ham_calc).hamiltonian() - sorted_terms = list( - sorted(zip(ham_cal_as_hamiltonian.terms()[0], ham_cal_as_hamiltonian.terms()[1])) - ) - for i, term in enumerate(sorted_terms): - assert np.allclose(term[0], ham_ref.terms()[0][i]) - assert term[1].compare(ham_ref.terms()[1][i]) + for term, ref_term in zip(sorted_terms, hamref_terms): + assert np.allclose(term[0], ref_term[0]) + assert pauli_sentence(term[1]) == pauli_sentence(ref_term[1]) @pytest.mark.parametrize( @@ -389,6 +353,7 @@ def test_transform_hamiltonian(symbols, geometry, generator, paulixops, paulix_s ), ], ) +@pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_optimal_sector(symbols, geometry, charge, generators, num_electrons, result): r"""Test that find_optimal_sector returns the correct result.""" mol = qml.qchem.Molecule(symbols, geometry, charge) @@ -397,15 +362,6 @@ def test_optimal_sector(symbols, geometry, charge, generators, num_electrons, re perm = optimal_sector(hamiltonian, generators, num_electrons) assert perm == result - # test arithmetic op compatibility: - h_as_op = pauli_sentence(hamiltonian).operation() - generators_as_ops = [pauli_sentence(g).operation() for g in generators] - - enable_new_opmath() - perm = optimal_sector(h_as_op, generators_as_ops, num_electrons) - disable_new_opmath() - assert perm == result - @pytest.mark.parametrize( ("symbols", "geometry", "generators"), @@ -503,6 +459,7 @@ def test_exceptions_optimal_sector(symbols, geometry, generators, num_electrons, ), ], ) +@pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_transform_hf(generators, paulixops, paulix_sector, num_electrons, num_wires, result): r"""Test that transform_hf returns the correct result.""" @@ -515,22 +472,7 @@ def test_transform_hf(generators, paulixops, paulix_sector, num_electrons, num_w ) assert np.all(tapered_hf_state == result) - # test arithmetic op compatibility: - generators_as_ops = [pauli_sentence(g).operation() for g in generators] - - enable_new_opmath() - tapered_hf_state = taper_hf( - generators_as_ops, - paulixops, - paulix_sector, - num_electrons, - num_wires, - ) - disable_new_opmath() - assert np.all(tapered_hf_state == result) - -@pytest.mark.parametrize("op_arithmetic", [False, True]) @pytest.mark.parametrize( ("symbols", "geometry", "charge"), [ @@ -565,11 +507,10 @@ def test_transform_hf(generators, paulixops, paulix_sector, num_electrons, num_w ), ], ) -def test_taper_obs(symbols, geometry, charge, op_arithmetic): +@pytest.mark.usefixtures("use_legacy_and_new_opmath") +def test_taper_obs(symbols, geometry, charge): r"""Test that the expectation values of tapered observables with respect to the tapered Hartree-Fock state (:math:`\langle HF|obs|HF \rangle`) are consistent.""" - status_op_math = active_new_opmath() - _ = enable_new_opmath() if op_arithmetic else disable_new_opmath() mol = qml.qchem.Molecule(symbols, geometry, charge) hamiltonian = qml.qchem.diff_hamiltonian(mol)(geometry) @@ -613,10 +554,7 @@ def test_taper_obs(symbols, geometry, charge, op_arithmetic): assert np.isclose(obs_val, obs_val_tapered) assert qml.equal(tapered_obs, tapered_ps) - _ = enable_new_opmath() if status_op_math else disable_new_opmath() - -@pytest.mark.parametrize("op_arithmetic", [False, True]) @pytest.mark.parametrize( ("symbols", "geometry", "charge", "generators", "paulixops", "paulix_sector", "num_commuting"), [ @@ -665,15 +603,13 @@ def test_taper_obs(symbols, geometry, charge, op_arithmetic): ), ], ) +@pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_taper_excitations( - symbols, geometry, charge, generators, paulixops, paulix_sector, num_commuting, op_arithmetic + symbols, geometry, charge, generators, paulixops, paulix_sector, num_commuting ): r"""Test that the tapered excitation operators using :func:`~.taper_operation` are consistent with the tapered Hartree-Fock state.""" - status_op_math = active_new_opmath() - _ = enable_new_opmath() if op_arithmetic else disable_new_opmath() - mol = qml.qchem.Molecule(symbols, geometry, charge) num_electrons, num_wires = mol.n_electrons, 2 * mol.n_orbitals hf_state = np.where(np.arange(num_wires) < num_electrons, 1, 0) @@ -732,8 +668,6 @@ def test_taper_excitations( ).toarray() assert np.isclose(expec_val, expec_val_tapered) - _ = enable_new_opmath() if status_op_math else disable_new_opmath() - @pytest.mark.parametrize( ("operation", "op_gen", "message_match"), @@ -771,7 +705,6 @@ def test_inconsistent_taper_ops(operation, op_gen, message_match): taper_operation(operation, generators, paulixops, paulix_sector, wire_order, op_gen=op_gen) -@pytest.mark.parametrize("op_arithmetic", [False, True]) @pytest.mark.parametrize( ("operation", "op_gen"), [ @@ -805,12 +738,10 @@ def test_inconsistent_taper_ops(operation, op_gen, message_match): ), ], ) -def test_consistent_taper_ops(operation, op_gen, op_arithmetic): +@pytest.mark.usefixtures("use_legacy_and_new_opmath") +def test_consistent_taper_ops(operation, op_gen): r"""Test that operations are tapered consistently when their generators are provided manually and when they are constructed internally""" - status_op_math = active_new_opmath() - _ = enable_new_opmath() if op_arithmetic else disable_new_opmath() - symbols, geometry, charge = ( ["He", "H"], np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 1.4588684632]]), @@ -884,8 +815,6 @@ def test_consistent_taper_ops(operation, op_gen, op_arithmetic): ).toarray() assert np.isclose(expec_val, expec_val_tapered) - _ = enable_new_opmath() if status_op_math else disable_new_opmath() - @pytest.mark.parametrize( ("operation", "op_wires", "op_gen"), @@ -946,7 +875,6 @@ def test_taper_callable_ops(operation, op_wires, op_gen): ) -@pytest.mark.parametrize("op_arithmetic", [False, True]) @pytest.mark.parametrize( ("operation", "op_wires", "op_gen"), [ @@ -967,12 +895,10 @@ def test_taper_callable_ops(operation, op_wires, op_gen): ), ], ) -def test_taper_matrix_ops(operation, op_wires, op_gen, op_arithmetic): +@pytest.mark.usefixtures("use_legacy_and_new_opmath") +def test_taper_matrix_ops(operation, op_wires, op_gen): """Test that taper_operation can be used with gate operation built using matrices""" - status_op_math = active_new_opmath() - _ = enable_new_opmath() if op_arithmetic else disable_new_opmath() - symbols, geometry, charge = ( ["He", "H"], np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 1.4588684632]]), @@ -1010,8 +936,6 @@ def test_taper_matrix_ops(operation, op_wires, op_gen, op_arithmetic): [qml.equal(op1.base, op2.base) for op1, op2 in zip(taper_op1(params), taper_op2)] ) - _ = enable_new_opmath() if status_op_math else disable_new_opmath() - @pytest.mark.parametrize( ("operation", "op_wires", "op_gen", "message_match"), diff --git a/tests/resource/test_error/test_error.py b/tests/resource/test_error/test_error.py index e2305fc1f55..8e07e0c1c03 100644 --- a/tests/resource/test_error/test_error.py +++ b/tests/resource/test_error/test_error.py @@ -139,7 +139,7 @@ def compute_matrix(self): exact_op = qml.RX(phi, 1) res = SpectralNormError.get_error(approx_op, exact_op) - assert res == expected + assert np.isclose(res, expected) def test_no_operator_matrix_defined(self): """Test that get_error fails if the operator matrix is not defined""" diff --git a/tests/shadow/test_shadow_class.py b/tests/shadow/test_shadow_class.py index 69d3826a536..bcd3de1c4ac 100644 --- a/tests/shadow/test_shadow_class.py +++ b/tests/shadow/test_shadow_class.py @@ -78,6 +78,7 @@ class TestIntegrationShadows: """Integration tests for classical shadows class""" @pytest.mark.parametrize("shadow", shadows) + @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_pauli_string_expval(self, shadow): """Testing the output of expectation values match those of exact evaluation""" @@ -101,8 +102,11 @@ def test_pauli_string_expval(self, shadow): @pytest.mark.parametrize("H", Hs) @pytest.mark.parametrize("shadow", shadows) + @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_expval_input_types(self, shadow, H): """Test ClassicalShadow.expval can handle different inputs""" + if not qml.operation.active_new_opmath(): + H = qml.operation.convert_to_legacy_H(H) assert qml.math.allclose(shadow.expval(H, k=2), 1.0, atol=1e-1) def test_reconstruct_bell_state(self): @@ -339,6 +343,7 @@ def test_max_entangled_expval(self): assert actual.dtype == np.float64 assert qml.math.allclose(actual, expected, atol=1e-1) + @pytest.mark.usefixtures("use_legacy_opmath") def test_non_pauli_error(self): """Test that an error is raised when a non-Pauli observable is passed""" circuit = hadamard_circuit(3) @@ -351,6 +356,18 @@ def test_non_pauli_error(self): with pytest.raises(ValueError, match=msg): shadow.expval(H, k=10) + def test_non_pauli_error_no_pauli_rep(self): + """Test that an error is raised when a non-Pauli observable is passed""" + circuit = hadamard_circuit(3) + bits, recipes = circuit() + shadow = ClassicalShadow(bits, recipes) + + H = qml.Hadamard(0) @ qml.Hadamard(2) + + msg = "Observable must have a valid pauli representation." + with pytest.raises(ValueError, match=msg): + shadow.expval(H, k=10) + @pytest.mark.all_interfaces class TestExpvalEstimationInterfaces: diff --git a/tests/tape/test_tape.py b/tests/tape/test_tape.py index 70d5157a58a..5a1e42d72f7 100644 --- a/tests/tape/test_tape.py +++ b/tests/tape/test_tape.py @@ -146,6 +146,7 @@ def test_tensor_observables_rmatmul(self): assert tape.measurements[0].return_type is qml.measurements.Expectation assert tape.measurements[0].obs is t_obs2 + @pytest.mark.usefixtures("use_legacy_opmath") def test_tensor_observables_tensor_init(self): """Test that tensor observables are correctly processed from the annotated queue. Here, we test multiple tensor observables constructed via explicit diff --git a/tests/test_debugging.py b/tests/test_debugging.py index e12781f9997..e0d368e5679 100644 --- a/tests/test_debugging.py +++ b/tests/test_debugging.py @@ -24,7 +24,7 @@ class TestSnapshot: # pylint: disable=protected-access @pytest.mark.parametrize("method", [None, "backprop", "parameter-shift", "adjoint"]) - def test_default_qubit_legacy(self, method): + def test_default_qubit_legacy_opmath(self, method): """Test that multiple snapshots are returned correctly on the state-vector simulator.""" dev = qml.device("default.qubit.legacy", wires=2) diff --git a/tests/test_device.py b/tests/test_device.py index fead0e735f0..0422e9dfd79 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -340,7 +340,8 @@ def test_check_validity_on_valid_queue(self, mock_device_supporting_paulis): # Raises an error if queue or observables are invalid dev.check_validity(queue, observables) - def test_check_validity_on_tensor_support(self, mock_device_supporting_paulis): + @pytest.mark.usefixtures("use_legacy_opmath") + def test_check_validity_on_tensor_support_legacy_opmath(self, mock_device_supporting_paulis): """Tests the function Device.check_validity with tensor support capability""" dev = mock_device_supporting_paulis() @@ -356,6 +357,23 @@ def test_check_validity_on_tensor_support(self, mock_device_supporting_paulis): with pytest.raises(DeviceError, match="Tensor observables not supported"): dev.check_validity(queue, observables) + def test_check_validity_on_prod_support(self, mock_device_supporting_paulis): + """Tests the function Device.check_validity with prod support capability""" + dev = mock_device_supporting_paulis() + + queue = [ + qml.PauliX(wires=0), + qml.PauliY(wires=1), + qml.PauliZ(wires=2), + ] + + observables = [qml.expval(qml.PauliZ(0) @ qml.PauliX(1))] + + # mock device does not support Tensor product + with pytest.raises(DeviceError, match="Observable Prod not supported"): + dev.check_validity(queue, observables) + + @pytest.mark.usefixtures("use_legacy_opmath") def test_check_validity_on_invalid_observable_with_tensor_support(self, monkeypatch): """Tests the function Device.check_validity with tensor support capability but with an invalid observable""" @@ -491,7 +509,10 @@ def test_device_default_expand_ops( without expanding measurements.""" ops = [qml.PauliX(0), qml.BasisEmbedding([1, 0], wires=[1, 2])] - measurements = [qml.expval(qml.PauliZ(0)), qml.expval(2 * qml.PauliX(0) @ qml.PauliY(1))] + measurements = [ + qml.expval(qml.PauliZ(0)), + qml.expval(qml.Hamiltonian([2], [qml.PauliX(0) @ qml.PauliY(1)])), + ] circuit = qml.tape.QuantumScript(ops=ops, measurements=measurements) dev = mock_device_with_paulis_hamiltonian_and_methods(wires=3) diff --git a/tests/test_operation.py b/tests/test_operation.py index 8a880b915d1..a6dd4e2d1bb 100644 --- a/tests/test_operation.py +++ b/tests/test_operation.py @@ -39,6 +39,7 @@ # pylint: disable=no-self-use, no-member, protected-access, redefined-outer-name, too-few-public-methods # pylint: disable=too-many-public-methods, unused-argument, unnecessary-lambda-assignment, unnecessary-dunder-call +# pylint: disable=use-implicit-booleaness-not-comparison Toffoli_broadcasted = np.tensordot([0.1, -4.2j], Toffoli, axes=0) CNOT_broadcasted = np.tensordot([1.4], CNOT, axes=0) @@ -1336,6 +1337,7 @@ def test_label_for_operations_with_id(self): assert '"test_with_id"' not in op.label(decimals=2) +@pytest.mark.usefixtures("use_legacy_opmath") class TestTensor: """Unit tests for the Tensor class""" @@ -1833,26 +1835,27 @@ def test_multiplication_matrix(self, tol, classes): herm_matrix = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]) - tensor_obs = [ - (qml.PauliZ(0) @ qml.Identity(1) @ qml.PauliZ(2), [qml.PauliZ(0), qml.PauliZ(2)]), - ( - qml.Identity(0) - @ qml.PauliX(1) - @ qml.Identity(2) - @ qml.PauliZ(3) - @ qml.PauliZ(4) - @ qml.Identity(5), - [qml.PauliX(1), qml.PauliZ(3), qml.PauliZ(4)], - ), - # List containing single observable is returned - (qml.PauliZ(0) @ qml.Identity(1), [qml.PauliZ(0)]), - (qml.Identity(0) @ qml.PauliX(1) @ qml.Identity(2), [qml.PauliX(1)]), - (qml.Identity(0) @ qml.Identity(1), [qml.Identity(0)]), - ( - qml.Identity(0) @ qml.Identity(1) @ qml.Hermitian(herm_matrix, wires=[2, 3]), - [qml.Hermitian(herm_matrix, wires=[2, 3])], - ), - ] + with qml.operation.disable_new_opmath_cm(): + tensor_obs = [ + (qml.PauliZ(0) @ qml.Identity(1) @ qml.PauliZ(2), [qml.PauliZ(0), qml.PauliZ(2)]), + ( + qml.Identity(0) + @ qml.PauliX(1) + @ qml.Identity(2) + @ qml.PauliZ(3) + @ qml.PauliZ(4) + @ qml.Identity(5), + [qml.PauliX(1), qml.PauliZ(3), qml.PauliZ(4)], + ), + # List containing single observable is returned + (qml.PauliZ(0) @ qml.Identity(1), [qml.PauliZ(0)]), + (qml.Identity(0) @ qml.PauliX(1) @ qml.Identity(2), [qml.PauliX(1)]), + (qml.Identity(0) @ qml.Identity(1), [qml.Identity(0)]), + ( + qml.Identity(0) @ qml.Identity(1) @ qml.Hermitian(herm_matrix, wires=[2, 3]), + [qml.Hermitian(herm_matrix, wires=[2, 3])], + ), + ] @pytest.mark.parametrize("tensor_observable, expected", tensor_obs) def test_non_identity_obs(self, tensor_observable, expected): @@ -1863,27 +1866,28 @@ def test_non_identity_obs(self, tensor_observable, expected): assert isinstance(obs, type(expected[idx])) assert obs.wires == expected[idx].wires - tensor_obs_pruning = [ - (qml.PauliZ(0) @ qml.Identity(1) @ qml.PauliZ(2), qml.PauliZ(0) @ qml.PauliZ(2)), - ( - qml.Identity(0) - @ qml.PauliX(1) - @ qml.Identity(2) - @ qml.PauliZ(3) - @ qml.PauliZ(4) - @ qml.Identity(5), - qml.PauliX(1) @ qml.PauliZ(3) @ qml.PauliZ(4), - ), - # Single observable is returned - (qml.PauliZ(0) @ qml.Identity(1), qml.PauliZ(0)), - (qml.Identity(0) @ qml.PauliX(1) @ qml.Identity(2), qml.PauliX(1)), - (qml.Identity(0) @ qml.Identity(1), qml.Identity(0)), - (qml.Identity(0) @ qml.Identity(1), qml.Identity(0)), - ( - qml.Identity(0) @ qml.Identity(1) @ qml.Hermitian(herm_matrix, wires=[2, 3]), - qml.Hermitian(herm_matrix, wires=[2, 3]), - ), - ] + with qml.operation.disable_new_opmath_cm(): + tensor_obs_pruning = [ + (qml.PauliZ(0) @ qml.Identity(1) @ qml.PauliZ(2), qml.PauliZ(0) @ qml.PauliZ(2)), + ( + qml.Identity(0) + @ qml.PauliX(1) + @ qml.Identity(2) + @ qml.PauliZ(3) + @ qml.PauliZ(4) + @ qml.Identity(5), + qml.PauliX(1) @ qml.PauliZ(3) @ qml.PauliZ(4), + ), + # Single observable is returned + (qml.PauliZ(0) @ qml.Identity(1), qml.PauliZ(0)), + (qml.Identity(0) @ qml.PauliX(1) @ qml.Identity(2), qml.PauliX(1)), + (qml.Identity(0) @ qml.Identity(1), qml.Identity(0)), + (qml.Identity(0) @ qml.Identity(1), qml.Identity(0)), + ( + qml.Identity(0) @ qml.Identity(1) @ qml.Hermitian(herm_matrix, wires=[2, 3]), + qml.Hermitian(herm_matrix, wires=[2, 3]), + ), + ] @pytest.mark.parametrize("tensor_observable, expected", tensor_obs_pruning) def test_prune(self, tensor_observable, expected): @@ -2039,112 +2043,120 @@ def test_matmul_not_implemented(self): _ = op @ 1.0 -equal_obs = [ - (qml.PauliZ(0), qml.PauliZ(0), True), - (qml.PauliZ(0) @ qml.PauliX(1), qml.PauliZ(0) @ qml.PauliX(1) @ qml.Identity(2), True), - (qml.PauliZ("b"), qml.PauliZ("b") @ qml.Identity(1.3), True), - (qml.PauliZ(0) @ qml.Identity(1), qml.PauliZ(0), True), - (qml.PauliZ(0), qml.PauliZ(1) @ qml.Identity(0), False), - ( - qml.Hermitian(np.array([[0, 1], [1, 0]]), 0), - qml.Identity(1) @ qml.Hermitian(np.array([[0, 1], [1, 0]]), 0), - True, - ), - (qml.PauliZ("a") @ qml.PauliX(1), qml.PauliX(1) @ qml.PauliZ("a"), True), - (qml.PauliZ("a"), qml.Hamiltonian([1], [qml.PauliZ("a")]), True), -] +with qml.operation.disable_new_opmath_cm(): + equal_obs = [ + (qml.PauliZ(0), qml.PauliZ(0), True), + (qml.PauliZ(0) @ qml.PauliX(1), qml.PauliZ(0) @ qml.PauliX(1) @ qml.Identity(2), True), + (qml.PauliZ("b"), qml.PauliZ("b") @ qml.Identity(1.3), True), + (qml.PauliZ(0) @ qml.Identity(1), qml.PauliZ(0), True), + (qml.PauliZ(0), qml.PauliZ(1) @ qml.Identity(0), False), + ( + qml.Hermitian(np.array([[0, 1], [1, 0]]), 0), + qml.Identity(1) @ qml.Hermitian(np.array([[0, 1], [1, 0]]), 0), + True, + ), + (qml.PauliZ("a") @ qml.PauliX(1), qml.PauliX(1) @ qml.PauliZ("a"), True), + (qml.PauliZ("a"), qml.Hamiltonian([1], [qml.PauliZ("a")]), True), + ] -add_obs = [ - (qml.PauliZ(0) @ qml.Identity(1), qml.PauliZ(0), qml.Hamiltonian([2], [qml.PauliZ(0)])), - ( - qml.PauliZ(0), - qml.PauliZ(0) @ qml.PauliX(1), - qml.Hamiltonian([1, 1], [qml.PauliZ(0), qml.PauliZ(0) @ qml.PauliX(1)]), - ), - ( - qml.PauliZ("b") @ qml.Identity(1), - qml.Hamiltonian([3], [qml.PauliZ("b")]), - qml.Hamiltonian([4], [qml.PauliZ("b")]), - ), - ( - qml.PauliX(0) @ qml.PauliZ(1), - qml.PauliZ(1) @ qml.Identity(2) @ qml.PauliX(0), - qml.Hamiltonian([2], [qml.PauliX(0) @ qml.PauliZ(1)]), - ), - ( - qml.Hermitian(np.array([[1, 0], [0, -1]]), 1.2), - qml.Hamiltonian([3], [qml.Hermitian(np.array([[1, 0], [0, -1]]), 1.2)]), - qml.Hamiltonian([4], [qml.Hermitian(np.array([[1, 0], [0, -1]]), 1.2)]), - ), -] + add_obs = [ + (qml.PauliZ(0) @ qml.Identity(1), qml.PauliZ(0), qml.Hamiltonian([2], [qml.PauliZ(0)])), + ( + qml.PauliZ(0), + qml.PauliZ(0) @ qml.PauliX(1), + qml.Hamiltonian([1, 1], [qml.PauliZ(0), qml.PauliZ(0) @ qml.PauliX(1)]), + ), + ( + qml.PauliZ("b") @ qml.Identity(1), + qml.Hamiltonian([3], [qml.PauliZ("b")]), + qml.Hamiltonian([4], [qml.PauliZ("b")]), + ), + ( + qml.PauliX(0) @ qml.PauliZ(1), + qml.PauliZ(1) @ qml.Identity(2) @ qml.PauliX(0), + qml.Hamiltonian([2], [qml.PauliX(0) @ qml.PauliZ(1)]), + ), + ( + qml.Hermitian(np.array([[1, 0], [0, -1]]), 1.2), + qml.Hamiltonian([3], [qml.Hermitian(np.array([[1, 0], [0, -1]]), 1.2)]), + qml.Hamiltonian([4], [qml.Hermitian(np.array([[1, 0], [0, -1]]), 1.2)]), + ), + ] -add_zero_obs = [ - qml.PauliX(0), - qml.Hermitian(np.array([[1, 0], [0, -1]]), 1.2), - qml.PauliX(0) @ qml.Hadamard(2), - # qml.Projector(np.array([1, 1]), wires=[0, 1]), - # qml.SparseHamiltonian(csr_matrix(np.array([[1, 0], [-1.5, 0]])), 1), - # CVObservables - qml.Identity(1), - cv.NumberOperator(wires=[1]), - cv.TensorN(wires=[1]), - cv.QuadX(wires=[1]), - cv.QuadP(wires=[1]), - # cv.QuadOperator(1.234, wires=0), - # cv.FockStateProjector([1,2,3], wires=[0, 1, 2]), - cv.PolyXP(np.array([1.0, 2.0, 3.0]), wires=[0]), -] + add_zero_obs = [ + qml.PauliX(0), + qml.Hermitian(np.array([[1, 0], [0, -1]]), 1.2), + qml.PauliX(0) @ qml.Hadamard(2), + # qml.Projector(np.array([1, 1]), wires=[0, 1]), + # qml.SparseHamiltonian(csr_matrix(np.array([[1, 0], [-1.5, 0]])), 1), + # CVObservables + qml.Identity(1), + cv.NumberOperator(wires=[1]), + cv.TensorN(wires=[1]), + cv.QuadX(wires=[1]), + cv.QuadP(wires=[1]), + # cv.QuadOperator(1.234, wires=0), + # cv.FockStateProjector([1,2,3], wires=[0, 1, 2]), + cv.PolyXP(np.array([1.0, 2.0, 3.0]), wires=[0]), + ] -mul_obs = [ - (qml.PauliZ(0), 3, qml.Hamiltonian([3], [qml.PauliZ(0)])), - (qml.PauliZ(0) @ qml.Identity(1), 3, qml.Hamiltonian([3], [qml.PauliZ(0)])), - (qml.PauliZ(0) @ qml.PauliX(1), 4.5, qml.Hamiltonian([4.5], [qml.PauliZ(0) @ qml.PauliX(1)])), - ( - qml.Hermitian(np.array([[1, 0], [0, -1]]), "c"), - 3, - qml.Hamiltonian([3], [qml.Hermitian(np.array([[1, 0], [0, -1]]), "c")]), - ), -] + mul_obs = [ + (qml.PauliZ(0), 3, qml.Hamiltonian([3], [qml.PauliZ(0)])), + (qml.PauliZ(0) @ qml.Identity(1), 3, qml.Hamiltonian([3], [qml.PauliZ(0)])), + ( + qml.PauliZ(0) @ qml.PauliX(1), + 4.5, + qml.Hamiltonian([4.5], [qml.PauliZ(0) @ qml.PauliX(1)]), + ), + ( + qml.Hermitian(np.array([[1, 0], [0, -1]]), "c"), + 3, + qml.Hamiltonian([3], [qml.Hermitian(np.array([[1, 0], [0, -1]]), "c")]), + ), + ] -matmul_obs = [ - (qml.PauliX(0), qml.PauliZ(1), Tensor(qml.PauliX(0), qml.PauliZ(1))), # obs @ obs - ( - qml.PauliX(0), - qml.PauliZ(1) @ qml.PauliY(2), - Tensor(qml.PauliX(0), qml.PauliZ(1), qml.PauliY(2)), - ), # obs @ tensor - ( - qml.PauliX(0), - qml.Hamiltonian([1.0], [qml.PauliY(1)]), - qml.Hamiltonian([1.0], [qml.PauliX(0) @ qml.PauliY(1)]), - ), # obs @ hamiltonian -] + matmul_obs = [ + (qml.PauliX(0), qml.PauliZ(1), Tensor(qml.PauliX(0), qml.PauliZ(1))), # obs @ obs + ( + qml.PauliX(0), + qml.PauliZ(1) @ qml.PauliY(2), + Tensor(qml.PauliX(0), qml.PauliZ(1), qml.PauliY(2)), + ), # obs @ tensor + ( + qml.PauliX(0), + qml.Hamiltonian([1.0], [qml.PauliY(1)]), + qml.Hamiltonian([1.0], [qml.PauliX(0) @ qml.PauliY(1)]), + ), # obs @ hamiltonian + ] -sub_obs = [ - (qml.PauliZ(0) @ qml.Identity(1), qml.PauliZ(0), qml.Hamiltonian([], [])), - ( - qml.PauliZ(0), - qml.PauliZ(0) @ qml.PauliX(1), - qml.Hamiltonian([1, -1], [qml.PauliZ(0), qml.PauliZ(0) @ qml.PauliX(1)]), - ), - ( - qml.PauliZ(0) @ qml.Identity(1), - qml.Hamiltonian([3], [qml.PauliZ(0)]), - qml.Hamiltonian([-2], [qml.PauliZ(0)]), - ), - ( - qml.PauliX(0) @ qml.PauliZ(1), - qml.PauliZ(3) @ qml.Identity(2) @ qml.PauliX(0), - qml.Hamiltonian([1, -1], [qml.PauliX(0) @ qml.PauliZ(1), qml.PauliZ(3) @ qml.PauliX(0)]), - ), - ( - qml.Hermitian(np.array([[1, 0], [0, -1]]), 1.2), - qml.Hamiltonian([3], [qml.Hermitian(np.array([[1, 0], [0, -1]]), 1.2)]), - qml.Hamiltonian([-2], [qml.Hermitian(np.array([[1, 0], [0, -1]]), 1.2)]), - ), -] + sub_obs = [ + (qml.PauliZ(0) @ qml.Identity(1), qml.PauliZ(0), qml.Hamiltonian([], [])), + ( + qml.PauliZ(0), + qml.PauliZ(0) @ qml.PauliX(1), + qml.Hamiltonian([1, -1], [qml.PauliZ(0), qml.PauliZ(0) @ qml.PauliX(1)]), + ), + ( + qml.PauliZ(0) @ qml.Identity(1), + qml.Hamiltonian([3], [qml.PauliZ(0)]), + qml.Hamiltonian([-2], [qml.PauliZ(0)]), + ), + ( + qml.PauliX(0) @ qml.PauliZ(1), + qml.PauliZ(3) @ qml.Identity(2) @ qml.PauliX(0), + qml.Hamiltonian( + [1, -1], [qml.PauliX(0) @ qml.PauliZ(1), qml.PauliZ(3) @ qml.PauliX(0)] + ), + ), + ( + qml.Hermitian(np.array([[1, 0], [0, -1]]), 1.2), + qml.Hamiltonian([3], [qml.Hermitian(np.array([[1, 0], [0, -1]]), 1.2)]), + qml.Hamiltonian([-2], [qml.Hermitian(np.array([[1, 0], [0, -1]]), 1.2)]), + ), + ] +@pytest.mark.usefixtures("use_legacy_opmath") class TestTensorObservableOperations: """Tests arithmetic operations between observables/tensors""" @@ -2604,11 +2616,11 @@ def test_composed(self): class TestNewOpMath: """Tests dunder operations with new operator arithmetic enabled.""" - @pytest.fixture(autouse=True, scope="class") - def run_before_and_after_tests(self): - qml.operation.enable_new_opmath() - yield - qml.operation.disable_new_opmath() + # @pytest.fixture(autouse=True, scope="function") # this came from a push to ham-tests but I think it should not be there as it explicitly disabled new opmath after each test, so also leaving it in that state for other tests. + # def run_before_and_after_tests(self): + # qml.operation.enable_new_opmath() + # yield + # qml.operation.disable_new_opmath() class TestAdd: """Test the __add__/__radd__/__sub__ dunders.""" @@ -2724,6 +2736,33 @@ def test_mul_does_not_auto_simplify(self): assert qml.equal(op[0], op0 @ op1) assert qml.equal(op[1], op2) + class TestHamiltonianLinearCombinationAlias: + """Unit tests for using qml.Hamiltonian as an alias for LinearCombination""" + + @pytest.mark.usefixtures("use_new_opmath") + def test_hamiltonian_linear_combination_alias_enabled(self): + """Test that qml.Hamiltonian is an alias for LinearCombination with new operator + arithmetic enabled""" + op = qml.Hamiltonian([1.0], [qml.X(0)]) + + assert isinstance(op, qml.ops.LinearCombination) + assert isinstance(op, qml.Hamiltonian) + assert not isinstance(op, qml.ops.Hamiltonian) + assert not isinstance(op, qml.ops.qubit.Hamiltonian) + assert not isinstance(op, qml.ops.qubit.hamiltonian.Hamiltonian) + + @pytest.mark.usefixtures("use_legacy_opmath") + def test_hamiltonian_linear_combination_alias_disabled(self): + """Test that qml.Hamiltonian is not an alias for LinearCombination with new operator + arithmetic disabled""" + op = qml.Hamiltonian([1.0], [qml.X(0)]) + + assert not isinstance(op, qml.ops.LinearCombination) + assert isinstance(op, qml.Hamiltonian) + assert isinstance(op, qml.ops.Hamiltonian) + assert isinstance(op, qml.ops.qubit.Hamiltonian) + assert isinstance(op, qml.ops.qubit.hamiltonian.Hamiltonian) + @pytest.mark.parametrize( "op", @@ -2762,16 +2801,16 @@ def test_symmetric_matrix_early_return(op, mocker): def test_op_arithmetic_toggle(): - """Tests toggling op arithmetic on and off, and that it is off by default.""" - assert not qml.operation.active_new_opmath() - - qml.operation.enable_new_opmath() + """Tests toggling op arithmetic on and off, and that it is on by default.""" assert qml.operation.active_new_opmath() - assert isinstance(qml.PauliX(0) @ qml.PauliZ(1), Prod) - qml.operation.disable_new_opmath() - assert not qml.operation.active_new_opmath() - assert isinstance(qml.PauliX(0) @ qml.PauliZ(1), Tensor) + with qml.operation.enable_new_opmath_cm(): + assert qml.operation.active_new_opmath() + assert isinstance(qml.PauliX(0) @ qml.PauliZ(1), Prod) + + with qml.operation.disable_new_opmath_cm(): + assert not qml.operation.active_new_opmath() + assert isinstance(qml.PauliX(0) @ qml.PauliZ(1), Tensor) def test_docstring_example_of_operator_class(tol): @@ -2847,59 +2886,70 @@ class CustomOperator(qml.operation.Operator): assert qml.equal(new_op, CustomOperator(2.3, wires=0)) -@pytest.mark.parametrize( - "coeffs, obs", - [ - ( - [1.5, 0.5, 1, 1], - [ - qml.Identity(1), - Tensor(qml.Z(1), qml.Z(2)), - Tensor(qml.X(1), qml.Y(2)), - qml.Hadamard(1), - ], - ), - ([0.5], [qml.X(1)]), - ([1], [Tensor(qml.X(0), qml.Y(1))]), - ( - [0.0625, 0.0625, -0.0625, 0.0625, -0.0625, 0.0625, -0.0625, -0.0625], - [ - Tensor(qml.Hadamard(0), qml.X(1), qml.X(2), qml.Y(3)), - Tensor(qml.X(0), qml.X(1), qml.Y(2), qml.X(3)), - Tensor(qml.X(0), qml.Y(1), qml.X(2), qml.X(3)), - Tensor(qml.X(0), qml.Y(1), qml.Y(2), qml.Y(3)), - Tensor(qml.Y(0), qml.X(1), qml.X(2), qml.X(3)), - Tensor(qml.Y(0), qml.X(1), qml.Hadamard(2), qml.Y(3)), - Tensor(qml.Y(0), qml.Y(1), qml.X(2), qml.Y(3)), - Tensor(qml.Y(0), qml.Y(1), qml.Y(2), qml.Hadamard(3)), - ], - ), - ( - [-0.5, 0.4, -0.3, 0.2], - [ - qml.Identity(0, 1), - Tensor(qml.X(1), qml.Y(2)), - qml.Identity(1), - Tensor(qml.Z(1), qml.Z(2)), - ], - ), - ], -) +@pytest.mark.usefixtures("use_new_opmath") +def test_use_new_opmath_fixture(): + """Test that the fixture for using new opmath in a context works as expected""" + assert qml.operation.active_new_opmath() + + +@pytest.mark.usefixtures("use_legacy_opmath") +def test_use_legacy_opmath_fixture(): + """Test that the fixture for using new opmath in a context works as expected""" + assert not qml.operation.active_new_opmath() + + +CONVERT_HAMILTONAIN = [ + ( + [1.5, 0.5, 1, 1], + [ + qml.Identity(1), + Tensor(qml.Z(1), qml.Z(2)), + Tensor(qml.X(1), qml.Y(2)), + qml.Hadamard(1), + ], + ), + ([0.5], [qml.X(1)]), + ([1], [Tensor(qml.X(0), qml.Y(1))]), + ( + [-0.5, 0.4, -0.3, 0.2], + [ + qml.Identity(0, 1), + Tensor(qml.X(1), qml.Y(2)), + qml.Identity(1), + Tensor(qml.Z(1), qml.Z(2)), + ], + ), + ( + [0.0625, 0.0625, -0.0625, 0.0625, -0.0625, 0.0625, -0.0625, -0.0625], + [ + Tensor(qml.Hadamard(0), qml.X(1), qml.X(2), qml.Y(3)), + Tensor(qml.X(0), qml.X(1), qml.Y(2), qml.X(3)), + Tensor(qml.X(0), qml.Y(1), qml.X(2), qml.X(3)), + Tensor(qml.X(0), qml.Y(1), qml.Y(2), qml.Y(3)), + Tensor(qml.Y(0), qml.X(1), qml.X(2), qml.X(3)), + Tensor(qml.Y(0), qml.X(1), qml.Hadamard(2), qml.Y(3)), + Tensor(qml.Y(0), qml.Y(1), qml.X(2), qml.Y(3)), + Tensor(qml.Y(0), qml.Y(1), qml.Y(2), qml.Hadamard(3)), + ], + ), +] + + +@pytest.mark.parametrize("coeffs, obs", CONVERT_HAMILTONAIN) def test_convert_to_hamiltonian(coeffs, obs): """Test that arithmetic operators can be converted to Hamiltonian instances""" opmath_instance = qml.dot(coeffs, obs) converted_opmath = convert_to_legacy_H(opmath_instance) - assert isinstance(converted_opmath, qml.Hamiltonian) + with qml.operation.disable_new_opmath_cm(): + assert isinstance(converted_opmath, qml.Hamiltonian) - opmath_was_active = qml.operation.active_new_opmath() - qml.operation.disable_new_opmath() - hamiltonian_instance = qml.Hamiltonian(coeffs, obs) - if opmath_was_active: - qml.operation.enable_new_opmath() - else: - qml.operation.disable_new_opmath() - assert qml.equal(hamiltonian_instance, converted_opmath) + with pytest.warns( + qml.PennyLaneDeprecationWarning, match="with new operator arithmetic is deprecated" + ): + hamiltonian_instance = qml.ops.Hamiltonian(coeffs, obs) + + assert qml.equal(converted_opmath, hamiltonian_instance) @pytest.mark.parametrize( @@ -2908,9 +2958,10 @@ def test_convert_to_hamiltonian(coeffs, obs): def test_convert_to_hamiltonian_trivial(coeffs, obs): """Test that non-arithmetic operator after simplification is returned as an Observable""" - opmath_instance = qml.dot(coeffs, obs) - converted_opmath = convert_to_legacy_H(opmath_instance) - assert isinstance(converted_opmath, qml.operation.Observable) + with qml.operation.disable_new_opmath_cm(): + opmath_instance = qml.dot(coeffs, obs) + converted_opmath = convert_to_legacy_H(opmath_instance) + assert isinstance(converted_opmath, qml.operation.Observable) @pytest.mark.parametrize( @@ -2929,6 +2980,39 @@ def test_convert_to_hamiltonian_error(coeffs, obs): convert_to_legacy_H(qml.dot(coeffs, obs)) +def test_convert_to_H(): + operator = ( + 2 * qml.X(0) + + 3 * qml.X(0) + + qml.Y(1) @ qml.Z(2) @ (2 * qml.X(3)) + + 2 * (qml.Hadamard(3) + 3 * qml.Z(2)) + ) + with qml.operation.disable_new_opmath_cm(): + legacy_H = qml.operation.convert_to_H(operator) + linear_combination = qml.operation.convert_to_H(operator) + + assert isinstance(legacy_H, qml.ops.Hamiltonian) + assert isinstance(linear_combination, qml.ops.LinearCombination) + + # coeffs match + legacy_coeffs, legacy_ops = legacy_H.terms() + coeffs, ops = linear_combination.terms() + assert np.all(legacy_coeffs == coeffs) + + # legacy version has Tensors and not Prods, new version opposite + assert Tensor in [type(o) for o in legacy_ops] + assert Tensor not in [type(o) for o in ops] + assert qml.ops.op_math.Prod not in [type(o) for o in legacy_ops] + assert qml.ops.op_math.Prod in [type(o) for o in ops] + + # ops match + for legacy_op, op in zip(legacy_ops, ops): + assert np.all(legacy_op.matrix() == op.matrix()) + + # the converted op is the same as the original op + assert qml.equal(operator.simplify(), linear_combination.simplify()) + + # pylint: disable=unused-import,no-name-in-module def test_get_attr(): """Test that importing attributes of operation work as expected""" diff --git a/tests/test_qaoa.py b/tests/test_qaoa.py index 3f511e8a5c6..84ed32f7446 100644 --- a/tests/test_qaoa.py +++ b/tests/test_qaoa.py @@ -74,13 +74,6 @@ b_rx.add_edges_from([(0, 1, ""), (1, 2, ""), (0, 2, "")]) -def decompose_hamiltonian(hamiltonian): - coeffs = list(qml.math.toarray(hamiltonian.coeffs)) - ops = [i.name for i in hamiltonian.ops] - wires = [i.wires for i in hamiltonian.ops] - return [coeffs, ops, wires] - - def lollipop_graph_rx(mesh_nodes: int, path_nodes: int, to_directed: bool = False): if to_directed: g = rx.generators.directed_mesh_graph(weights=[*range(mesh_nodes)]) @@ -110,20 +103,24 @@ def matrix(hamiltonian: qml.Hamiltonian, n_wires: int) -> csc_matrix: ops_matrices = [] for op in hamiltonian.ops: - op_wires = np.array(op.wires.tolist()) - op_list = op.non_identity_obs if isinstance(op, qml.operation.Tensor) else [op] - op_matrices = [] - for wire in range(n_wires): - loc = np.argwhere(op_wires == wire).flatten() - mat = np.eye(2) if len(loc) == 0 else op_list[loc[0]].matrix() - mat = csc_matrix(mat) - op_matrices.append(mat) + if isinstance(op, qml.ops.Prod): + op_matrix = op.sparse_matrix(wire_order=list(range(n_wires))) + else: + op_wires = np.array(op.wires.tolist()) + op_list = op.non_identity_obs if isinstance(op, qml.operation.Tensor) else [op] + op_matrices = [] - op_matrix = op_matrices.pop(0) + for wire in range(n_wires): + loc = np.argwhere(op_wires == wire).flatten() + mat = np.eye(2) if len(loc) == 0 else op_list[loc[0]].matrix() + mat = csc_matrix(mat) + op_matrices.append(mat) - for mat in op_matrices: - op_matrix = kron(op_matrix, mat) + op_matrix = op_matrices.pop(0) + + for mat in op_matrices: + op_matrix = kron(op_matrix, mat) ops_matrices.append(op_matrix) @@ -131,23 +128,219 @@ def matrix(hamiltonian: qml.Hamiltonian, n_wires: int) -> csc_matrix: return csc_matrix(mat) +def make_xy_mixer_test_cases(): + return [ + ( + g2, + qml.Hamiltonian( + [0.5, 0.5, 0.5, 0.5, 0.5, 0.5], + [ + qml.PauliX(0) @ qml.PauliX(1), + qml.PauliY(0) @ qml.PauliY(1), + qml.PauliX(1) @ qml.PauliX(2), + qml.PauliY(1) @ qml.PauliY(2), + qml.PauliX(2) @ qml.PauliX(3), + qml.PauliY(2) @ qml.PauliY(3), + ], + ), + ), + ( + line_graph, + qml.Hamiltonian( + [0.5, 0.5, 0.5, 0.5], + [ + qml.PauliX(0) @ qml.PauliX(1), + qml.PauliY(0) @ qml.PauliY(1), + qml.PauliX(1) @ qml.PauliX(2), + qml.PauliY(1) @ qml.PauliY(2), + ], + ), + ), + ( + non_consecutive_graph, + qml.Hamiltonian( + [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5], + [ + qml.PauliX(0) @ qml.PauliX(4), + qml.PauliY(0) @ qml.PauliY(4), + qml.PauliX(0) @ qml.PauliX(2), + qml.PauliY(0) @ qml.PauliY(2), + qml.PauliX(4) @ qml.PauliX(3), + qml.PauliY(4) @ qml.PauliY(3), + qml.PauliX(2) @ qml.PauliX(1), + qml.PauliY(2) @ qml.PauliY(1), + ], + ), + ), + ( + g2_rx, + qml.Hamiltonian( + [0.5, 0.5, 0.5, 0.5, 0.5, 0.5], + [ + qml.PauliX(0) @ qml.PauliX(1), + qml.PauliY(0) @ qml.PauliY(1), + qml.PauliX(1) @ qml.PauliX(2), + qml.PauliY(1) @ qml.PauliY(2), + qml.PauliX(2) @ qml.PauliX(3), + qml.PauliY(2) @ qml.PauliY(3), + ], + ), + ), + ( + graph_rx, + qml.Hamiltonian( + [0.5, 0.5, 0.5, 0.5], + [ + qml.PauliX(0) @ qml.PauliX(1), + qml.PauliY(0) @ qml.PauliY(1), + qml.PauliX(1) @ qml.PauliX(2), + qml.PauliY(1) @ qml.PauliY(2), + ], + ), + ), + ( + non_consecutive_graph_rx, + qml.Hamiltonian( + [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5], + [ + qml.PauliX(0) @ qml.PauliX(4), + qml.PauliY(0) @ qml.PauliY(4), + qml.PauliX(0) @ qml.PauliX(2), + qml.PauliY(0) @ qml.PauliY(2), + qml.PauliX(4) @ qml.PauliX(3), + qml.PauliY(4) @ qml.PauliY(3), + qml.PauliX(2) @ qml.PauliX(1), + qml.PauliY(2) @ qml.PauliY(1), + ], + ), + ), + ( + Graph((np.array([0, 1]), np.array([1, 2]), np.array([2, 0]))), + qml.Hamiltonian( + [0.5, 0.5, 0.5, 0.5, 0.5, 0.5], + [ + qml.PauliX(0) @ qml.PauliX(1), + qml.PauliY(0) @ qml.PauliY(1), + qml.PauliX(0) @ qml.PauliX(2), + qml.PauliY(0) @ qml.PauliY(2), + qml.PauliX(1) @ qml.PauliX(2), + qml.PauliY(1) @ qml.PauliY(2), + ], + ), + ), + ] + + +def make_bit_flip_mixer_test_cases(): + return [ + ( + Graph([(0, 1)]), + 1, + qml.Hamiltonian( + [0.5, -0.5, 0.5, -0.5], + [ + qml.PauliX(0), + qml.PauliX(0) @ qml.PauliZ(1), + qml.PauliX(1), + qml.PauliX(1) @ qml.PauliZ(0), + ], + ), + ), + ( + g1, + 0, + qml.Hamiltonian( + [0.5, 0.5, 0.25, 0.25, 0.25, 0.25, 0.5, 0.5], + [ + qml.PauliX(0), + qml.PauliX(0) @ qml.PauliZ(1), + qml.PauliX(1), + qml.PauliX(1) @ qml.PauliZ(2), + qml.PauliX(1) @ qml.PauliZ(0), + qml.PauliX(1) @ qml.PauliZ(0) @ qml.PauliZ(2), + qml.PauliX(2), + qml.PauliX(2) @ qml.PauliZ(1), + ], + ), + ), + ( + g1_rx, + 0, + qml.Hamiltonian( + [0.5, 0.5, 0.25, 0.25, 0.25, 0.25, 0.5, 0.5], + [ + qml.PauliX(0), + qml.PauliX(0) @ qml.PauliZ(1), + qml.PauliX(1), + qml.PauliX(1) @ qml.PauliZ(2), + qml.PauliX(1) @ qml.PauliZ(0), + qml.PauliX(1) @ qml.PauliZ(0) @ qml.PauliZ(2), + qml.PauliX(2), + qml.PauliX(2) @ qml.PauliZ(1), + ], + ), + ), + ( + Graph([("b", 1), (1, 0.3), (0.3, "b")]), + 1, + qml.Hamiltonian( + [0.25, -0.25, -0.25, 0.25, 0.25, -0.25, -0.25, 0.25, 0.25, -0.25, -0.25, 0.25], + [ + qml.PauliX("b"), + qml.PauliX("b") @ qml.PauliZ(0.3), + qml.PauliX("b") @ qml.PauliZ(1), + qml.PauliX("b") @ qml.PauliZ(1) @ qml.PauliZ(0.3), + qml.PauliX(1), + qml.PauliX(1) @ qml.PauliZ(0.3), + qml.PauliX(1) @ qml.PauliZ("b"), + qml.PauliX(1) @ qml.PauliZ("b") @ qml.PauliZ(0.3), + qml.PauliX(0.3), + qml.PauliX(0.3) @ qml.PauliZ("b"), + qml.PauliX(0.3) @ qml.PauliZ(1), + qml.PauliX(0.3) @ qml.PauliZ(1) @ qml.PauliZ("b"), + ], + ), + ), + ( + b_rx, + 1, + qml.Hamiltonian( + [0.25, -0.25, -0.25, 0.25, 0.25, -0.25, -0.25, 0.25, 0.25, -0.25, -0.25, 0.25], + [ + qml.PauliX("b"), + qml.PauliX("b") @ qml.PauliZ(0.3), + qml.PauliX("b") @ qml.PauliZ(1), + qml.PauliX("b") @ qml.PauliZ(1) @ qml.PauliZ(0.3), + qml.PauliX(1), + qml.PauliX(1) @ qml.PauliZ(0.3), + qml.PauliX(1) @ qml.PauliZ("b"), + qml.PauliX(1) @ qml.PauliZ("b") @ qml.PauliZ(0.3), + qml.PauliX(0.3), + qml.PauliX(0.3) @ qml.PauliZ(1), + qml.PauliX(0.3) @ qml.PauliZ("b"), + qml.PauliX(0.3) @ qml.PauliZ("b") @ qml.PauliZ(1), + ], + ), + ), + ] + + class TestMixerHamiltonians: """Tests that the mixer Hamiltonians are being generated correctly""" + @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_x_mixer_output(self): """Tests that the output of the Pauli-X mixer is correct""" wires = range(4) mixer_hamiltonian = qaoa.x_mixer(wires) + expected_hamiltonian = qml.Hamiltonian( + [1, 1, 1, 1], + [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2), qml.PauliX(3)], + ) + assert mixer_hamiltonian.compare(expected_hamiltonian) - mixer_coeffs = mixer_hamiltonian.coeffs - mixer_ops = [i.name for i in mixer_hamiltonian.ops] - mixer_wires = [i.wires[0] for i in mixer_hamiltonian.ops] - - assert mixer_coeffs == [1, 1, 1, 1] - assert mixer_ops == ["PauliX", "PauliX", "PauliX", "PauliX"] - assert mixer_wires == [0, 1, 2, 3] - + @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_x_mixer_grouping(self): """Tests that the grouping information is set and correct""" @@ -170,125 +363,15 @@ def test_xy_mixer_type_error(self): ): qaoa.xy_mixer(graph) - @pytest.mark.parametrize( - ("graph", "target_hamiltonian"), - [ - ( - g2, - qml.Hamiltonian( - [0.5, 0.5, 0.5, 0.5, 0.5, 0.5], - [ - qml.PauliX(0) @ qml.PauliX(1), - qml.PauliY(0) @ qml.PauliY(1), - qml.PauliX(1) @ qml.PauliX(2), - qml.PauliY(1) @ qml.PauliY(2), - qml.PauliX(2) @ qml.PauliX(3), - qml.PauliY(2) @ qml.PauliY(3), - ], - ), - ), - ( - line_graph, - qml.Hamiltonian( - [0.5, 0.5, 0.5, 0.5], - [ - qml.PauliX(0) @ qml.PauliX(1), - qml.PauliY(0) @ qml.PauliY(1), - qml.PauliX(1) @ qml.PauliX(2), - qml.PauliY(1) @ qml.PauliY(2), - ], - ), - ), - ( - non_consecutive_graph, - qml.Hamiltonian( - [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5], - [ - qml.PauliX(0) @ qml.PauliX(4), - qml.PauliY(0) @ qml.PauliY(4), - qml.PauliX(0) @ qml.PauliX(2), - qml.PauliY(0) @ qml.PauliY(2), - qml.PauliX(4) @ qml.PauliX(3), - qml.PauliY(4) @ qml.PauliY(3), - qml.PauliX(2) @ qml.PauliX(1), - qml.PauliY(2) @ qml.PauliY(1), - ], - ), - ), - ( - g2_rx, - qml.Hamiltonian( - [0.5, 0.5, 0.5, 0.5, 0.5, 0.5], - [ - qml.PauliX(0) @ qml.PauliX(1), - qml.PauliY(0) @ qml.PauliY(1), - qml.PauliX(1) @ qml.PauliX(2), - qml.PauliY(1) @ qml.PauliY(2), - qml.PauliX(2) @ qml.PauliX(3), - qml.PauliY(2) @ qml.PauliY(3), - ], - ), - ), - ( - graph_rx, - qml.Hamiltonian( - [0.5, 0.5, 0.5, 0.5], - [ - qml.PauliX(0) @ qml.PauliX(1), - qml.PauliY(0) @ qml.PauliY(1), - qml.PauliX(1) @ qml.PauliX(2), - qml.PauliY(1) @ qml.PauliY(2), - ], - ), - ), - ( - non_consecutive_graph_rx, - qml.Hamiltonian( - [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5], - [ - qml.PauliX(0) @ qml.PauliX(4), - qml.PauliY(0) @ qml.PauliY(4), - qml.PauliX(0) @ qml.PauliX(2), - qml.PauliY(0) @ qml.PauliY(2), - qml.PauliX(4) @ qml.PauliX(3), - qml.PauliY(4) @ qml.PauliY(3), - qml.PauliX(2) @ qml.PauliX(1), - qml.PauliY(2) @ qml.PauliY(1), - ], - ), - ), - ( - Graph((np.array([0, 1]), np.array([1, 2]), np.array([2, 0]))), - qml.Hamiltonian( - [0.5, 0.5, 0.5, 0.5, 0.5, 0.5], - [ - qml.PauliX(0) @ qml.PauliX(1), - qml.PauliY(0) @ qml.PauliY(1), - qml.PauliX(0) @ qml.PauliX(2), - qml.PauliY(0) @ qml.PauliY(2), - qml.PauliX(1) @ qml.PauliX(2), - qml.PauliY(1) @ qml.PauliY(2), - ], - ), - ), - ], - ) + @pytest.mark.usefixtures("use_legacy_and_new_opmath") + @pytest.mark.parametrize(("graph", "target_hamiltonian"), make_xy_mixer_test_cases()) def test_xy_mixer_output(self, graph, target_hamiltonian): """Tests that the output of the XY mixer is correct""" - mixer_hamiltonian = qaoa.xy_mixer(graph) - - mixer_coeffs = mixer_hamiltonian.coeffs - mixer_ops = [i.name for i in mixer_hamiltonian.ops] - mixer_wires = [i.wires for i in mixer_hamiltonian.ops] - - target_coeffs = target_hamiltonian.coeffs - target_ops = [i.name for i in target_hamiltonian.ops] - target_wires = [i.wires for i in target_hamiltonian.ops] - - assert mixer_coeffs == target_coeffs - assert mixer_ops == target_ops - assert mixer_wires == target_wires + if not qml.operation.active_new_opmath(): + target_hamiltonian = qml.operation.convert_to_legacy_H(target_hamiltonian) + hamiltonian = qaoa.xy_mixer(graph) + assert hamiltonian.compare(target_hamiltonian) def test_bit_flip_mixer_errors(self): """Tests that the bit-flip mixer throws the correct errors""" @@ -305,106 +388,17 @@ def test_bit_flip_mixer_errors(self): @pytest.mark.parametrize( ("graph", "n", "target_hamiltonian"), - [ - ( - Graph([(0, 1)]), - 1, - qml.Hamiltonian( - [0.5, -0.5, 0.5, -0.5], - [ - qml.PauliX(0), - qml.PauliX(0) @ qml.PauliZ(1), - qml.PauliX(1), - qml.PauliX(1) @ qml.PauliZ(0), - ], - ), - ), - ( - g1, - 0, - qml.Hamiltonian( - [0.5, 0.5, 0.25, 0.25, 0.25, 0.25, 0.5, 0.5], - [ - qml.PauliX(0), - qml.PauliX(0) @ qml.PauliZ(1), - qml.PauliX(1), - qml.PauliX(1) @ qml.PauliZ(2), - qml.PauliX(1) @ qml.PauliZ(0), - qml.PauliX(1) @ qml.PauliZ(0) @ qml.PauliZ(2), - qml.PauliX(2), - qml.PauliX(2) @ qml.PauliZ(1), - ], - ), - ), - ( - g1_rx, - 0, - qml.Hamiltonian( - [0.5, 0.5, 0.25, 0.25, 0.25, 0.25, 0.5, 0.5], - [ - qml.PauliX(0), - qml.PauliX(0) @ qml.PauliZ(1), - qml.PauliX(1), - qml.PauliX(1) @ qml.PauliZ(2), - qml.PauliX(1) @ qml.PauliZ(0), - qml.PauliX(1) @ qml.PauliZ(0) @ qml.PauliZ(2), - qml.PauliX(2), - qml.PauliX(2) @ qml.PauliZ(1), - ], - ), - ), - ( - Graph([("b", 1), (1, 0.3), (0.3, "b")]), - 1, - qml.Hamiltonian( - [0.25, -0.25, -0.25, 0.25, 0.25, -0.25, -0.25, 0.25, 0.25, -0.25, -0.25, 0.25], - [ - qml.PauliX("b"), - qml.PauliX("b") @ qml.PauliZ(0.3), - qml.PauliX("b") @ qml.PauliZ(1), - qml.PauliX("b") @ qml.PauliZ(1) @ qml.PauliZ(0.3), - qml.PauliX(1), - qml.PauliX(1) @ qml.PauliZ(0.3), - qml.PauliX(1) @ qml.PauliZ("b"), - qml.PauliX(1) @ qml.PauliZ("b") @ qml.PauliZ(0.3), - qml.PauliX(0.3), - qml.PauliX(0.3) @ qml.PauliZ("b"), - qml.PauliX(0.3) @ qml.PauliZ(1), - qml.PauliX(0.3) @ qml.PauliZ(1) @ qml.PauliZ("b"), - ], - ), - ), - ( - b_rx, - 1, - qml.Hamiltonian( - [0.25, -0.25, -0.25, 0.25, 0.25, -0.25, -0.25, 0.25, 0.25, -0.25, -0.25, 0.25], - [ - qml.PauliX("b"), - qml.PauliX("b") @ qml.PauliZ(0.3), - qml.PauliX("b") @ qml.PauliZ(1), - qml.PauliX("b") @ qml.PauliZ(1) @ qml.PauliZ(0.3), - qml.PauliX(1), - qml.PauliX(1) @ qml.PauliZ(0.3), - qml.PauliX(1) @ qml.PauliZ("b"), - qml.PauliX(1) @ qml.PauliZ("b") @ qml.PauliZ(0.3), - qml.PauliX(0.3), - qml.PauliX(0.3) @ qml.PauliZ(1), - qml.PauliX(0.3) @ qml.PauliZ("b"), - qml.PauliX(0.3) @ qml.PauliZ("b") @ qml.PauliZ(1), - ], - ), - ), - ], + make_bit_flip_mixer_test_cases(), ) + @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_bit_flip_mixer_output(self, graph, n, target_hamiltonian): """Tests that the output of the bit-flip mixer is correct""" - mixer_hamiltonian = qaoa.bit_flip_mixer(graph, n) - assert decompose_hamiltonian(mixer_hamiltonian) == decompose_hamiltonian(target_hamiltonian) - + if not qml.operation.active_new_opmath(): + target_hamiltonian = qml.operation.convert_to_legacy_H(target_hamiltonian) + hamiltonian = qaoa.bit_flip_mixer(graph, n) + assert hamiltonian.compare(target_hamiltonian) -# GENERATES CASES TO TEST THE MAXCUT PROBLEM GRAPHS = [ g1, @@ -414,50 +408,53 @@ def test_bit_flip_mixer_output(self, graph, n, target_hamiltonian): graph_rx, ] -COST_COEFFS = [ - [0.5, 0.5, -1.0], - [0.5, 0.5, -1.0], - [0.5, 0.5, 0.5, -1.5], - [0.5, 0.5, -1.0], - [0.5, 0.5, -1.0], -] -COST_TERMS = [ - [qml.PauliZ(0) @ qml.PauliZ(1), qml.PauliZ(1) @ qml.PauliZ(2), qml.Identity(0)], - [qml.PauliZ(0) @ qml.PauliZ(1), qml.PauliZ(1) @ qml.PauliZ(2), qml.Identity(0)], - [ - qml.PauliZ(0) @ qml.PauliZ(1), - qml.PauliZ(0) @ qml.PauliZ(2), - qml.PauliZ(1) @ qml.PauliZ(2), - qml.Identity(0), - ], - [qml.PauliZ(0) @ qml.PauliZ(1), qml.PauliZ(1) @ qml.PauliZ(2), qml.Identity(0)], - [qml.PauliZ(0) @ qml.PauliZ(1), qml.PauliZ(1) @ qml.PauliZ(2), qml.Identity(0)], -] +def make_max_cut_test_cases(): + """Generates test cases for the maxcut problem""" -COST_HAMILTONIANS = [qml.Hamiltonian(COST_COEFFS[i], COST_TERMS[i]) for i in range(5)] + cost_coeffs = [ + [0.5, 0.5, -1.0], + [0.5, 0.5, -1.0], + [0.5, 0.5, 0.5, -1.5], + [0.5, 0.5, -1.0], + [0.5, 0.5, -1.0], + ] -MIXER_COEFFS = [ - [1, 1, 1], - [1, 1, 1], - [1, 1, 1], - [1, 1, 1], - [1, 1, 1], -] + cost_terms = [ + [qml.PauliZ(0) @ qml.PauliZ(1), qml.PauliZ(1) @ qml.PauliZ(2), qml.Identity(0)], + [qml.PauliZ(0) @ qml.PauliZ(1), qml.PauliZ(1) @ qml.PauliZ(2), qml.Identity(0)], + [ + qml.PauliZ(0) @ qml.PauliZ(1), + qml.PauliZ(0) @ qml.PauliZ(2), + qml.PauliZ(1) @ qml.PauliZ(2), + qml.Identity(0), + ], + [qml.PauliZ(0) @ qml.PauliZ(1), qml.PauliZ(1) @ qml.PauliZ(2), qml.Identity(0)], + [qml.PauliZ(0) @ qml.PauliZ(1), qml.PauliZ(1) @ qml.PauliZ(2), qml.Identity(0)], + ] -MIXER_TERMS = [ - [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], - [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], - [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], - [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], - [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], -] + cost_hamiltonians = [qml.Hamiltonian(cost_coeffs[i], cost_terms[i]) for i in range(5)] + + mixer_coeffs = [ + [1, 1, 1], + [1, 1, 1], + [1, 1, 1], + [1, 1, 1], + [1, 1, 1], + ] -MIXER_HAMILTONIANS = [qml.Hamiltonian(MIXER_COEFFS[i], MIXER_TERMS[i]) for i in range(5)] + mixer_terms = [ + [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], + [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], + [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], + [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], + [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], + ] -MAXCUT = list(zip(GRAPHS, COST_HAMILTONIANS, MIXER_HAMILTONIANS)) + mixer_hamiltonians = [qml.Hamiltonian(mixer_coeffs[i], mixer_terms[i]) for i in range(5)] + + return list(zip(GRAPHS, cost_hamiltonians, mixer_hamiltonians)) -"""GENERATES THE CASES TO TEST THE MAX INDEPENDENT SET PROBLEM""" CONSTRAINED = [ True, @@ -467,407 +464,462 @@ def test_bit_flip_mixer_output(self, graph, n, target_hamiltonian): False, ] -COST_COEFFS = [ - [1, 1, 1], - [1, 1, 1], - [1, 1, 1], - [0.75, 0.25, -0.5, 0.75, 0.25], - [0.75, 0.25, -0.5, 0.75, 0.25], -] -COST_TERMS = [ - [qml.PauliZ(0), qml.PauliZ(1), qml.PauliZ(2)], - [qml.PauliZ(0), qml.PauliZ(1), qml.PauliZ(2)], - [qml.PauliZ(0), qml.PauliZ(1), qml.PauliZ(2)], - [ - qml.PauliZ(0) @ qml.PauliZ(1), - qml.PauliZ(0), - qml.PauliZ(1), - qml.PauliZ(1) @ qml.PauliZ(2), - qml.PauliZ(2), - ], - # [qml.PauliZ(0), qml.PauliZ(1), qml.PauliZ(2)], - [ - qml.PauliZ(0) @ qml.PauliZ(1), - qml.PauliZ(0), - qml.PauliZ(1), - qml.PauliZ(1) @ qml.PauliZ(2), - qml.PauliZ(2), - ], -] +def make_max_independent_test_cases(): + """Generates test cases for the max independent set problem""" -COST_HAMILTONIANS = [qml.Hamiltonian(COST_COEFFS[i], COST_TERMS[i]) for i in range(5)] + cost_coeffs = [ + [1, 1, 1], + [1, 1, 1], + [1, 1, 1], + [0.75, 0.25, -0.5, 0.75, 0.25], + [0.75, 0.25, -0.5, 0.75, 0.25], + ] -MIXER_COEFFS = [ - [0.5, 0.5, 0.25, 0.25, 0.25, 0.25, 0.5, 0.5], - [0.5, 0.5, 0.25, 0.25, 0.25, 0.25, 0.5, 0.5], - [0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25], - [1, 1, 1], - [1, 1, 1], -] + cost_terms = [ + [qml.PauliZ(0), qml.PauliZ(1), qml.PauliZ(2)], + [qml.PauliZ(0), qml.PauliZ(1), qml.PauliZ(2)], + [qml.PauliZ(0), qml.PauliZ(1), qml.PauliZ(2)], + [ + qml.PauliZ(0) @ qml.PauliZ(1), + qml.PauliZ(0), + qml.PauliZ(1), + qml.PauliZ(1) @ qml.PauliZ(2), + qml.PauliZ(2), + ], + # [qml.PauliZ(0), qml.PauliZ(1), qml.PauliZ(2)], + [ + qml.PauliZ(0) @ qml.PauliZ(1), + qml.PauliZ(0), + qml.PauliZ(1), + qml.PauliZ(1) @ qml.PauliZ(2), + qml.PauliZ(2), + ], + ] -MIXER_TERMS = [ - [ - qml.PauliX(0), - qml.PauliX(0) @ qml.PauliZ(1), - qml.PauliX(1), - qml.PauliX(1) @ qml.PauliZ(2), - qml.PauliX(1) @ qml.PauliZ(0), - qml.PauliX(1) @ qml.PauliZ(0) @ qml.PauliZ(2), - qml.PauliX(2), - qml.PauliX(2) @ qml.PauliZ(1), - ], - [ - qml.PauliX(0), - qml.PauliX(0) @ qml.PauliZ(1), - qml.PauliX(1), - qml.PauliX(1) @ qml.PauliZ(2), - qml.PauliX(1) @ qml.PauliZ(0), - qml.PauliX(1) @ qml.PauliZ(0) @ qml.PauliZ(2), - qml.PauliX(2), - qml.PauliX(2) @ qml.PauliZ(1), - ], - [ - qml.PauliX(0), - qml.PauliX(0) @ qml.PauliZ(2), - qml.PauliX(0) @ qml.PauliZ(1), - qml.PauliX(0) @ qml.PauliZ(1) @ qml.PauliZ(2), - qml.PauliX(1), - qml.PauliX(1) @ qml.PauliZ(2), - qml.PauliX(1) @ qml.PauliZ(0), - qml.PauliX(1) @ qml.PauliZ(0) @ qml.PauliZ(2), - qml.PauliX(2), - qml.PauliX(2) @ qml.PauliZ(0), - qml.PauliX(2) @ qml.PauliZ(1), - qml.PauliX(2) @ qml.PauliZ(1) @ qml.PauliZ(0), - ], - [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], - [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], -] + cost_hamiltonians = [qml.Hamiltonian(cost_coeffs[i], cost_terms[i]) for i in range(5)] + + mixer_coeffs = [ + [0.5, 0.5, 0.25, 0.25, 0.25, 0.25, 0.5, 0.5], + [0.5, 0.5, 0.25, 0.25, 0.25, 0.25, 0.5, 0.5], + [0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25], + [1, 1, 1], + [1, 1, 1], + ] -MIXER_HAMILTONIANS = [qml.Hamiltonian(MIXER_COEFFS[i], MIXER_TERMS[i]) for i in range(5)] + mixer_terms = [ + [ + qml.PauliX(0), + qml.PauliX(0) @ qml.PauliZ(1), + qml.PauliX(1), + qml.PauliX(1) @ qml.PauliZ(2), + qml.PauliX(1) @ qml.PauliZ(0), + qml.PauliX(1) @ qml.PauliZ(0) @ qml.PauliZ(2), + qml.PauliX(2), + qml.PauliX(2) @ qml.PauliZ(1), + ], + [ + qml.PauliX(0), + qml.PauliX(0) @ qml.PauliZ(1), + qml.PauliX(1), + qml.PauliX(1) @ qml.PauliZ(2), + qml.PauliX(1) @ qml.PauliZ(0), + qml.PauliX(1) @ qml.PauliZ(0) @ qml.PauliZ(2), + qml.PauliX(2), + qml.PauliX(2) @ qml.PauliZ(1), + ], + [ + qml.PauliX(0), + qml.PauliX(0) @ qml.PauliZ(2), + qml.PauliX(0) @ qml.PauliZ(1), + qml.PauliX(0) @ qml.PauliZ(1) @ qml.PauliZ(2), + qml.PauliX(1), + qml.PauliX(1) @ qml.PauliZ(2), + qml.PauliX(1) @ qml.PauliZ(0), + qml.PauliX(1) @ qml.PauliZ(0) @ qml.PauliZ(2), + qml.PauliX(2), + qml.PauliX(2) @ qml.PauliZ(0), + qml.PauliX(2) @ qml.PauliZ(1), + qml.PauliX(2) @ qml.PauliZ(1) @ qml.PauliZ(0), + ], + [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], + [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], + ] -MIS = list(zip(GRAPHS, CONSTRAINED, COST_HAMILTONIANS, MIXER_HAMILTONIANS)) + mixer_hamiltonians = [qml.Hamiltonian(mixer_coeffs[i], mixer_terms[i]) for i in range(5)] -"""GENERATES THE CASES TO TEST THE MIN VERTEX COVER PROBLEM""" + return list(zip(GRAPHS, CONSTRAINED, cost_hamiltonians, mixer_hamiltonians)) -COST_COEFFS = [ - [-1, -1, -1], - [-1, -1, -1], - [-1, -1, -1], - [0.75, -0.25, 0.5, 0.75, -0.25], - [0.75, -0.25, 0.5, 0.75, -0.25], -] -COST_TERMS = [ - [qml.PauliZ(0), qml.PauliZ(1), qml.PauliZ(2)], - [qml.PauliZ(0), qml.PauliZ(1), qml.PauliZ(2)], - [qml.PauliZ(0), qml.PauliZ(1), qml.PauliZ(2)], - [ - qml.PauliZ(0) @ qml.PauliZ(1), - qml.PauliZ(0), - qml.PauliZ(1), - qml.PauliZ(1) @ qml.PauliZ(2), - qml.PauliZ(2), - ], - [ - qml.PauliZ(0) @ qml.PauliZ(1), - qml.PauliZ(0), - qml.PauliZ(1), - qml.PauliZ(1) @ qml.PauliZ(2), - qml.PauliZ(2), - ], -] +def make_min_vertex_cover_test_cases(): + """Generates the test cases for the min vertex cover problem""" -COST_HAMILTONIANS = [qml.Hamiltonian(COST_COEFFS[i], COST_TERMS[i]) for i in range(5)] + cost_coeffs = [ + [-1, -1, -1], + [-1, -1, -1], + [-1, -1, -1], + [0.75, -0.25, 0.5, 0.75, -0.25], + [0.75, -0.25, 0.5, 0.75, -0.25], + ] -MIXER_COEFFS = [ - [0.5, -0.5, 0.25, -0.25, -0.25, 0.25, 0.5, -0.5], - [0.5, -0.5, 0.25, -0.25, -0.25, 0.25, 0.5, -0.5], - [0.25, -0.25, -0.25, 0.25, 0.25, -0.25, -0.25, 0.25, 0.25, -0.25, -0.25, 0.25], - [1, 1, 1], - [1, 1, 1], -] + cost_terms = [ + [qml.PauliZ(0), qml.PauliZ(1), qml.PauliZ(2)], + [qml.PauliZ(0), qml.PauliZ(1), qml.PauliZ(2)], + [qml.PauliZ(0), qml.PauliZ(1), qml.PauliZ(2)], + [ + qml.PauliZ(0) @ qml.PauliZ(1), + qml.PauliZ(0), + qml.PauliZ(1), + qml.PauliZ(1) @ qml.PauliZ(2), + qml.PauliZ(2), + ], + [ + qml.PauliZ(0) @ qml.PauliZ(1), + qml.PauliZ(0), + qml.PauliZ(1), + qml.PauliZ(1) @ qml.PauliZ(2), + qml.PauliZ(2), + ], + ] -MIXER_HAMILTONIANS = [qml.Hamiltonian(MIXER_COEFFS[i], MIXER_TERMS[i]) for i in range(5)] + cost_hamiltonians = [qml.Hamiltonian(cost_coeffs[i], cost_terms[i]) for i in range(5)] -MVC = list(zip(GRAPHS, CONSTRAINED, COST_HAMILTONIANS, MIXER_HAMILTONIANS)) + mixer_coeffs = [ + [0.5, -0.5, 0.25, -0.25, -0.25, 0.25, 0.5, -0.5], + [0.5, -0.5, 0.25, -0.25, -0.25, 0.25, 0.5, -0.5], + [0.25, -0.25, -0.25, 0.25, 0.25, -0.25, -0.25, 0.25, 0.25, -0.25, -0.25, 0.25], + [1, 1, 1], + [1, 1, 1], + ] -"""GENERATES THE CASES TO TEST THE MAXCLIQUE PROBLEM""" + mixer_terms = [ + [ + qml.PauliX(0), + qml.PauliX(0) @ qml.PauliZ(1), + qml.PauliX(1), + qml.PauliX(1) @ qml.PauliZ(2), + qml.PauliX(1) @ qml.PauliZ(0), + qml.PauliX(1) @ qml.PauliZ(0) @ qml.PauliZ(2), + qml.PauliX(2), + qml.PauliX(2) @ qml.PauliZ(1), + ], + [ + qml.PauliX(0), + qml.PauliX(0) @ qml.PauliZ(1), + qml.PauliX(1), + qml.PauliX(1) @ qml.PauliZ(2), + qml.PauliX(1) @ qml.PauliZ(0), + qml.PauliX(1) @ qml.PauliZ(0) @ qml.PauliZ(2), + qml.PauliX(2), + qml.PauliX(2) @ qml.PauliZ(1), + ], + [ + qml.PauliX(0), + qml.PauliX(0) @ qml.PauliZ(2), + qml.PauliX(0) @ qml.PauliZ(1), + qml.PauliX(0) @ qml.PauliZ(1) @ qml.PauliZ(2), + qml.PauliX(1), + qml.PauliX(1) @ qml.PauliZ(2), + qml.PauliX(1) @ qml.PauliZ(0), + qml.PauliX(1) @ qml.PauliZ(0) @ qml.PauliZ(2), + qml.PauliX(2), + qml.PauliX(2) @ qml.PauliZ(0), + qml.PauliX(2) @ qml.PauliZ(1), + qml.PauliX(2) @ qml.PauliZ(1) @ qml.PauliZ(0), + ], + [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], + [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], + ] -COST_COEFFS = [ - [1, 1, 1], - [1, 1, 1], - [1, 1, 1], - [0.75, 0.25, 0.25, 1], - [0.75, 0.25, 0.25, 1], -] + mixer_hamiltonians = [qml.Hamiltonian(mixer_coeffs[i], mixer_terms[i]) for i in range(5)] -COST_TERMS = [ - [qml.PauliZ(0), qml.PauliZ(1), qml.PauliZ(2)], - [qml.PauliZ(0), qml.PauliZ(1), qml.PauliZ(2)], - [qml.PauliZ(0), qml.PauliZ(1), qml.PauliZ(2)], - [qml.PauliZ(0) @ qml.PauliZ(2), qml.PauliZ(0), qml.PauliZ(2), qml.PauliZ(1)], - [qml.PauliZ(0) @ qml.PauliZ(2), qml.PauliZ(0), qml.PauliZ(2), qml.PauliZ(1)], -] + return list(zip(GRAPHS, CONSTRAINED, cost_hamiltonians, mixer_hamiltonians)) -COST_HAMILTONIANS = [qml.Hamiltonian(COST_COEFFS[i], COST_TERMS[i]) for i in range(5)] -MIXER_COEFFS = [ - [0.5, 0.5, 1.0, 0.5, 0.5], - [0.5, 0.5, 1.0, 0.5, 0.5], - [1.0, 1.0, 1.0], - [1, 1, 1], - [1, 1, 1], -] +def make_max_clique_test_cases(): + """Generates the test cases for the max clique problem""" -MIXER_TERMS = [ - [ - qml.PauliX(0), - qml.PauliX(0) @ qml.PauliZ(2), - qml.PauliX(1), - qml.PauliX(2), - qml.PauliX(2) @ qml.PauliZ(0), - ], - [ - qml.PauliX(0), - qml.PauliX(0) @ qml.PauliZ(2), - qml.PauliX(1), - qml.PauliX(2), - qml.PauliX(2) @ qml.PauliZ(0), - ], - [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], - [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], - [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], -] + cost_coeffs = [ + [1, 1, 1], + [1, 1, 1], + [1, 1, 1], + [0.75, 0.25, 0.25, 1], + [0.75, 0.25, 0.25, 1], + ] -MIXER_HAMILTONIANS = [qml.Hamiltonian(MIXER_COEFFS[i], MIXER_TERMS[i]) for i in range(5)] + cost_terms = [ + [qml.PauliZ(0), qml.PauliZ(1), qml.PauliZ(2)], + [qml.PauliZ(0), qml.PauliZ(1), qml.PauliZ(2)], + [qml.PauliZ(0), qml.PauliZ(1), qml.PauliZ(2)], + [qml.PauliZ(0) @ qml.PauliZ(2), qml.PauliZ(0), qml.PauliZ(2), qml.PauliZ(1)], + [qml.PauliZ(0) @ qml.PauliZ(2), qml.PauliZ(0), qml.PauliZ(2), qml.PauliZ(1)], + ] -MAXCLIQUE = list(zip(GRAPHS, CONSTRAINED, COST_HAMILTONIANS, MIXER_HAMILTONIANS)) + cost_hamiltonians = [qml.Hamiltonian(cost_coeffs[i], cost_terms[i]) for i in range(5)] -"""GENERATES CASES TO TEST EDGE DRIVER COST HAMILTONIAN""" -GRAPHS = GRAPHS[1:-2] -GRAPHS.append(line_graph) -GRAPHS.append(Graph([("b", 1), (1, 2.3)])) -GRAPHS.append(graph_rx) + mixer_coeffs = [ + [0.5, 0.5, 1.0, 0.5, 0.5], + [0.5, 0.5, 1.0, 0.5, 0.5], + [1.0, 1.0, 1.0], + [1, 1, 1], + [1, 1, 1], + ] -b1_rx = rx.PyGraph() -b1_rx.add_nodes_from(["b", 1, 2.3]) -b1_rx.add_edges_from([(0, 1, ""), (1, 2, "")]) + mixer_terms = [ + [ + qml.PauliX(0), + qml.PauliX(0) @ qml.PauliZ(2), + qml.PauliX(1), + qml.PauliX(2), + qml.PauliX(2) @ qml.PauliZ(0), + ], + [ + qml.PauliX(0), + qml.PauliX(0) @ qml.PauliZ(2), + qml.PauliX(1), + qml.PauliX(2), + qml.PauliX(2) @ qml.PauliZ(0), + ], + [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], + [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], + [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], + ] -GRAPHS.append(b1_rx) + mixer_hamiltonians = [qml.Hamiltonian(mixer_coeffs[i], mixer_terms[i]) for i in range(5)] -REWARDS = [ - ["00"], - ["00", "11"], - ["00", "11", "01", "10"], - ["00", "01", "10"], - ["00", "11", "01", "10"], - ["00", "01", "10"], -] + return list(zip(GRAPHS, CONSTRAINED, cost_hamiltonians, mixer_hamiltonians)) + + +def make_edge_driver_cost_test_cases(): + """Generates the test cases for the edge driver cost Hamiltonian""" + + graphs = GRAPHS[1:-2] + graphs.append(line_graph) + graphs.append(Graph([("b", 1), (1, 2.3)])) + graphs.append(graph_rx) + + b1_rx = rx.PyGraph() + b1_rx.add_nodes_from(["b", 1, 2.3]) + b1_rx.add_edges_from([(0, 1, ""), (1, 2, "")]) + + graphs.append(b1_rx) + + rewards = [ + ["00"], + ["00", "11"], + ["00", "11", "01", "10"], + ["00", "01", "10"], + ["00", "11", "01", "10"], + ["00", "01", "10"], + ] + + hamiltonians = [ + qml.Hamiltonian( + [-0.25, -0.25, -0.25, -0.25, -0.25, -0.25], + [ + qml.PauliZ(0) @ qml.PauliZ(1), + qml.PauliZ(0), + qml.PauliZ(1), + qml.PauliZ(1) @ qml.PauliZ(2), + qml.PauliZ(1), + qml.PauliZ(2), + ], + ), + qml.Hamiltonian( + [-0.5, -0.5, -0.5], + [ + qml.PauliZ(0) @ qml.PauliZ(1), + qml.PauliZ(0) @ qml.PauliZ(2), + qml.PauliZ(1) @ qml.PauliZ(2), + ], + ), + qml.Hamiltonian([1, 1, 1], [qml.Identity(0), qml.Identity(1), qml.Identity(2)]), + qml.Hamiltonian( + [0.25, -0.25, -0.25, 0.25, -0.25, -0.25], + [ + qml.PauliZ("b") @ qml.PauliZ(1), + qml.PauliZ("b"), + qml.PauliZ(1), + qml.PauliZ(1) @ qml.PauliZ(2.3), + qml.PauliZ(1), + qml.PauliZ(2.3), + ], + ), + qml.Hamiltonian([1, 1, 1], [qml.Identity(0), qml.Identity(1), qml.Identity(2)]), + qml.Hamiltonian( + [0.25, -0.25, -0.25, 0.25, -0.25, -0.25], + [ + qml.PauliZ("b") @ qml.PauliZ(1), + qml.PauliZ("b"), + qml.PauliZ(1), + qml.PauliZ(1) @ qml.PauliZ(2.3), + qml.PauliZ(1), + qml.PauliZ(2.3), + ], + ), + ] -HAMILTONIANS = [ - qml.Hamiltonian( - [-0.25, -0.25, -0.25, -0.25, -0.25, -0.25], + return zip(graphs, rewards, hamiltonians) + + +def make_max_weighted_cycle_test_cases(): + """Generates the test cases for the maximum weighted cycle problem""" + + digraph_complete = nx.complete_graph(3).to_directed() + complete_edge_weight_data = { + edge: (i + 1) * 0.5 for i, edge in enumerate(digraph_complete.edges) + } + for _k, _v in complete_edge_weight_data.items(): + digraph_complete[_k[0]][_k[1]]["weight"] = _v + + digraph_complete_rx = rx.generators.directed_mesh_graph(3, [0, 1, 2]) + complete_edge_weight_data = { + edge: (i + 1) * 0.5 for i, edge in enumerate(sorted(digraph_complete_rx.edge_list())) + } + for _k, _v in complete_edge_weight_data.items(): + digraph_complete_rx.update_edge(_k[0], _k[1], {"weight": _v}) + + digraphs = [digraph_complete] * 2 + + mwc_constrained = [True, False] + + cost_coeffs = [ [ - qml.PauliZ(0) @ qml.PauliZ(1), - qml.PauliZ(0), - qml.PauliZ(1), - qml.PauliZ(1) @ qml.PauliZ(2), - qml.PauliZ(1), - qml.PauliZ(2), + -0.6931471805599453, + 0.0, + 0.4054651081081644, + 0.6931471805599453, + 0.9162907318741551, + 1.0986122886681098, ], - ), - qml.Hamiltonian( - [-0.5, -0.5, -0.5], [ - qml.PauliZ(0) @ qml.PauliZ(1), - qml.PauliZ(0) @ qml.PauliZ(2), - qml.PauliZ(1) @ qml.PauliZ(2), + -6.693147180559945, + -6.0, + -5.594534891891835, + -5.306852819440055, + -5.083709268125845, + -4.90138771133189, + 54, + 12, + -12, + -6, + -6, + -12, + 6, + 12, + -6, + -6, + -12, + 6, + 12, + -6, + -6, + 6, ], - ), - qml.Hamiltonian([1, 1, 1], [qml.Identity(0), qml.Identity(1), qml.Identity(2)]), - qml.Hamiltonian( - [0.25, -0.25, -0.25, 0.25, -0.25, -0.25], + ] + + cost_terms = [ [ - qml.PauliZ("b") @ qml.PauliZ(1), - qml.PauliZ("b"), - qml.PauliZ(1), - qml.PauliZ(1) @ qml.PauliZ(2.3), - qml.PauliZ(1), - qml.PauliZ(2.3), + qml.PauliZ(wires=[0]), + qml.PauliZ(wires=[1]), + qml.PauliZ(wires=[2]), + qml.PauliZ(wires=[3]), + qml.PauliZ(wires=[4]), + qml.PauliZ(wires=[5]), ], - ), - qml.Hamiltonian([1, 1, 1], [qml.Identity(0), qml.Identity(1), qml.Identity(2)]), - qml.Hamiltonian( - [0.25, -0.25, -0.25, 0.25, -0.25, -0.25], [ - qml.PauliZ("b") @ qml.PauliZ(1), - qml.PauliZ("b"), - qml.PauliZ(1), - qml.PauliZ(1) @ qml.PauliZ(2.3), - qml.PauliZ(1), - qml.PauliZ(2.3), + qml.PauliZ(wires=[0]), + qml.PauliZ(wires=[1]), + qml.PauliZ(wires=[2]), + qml.PauliZ(wires=[3]), + qml.PauliZ(wires=[4]), + qml.PauliZ(wires=[5]), + qml.Identity(wires=[0]), + qml.PauliZ(wires=[0]) @ qml.PauliZ(wires=[1]), + qml.PauliZ(wires=[0]) @ qml.PauliZ(wires=[2]), + qml.PauliZ(wires=[0]) @ qml.PauliZ(wires=[4]), + qml.PauliZ(wires=[1]) @ qml.PauliZ(wires=[2]), + qml.PauliZ(wires=[1]) @ qml.PauliZ(wires=[4]), + qml.PauliZ(wires=[2]) @ qml.PauliZ(wires=[4]), + qml.PauliZ(wires=[2]) @ qml.PauliZ(wires=[3]), + qml.PauliZ(wires=[2]) @ qml.PauliZ(wires=[5]), + qml.PauliZ(wires=[0]) @ qml.PauliZ(wires=[3]), + qml.PauliZ(wires=[3]) @ qml.PauliZ(wires=[5]), + qml.PauliZ(wires=[0]) @ qml.PauliZ(wires=[5]), + qml.PauliZ(wires=[4]) @ qml.PauliZ(wires=[5]), + qml.PauliZ(wires=[3]) @ qml.PauliZ(wires=[4]), + qml.PauliZ(wires=[1]) @ qml.PauliZ(wires=[5]), + qml.PauliZ(wires=[1]) @ qml.PauliZ(wires=[3]), ], - ), -] + ] -EDGE_DRIVER = zip(GRAPHS, REWARDS, HAMILTONIANS) - -"""GENERATES THE CASES TO TEST THE MAXIMUM WEIGHTED CYCLE PROBLEM""" -digraph_complete = nx.complete_graph(3).to_directed() -complete_edge_weight_data = {edge: (i + 1) * 0.5 for i, edge in enumerate(digraph_complete.edges)} -for _k, _v in complete_edge_weight_data.items(): - digraph_complete[_k[0]][_k[1]]["weight"] = _v - -digraph_complete_rx = rx.generators.directed_mesh_graph(3, [0, 1, 2]) -complete_edge_weight_data = { - edge: (i + 1) * 0.5 for i, edge in enumerate(sorted(digraph_complete_rx.edge_list())) -} -for _k, _v in complete_edge_weight_data.items(): - digraph_complete_rx.update_edge(_k[0], _k[1], {"weight": _v}) - -DIGRAPHS = [digraph_complete] * 2 - -MWC_CONSTRAINED = [True, False] - -COST_COEFFS = [ - [ - -0.6931471805599453, - 0.0, - 0.4054651081081644, - 0.6931471805599453, - 0.9162907318741551, - 1.0986122886681098, - ], - [ - -6.693147180559945, - -6.0, - -5.594534891891835, - -5.306852819440055, - -5.083709268125845, - -4.90138771133189, - 54, - 12, - -12, - -6, - -6, - -12, - 6, - 12, - -6, - -6, - -12, - 6, - 12, - -6, - -6, - 6, - ], -] + cost_hamiltonians = [qml.Hamiltonian(cost_coeffs[i], cost_terms[i]) for i in range(2)] -COST_TERMS = [ - [ - qml.PauliZ(wires=[0]), - qml.PauliZ(wires=[1]), - qml.PauliZ(wires=[2]), - qml.PauliZ(wires=[3]), - qml.PauliZ(wires=[4]), - qml.PauliZ(wires=[5]), - ], - [ - qml.PauliZ(wires=[0]), - qml.PauliZ(wires=[1]), - qml.PauliZ(wires=[2]), - qml.PauliZ(wires=[3]), - qml.PauliZ(wires=[4]), - qml.PauliZ(wires=[5]), - qml.Identity(wires=[0]), - qml.PauliZ(wires=[0]) @ qml.PauliZ(wires=[1]), - qml.PauliZ(wires=[0]) @ qml.PauliZ(wires=[2]), - qml.PauliZ(wires=[0]) @ qml.PauliZ(wires=[4]), - qml.PauliZ(wires=[1]) @ qml.PauliZ(wires=[2]), - qml.PauliZ(wires=[1]) @ qml.PauliZ(wires=[4]), - qml.PauliZ(wires=[2]) @ qml.PauliZ(wires=[4]), - qml.PauliZ(wires=[2]) @ qml.PauliZ(wires=[3]), - qml.PauliZ(wires=[2]) @ qml.PauliZ(wires=[5]), - qml.PauliZ(wires=[0]) @ qml.PauliZ(wires=[3]), - qml.PauliZ(wires=[3]) @ qml.PauliZ(wires=[5]), - qml.PauliZ(wires=[0]) @ qml.PauliZ(wires=[5]), - qml.PauliZ(wires=[4]) @ qml.PauliZ(wires=[5]), - qml.PauliZ(wires=[3]) @ qml.PauliZ(wires=[4]), - qml.PauliZ(wires=[1]) @ qml.PauliZ(wires=[5]), - qml.PauliZ(wires=[1]) @ qml.PauliZ(wires=[3]), - ], -] - -COST_HAMILTONIANS = [qml.Hamiltonian(COST_COEFFS[i], COST_TERMS[i]) for i in range(2)] - -MIXER_COEFFS = [ - [ - 0.25, - 0.25, - 0.25, - -0.25, - 0.25, - 0.25, - 0.25, - -0.25, - 0.25, - 0.25, - 0.25, - -0.25, - 0.25, - 0.25, - 0.25, - -0.25, - 0.25, - 0.25, - 0.25, - -0.25, - 0.25, - 0.25, - 0.25, - -0.25, - ], - [1] * 6, -] + mixer_coeffs = [ + [ + 0.25, + 0.25, + 0.25, + -0.25, + 0.25, + 0.25, + 0.25, + -0.25, + 0.25, + 0.25, + 0.25, + -0.25, + 0.25, + 0.25, + 0.25, + -0.25, + 0.25, + 0.25, + 0.25, + -0.25, + 0.25, + 0.25, + 0.25, + -0.25, + ], + [1] * 6, + ] -MIXER_TERMS = [ - [ - qml.PauliX(wires=[0]) @ qml.PauliX(wires=[1]) @ qml.PauliX(wires=[5]), - qml.PauliY(wires=[0]) @ qml.PauliY(wires=[1]) @ qml.PauliX(wires=[5]), - qml.PauliY(wires=[0]) @ qml.PauliX(wires=[1]) @ qml.PauliY(wires=[5]), - qml.PauliX(wires=[0]) @ qml.PauliY(wires=[1]) @ qml.PauliY(wires=[5]), - qml.PauliX(wires=[1]) @ qml.PauliX(wires=[0]) @ qml.PauliX(wires=[3]), - qml.PauliY(wires=[1]) @ qml.PauliY(wires=[0]) @ qml.PauliX(wires=[3]), - qml.PauliY(wires=[1]) @ qml.PauliX(wires=[0]) @ qml.PauliY(wires=[3]), - qml.PauliX(wires=[1]) @ qml.PauliY(wires=[0]) @ qml.PauliY(wires=[3]), - qml.PauliX(wires=[2]) @ qml.PauliX(wires=[3]) @ qml.PauliX(wires=[4]), - qml.PauliY(wires=[2]) @ qml.PauliY(wires=[3]) @ qml.PauliX(wires=[4]), - qml.PauliY(wires=[2]) @ qml.PauliX(wires=[3]) @ qml.PauliY(wires=[4]), - qml.PauliX(wires=[2]) @ qml.PauliY(wires=[3]) @ qml.PauliY(wires=[4]), - qml.PauliX(wires=[3]) @ qml.PauliX(wires=[2]) @ qml.PauliX(wires=[1]), - qml.PauliY(wires=[3]) @ qml.PauliY(wires=[2]) @ qml.PauliX(wires=[1]), - qml.PauliY(wires=[3]) @ qml.PauliX(wires=[2]) @ qml.PauliY(wires=[1]), - qml.PauliX(wires=[3]) @ qml.PauliY(wires=[2]) @ qml.PauliY(wires=[1]), - qml.PauliX(wires=[4]) @ qml.PauliX(wires=[5]) @ qml.PauliX(wires=[2]), - qml.PauliY(wires=[4]) @ qml.PauliY(wires=[5]) @ qml.PauliX(wires=[2]), - qml.PauliY(wires=[4]) @ qml.PauliX(wires=[5]) @ qml.PauliY(wires=[2]), - qml.PauliX(wires=[4]) @ qml.PauliY(wires=[5]) @ qml.PauliY(wires=[2]), - qml.PauliX(wires=[5]) @ qml.PauliX(wires=[4]) @ qml.PauliX(wires=[0]), - qml.PauliY(wires=[5]) @ qml.PauliY(wires=[4]) @ qml.PauliX(wires=[0]), - qml.PauliY(wires=[5]) @ qml.PauliX(wires=[4]) @ qml.PauliY(wires=[0]), - qml.PauliX(wires=[5]) @ qml.PauliY(wires=[4]) @ qml.PauliY(wires=[0]), - ], - [qml.PauliX(wires=i) for i in range(6)], -] + mixer_terms = [ + [ + qml.PauliX(wires=[0]) @ qml.PauliX(wires=[1]) @ qml.PauliX(wires=[5]), + qml.PauliY(wires=[0]) @ qml.PauliY(wires=[1]) @ qml.PauliX(wires=[5]), + qml.PauliY(wires=[0]) @ qml.PauliX(wires=[1]) @ qml.PauliY(wires=[5]), + qml.PauliX(wires=[0]) @ qml.PauliY(wires=[1]) @ qml.PauliY(wires=[5]), + qml.PauliX(wires=[1]) @ qml.PauliX(wires=[0]) @ qml.PauliX(wires=[3]), + qml.PauliY(wires=[1]) @ qml.PauliY(wires=[0]) @ qml.PauliX(wires=[3]), + qml.PauliY(wires=[1]) @ qml.PauliX(wires=[0]) @ qml.PauliY(wires=[3]), + qml.PauliX(wires=[1]) @ qml.PauliY(wires=[0]) @ qml.PauliY(wires=[3]), + qml.PauliX(wires=[2]) @ qml.PauliX(wires=[3]) @ qml.PauliX(wires=[4]), + qml.PauliY(wires=[2]) @ qml.PauliY(wires=[3]) @ qml.PauliX(wires=[4]), + qml.PauliY(wires=[2]) @ qml.PauliX(wires=[3]) @ qml.PauliY(wires=[4]), + qml.PauliX(wires=[2]) @ qml.PauliY(wires=[3]) @ qml.PauliY(wires=[4]), + qml.PauliX(wires=[3]) @ qml.PauliX(wires=[2]) @ qml.PauliX(wires=[1]), + qml.PauliY(wires=[3]) @ qml.PauliY(wires=[2]) @ qml.PauliX(wires=[1]), + qml.PauliY(wires=[3]) @ qml.PauliX(wires=[2]) @ qml.PauliY(wires=[1]), + qml.PauliX(wires=[3]) @ qml.PauliY(wires=[2]) @ qml.PauliY(wires=[1]), + qml.PauliX(wires=[4]) @ qml.PauliX(wires=[5]) @ qml.PauliX(wires=[2]), + qml.PauliY(wires=[4]) @ qml.PauliY(wires=[5]) @ qml.PauliX(wires=[2]), + qml.PauliY(wires=[4]) @ qml.PauliX(wires=[5]) @ qml.PauliY(wires=[2]), + qml.PauliX(wires=[4]) @ qml.PauliY(wires=[5]) @ qml.PauliY(wires=[2]), + qml.PauliX(wires=[5]) @ qml.PauliX(wires=[4]) @ qml.PauliX(wires=[0]), + qml.PauliY(wires=[5]) @ qml.PauliY(wires=[4]) @ qml.PauliX(wires=[0]), + qml.PauliY(wires=[5]) @ qml.PauliX(wires=[4]) @ qml.PauliY(wires=[0]), + qml.PauliX(wires=[5]) @ qml.PauliY(wires=[4]) @ qml.PauliY(wires=[0]), + ], + [qml.PauliX(wires=i) for i in range(6)], + ] -MIXER_HAMILTONIANS = [qml.Hamiltonian(MIXER_COEFFS[i], MIXER_TERMS[i]) for i in range(2)] + mixer_hamiltonians = [qml.Hamiltonian(mixer_coeffs[i], mixer_terms[i]) for i in range(2)] -MAPPINGS = [qaoa.cycle.wires_to_edges(digraph_complete)] * 2 + mappings = [qaoa.cycle.wires_to_edges(digraph_complete)] * 2 -MWC = list(zip(DIGRAPHS, MWC_CONSTRAINED, COST_HAMILTONIANS, MIXER_HAMILTONIANS, MAPPINGS)) + return list(zip(digraphs, mwc_constrained, cost_hamiltonians, mixer_hamiltonians, mappings)) class TestCostHamiltonians: @@ -879,13 +931,13 @@ def test_bit_driver_error(self): with pytest.raises(ValueError, match=r"'b' must be either 0 or 1"): qaoa.bit_driver(range(3), 2) + @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_bit_driver_output(self): """Tests that the bit driver Hamiltonian has the correct output""" H = qaoa.bit_driver(range(3), 1) hamiltonian = qml.Hamiltonian([1, 1, 1], [qml.PauliZ(0), qml.PauliZ(1), qml.PauliZ(2)]) - - assert decompose_hamiltonian(H) == decompose_hamiltonian(hamiltonian) + assert hamiltonian.compare(H) def test_edge_driver_errors(self): """Tests that the edge driver Hamiltonian throws the correct errors""" @@ -904,12 +956,15 @@ def test_edge_driver_errors(self): with pytest.raises(ValueError, match=r"Input graph must be a nx.Graph or rx.PyGraph"): qaoa.edge_driver([(0, 1), (1, 2)], ["00", "11"]) - @pytest.mark.parametrize(("graph", "reward", "hamiltonian"), EDGE_DRIVER) + @pytest.mark.usefixtures("use_legacy_and_new_opmath") + @pytest.mark.parametrize(("graph", "reward", "hamiltonian"), make_edge_driver_cost_test_cases()) def test_edge_driver_output(self, graph, reward, hamiltonian): """Tests that the edge driver Hamiltonian throws the correct errors""" + if not qml.operation.active_new_opmath(): + hamiltonian = qml.operation.convert_to_legacy_H(hamiltonian) H = qaoa.edge_driver(graph, reward) - assert decompose_hamiltonian(H) == decompose_hamiltonian(hamiltonian) + assert hamiltonian.compare(H) def test_max_weight_cycle_errors(self): """Tests that the max weight cycle Hamiltonian throws the correct errors""" @@ -933,19 +988,26 @@ def test_cost_graph_error(self): with pytest.raises(ValueError, match=r"Input graph must be a nx\.Graph or rx\.PyGraph"): qaoa.max_clique(graph) - @pytest.mark.parametrize(("graph", "cost_hamiltonian", "mixer_hamiltonian"), MAXCUT) + @pytest.mark.parametrize( + ("graph", "cost_hamiltonian", "mixer_hamiltonian"), make_max_cut_test_cases() + ) + @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_maxcut_output(self, graph, cost_hamiltonian, mixer_hamiltonian): """Tests that the output of the MaxCut method is correct""" + if not qml.operation.active_new_opmath(): + cost_hamiltonian = qml.operation.convert_to_legacy_H(cost_hamiltonian) + mixer_hamiltonian = qml.operation.convert_to_legacy_H(mixer_hamiltonian) cost_h, mixer_h = qaoa.maxcut(graph) + assert cost_h.compare(cost_hamiltonian) + assert mixer_h.compare(mixer_hamiltonian) - assert decompose_hamiltonian(cost_hamiltonian) == decompose_hamiltonian(cost_h) - assert decompose_hamiltonian(mixer_hamiltonian) == decompose_hamiltonian(mixer_h) - + @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_maxcut_grouping(self): """Tests that the grouping information is set and correct""" - graph = MAXCUT[0][0] + maxcut = make_max_cut_test_cases() + graph = maxcut[0][0] cost_h, _ = qaoa.maxcut(graph) # check that all observables commute @@ -954,19 +1016,27 @@ def test_maxcut_grouping(self): assert cost_h.grouping_indices is not None assert cost_h.grouping_indices == (tuple(range(len(cost_h.ops))),) - @pytest.mark.parametrize(("graph", "constrained", "cost_hamiltonian", "mixer_hamiltonian"), MIS) + @pytest.mark.parametrize( + ("graph", "constrained", "cost_hamiltonian", "mixer_hamiltonian"), + make_max_independent_test_cases(), + ) + @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_mis_output(self, graph, constrained, cost_hamiltonian, mixer_hamiltonian): """Tests that the output of the Max Indepenent Set method is correct""" + if not qml.operation.active_new_opmath(): + cost_hamiltonian = qml.operation.convert_to_legacy_H(cost_hamiltonian) + mixer_hamiltonian = qml.operation.convert_to_legacy_H(mixer_hamiltonian) cost_h, mixer_h = qaoa.max_independent_set(graph, constrained=constrained) + assert cost_h.compare(cost_hamiltonian) + assert mixer_h.compare(mixer_hamiltonian) - assert decompose_hamiltonian(cost_hamiltonian) == decompose_hamiltonian(cost_h) - assert decompose_hamiltonian(mixer_hamiltonian) == decompose_hamiltonian(mixer_h) - + @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_mis_grouping(self): """Tests that the grouping information is set and correct""" - graph = MIS[0][0] + mis = make_max_independent_test_cases() + graph = mis[0][0] cost_h, _ = qaoa.max_independent_set(graph) # check that all observables commute @@ -975,19 +1045,27 @@ def test_mis_grouping(self): assert cost_h.grouping_indices is not None assert cost_h.grouping_indices == (tuple(range(len(cost_h.ops))),) - @pytest.mark.parametrize(("graph", "constrained", "cost_hamiltonian", "mixer_hamiltonian"), MVC) + @pytest.mark.parametrize( + ("graph", "constrained", "cost_hamiltonian", "mixer_hamiltonian"), + make_min_vertex_cover_test_cases(), + ) + @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_mvc_output(self, graph, constrained, cost_hamiltonian, mixer_hamiltonian): """Tests that the output of the Min Vertex Cover method is correct""" + if not qml.operation.active_new_opmath(): + cost_hamiltonian = qml.operation.convert_to_legacy_H(cost_hamiltonian) + mixer_hamiltonian = qml.operation.convert_to_legacy_H(mixer_hamiltonian) cost_h, mixer_h = qaoa.min_vertex_cover(graph, constrained=constrained) + assert cost_h.compare(cost_hamiltonian) + assert mixer_h.compare(mixer_hamiltonian) - assert decompose_hamiltonian(cost_hamiltonian) == decompose_hamiltonian(cost_h) - assert decompose_hamiltonian(mixer_hamiltonian) == decompose_hamiltonian(mixer_h) - + @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_mvc_grouping(self): """Tests that the grouping information is set and correct""" - graph = MVC[0][0] + mvc = make_min_vertex_cover_test_cases() + graph = mvc[0][0] cost_h, _ = qaoa.min_vertex_cover(graph) # check that all observables commute @@ -997,20 +1075,26 @@ def test_mvc_grouping(self): assert cost_h.grouping_indices == (tuple(range(len(cost_h.ops))),) @pytest.mark.parametrize( - ("graph", "constrained", "cost_hamiltonian", "mixer_hamiltonian"), MAXCLIQUE + ("graph", "constrained", "cost_hamiltonian", "mixer_hamiltonian"), + make_max_clique_test_cases(), ) + @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_max_clique_output(self, graph, constrained, cost_hamiltonian, mixer_hamiltonian): """Tests that the output of the Maximum Clique method is correct""" + if not qml.operation.active_new_opmath(): + cost_hamiltonian = qml.operation.convert_to_legacy_H(cost_hamiltonian) + mixer_hamiltonian = qml.operation.convert_to_legacy_H(mixer_hamiltonian) cost_h, mixer_h = qaoa.max_clique(graph, constrained=constrained) + assert cost_h.compare(cost_hamiltonian) + assert mixer_h.compare(mixer_hamiltonian) - assert decompose_hamiltonian(cost_hamiltonian) == decompose_hamiltonian(cost_h) - assert decompose_hamiltonian(mixer_hamiltonian) == decompose_hamiltonian(mixer_h) - + @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_max_clique_grouping(self): """Tests that the grouping information is set and correct""" - graph = MAXCLIQUE[0][0] + maxclique = make_max_clique_test_cases() + graph = maxclique[0][0] cost_h, _ = qaoa.max_clique(graph) # check that all observables commute @@ -1021,31 +1105,29 @@ def test_max_clique_grouping(self): # pylint: disable=too-many-arguments @pytest.mark.parametrize( - ("graph", "constrained", "cost_hamiltonian", "mixer_hamiltonian", "mapping"), MWC + ("graph", "constrained", "cost_hamiltonian", "mixer_hamiltonian", "mapping"), + make_max_weighted_cycle_test_cases(), ) + @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_max_weight_cycle_output( self, graph, constrained, cost_hamiltonian, mixer_hamiltonian, mapping ): """Tests that the output of the maximum weighted cycle method is correct""" + if not qml.operation.active_new_opmath(): + cost_hamiltonian = qml.operation.convert_to_legacy_H(cost_hamiltonian) + mixer_hamiltonian = qml.operation.convert_to_legacy_H(mixer_hamiltonian) cost_h, mixer_h, m = qaoa.max_weight_cycle(graph, constrained=constrained) - + assert cost_h.compare(cost_hamiltonian) + assert mixer_h.compare(mixer_hamiltonian) assert mapping == m - c1, t1, w1 = decompose_hamiltonian(cost_hamiltonian) - c2, t2, w2 = decompose_hamiltonian(cost_h) - - # There may be a very small numeric difference in the coeffs - assert np.allclose(c1, c2) - assert t1 == t2 - assert w1 == w2 - - assert decompose_hamiltonian(mixer_hamiltonian) == decompose_hamiltonian(mixer_h) - + @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_max_weight_cycle_grouping(self): """Tests that the grouping information is set and correct""" - graph = MWC[0][0] + mwc = make_max_weighted_cycle_test_cases() + graph = mwc[0][0] cost_h, _, _ = qaoa.max_weight_cycle(graph) # check that all observables commute @@ -1060,6 +1142,7 @@ class TestUtils: """Tests that the utility functions are working properly""" # pylint: disable=protected-access + @pytest.mark.usefixtures("use_legacy_opmath") @pytest.mark.parametrize( ("hamiltonian", "value"), ( @@ -1070,9 +1153,47 @@ class TestUtils: ), ) def test_diagonal_terms(self, hamiltonian, value): + hamiltonian = qml.operation.convert_to_legacy_H(hamiltonian) assert qaoa.layers._diagonal_terms(hamiltonian) == value +def make_mixer_layer_test_cases(): + return [ + [ + qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.PauliX(1)]), + [qml.PauliRot(2, "X", wires=[0]), qml.PauliRot(2, "X", wires=[1])], + ], + [ + qaoa.xy_mixer(Graph([(0, 1), (1, 2), (2, 0)])), + [ + qml.PauliRot(1.0, "XX", wires=[1, 0]), + qml.PauliRot(1.0, "YY", wires=[1, 0]), + qml.PauliRot(1.0, "XX", wires=[2, 0]), + qml.PauliRot(1.0, "YY", wires=[2, 0]), + qml.PauliRot(1.0, "XX", wires=[2, 1]), + qml.PauliRot(1.0, "YY", wires=[2, 1]), + ], + ], + ] + + +def make_cost_layer_test_cases(): + return [ + [ + qml.Hamiltonian([1, 1], [qml.PauliZ(0), qml.PauliZ(1)]), + [qml.PauliRot(2, "Z", wires=[0]), qml.PauliRot(2, "Z", wires=[1])], + ], + [ + qaoa.maxcut(Graph([(0, 1), (1, 2), (2, 0)]))[0], + [ + qml.PauliRot(1.0, "ZZ", wires=[1, 0]), + qml.PauliRot(1.0, "ZZ", wires=[2, 0]), + qml.PauliRot(1.0, "ZZ", wires=[2, 1]), + ], + ], + ] + + class TestLayers: """Tests that the cost and mixer layers are being constructed properly""" @@ -1100,31 +1221,13 @@ def test_cost_layer_errors(self): ): qaoa.cost_layer(0.1, hamiltonian) - @pytest.mark.parametrize( - ("mixer", "gates"), - [ - [ - qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.PauliX(1)]), - [qml.PauliRot(2, "X", wires=[0]), qml.PauliRot(2, "X", wires=[1])], - ], - [ - qaoa.xy_mixer(Graph([(0, 1), (1, 2), (2, 0)])), - [ - qml.PauliRot(1, "XX", wires=[1, 0]), - qml.PauliRot(1, "YY", wires=[1, 0]), - qml.PauliRot(1, "XX", wires=[2, 0]), - qml.PauliRot(1, "YY", wires=[2, 0]), - qml.PauliRot(1, "XX", wires=[2, 1]), - qml.PauliRot(1, "YY", wires=[2, 1]), - ], - ], - ], - ) + mixer_layer_test_cases = make_mixer_layer_test_cases() + + @pytest.mark.parametrize(("mixer", "gates"), mixer_layer_test_cases) def test_mixer_layer_output(self, mixer, gates): """Tests that the gates of the mixer layer are correct""" alpha = 1 - with qml.tape.OperationRecorder() as rec: qaoa.mixer_layer(alpha, mixer) @@ -1133,25 +1236,32 @@ def test_mixer_layer_output(self, mixer, gates): for i, j in zip(rec.operations, gates): prep = [i.name, i.parameters, i.wires] target = [j.name, j.parameters, j.wires] + assert prep == target + + with qml.operation.disable_new_opmath_cm(): + mixer_layer_test_cases_legacy = make_mixer_layer_test_cases() + + @pytest.mark.usefixtures("use_legacy_opmath") + @pytest.mark.parametrize(("mixer", "gates"), mixer_layer_test_cases_legacy) + def test_mixer_layer_output_legacy_opmath(self, mixer, gates): + """Tests that the gates of the mixer layer are correct""" + + alpha = 1 + with qml.tape.OperationRecorder() as rec: + qaoa.mixer_layer(alpha, mixer) + + rec = rec.expand() + for i, j in zip(rec.operations, gates): + prep = [i.name, i.parameters, i.wires] + target = [j.name, j.parameters, j.wires] assert prep == target + cost_layer_test_cases = make_cost_layer_test_cases() + @pytest.mark.parametrize( ("cost", "gates"), - [ - [ - qml.Hamiltonian([1, 1], [qml.PauliZ(0), qml.PauliZ(1)]), - [qml.PauliRot(2, "Z", wires=[0]), qml.PauliRot(2, "Z", wires=[1])], - ], - [ - qaoa.maxcut(Graph([(0, 1), (1, 2), (2, 0)]))[0], - [ - qml.PauliRot(1, "ZZ", wires=[1, 0]), - qml.PauliRot(1, "ZZ", wires=[2, 0]), - qml.PauliRot(1, "ZZ", wires=[2, 1]), - ], - ], - ], + cost_layer_test_cases, ) def test_cost_layer_output(self, cost, gates): """Tests that the gates of the cost layer is correct""" @@ -1166,13 +1276,34 @@ def test_cost_layer_output(self, cost, gates): for i, j in zip(rec.operations, gates): prep = [i.name, i.parameters, i.wires] target = [j.name, j.parameters, j.wires] + assert prep == target + + with qml.operation.disable_new_opmath_cm(): + cost_layer_test_cases_legacy = make_cost_layer_test_cases() + + @pytest.mark.usefixtures("use_legacy_opmath") + @pytest.mark.parametrize(("cost", "gates"), cost_layer_test_cases_legacy) + def test_cost_layer_output_legacy_opmath(self, cost, gates): + """Tests that the gates of the cost layer is correct""" + + gamma = 1 - assert prep == target + with qml.tape.OperationRecorder() as rec: + cost = qml.operation.convert_to_legacy_H(cost) + qaoa.cost_layer(gamma, cost) + + rec = rec.expand() + + for i, j in zip(rec.operations, gates): + prep = [i.name, i.parameters, i.wires] + target = [j.name, j.parameters, j.wires] + assert prep == target class TestIntegration: """Test integration of the QAOA module with PennyLane""" + @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_module_example(self, tol): """Test the example in the QAOA module docstring""" @@ -1209,6 +1340,7 @@ def cost_function(params): assert np.allclose(res, expected, atol=tol, rtol=0) + @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_module_example_rx(self, tol): """Test the example in the QAOA module docstring""" @@ -1257,7 +1389,6 @@ class TestCycles: def test_edges_to_wires(self, g): """Test that edges_to_wires returns the correct mapping""" r = edges_to_wires(g) - assert r == {(0, 1): 0, (0, 2): 1, (0, 3): 2, (1, 2): 3, (1, 3): 4, (2, 3): 5, (3, 4): 6} def test_edges_to_wires_error(self): @@ -1327,6 +1458,7 @@ def test_wires_to_edges_rx(self): "g", [nx.complete_graph(4).to_directed(), rx.generators.directed_mesh_graph(4, [0, 1, 2, 3])], ) + @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_partial_cycle_mixer_complete(self, g): """Test if the _partial_cycle_mixer function returns the expected Hamiltonian for a fixed example""" @@ -1374,6 +1506,31 @@ def test_partial_cycle_mixer_incomplete(self, g): assert all(op.wires == op_e.wires for op, op_e in zip(h.ops, ops_expected)) assert all(op.name == op_e.name for op, op_e in zip(h.ops, ops_expected)) + @pytest.mark.usefixtures("use_legacy_opmath") + @pytest.mark.parametrize( + "g", + [nx.complete_graph(4).to_directed(), rx.generators.directed_mesh_graph(4, [0, 1, 2, 3])], + ) + def test_partial_cycle_mixer_incomplete_legacy_opmath(self, g): + """Test if the _partial_cycle_mixer function returns the expected Hamiltonian for a fixed + example""" + g.remove_edge(2, 1) # remove an egde to make graph incomplete + edge = (0, 1) + + h = _partial_cycle_mixer(g, edge) + + ops_expected = [ + qml.PauliX(0) @ qml.PauliX(2) @ qml.PauliX(9), + qml.PauliY(0) @ qml.PauliY(2) @ qml.PauliX(9), + qml.PauliY(0) @ qml.PauliX(2) @ qml.PauliY(9), + qml.PauliX(0) @ qml.PauliY(2) @ qml.PauliY(9), + ] + coeffs_expected = [0.25, 0.25, 0.25, -0.25] + + assert h.coeffs == coeffs_expected + assert all(op.wires == op_e.wires for op, op_e in zip(h.ops, ops_expected)) + assert all(op.name == op_e.name for op, op_e in zip(h.ops, ops_expected)) + @pytest.mark.parametrize("g", [nx.complete_graph(4), rx.generators.mesh_graph(4, [0, 1, 2, 3])]) def test_partial_cycle_mixer_error(self, g): """Test if the _partial_cycle_mixer raises ValueError""" @@ -1388,6 +1545,7 @@ def test_partial_cycle_mixer_error(self, g): "g", [nx.complete_graph(3).to_directed(), rx.generators.directed_mesh_graph(3, [0, 1, 2])], ) + @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_cycle_mixer(self, g): """Test if the cycle_mixer Hamiltonian maps valid cycles to valid cycles""" @@ -1462,6 +1620,7 @@ def test_cycle_mixer_error(self, g): cycle_mixer(g) @pytest.mark.parametrize("g", [nx.lollipop_graph(3, 1), lollipop_graph_rx(3, 1)]) + @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_matrix(self, g): """Test that the matrix function works as expected on a fixed example""" h = qml.qaoa.bit_flip_mixer(g, 0) @@ -1490,6 +1649,7 @@ def test_matrix(self, g): assert np.allclose(mat.toarray(), mat_expected) + @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_matrix_rx(self): """Test that the matrix function works as expected on a fixed example""" g = rx.generators.star_graph(4, [0, 1, 2, 3]) @@ -1570,6 +1730,7 @@ def test_wires_to_edges_directed(self, g): @pytest.mark.parametrize( "g", [nx.complete_graph(3).to_directed(), rx.generators.directed_mesh_graph(3, [0, 1, 2])] ) + @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_loss_hamiltonian_complete(self, g): """Test if the loss_hamiltonian function returns the expected result on a manually-calculated example of a 3-node complete digraph""" @@ -1608,6 +1769,7 @@ def test_loss_hamiltonian_error(self): @pytest.mark.parametrize( "g", [nx.lollipop_graph(4, 1).to_directed(), lollipop_graph_rx(4, 1, to_directed=True)] ) + @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_loss_hamiltonian_incomplete(self, g): """Test if the loss_hamiltonian function returns the expected result on a manually-calculated example of a 4-node incomplete digraph""" @@ -1664,6 +1826,17 @@ def test_loss_hamiltonian_incomplete(self, g): def test_self_loop_raises_error(self, g): """Test graphs with self loop raises ValueError""" + digraph_complete = nx.complete_graph(3).to_directed() + complete_edge_weight_data = { + edge: (i + 1) * 0.5 for i, edge in enumerate(digraph_complete.edges) + } + for _k, _v in complete_edge_weight_data.items(): + digraph_complete[_k[0]][_k[1]]["weight"] = _v + digraph_complete_rx = rx.generators.directed_mesh_graph(3, [0, 1, 2]) + complete_edge_weight_data = { + edge: (i + 1) * 0.5 for i, edge in enumerate(sorted(digraph_complete_rx.edge_list())) + } + if isinstance(g, rx.PyDiGraph): edge_weight_data = {edge: (i + 1) * 0.5 for i, edge in enumerate(g.edges())} for k, v in complete_edge_weight_data.items(): @@ -1691,6 +1864,7 @@ def test_missing_edge_weight_data_without_weights(self): with pytest.raises(TypeError, match="does not contain weight data"): loss_hamiltonian(g) + @pytest.mark.usefixtures("use_legacy_and_new_opmath") def test_square_hamiltonian_terms(self): """Test if the _square_hamiltonian_terms function returns the expected result on a fixed example""" @@ -1749,20 +1923,35 @@ def test_inner_out_flow_constraint_hamiltonian(self, g): """Test if the _inner_out_flow_constraint_hamiltonian function returns the expected result on a manually-calculated example of a 3-node complete digraph relative to the 0 node""" h = _inner_out_flow_constraint_hamiltonian(g, 0) - expected_ops = [ qml.Identity(0), - qml.PauliZ(0) @ qml.PauliZ(1), + qml.PauliZ(1) @ qml.PauliZ(0), qml.PauliZ(0), qml.PauliZ(1), ] + expected_coeffs = [2, 2, -2, -2] + + expected_hamiltonian = qml.Hamiltonian(expected_coeffs, expected_ops) + assert h.compare(expected_hamiltonian) + @pytest.mark.usefixtures("use_legacy_opmath") + @pytest.mark.parametrize( + "g", [nx.complete_graph(3).to_directed(), rx.generators.directed_mesh_graph(3, [0, 1, 2])] + ) + def test_inner_out_flow_constraint_hamiltonian_legacy_opmath(self, g): + """Test if the _inner_out_flow_constraint_hamiltonian function returns the expected result + on a manually-calculated example of a 3-node complete digraph relative to the 0 node""" + h = _inner_out_flow_constraint_hamiltonian(g, 0) + expected_ops = [ + qml.Identity(0), + qml.PauliZ(1) @ qml.PauliZ(0), + qml.PauliZ(0), + qml.PauliZ(1), + ] expected_coeffs = [2, 2, -2, -2] - assert np.allclose(expected_coeffs, h.coeffs) - for i, expected_op in enumerate(expected_ops): - assert str(h.ops[i]) == str(expected_op) - assert all(op.wires == exp.wires for op, exp in zip(h.ops, expected_ops)) + expected_hamiltonian = qml.Hamiltonian(expected_coeffs, expected_ops) + assert h.compare(expected_hamiltonian) @pytest.mark.parametrize("g", [nx.complete_graph(3), rx.generators.mesh_graph(3, [0, 1, 2])]) def test_inner_out_flow_constraint_hamiltonian_error(self, g): @@ -1777,6 +1966,32 @@ def test_inner_net_flow_constraint_hamiltonian(self, g): """Test if the _inner_net_flow_constraint_hamiltonian function returns the expected result on a manually-calculated example of a 3-node complete digraph relative to the 0 node""" h = _inner_net_flow_constraint_hamiltonian(g, 0) + expected_ops = [ + qml.Identity(0), + qml.PauliZ(0) @ qml.PauliZ(1), + qml.PauliZ(0) @ qml.PauliZ(2), + qml.PauliZ(0) @ qml.PauliZ(4), + qml.PauliZ(1) @ qml.PauliZ(2), + qml.PauliZ(1) @ qml.PauliZ(4), + qml.PauliZ(2) @ qml.PauliZ(4), + ] + expected_coeffs = [4, 2, -2, -2, -2, -2, 2] + _, ops = h.terms() + non_zero_terms = [(coeff, op) for coeff, op in zip(h.coeffs, ops) if coeff != 0] + coeffs = [term[0] for term in non_zero_terms] + assert qml.math.allclose(coeffs, expected_coeffs) + non_zero_ops = [term[1] for term in non_zero_terms] + for op, expected_op in zip(non_zero_ops, expected_ops): + assert op.pauli_rep == expected_op.pauli_rep + + @pytest.mark.parametrize( + "g", [nx.complete_graph(3).to_directed(), rx.generators.directed_mesh_graph(3, [0, 1, 2])] + ) + @pytest.mark.usefixtures("use_legacy_opmath") + def test_inner_net_flow_constraint_hamiltonian_legacy_opmath(self, g): + """Test if the _inner_net_flow_constraint_hamiltonian function returns the expected result on a manually-calculated + example of a 3-node complete digraph relative to the 0 node""" + h = _inner_net_flow_constraint_hamiltonian(g, 0) expected_ops = [ qml.Identity(0), @@ -1809,6 +2024,25 @@ def test_inner_out_flow_constraint_hamiltonian_non_complete(self, g): the (0, 1) edge removed""" g.remove_edge(0, 1) h = _inner_out_flow_constraint_hamiltonian(g, 0) + h = h.simplify() + expected_ops = [qml.Identity(0), qml.PauliZ(wires=[0])] + expected_coeffs = [0, 0] + + coeffs, ops = h.terms() + assert qml.math.allclose(expected_coeffs, coeffs) + for op, expected_op in zip(ops, expected_ops): + assert op.pauli_rep == expected_op.pauli_rep + + @pytest.mark.parametrize( + "g", [nx.complete_graph(3).to_directed(), rx.generators.directed_mesh_graph(3, [0, 1, 2])] + ) + @pytest.mark.usefixtures("use_legacy_opmath") + def test_inner_out_flow_constraint_hamiltonian_non_complete_legacy_opmath(self, g): + """Test if the _inner_out_flow_constraint_hamiltonian function returns the expected result + on a manually-calculated example of a 3-node complete digraph relative to the 0 node, with + the (0, 1) edge removed""" + g.remove_edge(0, 1) + h = _inner_out_flow_constraint_hamiltonian(g, 0) expected_ops = [qml.PauliZ(wires=[0])] expected_coeffs = [0] @@ -1826,6 +2060,31 @@ def test_inner_net_flow_constraint_hamiltonian_non_complete(self, g): example of a 3-node complete digraph relative to the 0 node, with the (1, 0) edge removed""" g.remove_edge(1, 0) h = _inner_net_flow_constraint_hamiltonian(g, 0) + h = h.simplify() + expected_ops = [ + qml.Identity(0), + qml.PauliZ(0), + qml.PauliZ(1), + qml.PauliZ(3), + qml.PauliZ(0) @ qml.PauliZ(1), + qml.PauliZ(0) @ qml.PauliZ(3), + qml.PauliZ(1) @ qml.PauliZ(3), + ] + expected_coeffs = [4, -2, -2, 2, 2, -2, -2] + coeffs, ops = h.terms() + assert qml.math.allclose(coeffs, expected_coeffs) + for op, expected_op in zip(ops, expected_ops): + assert op.pauli_rep == expected_op.pauli_rep + + @pytest.mark.parametrize( + "g", [nx.complete_graph(3).to_directed(), rx.generators.directed_mesh_graph(3, [0, 1, 2])] + ) + @pytest.mark.usefixtures("use_legacy_opmath") + def test_inner_net_flow_constraint_hamiltonian_non_complete_legacy_opmath(self, g): + """Test if the _inner_net_flow_constraint_hamiltonian function returns the expected result on a manually-calculated + example of a 3-node complete digraph relative to the 0 node, with the (1, 0) edge removed""" + g.remove_edge(1, 0) + h = _inner_net_flow_constraint_hamiltonian(g, 0) expected_ops = [ qml.Identity(0), @@ -1904,6 +2163,56 @@ def cost(params): elif max(num_edges_leaving_node.values()) <= 1: assert energy == min(energies_bitstrings)[0] + @pytest.mark.parametrize( + "g", [nx.complete_graph(3).to_directed(), rx.generators.directed_mesh_graph(3, [0, 1, 2])] + ) + @pytest.mark.usefixtures("use_legacy_opmath") + def test_out_flow_constraint_legacy_opmath(self, g): + """Test the out-flow constraint Hamiltonian is minimised by states that correspond to + subgraphs that only ever have 0 or 1 edge leaving each node + """ + h = out_flow_constraint(g) + m = wires_to_edges(g) + wires = len(g.edge_list() if isinstance(g, rx.PyDiGraph) else g.edges) + + # We use PL to find the energies corresponding to each possible bitstring + dev = qml.device("default.qubit", wires=wires) + + # pylint: disable=unused-argument + def states(basis_state, **kwargs): + qml.BasisState(basis_state, wires=range(wires)) + + @qml.qnode(dev) + def cost(params): + states(params) + return qml.expval(h) + + # Calculate the set of all bitstrings + bitstrings = itertools.product([0, 1], repeat=wires) + + # Calculate the corresponding energies + energies_bitstrings = ((cost(np.array(bitstring)), bitstring) for bitstring in bitstrings) + + for energy, bs in energies_bitstrings: + # convert binary string to wires then wires to edges + wires_ = tuple(i for i, s in enumerate(bs) if s != 0) + edges = tuple(m[w] for w in wires_) + + # find the number of edges leaving each node + if isinstance(g, rx.PyDiGraph): + num_edges_leaving_node = {node: 0 for node in g.nodes()} + else: + num_edges_leaving_node = {node: 0 for node in g.nodes} + for e in edges: + num_edges_leaving_node[e[0]] += 1 + + # check that if the max number of edges is <=1 it corresponds to a state that minimizes + # the out_flow_constraint Hamiltonian + if max(num_edges_leaving_node.values()) > 1: + assert energy > min(energies_bitstrings)[0] + elif max(num_edges_leaving_node.values()) <= 1: + assert energy == min(energies_bitstrings)[0] + @pytest.mark.parametrize("g", [nx.complete_graph(3), rx.generators.mesh_graph(3, [0, 1, 2])]) def test_out_flow_constraint_undirected_raises_error(self, g): """Test `out_flow_constraint` raises ValueError if input graph is not directed""" @@ -1962,6 +2271,59 @@ def cost(basis_state): else: assert energy > min(energies_states)[0] + @pytest.mark.parametrize( + "g", [nx.complete_graph(3).to_directed(), rx.generators.directed_mesh_graph(3, [0, 1, 2])] + ) + @pytest.mark.usefixtures("use_legacy_opmath") + def test_net_flow_constraint_legacy_opmath(self, g): + """Test if the net_flow_constraint Hamiltonian is minimized by states that correspond to a + collection of edges with zero flow""" + h = net_flow_constraint(g) + m = wires_to_edges(g) + wires = len(g.edge_list() if isinstance(g, rx.PyDiGraph) else g.edges) + + # We use PL to find the energies corresponding to each possible bitstring + dev = qml.device("default.qubit", wires=wires) + + @qml.qnode(dev) + def cost(basis_state): + qml.BasisState(basis_state, wires=range(wires)) + return qml.expval(h) + + # Calculate the set of all bitstrings + states = itertools.product([0, 1], repeat=wires) + + # Calculate the corresponding energies + energies_states = ((cost(np.array(state)), state) for state in states) + + # We now have the energies of each bitstring/state. We also want to calculate the net flow of + # the corresponding edges + for energy, state in energies_states: + # This part converts from a binary string of wires selected to graph edges + wires_ = tuple(i for i, s in enumerate(state) if s != 0) + edges = tuple(m[w] for w in wires_) + + # Calculates the number of edges entering and leaving a given node + if isinstance(g, rx.PyDiGraph): + in_flows = np.zeros(len(g.nodes())) + out_flows = np.zeros(len(g.nodes())) + else: + in_flows = np.zeros(len(g.nodes)) + out_flows = np.zeros(len(g.nodes)) + + for e in edges: + in_flows[e[0]] += 1 + out_flows[e[1]] += 1 + + net_flow = np.sum(np.abs(in_flows - out_flows)) + + # The test requires that a set of edges with zero net flow must have a corresponding + # bitstring that minimized the energy of the Hamiltonian + if net_flow == 0: + assert energy == min(energies_states)[0] + else: + assert energy > min(energies_states)[0] + @pytest.mark.parametrize("g", [nx.complete_graph(3), rx.generators.mesh_graph(3, [0, 1, 2])]) def test_net_flow_constraint_wrong_graph_type_raises_error(self, g): """Test `net_flow_constraint` raises ValueError if input graph is not @@ -2047,3 +2409,70 @@ def find_simple_cycle(list_of_edges): assert energy == min(energies_bitstrings)[0] elif len(edges) > 0 and not find_simple_cycle(edges): assert energy > min(energies_bitstrings)[0] + + @pytest.mark.parametrize( + "g", [nx.complete_graph(3).to_directed(), rx.generators.directed_mesh_graph(3, [0, 1, 2])] + ) + @pytest.mark.usefixtures("use_legacy_opmath") + def test_net_flow_and_out_flow_constraint_legacy_opmath(self, g): + """Test the combined net-flow and out-flow constraint Hamiltonian is minimised by states that correspond to subgraphs + that qualify as simple_cycles + """ + g = nx.complete_graph(3).to_directed() + h = net_flow_constraint(g) + out_flow_constraint(g) + m = wires_to_edges(g) + wires = len(g.edge_list() if isinstance(g, rx.PyDiGraph) else g.edges) + + # Find the energies corresponding to each possible bitstring + dev = qml.device("default.qubit", wires=wires) + + # pylint: disable=unused-argument + def states(basis_state, **kwargs): + qml.BasisState(basis_state, wires=range(wires)) + + @qml.qnode(dev) + def cost(params): + states(params) + return qml.expval(h) + + # Calculate the set of all bitstrings + bitstrings = itertools.product([0, 1], repeat=wires) + + # Calculate the corresponding energies + energies_bitstrings = ((cost(np.array(bitstring)), bitstring) for bitstring in bitstrings) + + def find_simple_cycle(list_of_edges): + """Returns True if list_of_edges contains a permutation corresponding to a simple cycle""" + permutations = list(itertools.permutations(list_of_edges)) + + for edges in permutations: + if edges[0][0] != edges[-1][-1]: # check first node is equal to last node + continue + all_nodes = [] + for edge in edges: + for n in edge: + all_nodes.append(n) + inner_nodes = all_nodes[ + 1:-1 + ] # find all nodes in all edges excluding the first and last nodes + nodes_out = [ + inner_nodes[i] for i in range(len(inner_nodes)) if i % 2 == 0 + ] # find the nodes each edge is leaving + node_in = [ + inner_nodes[i] for i in range(len(inner_nodes)) if i % 2 != 0 + ] # find the nodes each edge is entering + if nodes_out == node_in and ( + len([all_nodes[0]] + nodes_out) == len(set([all_nodes[0]] + nodes_out)) + ): # check that each edge connect to the next via a common node and that no node is crossed more than once + return True + return False + + for energy, bs in energies_bitstrings: + # convert binary string to wires then wires to edges + wires_ = tuple(i for i, s in enumerate(bs) if s != 0) + edges = tuple(m[w] for w in wires_) + + if len(edges) > 0 and find_simple_cycle(edges): + assert energy == min(energies_bitstrings)[0] + elif len(edges) > 0 and not find_simple_cycle(edges): + assert energy > min(energies_bitstrings)[0] diff --git a/tests/test_qubit_device.py b/tests/test_qubit_device.py index b27f1c67bc7..93bfecc250f 100644 --- a/tests/test_qubit_device.py +++ b/tests/test_qubit_device.py @@ -753,8 +753,17 @@ def test_no_eigval_error(self, mock_qubit_device_with_original_statistics): that does not have eigenvalues defined.""" dev = mock_qubit_device_with_original_statistics() dev._samples = np.array([[1, 0], [0, 0]]) + + class MyObs(qml.operation.Observable): + """Observable with no eigenvalue representation defined.""" + + num_wires = 1 + + def eigvals(self): + raise qml.operation.EigvalsUndefinedError + with pytest.raises(qml.operation.EigvalsUndefinedError, match="Cannot compute samples"): - dev.sample(qml.Hamiltonian([1.0], [qml.PauliX(0)])) + dev.sample(MyObs(wires=[0])) class TestSampleWithBroadcasting: @@ -814,8 +823,17 @@ def test_no_eigval_error(self, mock_qubit_device_with_original_statistics): that does not have eigenvalues defined when using broadcasting.""" dev = mock_qubit_device_with_original_statistics() dev._samples = np.array([[[1, 0], [1, 1]], [[1, 1], [0, 0]], [[0, 1], [1, 0]]]) + + class MyObs(qml.operation.Observable): + """Observable with no eigenvalue representation defined.""" + + num_wires = 1 + + def eigvals(self): + raise qml.operation.EigvalsUndefinedError + with pytest.raises(qml.operation.EigvalsUndefinedError, match="Cannot compute samples"): - dev.sample(qml.Hamiltonian([1.0], [qml.PauliX(0)])) + dev.sample(MyObs(wires=[0])) class TestEstimateProb: diff --git a/tests/test_queuing.py b/tests/test_queuing.py index f0f0eacd0b9..7f335027553 100644 --- a/tests/test_queuing.py +++ b/tests/test_queuing.py @@ -27,6 +27,7 @@ ) +# pylint: disable=use-implicit-booleaness-not-comparison, unnecessary-dunder-call class TestStopRecording: """Test the stop_recording method of QueuingManager.""" @@ -211,6 +212,7 @@ def test_append_tensor_ops(self): assert q.queue == [tensor_op] assert tensor_op.obs == [A, B] + @pytest.mark.usefixtures("use_legacy_opmath") def test_append_tensor_ops_overloaded(self): """Test that Tensor ops created using `@` are successfully added to the queue, as well as the `Tensor` object.""" @@ -222,6 +224,17 @@ def test_append_tensor_ops_overloaded(self): assert q.queue == [tensor_op] assert tensor_op.obs == [A, B] + def test_append_prod_ops_overloaded(self): + """Test that Prod ops created using `@` + are successfully added to the queue, as well as the `Prod` object.""" + + with AnnotatedQueue() as q: + A = qml.PauliZ(0) + B = qml.PauliY(1) + prod_op = A @ B + assert q.queue == [prod_op] + assert prod_op.operands == (A, B) + def test_get_info(self): """Test that get_info correctly returns an annotation""" A = qml.RZ(0.5, wires=1) diff --git a/tests/test_vqe.py b/tests/test_vqe.py index 1f274c2987c..b19ec3c4861 100644 --- a/tests/test_vqe.py +++ b/tests/test_vqe.py @@ -890,6 +890,9 @@ def circuit1(): assert res[0] == circuit1() assert res[1] == circuit1() + # the LinearCombination implementation does have diagonalizing gates, + # but legacy Hamiltonian does not and fails + @pytest.mark.usefixtures("use_legacy_opmath") def test_error_var_measurement(self): """Tests that error is thrown if var(H) is measured.""" observables = [qml.PauliZ(0), qml.PauliY(0), qml.PauliZ(1)] @@ -904,6 +907,9 @@ def circuit(): with pytest.raises(NotImplementedError): circuit() + # the LinearCombination implementation does have diagonalizing gates, + # but legacy Hamiltonian does not and fails + @pytest.mark.usefixtures("use_legacy_opmath") def test_error_sample_measurement(self): """Tests that error is thrown if sample(H) is measured.""" observables = [qml.PauliZ(0), qml.PauliY(0), qml.PauliZ(1)] @@ -1013,6 +1019,9 @@ def circuit(w): dc = jax.grad(circuit)(w) assert np.allclose(dc, big_hamiltonian_grad, atol=tol) + @pytest.mark.xfail( + reason="diagonalizing gates defined but not used, should not be included in specs" + ) def test_specs(self): """Test that the specs of a VQE circuit can be computed""" dev = qml.device("default.qubit", wires=2) @@ -1026,6 +1035,27 @@ def circuit(): res = qml.specs(circuit)() + assert res["num_observables"] == 1 + + # currently this returns 1 instead, because diagonalizing gates exist for H, + # but they aren't used in executing this qnode + # to be revisited in [sc-59117] + assert res["num_diagonalizing_gates"] == 0 + + @pytest.mark.usefixtures("use_legacy_opmath") + def test_specs_legacy_opmath(self): + """Test that the specs of a VQE circuit can be computed""" + dev = qml.device("default.qubit", wires=2) + H = qml.Hamiltonian([0.1, 0.2], [qml.PauliZ(0), qml.PauliZ(0) @ qml.PauliX(1)]) + + @qml.qnode(dev) + def circuit(): + qml.Hadamard(wires=0) + qml.CNOT(wires=[0, 1]) + return qml.expval(H) + + res = qml.specs(circuit)() + assert res["num_observables"] == 1 assert res["num_diagonalizing_gates"] == 0 diff --git a/tests/transforms/test_batch_transform.py b/tests/transforms/test_batch_transform.py index 6c5cc32f893..aaba0a8148a 100644 --- a/tests/transforms/test_batch_transform.py +++ b/tests/transforms/test_batch_transform.py @@ -26,7 +26,7 @@ class TestMapBatchTransform: def test_result(self, mocker): """Test that it correctly applies the transform to be mapped""" dev = qml.device("default.qubit.legacy", wires=2) - H = qml.PauliZ(0) @ qml.PauliZ(1) - qml.PauliX(0) + H = qml.Hamiltonian([1, -1], [qml.PauliZ(0) @ qml.PauliZ(1), qml.PauliX(0)]) x = 0.6 y = 0.7 @@ -41,7 +41,7 @@ def test_result(self, mocker): qml.Hadamard(wires=0) qml.CRX(x, wires=[0, 1]) qml.CNOT(wires=[0, 1]) - qml.expval(H + 0.5 * qml.PauliY(0)) + qml.expval(H + qml.Hamiltonian([0.5], [qml.PauliY(0)])) tape2 = qml.tape.QuantumScript.from_queue(q2) spy = mocker.spy(qml.transforms, "hamiltonian_expand") @@ -60,7 +60,7 @@ def test_result(self, mocker): def test_differentiation(self): """Test that an execution using map_batch_transform can be differentiated""" dev = qml.device("default.qubit.legacy", wires=2) - H = qml.PauliZ(0) @ qml.PauliZ(1) - qml.PauliX(0) + H = qml.Hamiltonian([1, -1], [qml.PauliZ(0) @ qml.PauliZ(1), qml.PauliX(0)]) weights = np.array([0.6, 0.8], requires_grad=True) @@ -76,7 +76,7 @@ def cost(weights): qml.Hadamard(wires=0) qml.CRX(weights[0], wires=[0, 1]) qml.CNOT(wires=[0, 1]) - qml.expval(H + 0.5 * qml.PauliY(0)) + qml.expval(H + qml.Hamiltonian([0.5], [qml.PauliY(0)])) tape2 = qml.tape.QuantumScript.from_queue(q2) tapes, fn = qml.transforms.map_batch_transform( diff --git a/tests/transforms/test_defer_measurements.py b/tests/transforms/test_defer_measurements.py index db587a86430..ca9215e5292 100644 --- a/tests/transforms/test_defer_measurements.py +++ b/tests/transforms/test_defer_measurements.py @@ -315,6 +315,8 @@ def test_multiple_postselection_qnode(self, phi, theta, shots, tol, tol_stochast is transformed correctly by defer_measurements""" dev = DefaultQubit() + np.random.seed(None) + # Initializing mid circuit measurements here so that id can be controlled (affects # wire ordering for qml.cond) mp0 = MidMeasureMP(wires=0, postselect=0, id=0) diff --git a/tests/transforms/test_experimental/test_transform_dispatcher.py b/tests/transforms/test_experimental/test_transform_dispatcher.py index 959b4b394ba..f41030dcca0 100644 --- a/tests/transforms/test_experimental/test_transform_dispatcher.py +++ b/tests/transforms/test_experimental/test_transform_dispatcher.py @@ -486,7 +486,10 @@ def comb_postproc(results: TensorLike, fn1: Callable, fn2: Callable): # Create a simple device and tape tmp_dev = qml.device("default.qubit", wires=3) - H = qml.PauliY(2) @ qml.PauliZ(1) + 0.5 * qml.PauliZ(2) + qml.PauliZ(1) + + H = qml.Hamiltonian( + [0.5, 1.0, 1.0], [qml.PauliZ(2), qml.PauliY(2) @ qml.PauliZ(1), qml.PauliZ(1)] + ) measur = [qml.expval(H)] ops = [qml.Hadamard(0), qml.RX(0.2, 0), qml.RX(0.6, 0), qml.CNOT((0, 1))] tape = qml.tape.QuantumTape(ops, measur) diff --git a/tests/transforms/test_hamiltonian_expand.py b/tests/transforms/test_hamiltonian_expand.py index c194f7a80a1..a6e594ce70d 100644 --- a/tests/transforms/test_hamiltonian_expand.py +++ b/tests/transforms/test_hamiltonian_expand.py @@ -51,7 +51,7 @@ qml.expval(H2) tape2 = QuantumScript.from_queue(q_tape2) -H3 = 1.5 * qml.PauliZ(0) @ qml.PauliZ(1) + 0.3 * qml.PauliX(1) +H3 = qml.Hamiltonian([1.5, 0.3], [qml.Z(0) @ qml.Z(1), qml.X(1)]) with AnnotatedQueue() as q3: qml.PauliX(0) @@ -59,14 +59,18 @@ tape3 = QuantumScript.from_queue(q3) -H4 = ( - qml.PauliX(0) @ qml.PauliZ(2) - + 3 * qml.PauliZ(2) - - 2 * qml.PauliX(0) - + qml.PauliZ(2) - + qml.PauliZ(2) -) -H4 += qml.PauliZ(0) @ qml.PauliX(1) @ qml.PauliY(2) + +H4 = qml.Hamiltonian( + [1, 3, -2, 1, 1, 1], + [ + qml.PauliX(0) @ qml.PauliZ(2), + qml.PauliZ(2), + qml.PauliX(0), + qml.PauliZ(2), + qml.PauliZ(2), + qml.PauliZ(0) @ qml.PauliX(1) @ qml.PauliY(2), + ], +).simplify() with AnnotatedQueue() as q4: qml.Hadamard(0) @@ -434,7 +438,7 @@ def test_sums(self, qscript, output): assert all(qml.math.allclose(o, e) for o, e in zip(output, expval)) @pytest.mark.parametrize(("qscript", "output"), zip(SUM_QSCRIPTS, SUM_OUTPUTS)) - def test_sums_legacy(self, qscript, output): + def test_sums_legacy_opmath(self, qscript, output): """Tests that the sum_expand transform returns the correct value""" dev_old = qml.device("default.qubit.legacy", wires=4) tapes, fn = sum_expand(qscript) diff --git a/tests/transforms/test_insert_ops.py b/tests/transforms/test_insert_ops.py index 08742f5da18..0270dbc4be9 100644 --- a/tests/transforms/test_insert_ops.py +++ b/tests/transforms/test_insert_ops.py @@ -91,7 +91,11 @@ def test_start(self): for o1, o2 in zip(tape.operations, tape_exp.operations) ) assert len(tape.measurements) == 1 - assert tape.observables[0].name == ["PauliZ", "PauliZ"] + assert ( + tape.observables[0].name == "Prod" + if qml.operation.active_new_opmath() + else ["PauliZ", "PauliZ"] + ) assert tape.observables[0].wires.tolist() == [0, 1] assert tape.measurements[0].return_type is Expectation @@ -122,7 +126,11 @@ def test_all(self): for o1, o2 in zip(tape.operations, tape_exp.operations) ) assert len(tape.measurements) == 1 - assert tape.observables[0].name == ["PauliZ", "PauliZ"] + assert ( + tape.observables[0].name == "Prod" + if qml.operation.active_new_opmath() + else ["PauliZ", "PauliZ"] + ) assert tape.observables[0].wires.tolist() == [0, 1] assert tape.measurements[0].return_type is Expectation @@ -152,7 +160,11 @@ def test_before(self): for o1, o2 in zip(tape.operations, tape_exp.operations) ) assert len(tape.measurements) == 1 - assert tape.observables[0].name == ["PauliZ", "PauliZ"] + assert ( + tape.observables[0].name == "Prod" + if qml.operation.active_new_opmath() + else ["PauliZ", "PauliZ"] + ) assert tape.observables[0].wires.tolist() == [0, 1] assert tape.measurements[0].return_type is Expectation @@ -191,7 +203,11 @@ def test_operation_as_position(self, op): for o1, o2 in zip(tape.operations, tape_exp.operations) ) assert len(tape.measurements) == 1 - assert tape.observables[0].name == ["PauliZ", "PauliZ"] + assert ( + tape.observables[0].name == "Prod" + if qml.operation.active_new_opmath() + else ["PauliZ", "PauliZ"] + ) assert tape.observables[0].wires.tolist() == [0, 1] assert tape.measurements[0].return_type is Expectation @@ -220,7 +236,11 @@ def test_operation_list_as_position(self): for o1, o2 in zip(tape.operations, tape_exp.operations) ) assert len(tape.measurements) == 1 - assert tape.observables[0].name == ["PauliZ", "PauliZ"] + assert ( + tape.observables[0].name == "Prod" + if qml.operation.active_new_opmath() + else ["PauliZ", "PauliZ"] + ) assert tape.observables[0].wires.tolist() == [0, 1] assert tape.measurements[0].return_type is Expectation @@ -247,7 +267,11 @@ def test_end(self): for o1, o2 in zip(tape.operations, tape_exp.operations) ) assert len(tape.measurements) == 1 - assert tape.observables[0].name == ["PauliZ", "PauliZ"] + assert ( + tape.observables[0].name == "Prod" + if qml.operation.active_new_opmath() + else ["PauliZ", "PauliZ"] + ) assert tape.observables[0].wires.tolist() == [0, 1] assert tape.measurements[0].return_type is Expectation @@ -276,7 +300,11 @@ def test_start_with_state_prep(self): for o1, o2 in zip(tape.operations, tape_exp.operations) ) assert len(tape.measurements) == 1 - assert tape.observables[0].name == ["PauliZ", "PauliZ"] + assert ( + tape.observables[0].name == "Prod" + if qml.operation.active_new_opmath() + else ["PauliZ", "PauliZ"] + ) assert tape.observables[0].wires.tolist() == [0, 1] assert tape.measurements[0].return_type is Expectation @@ -309,7 +337,11 @@ def test_all_with_state_prep(self): for o1, o2 in zip(tape.operations, tape_exp.operations) ) assert len(tape.measurements) == 1 - assert tape.observables[0].name == ["PauliZ", "PauliZ"] + assert ( + tape.observables[0].name == "Prod" + if qml.operation.active_new_opmath() + else ["PauliZ", "PauliZ"] + ) assert tape.observables[0].wires.tolist() == [0, 1] assert tape.measurements[0].return_type is Expectation @@ -340,7 +372,11 @@ def test_end_with_state_prep(self): for o1, o2 in zip(tape.operations, tape_exp.operations) ) assert len(tape.measurements) == 1 - assert tape.observables[0].name == ["PauliZ", "PauliZ"] + assert ( + tape.observables[0].name == "Prod" + if qml.operation.active_new_opmath() + else ["PauliZ", "PauliZ"] + ) assert tape.observables[0].wires.tolist() == [0, 1] assert tape.measurements[0].return_type is Expectation @@ -375,7 +411,11 @@ def op(x, y, wires): for o1, o2 in zip(tape.operations, tape_exp.operations) ) assert len(tape.measurements) == 1 - assert tape.observables[0].name == ["PauliZ", "PauliZ"] + assert ( + tape.observables[0].name == "Prod" + if qml.operation.active_new_opmath() + else ["PauliZ", "PauliZ"] + ) assert tape.observables[0].wires.tolist() == [0, 1] assert tape.measurements[0].return_type is Expectation @@ -457,7 +497,11 @@ def test_insert_dev(): for o1, o2 in zip(tape.operations, tape_exp.operations) ) assert len(tape.measurements) == 2 - assert tape.observables[0].name == ["PauliZ", "PauliZ"] + assert ( + tape.observables[0].name == "Prod" + if qml.operation.active_new_opmath() + else ["PauliZ", "PauliZ"] + ) assert tape.observables[0].wires.tolist() == [0, 1] assert tape.measurements[0].return_type is Expectation assert tape.observables[1].name == "PauliZ" @@ -512,7 +556,11 @@ def test_insert_old_dev(mocker): for o1, o2 in zip(tape.operations, tape_exp.operations) ) assert len(tape.measurements) == 2 - assert tape.observables[0].name == ["PauliZ", "PauliZ"] + assert ( + tape.observables[0].name == "Prod" + if qml.operation.active_new_opmath() + else ["PauliZ", "PauliZ"] + ) assert tape.observables[0].wires.tolist() == [0, 1] assert tape.measurements[0].return_type is Expectation assert tape.observables[1].name == "PauliZ" diff --git a/tests/transforms/test_optimization/test_merge_rotations.py b/tests/transforms/test_optimization/test_merge_rotations.py index cde3a9ef670..03821ef7606 100644 --- a/tests/transforms/test_optimization/test_merge_rotations.py +++ b/tests/transforms/test_optimization/test_merge_rotations.py @@ -510,3 +510,18 @@ def test_qnode(self): res = transformed_qnode([0.1, 0.2, 0.3, 0.4]) exp_res = qnode_circuit([0.1, 0.2, 0.3, 0.4]) assert np.allclose(res, exp_res) + + +@pytest.mark.xfail +def test_merge_rotations_non_commuting_observables(): + """Test that merge_roatations works with non-commuting observables.""" + + @qml.transforms.merge_rotations + def circuit(x): + qml.RX(x, wires=0) + qml.RX(-x, wires=0) + return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliX(0)) + + res = circuit(0.5) + assert qml.math.allclose(res[0], 1.0) + assert qml.math.allclose(res[1], 0.0) diff --git a/tests/transforms/test_qcut.py b/tests/transforms/test_qcut.py index 09ed1a31b86..aa2a6e6effd 100644 --- a/tests/transforms/test_qcut.py +++ b/tests/transforms/test_qcut.py @@ -1554,8 +1554,15 @@ def test_single_measurement(self): assert obs[0].wires.tolist() == [1, 0, 2] assert obs[1].wires.tolist() == [1, 0] - assert [get_name(o) for o in obs[0].obs] == ["PauliZ", "PauliX", "PauliZ"] - assert [get_name(o) for o in obs[1].obs] == ["PauliZ", "PauliX"] + if qml.operation.active_new_opmath(): + + assert [get_name(o) for o in obs[0].terms()[1]] == ["Prod"] + assert [get_name(o) for o in obs[1].terms()[1]] == ["Prod"] + + else: + + assert [get_name(o) for o in obs[0].obs] == ["PauliZ", "PauliX", "PauliZ"] + assert [get_name(o) for o in obs[1].obs] == ["PauliZ", "PauliX"] class TestExpandFragmentTapes: @@ -2520,7 +2527,7 @@ def circuit(v): cut_res_mc = circuit(v) target = target_circuit(v) - assert np.isclose(cut_res_mc, target, atol=0.1) # not guaranteed to pass each time + assert np.isclose(cut_res_mc, target, atol=0.15) # not guaranteed to pass each time def test_cut_circuit_mc_sample(self, dev_fn): """ @@ -5060,6 +5067,7 @@ def test_place_wire_cuts(self): range(len(graph.nodes) + len(cut_edges)) ) + @pytest.mark.usefixtures("use_legacy_opmath") @pytest.mark.parametrize("local_measurement", [False, True]) @pytest.mark.parametrize("with_manual_cut", [False, True]) @pytest.mark.parametrize( @@ -5174,6 +5182,122 @@ def test_find_and_place_cuts(self, local_measurement, with_manual_cut, cut_strat in expected_num_cut_edges ) + @pytest.mark.parametrize("local_measurement", [False, True]) + @pytest.mark.parametrize("with_manual_cut", [False, True]) + @pytest.mark.parametrize( + "cut_strategy", + [ + None, + qcut.CutStrategy(qml.device("default.qubit", wires=3)), + qcut.CutStrategy(max_free_wires=4), + qcut.CutStrategy(max_free_wires=2), # extreme constraint forcing exhaustive probing. + qcut.CutStrategy(max_free_wires=2, num_fragments_probed=5), # impossible to cut + ], + ) + def test_find_and_place_cuts_opmath(self, local_measurement, with_manual_cut, cut_strategy): + """Integration tests for auto cutting pipeline with opmath enabled.""" + pytest.importorskip("kahypar") + + with qml.queuing.AnnotatedQueue() as q: + qml.RX(0.1, wires=0) + qml.RY(0.2, wires=1) + qml.RX(0.3, wires="a") + qml.RY(0.4, wires="b") + qml.CNOT(wires=[0, 1]) + if with_manual_cut: + qml.WireCut(wires=1) + qml.CNOT(wires=["a", "b"]) + qml.CNOT(wires=[1, "a"]) + qml.CNOT(wires=[0, 1]) + qml.CNOT(wires=["a", "b"]) + qml.RX(0.5, wires="a") + qml.RY(0.6, wires="b") + qml.expval( + qml.prod(qml.PauliX(wires=[0]), qml.PauliY(wires=["a"]), qml.PauliZ(wires=["b"])) + ) + + tape = qml.tape.QuantumScript.from_queue(q) + graph = qcut.tape_to_graph(tape) + + if cut_strategy is None: + expected_num_cut_edges = 2 + num_frags = 2 + cut_graph = qcut.find_and_place_cuts( + graph=graph, + num_fragments=num_frags, + imbalance=0.5, + replace_wire_cuts=True, + seed=self.seed, + local_measurement=local_measurement, + ) + + elif cut_strategy.num_fragments_probed: + with pytest.raises(ValueError): + cut_graph = qcut.find_and_place_cuts( + graph=graph, + cut_strategy=cut_strategy, + local_measurement=local_measurement, + ) + return + + else: + cut_graph = qcut.find_and_place_cuts( + graph=graph, + cut_strategy=cut_strategy, + replace_wire_cuts=True, + seed=self.seed, + local_measurement=local_measurement, + ) + + if cut_strategy.max_free_wires > 2: + expected_num_cut_edges = 2 + num_frags = 2 + else: + # There's some inherent randomness in Kahypar that's not fixable by seed. + # Need to make this condition a bit relaxed for the extreme case. + expected_num_cut_edges = [10, 11, 14, 15] + num_frags = [9, 10, 13, 14] + + frags, comm_graph = qcut.fragment_graph(cut_graph) + + if num_frags == 2: + assert len(frags) == num_frags + assert len(comm_graph.edges) == expected_num_cut_edges + + assert ( + len([n for n in cut_graph.nodes if isinstance(n.obj, qcut.MeasureNode)]) + == expected_num_cut_edges + ) + assert ( + len([n for n in cut_graph.nodes if isinstance(n.obj, qcut.PrepareNode)]) + == expected_num_cut_edges + ) + + # Cutting wire "a" is more balanced, thus will be cut if there's no manually placed cut on + # wire 1: + expected_cut_wire = 1 if with_manual_cut else "a" + assert all( + list(n.obj.wires) == [expected_cut_wire] + for n in cut_graph.nodes + if isinstance(n.obj, (qcut.MeasureNode, qcut.PrepareNode)) + ) + + expected_fragment_sizes = [7, 11] if with_manual_cut else [8, 10] + assert expected_fragment_sizes == [f.number_of_nodes() for f in frags] + + else: + assert len(frags) in num_frags + assert len(comm_graph.edges) in expected_num_cut_edges + + assert ( + len([n for n in cut_graph.nodes if isinstance(n.obj, qcut.MeasureNode)]) + in expected_num_cut_edges + ) + assert ( + len([n for n in cut_graph.nodes if isinstance(n.obj, qcut.PrepareNode)]) + in expected_num_cut_edges + ) + class TestAutoCutCircuit: """Integration tests for automatic-cutting-enabled `cut_circuit` transform. @@ -5353,6 +5477,7 @@ def circuit(x): assert cut_res_bs.shape == target.shape assert isinstance(cut_res_bs, type(target)) + @pytest.mark.usefixtures("use_legacy_opmath") @pytest.mark.parametrize("measure_all_wires", [False, True]) def test_cut_mps(self, measure_all_wires): """Test auto cut this circuit: @@ -5420,10 +5545,89 @@ def block(weights, wires): # each frag should have the device size constraint satisfied. assert all(len(set(e[2] for e in f.edges.data("wire"))) <= device_size for f in frags) + @pytest.mark.parametrize("measure_all_wires", [False, True]) + def test_cut_mps_opmath(self, measure_all_wires): + """Test auto cut this circuit with opmath enabled: + 0: ─╭C──RY──────────────────────────────────────────── β•­ + 1: ─╰X──RY─╭C──RY───────────────────────────────────── β”œ + 2: ────────╰X──RY─╭C──RY────────────────────────────── β”œ + 3: ───────────────╰X──RY─╭C──RY─────────────────────── β”œ + 4: ──────────────────────╰X──RY─╭C──RY──────────────── β”œ + 5: ─────────────────────────────╰X──RY─╭C──RY───────── β”œ + 6: ────────────────────────────────────╰X──RY─╭C──RY── β”œ + 7: ───────────────────────────────────────────╰X──RY── β•° + + into this: + + 0: ─╭C──RY──────────────────────────────────────────────────────────────────── β•­ + 1: ─╰X──RY──//─╭C──RY───────────────────────────────────────────────────────── β”œ + 2: ────────────╰X──RY──//─╭C──RY────────────────────────────────────────────── β”œ + 3: ───────────────────────╰X──RY──//─╭C──RY─────────────────────────────────── β”œ + 4: ──────────────────────────────────╰X──RY──//─╭C──RY──────────────────────── β”œ + 5: ─────────────────────────────────────────────╰X──RY──//─╭C──RY───────────── β”œ + 6: ────────────────────────────────────────────────────────╰X──RY──//─╭C──RY── β”œ + 7: ───────────────────────────────────────────────────────────────────╰X──RY── β•° + """ + + pytest.importorskip("kahypar") + + def block(weights, wires): + qml.CNOT(wires=[wires[0], wires[1]]) + qml.RY(weights[0], wires=wires[0]) + qml.RY(weights[1], wires=wires[1]) + + n_wires = 8 + n_block_wires = 2 + n_params_block = 2 + n_blocks = qml.MPS.get_n_blocks(range(n_wires), n_block_wires) + template_weights = [[0.1, -0.3]] * n_blocks + + device_size = 2 + cut_strategy = qml.qcut.CutStrategy(max_free_wires=device_size) + + with qml.queuing.AnnotatedQueue() as q0: + qml.MPS(range(n_wires), n_block_wires, block, n_params_block, template_weights) + if measure_all_wires: + qml.expval( + qml.prod( + qml.Z(0), + qml.Z(1), + qml.Z(2), + qml.Z(3), + qml.Z(4), + qml.Z(5), + qml.Z(6), + qml.Z(7), + ) + ) + else: + qml.expval(qml.PauliZ(wires=n_wires - 1)) + + tape0 = qml.tape.QuantumScript.from_queue(q0) + tape = tape0.expand() + graph = qcut.tape_to_graph(tape) + cut_graph = qcut.find_and_place_cuts( + graph=graph, + cut_strategy=cut_strategy, + replace_wire_cuts=True, + ) + frags, _ = qcut.fragment_graph(cut_graph) + assert len(frags) == 7 + + if measure_all_wires: + lower, upper = 5, 6 + else: + lower, upper = 4, 5 + assert all(lower <= f.order() <= upper for f in frags) + + # each frag should have the device size constraint satisfied. + assert all(len(set(e[2] for e in f.edges.data("wire"))) <= device_size for f in frags) + class TestCutCircuitWithHamiltonians: """Integration tests for `cut_circuit` transform with Hamiltonians.""" + @pytest.mark.usefixtures("use_legacy_opmath") def test_circuit_with_hamiltonian(self, mocker): """ Tests that the full automatic circuit cutting pipeline returns the correct value and @@ -5494,6 +5698,76 @@ def f(params): assert np.isclose(res, res_expected) assert np.allclose(grad, grad_expected) + def test_circuit_with_hamiltonian_opmath(self, mocker): + """ + Tests that the full automatic circuit cutting pipeline returns the correct value and + gradient for a complex circuit with multiple wire cut scenarios with opmath enabled. The circuit is the + uncut version of the circuit in ``TestCutCircuitTransform.test_complicated_circuit``. + + 0: ──BasisState(M0)─╭C───RX─╭C──╭C───────────────────── + 1: ─────────────────╰X──────╰X──╰Z────────────────╭RX── β•­ + 2: ──H──────────────╭C─────────────╭RY────────╭RY─│──── β”œ + 3: ─────────────────╰RY──H──╭C───H─╰C──╭RY──H─╰C──│──── β•° + 4: ─────────────────────────╰RY──H─────╰C─────────╰C─── + """ + + dev_original = qml.device("default.qubit", wires=5) + dev_cut = qml.device("default.qubit", wires=4) + + hamiltonian = qml.Hamiltonian( + [1.0, 1.0], + [qml.prod(qml.PauliZ(1), qml.PauliZ(2), qml.PauliZ(3)), qml.PauliY(0) @ qml.PauliX(1)], + ) + + def two_qubit_unitary(param, wires): + qml.Hadamard(wires=[wires[0]]) + qml.CRY(param, wires=[wires[0], wires[1]]) + + def f(params): + qml.BasisState(np.array([1]), wires=[0]) + qml.WireCut(wires=0) + + qml.CNOT(wires=[0, 1]) + qml.WireCut(wires=0) + qml.RX(params[0], wires=0) + qml.CNOT(wires=[0, 1]) + + qml.WireCut(wires=0) + qml.WireCut(wires=1) + + qml.CZ(wires=[0, 1]) + qml.WireCut(wires=[0, 1]) + + two_qubit_unitary(params[1], wires=[2, 3]) + qml.WireCut(wires=3) + two_qubit_unitary(params[2] ** 2, wires=[3, 4]) + qml.WireCut(wires=3) + two_qubit_unitary(np.sin(params[3]), wires=[3, 2]) + qml.WireCut(wires=3) + two_qubit_unitary(np.sqrt(params[4]), wires=[4, 3]) + qml.WireCut(wires=3) + two_qubit_unitary(np.cos(params[1]), wires=[3, 2]) + qml.CRX(params[2], wires=[4, 1]) + + return qml.expval(hamiltonian) + + params = np.array([0.4, 0.5, 0.6, 0.7, 0.8], requires_grad=True) + + circuit = qml.QNode(f, dev_original) + cut_circuit = qcut.cut_circuit(qml.QNode(f, dev_cut)) + + res_expected = circuit(params) + grad_expected = qml.grad(circuit)(params) + + spy = mocker.spy(qcut.cutcircuit, "qcut_processing_fn") + res = cut_circuit(params) + assert spy.call_count == len(hamiltonian.ops) + + grad = qml.grad(cut_circuit)(params) + + assert np.isclose(res, res_expected) + assert np.allclose(grad, grad_expected) + def test_autoscale_and_grouped_with_hamiltonian(self, mocker): """ Tests that the full circuit cutting pipeline returns the correct value for a typical @@ -5565,7 +5839,10 @@ def block(weights, wires): hamiltonian = qml.Hamiltonian( [1.0, 1.0], - [qml.PauliZ(1) @ qml.PauliZ(8) @ qml.PauliZ(3), qml.PauliY(5) @ qml.PauliX(4)], + [ + qml.prod(qml.PauliZ(1), qml.PauliZ(8), qml.PauliZ(3)), + qml.prod(qml.PauliY(5), qml.PauliX(4)), + ], ) with qml.queuing.AnnotatedQueue() as q0: @@ -5594,6 +5871,7 @@ def block(weights, wires): # each frag should have the device size constraint satisfied. assert all(len(set(e[2] for e in f.edges.data("wire"))) <= device_size for f in frags) + @pytest.mark.xfail def test_hamiltonian_with_tape(self): """Test that an expand function that generates multiple tapes is applied before the transform and the transform returns correct results.""" diff --git a/tests/transforms/test_sign_expand.py b/tests/transforms/test_sign_expand.py index 2cb25612223..7bdf9a7cdd5 100644 --- a/tests/transforms/test_sign_expand.py +++ b/tests/transforms/test_sign_expand.py @@ -41,19 +41,16 @@ ) qml.expval(H2) -H3 = 1.5 * qml.PauliZ(0) @ qml.PauliZ(1) + 0.3 * qml.PauliX(2) +H3 = qml.Hamiltonian([1.5, 0.3], [qml.PauliZ(0) @ qml.PauliZ(1), qml.PauliX(2)]) with qml.tape.QuantumTape() as tape3: qml.PauliX(0) qml.expval(H3) -H4 = ( - qml.PauliX(0) @ qml.PauliZ(2) - + 3 * qml.PauliZ(2) - - 2 * qml.PauliX(0) - + qml.PauliZ(2) - + qml.PauliZ(2) +H4 = qml.Hamiltonian( + [1, 3, -2, 1, 1], + [qml.PauliX(0) @ qml.PauliZ(2), qml.PauliZ(2), qml.PauliX(0), qml.PauliZ(2), qml.PauliZ(2)], ) with qml.tape.QuantumTape() as tape4: @@ -174,7 +171,7 @@ def test_hamiltonian_error_not_jointly_measurable(self): """Test if hamiltonians that are not jointly measurable throw an error""" with pennylane.tape.QuantumTape() as tape: - H_mult = 1.5 * qml.PauliZ(0) + 2 * qml.PauliZ(1) + 0.3 * qml.PauliX(0) + H_mult = qml.Hamiltonian([1.5, 2, 0.3], [qml.PauliZ(0), qml.PauliZ(1), qml.PauliX(0)]) qml.expval(H_mult) with pytest.raises(ValueError, match=r"Passed hamiltonian"): diff --git a/tests/transforms/test_tape_expand.py b/tests/transforms/test_tape_expand.py index b2ea27d683f..2ac84633c41 100644 --- a/tests/transforms/test_tape_expand.py +++ b/tests/transforms/test_tape_expand.py @@ -808,7 +808,7 @@ def circuit(): # check that new instances of the operator are not affected by the modifications made to get the decomposition assert [op1 == op2 for op1, op2 in zip(CustomOp(0).decomposition(), original_decomp)] - def test_custom_decomp_in_separate_context_legacy(self): + def test_custom_decomp_in_separate_context_legacy_opmath(self): """Test that the set_decomposition context manager works.""" dev = qml.device("default.qubit.legacy", wires=2) diff --git a/tests/transforms/test_transpile.py b/tests/transforms/test_transpile.py index 4a8035d20b2..726ddebaabb 100644 --- a/tests/transforms/test_transpile.py +++ b/tests/transforms/test_transpile.py @@ -76,12 +76,11 @@ def circuit(): # build circuit transpiled_qfunc = transpile(circuit, coupling_map=[(0, 1), (1, 2), (2, 3)]) transpiled_qnode = qml.QNode(transpiled_qfunc, dev) - err_msg = ( - "Measuring expectation values of tensor products or Hamiltonians is not yet supported" - ) + err_msg = "Measuring expectation values of tensor products, Prods, or Hamiltonians is not yet supported" with pytest.raises(NotImplementedError, match=err_msg): transpiled_qnode() + @pytest.mark.usefixtures("use_legacy_opmath") def test_transpile_raise_not_implemented_tensorproduct_mmt(self): """test that error is raised when measurement is expectation of a Tensor product""" dev = qml.device("default.qubit", wires=[0, 1, 2, 3]) @@ -94,9 +93,23 @@ def circuit(): # build circuit transpiled_qfunc = transpile(circuit, coupling_map=[(0, 1), (1, 2), (2, 3)]) transpiled_qnode = qml.QNode(transpiled_qfunc, dev) - err_msg = ( - r"Measuring expectation values of tensor products or Hamiltonians is not yet supported" - ) + err_msg = r"Measuring expectation values of tensor products, Prods, or Hamiltonians is not yet supported" + with pytest.raises(NotImplementedError, match=err_msg): + transpiled_qnode() + + def test_transpile_raise_not_implemented_prod_mmt(self): + """test that error is raised when measurement is expectation of a Prod""" + dev = qml.device("default.qubit", wires=[0, 1, 2, 3]) + + def circuit(): + qml.CNOT(wires=[0, 1]) + qml.CNOT(wires=[0, 3]) + return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) + + # build circuit + transpiled_qfunc = transpile(circuit, coupling_map=[(0, 1), (1, 2), (2, 3)]) + transpiled_qnode = qml.QNode(transpiled_qfunc, dev) + err_msg = r"Measuring expectation values of tensor products, Prods, or Hamiltonians is not yet supported" with pytest.raises(NotImplementedError, match=err_msg): transpiled_qnode() diff --git a/tests/workflow/test_construct_batch.py b/tests/workflow/test_construct_batch.py index bc6113d5b41..536ac33e5c7 100644 --- a/tests/workflow/test_construct_batch.py +++ b/tests/workflow/test_construct_batch.py @@ -322,17 +322,17 @@ def test_device_transforms(self, level): @pytest.mark.parametrize("level", ("device", None)) def test_device_transforms_legacy_interface(self, level): - """Test that the device transforms can be selected with level=device or None.""" + """Test that the device transforms can be selected with level=device or None without trainable parameters""" - @qml.transforms.merge_rotations + @qml.transforms.cancel_inverses @qml.qnode(qml.device("default.qubit.legacy", wires=2, shots=50)) def circuit(order): qml.Permute(order, wires=(0, 1, 2)) - qml.RX(0.5, wires=0) - qml.RX(-0.5, wires=0) - return qml.expval(qml.PauliX(0) + qml.PauliY(0)) + qml.X(0) + qml.X(0) + return [qml.expval(qml.PauliX(0)), qml.expval(qml.PauliY(0))] - batch, fn = construct_batch(circuit, level=level)((2, 1, 0)) + batch, fn = qml.workflow.construct_batch(circuit, level=level)((2, 1, 0)) expected0 = qml.tape.QuantumScript( [qml.SWAP((0, 2))], [qml.expval(qml.PauliX(0))], shots=50 @@ -344,7 +344,7 @@ def circuit(order): assert qml.equal(expected1, batch[1]) assert len(batch) == 2 - assert fn((1.0, 2.0)) == (3.0,) + assert fn((1.0, 2.0)) == ((1.0, 2.0),) def test_final_transform(self): """Test that the final transform is included when level=None."""