Source code for pennylane.ops.functions.equal

# Copyright 2018-2021 Xanadu Quantum Technologies Inc.

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

#     http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
This module contains the qml.equal function.
"""
# pylint: disable=too-many-arguments,too-many-return-statements,too-many-branches
from collections.abc import Iterable
from functools import singledispatch
from typing import Union

import pennylane as qml
from pennylane import Hermitian
from pennylane.measurements import MeasurementProcess
from pennylane.measurements.classical_shadow import ShadowExpvalMP
from pennylane.measurements.counts import CountsMP
from pennylane.measurements.mid_measure import MeasurementValue, MidMeasureMP
from pennylane.measurements.mutual_info import MutualInfoMP
from pennylane.measurements.vn_entropy import VnEntropyMP
from pennylane.operation import Observable, Operator, Tensor
from pennylane.ops import (
    Adjoint,
    CompositeOp,
    Conditional,
    Controlled,
    Exp,
    Hamiltonian,
    LinearCombination,
    Pow,
    SProd,
)
from pennylane.pulse.parametrized_evolution import ParametrizedEvolution
from pennylane.tape import QuantumScript
from pennylane.templates.subroutines import ControlledSequence, PrepSelPrep

OPERANDS_MISMATCH_ERROR_MESSAGE = "op1 and op2 have different operands because "

BASE_OPERATION_MISMATCH_ERROR_MESSAGE = "op1 and op2 have different base operations because "


[docs]def equal( op1: Union[Operator, MeasurementProcess, QuantumScript], op2: Union[Operator, MeasurementProcess, QuantumScript], check_interface=True, check_trainability=True, rtol=1e-5, atol=1e-9, ) -> bool: r"""Function for determining operator, measurement, and tape equality. .. Warning:: The ``qml.equal`` function is based on a comparison of the types and attributes of the measurements or operators, not their mathematical representations. While mathematical comparisons between some classes, such as ``Tensor`` and ``Hamiltonian``, are supported, mathematically equivalent operators defined via different classes may return False when compared via ``qml.equal``. To be more thorough would require the matrix forms to be calculated, which may drastically increase runtime. .. Warning:: The interfaces and trainability of data within some observables including ``Tensor``, ``Hamiltonian``, ``Prod``, ``Sum`` are sometimes ignored, regardless of what the user specifies for ``check_interface`` and ``check_trainability``. Args: op1 (.Operator or .MeasurementProcess or .QuantumTape): First object to compare op2 (.Operator or .MeasurementProcess or .QuantumTape): Second object to compare check_interface (bool, optional): Whether to compare interfaces. Default: ``True``. check_trainability (bool, optional): Whether to compare trainability status. Default: ``True``. rtol (float, optional): Relative tolerance for parameters. atol (float, optional): Absolute tolerance for parameters. Returns: bool: ``True`` if the operators, measurement processes, or tapes are equal, else ``False`` **Example** Given two operators or measurement processes, ``qml.equal`` determines their equality. >>> op1 = qml.RX(np.array(.12), wires=0) >>> op2 = qml.RY(np.array(1.23), wires=0) >>> qml.equal(op1, op1), qml.equal(op1, op2) (True, False) >>> T1 = qml.X(0) @ qml.Y(1) >>> T2 = qml.Y(1) @ qml.X(0) >>> T3 = qml.X(1) @ qml.Y(0) >>> qml.equal(T1, T2), qml.equal(T1, T3) (True, False) >>> T = qml.X(0) @ qml.Y(1) >>> H = qml.Hamiltonian([1], [qml.X(0) @ qml.Y(1)]) >>> qml.equal(T, H) True >>> H1 = qml.Hamiltonian([0.5, 0.5], [qml.Z(0) @ qml.Y(1), qml.Y(1) @ qml.Z(0) @ qml.Identity("a")]) >>> H2 = qml.Hamiltonian([1], [qml.Z(0) @ qml.Y(1)]) >>> H3 = qml.Hamiltonian([2], [qml.Z(0) @ qml.Y(1)]) >>> qml.equal(H1, H2), qml.equal(H1, H3) (True, False) >>> qml.equal(qml.expval(qml.X(0)), qml.expval(qml.X(0))) True >>> qml.equal(qml.probs(wires=(0,1)), qml.probs(wires=(1,2))) False >>> qml.equal(qml.classical_shadow(wires=[0,1]), qml.classical_shadow(wires=[0,1])) True >>> tape1 = qml.tape.QuantumScript([qml.RX(1.2, wires=0)], [qml.expval(qml.Z(0))]) >>> tape2 = qml.tape.QuantumScript([qml.RX(1.2 + 1e-6, wires=0)], [qml.expval(qml.Z(0))]) >>> qml.equal(tape1, tape2, tol=0, atol=1e-7) False >>> qml.equal(tape1, tape2, tol=0, atol=1e-5) True .. details:: :title: Usage Details You can use the optional arguments to get more specific results: >>> op1 = qml.RX(torch.tensor(1.2), wires=0) >>> op2 = qml.RX(jax.numpy.array(1.2), wires=0) >>> qml.equal(op1, op2) False >>> qml.equal(op1, op2, check_interface=False, check_trainability=False) True >>> op3 = qml.RX(np.array(1.2, requires_grad=True), wires=0) >>> op4 = qml.RX(np.array(1.2, requires_grad=False), wires=0) >>> qml.equal(op3, op4) False >>> qml.equal(op3, op4, check_trainability=False) True >>> qml.equal(Controlled(op3, control_wires=1), Controlled(op4, control_wires=1)) False >>> qml.equal(Controlled(op3, control_wires=1), Controlled(op4, control_wires=1), check_trainability=False) True """ if isinstance(op2, (Hamiltonian, Tensor)): op1, op2 = op2, op1 dispatch_result = _equal( op1, op2, check_interface=check_interface, check_trainability=check_trainability, atol=atol, rtol=rtol, ) if isinstance(dispatch_result, str): return False return dispatch_result
[docs]def assert_equal( op1: Union[Operator, MeasurementProcess, QuantumScript], op2: Union[Operator, MeasurementProcess, QuantumScript], check_interface=True, check_trainability=True, rtol=1e-5, atol=1e-9, ) -> None: """Function to assert that two operators, measurements, or tapes are equal Args: op1 (.Operator or .MeasurementProcess or .QuantumTape): First object to compare op2 (.Operator or .MeasurementProcess or .QuantumTape): Second object to compare check_interface (bool, optional): Whether to compare interfaces. Default: ``True``. check_trainability (bool, optional): Whether to compare trainability status. Default: ``True``. rtol (float, optional): Relative tolerance for parameters. atol (float, optional): Absolute tolerance for parameters. Returns: None Raises: AssertionError: An ``AssertionError`` is raised if the two operators are not equal. .. seealso:: :func:`~.equal` **Example** >>> op1 = qml.RX(np.array(0.12), wires=0) >>> op2 = qml.RX(np.array(1.23), wires=0) >>> qml.assert_equal(op1, op2) AssertionError: op1 and op2 have different data. Got (array(0.12),) and (array(1.23),) >>> h1 = qml.Hamiltonian([1, 2], [qml.PauliX(0), qml.PauliY(1)]) >>> h2 = qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.PauliY(1)]) >>> qml.assert_equal(h1, h2) AssertionError: op1 and op2 have different operands because op1 and op2 have different scalars. Got 2 and 1 """ dispatch_result = _equal( op1, op2, check_interface=check_interface, check_trainability=check_trainability, atol=atol, rtol=rtol, ) if isinstance(dispatch_result, str): raise AssertionError(dispatch_result) if not dispatch_result: raise AssertionError(f"{op1} and {op2} are not equal for an unspecified reason.")
def _equal( op1, op2, check_interface=True, check_trainability=True, rtol=1e-5, atol=1e-9, ) -> Union[bool, str]: # pylint: disable=unused-argument if not isinstance(op2, type(op1)) and not isinstance(op1, Observable): return f"op1 and op2 are of different types. Got {type(op1)} and {type(op2)}." return _equal_dispatch( op1, op2, check_interface=check_interface, check_trainability=check_trainability, atol=atol, rtol=rtol, ) @singledispatch def _equal_dispatch( op1, op2, check_interface=True, check_trainability=True, rtol=1e-5, atol=1e-9, ) -> Union[bool, str]: # pylint: disable=unused-argument raise NotImplementedError(f"Comparison of {type(op1)} and {type(op2)} not implemented") @_equal_dispatch.register def _equal_circuit( op1: qml.tape.QuantumScript, op2: qml.tape.QuantumScript, check_interface=True, check_trainability=True, rtol=1e-5, atol=1e-9, ): # operations if len(op1.operations) != len(op2.operations): return False for comparands in zip(op1.operations, op2.operations): if not qml.equal( comparands[0], comparands[1], check_interface=check_interface, check_trainability=check_trainability, rtol=rtol, atol=atol, ): return False # measurements if len(op1.measurements) != len(op2.measurements): return False for comparands in zip(op1.measurements, op2.measurements): if not qml.equal( comparands[0], comparands[1], check_interface=check_interface, check_trainability=check_trainability, rtol=rtol, atol=atol, ): return False if op1.shots != op2.shots: return False if op1.trainable_params != op2.trainable_params: return False return True @_equal_dispatch.register def _equal_operators( op1: Operator, op2: Operator, check_interface=True, check_trainability=True, rtol=1e-5, atol=1e-9, ): """Default function to determine whether two Operator objects are equal.""" if not isinstance( op2, type(op1) ): # clarifies cases involving PauliX/Y/Z (Observable/Operation) return f"op1 and op2 are of different types. Got {type(op1)} and {type(op2)}" if isinstance(op1, qml.Identity): # All Identities are equivalent, independent of wires. # We already know op1 and op2 are of the same type, so no need to check # that op2 is also an Identity return True if op1.arithmetic_depth != op2.arithmetic_depth: return f"op1 and op2 have different arithmetic depths. Got {op1.arithmetic_depth} and {op2.arithmetic_depth}" if op1.arithmetic_depth > 0: # Other dispatches cover cases of operations with arithmetic depth > 0. # If any new operations are added with arithmetic depth > 0, a new dispatch # should be created for them. return f"op1 and op2 have arithmetic depth > 0. Got arithmetic depth {op1.arithmetic_depth}" if op1.wires != op2.wires: return f"op1 and op2 have different wires. Got {op1.wires} and {op2.wires}." if op1.hyperparameters != op2.hyperparameters: return ( "The hyperparameters are not equal for op1 and op2.\n" f"Got {op1.hyperparameters}\n and {op2.hyperparameters}." ) if any(qml.math.is_abstract(d) for d in op1.data + op2.data): # assume all tracers are independent return "Data contains a tracer. Abstract tracers are assumed to be unique." if not all( qml.math.allclose(d1, d2, rtol=rtol, atol=atol) for d1, d2 in zip(op1.data, op2.data) ): return f"op1 and op2 have different data.\nGot {op1.data} and {op2.data}" if check_trainability: for params1, params2 in zip(op1.data, op2.data): params1_train = qml.math.requires_grad(params1) params2_train = qml.math.requires_grad(params2) if params1_train != params2_train: return ( "Parameters have different trainability.\n " f"{params1} trainability is {params1_train} and {params2} trainability is {params2_train}" ) if check_interface: for params1, params2 in zip(op1.data, op2.data): params1_interface = qml.math.get_interface(params1) params2_interface = qml.math.get_interface(params2) if params1_interface != params2_interface: return ( "Parameters have different interfaces.\n " f"{params1} interface is {params1_interface} and {params2} interface is {params2_interface}" ) return True @_equal_dispatch.register # pylint: disable=unused-argument, protected-access def _equal_prod_and_sum(op1: CompositeOp, op2: CompositeOp, **kwargs): """Determine whether two Prod or Sum objects are equal""" if op1.pauli_rep is not None and (op1.pauli_rep == op2.pauli_rep): # shortcut check return True if len(op1.operands) != len(op2.operands): return f"op1 and op2 have different number of operands. Got {len(op1.operands)} and {len(op2.operands)}" # organizes by wire indicies while respecting commutation relations sorted_ops1 = op1._sort(op1.operands) sorted_ops2 = op2._sort(op2.operands) for o1, o2 in zip(sorted_ops1, sorted_ops2): op_check = _equal(o1, o2, **kwargs) if isinstance(op_check, str): return OPERANDS_MISMATCH_ERROR_MESSAGE + op_check return True @_equal_dispatch.register def _equal_controlled(op1: Controlled, op2: Controlled, **kwargs): """Determine whether two Controlled or ControlledOp objects are equal""" if op1.arithmetic_depth != op2.arithmetic_depth: return f"op1 and op2 have different arithmetic depths. Got {op1.arithmetic_depth} and {op2.arithmetic_depth}" # op.base.wires compared in return if op1.work_wires != op2.work_wires: return f"op1 and op2 have different work wires. Got {op1.work_wires} and {op2.work_wires}" # work wires and control_wire/control_value combinations compared here op1_control_dict = dict(zip(op1.control_wires, op1.control_values)) op2_control_dict = dict(zip(op2.control_wires, op2.control_values)) if op1_control_dict != op2_control_dict: return f"op1 and op2 have different control dictionaries. Got {op1_control_dict} and {op2_control_dict}" base_equal_check = _equal(op1.base, op2.base, **kwargs) if isinstance(base_equal_check, str): return BASE_OPERATION_MISMATCH_ERROR_MESSAGE + base_equal_check return True @_equal_dispatch.register def _equal_controlled_sequence(op1: ControlledSequence, op2: ControlledSequence, **kwargs): """Determine whether two ControlledSequences are equal""" if op1.wires != op2.wires: return f"op1 and op2 have different wires. Got {op1.wires} and {op2.wires}." if op1.arithmetic_depth != op2.arithmetic_depth: return f"op1 and op2 have different arithmetic depths. Got {op1.arithmetic_depth} and {op2.arithmetic_depth}" base_equal_check = _equal(op1.base, op2.base, **kwargs) if isinstance(base_equal_check, str): return BASE_OPERATION_MISMATCH_ERROR_MESSAGE + base_equal_check return True @_equal_dispatch.register # pylint: disable=unused-argument def _equal_pow(op1: Pow, op2: Pow, **kwargs): """Determine whether two Pow objects are equal""" check_interface, check_trainability = kwargs["check_interface"], kwargs["check_trainability"] if check_interface: interface1 = qml.math.get_interface(op1.z) interface2 = qml.math.get_interface(op2.z) if interface1 != interface2: return ( "Exponent have different interfaces.\n" f"{op1.z} interface is {interface1} and {op2.z} interface is {interface2}" ) if check_trainability: grad1 = qml.math.requires_grad(op1.z) grad2 = qml.math.requires_grad(op2.z) if grad1 != grad2: return ( "Exponent have different trainability.\n" f"{op1.z} interface is {grad1} and {op2.z} interface is {grad2}" ) if op1.z != op2.z: return f"Exponent are different. Got {op1.z} and {op2.z}" base_equal_check = _equal(op1.base, op2.base, **kwargs) if isinstance(base_equal_check, str): return BASE_OPERATION_MISMATCH_ERROR_MESSAGE + base_equal_check return True @_equal_dispatch.register # pylint: disable=unused-argument def _equal_adjoint(op1: Adjoint, op2: Adjoint, **kwargs): """Determine whether two Adjoint objects are equal""" # first line of top-level equal function already confirms both are Adjoint - only need to compare bases base_equal_check = _equal(op1.base, op2.base, **kwargs) if isinstance(base_equal_check, str): return BASE_OPERATION_MISMATCH_ERROR_MESSAGE + base_equal_check return True @_equal_dispatch.register def _equal_conditional(op1: Conditional, op2: Conditional, **kwargs): """Determine whether two Conditional objects are equal""" # first line of top-level equal function already confirms both are Conditionaly - only need to compare bases and meas_val return qml.equal(op1.base, op2.base, **kwargs) and qml.equal( op1.meas_val, op2.meas_val, **kwargs ) @_equal_dispatch.register # pylint: disable=unused-argument def _equal_measurement_value(op1: MeasurementValue, op2: MeasurementValue, **kwargs): """Determine whether two MeasurementValue objects are equal""" return op1.measurements == op2.measurements @_equal_dispatch.register # pylint: disable=unused-argument def _equal_exp(op1: Exp, op2: Exp, **kwargs): """Determine whether two Exp objects are equal""" check_interface, check_trainability, rtol, atol = ( kwargs["check_interface"], kwargs["check_trainability"], kwargs["rtol"], kwargs["atol"], ) if check_interface: for params1, params2 in zip(op1.data, op2.data): params1_interface = qml.math.get_interface(params1) params2_interface = qml.math.get_interface(params2) if params1_interface != params2_interface: return ( "Parameters have different interfaces.\n" f"{params1} interface is {params1_interface} and {params2} interface is {params2_interface}" ) if check_trainability: for params1, params2 in zip(op1.data, op2.data): params1_trainability = qml.math.requires_grad(params1) params2_trainability = qml.math.requires_grad(params2) if params1_trainability != params2_trainability: return ( "Parameters have different trainability.\n" f"{params1} trainability is {params1_trainability} and {params2} trainability is {params2_trainability}" ) if not qml.math.allclose(op1.coeff, op2.coeff, rtol=rtol, atol=atol): return f"op1 and op2 have different coefficients. Got {op1.coeff} and {op2.coeff}" equal_check = _equal(op1.base, op2.base, **kwargs) if isinstance(equal_check, str): return BASE_OPERATION_MISMATCH_ERROR_MESSAGE + equal_check return True @_equal_dispatch.register # pylint: disable=unused-argument def _equal_sprod(op1: SProd, op2: SProd, **kwargs): """Determine whether two SProd objects are equal""" check_interface, check_trainability, rtol, atol = ( kwargs["check_interface"], kwargs["check_trainability"], kwargs["rtol"], kwargs["atol"], ) if check_interface: for params1, params2 in zip(op1.data, op2.data): params1_interface = qml.math.get_interface(params1) params2_interface = qml.math.get_interface(params2) if params1_interface != params2_interface: return ( "Parameters have different interfaces.\n " f"{params1} interface is {params1_interface} and {params2} interface is {params2_interface}" ) if check_trainability: for params1, params2 in zip(op1.data, op2.data): params1_train = qml.math.requires_grad(params1) params2_train = qml.math.requires_grad(params2) if params1_train != params2_train: return ( "Parameters have different trainability.\n " f"{params1} trainability is {params1_train} and {params2} trainability is {params2_train}" ) if op1.pauli_rep is not None and (op1.pauli_rep == op2.pauli_rep): # shortcut check return True if not qml.math.allclose(op1.scalar, op2.scalar, rtol=rtol, atol=atol): return f"op1 and op2 have different scalars. Got {op1.scalar} and {op2.scalar}" equal_check = _equal(op1.base, op2.base, **kwargs) if isinstance(equal_check, str): return BASE_OPERATION_MISMATCH_ERROR_MESSAGE + equal_check return True @_equal_dispatch.register # pylint: disable=unused-argument def _equal_tensor(op1: Tensor, op2: Observable, **kwargs): """Determine whether a Tensor object is equal to a Hamiltonian/Tensor""" if not isinstance(op2, Observable): return f"{op2} is not of type Observable" if isinstance(op2, (Hamiltonian, LinearCombination, Hermitian)): return ( op2.compare(op1) or f"'{op1}' and '{op2}' are not the same for an unspecified reason." ) if isinstance(op2, Tensor): return ( op1._obs_data() == op2._obs_data() # pylint: disable=protected-access or f"{op1} and {op2} have different _obs_data outputs" ) return f"{op1} is of type {type(op1)} and {op2} is of type {type(op2)}" @_equal_dispatch.register # pylint: disable=unused-argument def _equal_hamiltonian(op1: Hamiltonian, op2: Observable, **kwargs): """Determine whether a Hamiltonian object is equal to a Hamiltonian/Tensor objects""" if not isinstance(op2, Observable): return f"{op2} is not of type Observable" return op1.compare(op2) or f"'{op1}' and '{op2}' are not the same for an unspecified reason" @_equal_dispatch.register def _equal_parametrized_evolution(op1: ParametrizedEvolution, op2: ParametrizedEvolution, **kwargs): # check times match if op1.t is None or op2.t is None: if not (op1.t is None and op2.t is None): return False elif not qml.math.allclose(op1.t, op2.t): return False # check parameters passed to operator match operator_check = _equal_operators(op1, op2, **kwargs) if isinstance(operator_check, str): return False # check H.coeffs match if not all(c1 == c2 for c1, c2 in zip(op1.H.coeffs, op2.H.coeffs)): return False # check that all the base operators on op1.H and op2.H match return all(equal(o1, o2, **kwargs) for o1, o2 in zip(op1.H.ops, op2.H.ops)) @_equal_dispatch.register # pylint: disable=unused-argument def _equal_measurements( op1: MeasurementProcess, op2: MeasurementProcess, check_interface=True, check_trainability=True, rtol=1e-5, atol=1e-9, ): """Determine whether two MeasurementProcess objects are equal""" if op1.obs is not None and op2.obs is not None: return equal( op1.obs, op2.obs, check_interface=check_interface, check_trainability=check_trainability, rtol=rtol, atol=atol, ) if op1.mv is not None and op2.mv is not None: if isinstance(op1.mv, MeasurementValue) and isinstance(op2.mv, MeasurementValue): return qml.equal(op1.mv, op2.mv) if qml.math.is_abstract(op1.mv) or qml.math.is_abstract(op2.mv): return op1.mv is op2.mv if isinstance(op1.mv, Iterable) and isinstance(op2.mv, Iterable): if len(op1.mv) == len(op2.mv): return all(mv1.measurements == mv2.measurements for mv1, mv2 in zip(op1.mv, op2.mv)) return False if op1.wires != op2.wires: return False if op1.obs is None and op2.obs is None: # only compare eigvals if both observables are None. # Can be expensive to compute for large observables if op1.eigvals() is not None and op2.eigvals() is not None: return qml.math.allclose(op1.eigvals(), op2.eigvals(), rtol=rtol, atol=atol) return op1.eigvals() is None and op2.eigvals() is None return False @_equal_dispatch.register def _equal_mid_measure(op1: MidMeasureMP, op2: MidMeasureMP, **_): return ( op1.wires == op2.wires and op1.id == op2.id and op1.reset == op2.reset and op1.postselect == op2.postselect ) @_equal_dispatch.register # pylint: disable=unused-argument def _(op1: VnEntropyMP, op2: VnEntropyMP, **kwargs): """Determine whether two MeasurementProcess objects are equal""" eq_m = _equal_measurements(op1, op2, **kwargs) log_base_match = op1.log_base == op2.log_base return eq_m and log_base_match @_equal_dispatch.register # pylint: disable=unused-argument def _(op1: MutualInfoMP, op2: MutualInfoMP, **kwargs): """Determine whether two MeasurementProcess objects are equal""" eq_m = _equal_measurements(op1, op2, **kwargs) log_base_match = op1.log_base == op2.log_base return eq_m and log_base_match @_equal_dispatch.register # pylint: disable=unused-argument def _equal_shadow_measurements(op1: ShadowExpvalMP, op2: ShadowExpvalMP, **_): """Determine whether two ShadowExpvalMP objects are equal""" wires_match = op1.wires == op2.wires if isinstance(op1.H, Operator) and isinstance(op2.H, Operator): H_match = equal(op1.H, op2.H) elif isinstance(op1.H, Iterable) and isinstance(op2.H, Iterable): H_match = all(equal(o1, o2) for o1, o2 in zip(op1.H, op2.H)) else: return False k_match = op1.k == op2.k return wires_match and H_match and k_match @_equal_dispatch.register def _equal_counts(op1: CountsMP, op2: CountsMP, **kwargs): return _equal_measurements(op1, op2, **kwargs) and op1.all_outcomes == op2.all_outcomes @_equal_dispatch.register # pylint: disable=unused-argument def _equal_basis_rotation( op1: qml.BasisRotation, op2: qml.BasisRotation, check_interface=True, check_trainability=True, rtol=1e-5, atol=1e-9, ): if not qml.math.allclose( op1.hyperparameters["unitary_matrix"], op2.hyperparameters["unitary_matrix"], atol=atol, rtol=rtol, ): return ( "The hyperparameter unitary_matrix is not equal for op1 and op2.\n" f"Got {op1.hyperparameters['unitary_matrix']}\n and {op2.hyperparameters['unitary_matrix']}." ) if op1.wires != op2.wires: return f"op1 and op2 have different wires. Got {op1.wires} and {op2.wires}." if check_interface: interface1 = qml.math.get_interface(op1.hyperparameters["unitary_matrix"]) interface2 = qml.math.get_interface(op2.hyperparameters["unitary_matrix"]) if interface1 != interface2: return ( "The hyperparameter unitary_matrix has different interfaces for op1 and op2." f" Got {interface1} and {interface2}." ) return True @_equal_dispatch.register def _equal_hilbert_schmidt( op1: qml.HilbertSchmidt, op2: qml.HilbertSchmidt, check_interface=True, check_trainability=True, rtol=1e-5, atol=1e-9, ): if not all( qml.math.allclose(d1, d2, rtol=rtol, atol=atol) for d1, d2 in zip(op1.data, op2.data) ): return False if check_trainability: for params_1, params_2 in zip(op1.data, op2.data): if qml.math.requires_grad(params_1) != qml.math.requires_grad(params_2): return False if check_interface: for params_1, params_2 in zip(op1.data, op2.data): if qml.math.get_interface(params_1) != qml.math.get_interface(params_2): return False equal_kwargs = { "check_interface": check_interface, "check_trainability": check_trainability, "atol": atol, "rtol": rtol, } # Check hyperparameters using qml.equal rather than == where necessary if op1.hyperparameters["v_wires"] != op2.hyperparameters["v_wires"]: return False if not qml.equal(op1.hyperparameters["u_tape"], op2.hyperparameters["u_tape"], **equal_kwargs): return False if not qml.equal(op1.hyperparameters["v_tape"], op2.hyperparameters["v_tape"], **equal_kwargs): return False if op1.hyperparameters["v_function"] != op2.hyperparameters["v_function"]: return False return True @_equal_dispatch.register def _equal_prep_sel_prep( op1: PrepSelPrep, op2: PrepSelPrep, **kwargs ): # pylint: disable=unused-argument """Determine whether two PrepSelPrep are equal""" if op1.control != op2.control: return f"op1 and op2 have different control wires. Got {op1.control} and {op2.control}." if op1.wires != op2.wires: return f"op1 and op2 have different wires. Got {op1.wires} and {op2.wires}." if not qml.equal(op1.lcu, op2.lcu): return f"op1 and op2 have different lcu. Got {op1.lcu} and {op2.lcu}" return True