diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 4b0d907e4f..741f3f12e4 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -6,6 +6,9 @@ ### Improvements +* Use native C++ kernels for controlled-adjoint and adjoint-controlled of supported operations. + [(#1063)](https://github.com/PennyLaneAI/pennylane-lightning/pull/1063) + * In Lightning-Tensor, allow `qml.MPSPrep` to accept an MPS with `len(MPS) = n_wires-1`. [(#1064)](https://github.com/PennyLaneAI/pennylane-lightning/pull/1064) diff --git a/pennylane_lightning/core/_serialize.py b/pennylane_lightning/core/_serialize.py index 40338b991f..0aaa938d5b 100644 --- a/pennylane_lightning/core/_serialize.py +++ b/pennylane_lightning/core/_serialize.py @@ -443,8 +443,18 @@ def serialize_ops(self, tape: QuantumTape, wires_map: dict = None) -> Tuple[ uses_stateprep = False def get_wires(operation, single_op): - if isinstance(operation, qml.ops.op_math.Controlled) and not isinstance( - operation, + # Serialize adjoint(op) and adjoint(ctrl(op)) + if isinstance(operation, qml.ops.op_math.Adjoint): + inverse = True + op_base = operation.base + single_op_base = single_op.base + else: + inverse = False + op_base = operation + single_op_base = single_op + + if isinstance(op_base, qml.ops.op_math.Controlled) and not isinstance( + op_base, ( qml.CNOT, qml.CY, @@ -457,19 +467,41 @@ def get_wires(operation, single_op): qml.CSWAP, ), ): - name = operation.base.name - wires_list = list(operation.target_wires) - controlled_wires_list = list(operation.control_wires) - control_values_list = operation.control_values + wires_list = list(op_base.target_wires) + controlled_wires_list = list(op_base.control_wires) + control_values_list = op_base.control_values + # Serialize ctrl(adjoint(op)) + if isinstance(op_base.base, qml.ops.op_math.Adjoint): + ctrl_adjoint = True + name = op_base.base.base.name + else: + ctrl_adjoint = False + name = op_base.base.name + + # Inside the controlled operation, if the base operation (of the adjoint) + # is supported natively, we apply the the base operation and invert the + # inverse flag; otherwise we apply the QubitUnitary of a matrix which + # contains the inverse and leave the inverse flag as is. if not hasattr(self.sv_type, name): - single_op = QubitUnitary(matrix(single_op.base), single_op.base.wires) - name = single_op.name + single_op_base = QubitUnitary( + matrix(single_op_base.base), single_op_base.base.wires + ) + name = single_op_base.name + else: + inverse ^= ctrl_adjoint else: - name = single_op.name - wires_list = single_op.wires.tolist() + name = single_op_base.name + wires_list = single_op_base.wires.tolist() controlled_wires_list = [] control_values_list = [] - return single_op, name, list(wires_list), controlled_wires_list, control_values_list + return ( + single_op_base, + name, + inverse, + list(wires_list), + controlled_wires_list, + control_values_list, + ) for operation in tape.operations: if isinstance(operation, (BasisState, StatePrep)): @@ -480,30 +512,29 @@ def get_wires(operation, single_op): else: op_list = [operation] - inverse = isinstance(operation, qml.ops.op_math.Adjoint) - for single_op in op_list: ( - single_op, + single_op_base, name, + inverse, wires_list, controlled_wires_list, controlled_values_list, ) = get_wires(operation, single_op) inverses.append(inverse) - names.append(single_op.base.name if inverse else name) + names.append(name) # QubitUnitary is a special case, it has a parameter which is not differentiable. # We thus pass a dummy 0.0 parameter which will not be referenced - if isinstance(single_op, qml.QubitUnitary): + if isinstance(single_op_base, qml.QubitUnitary): params.append([0.0]) - mats.append(matrix(single_op)) + mats.append(matrix(single_op_base)) else: - if hasattr(self.sv_type, single_op.base.name if inverse else name): - params.append(single_op.parameters) + if hasattr(self.sv_type, name): + params.append(single_op_base.parameters) mats.append([]) else: params.append([]) - mats.append(matrix(single_op)) + mats.append(matrix(single_op_base)) controlled_values.append(controlled_values_list) controlled_wires.append( diff --git a/pennylane_lightning/core/_state_vector_base.py b/pennylane_lightning/core/_state_vector_base.py index 8815e13a04..a717777f55 100644 --- a/pennylane_lightning/core/_state_vector_base.py +++ b/pennylane_lightning/core/_state_vector_base.py @@ -21,6 +21,7 @@ import numpy as np from pennylane import BasisState, StatePrep from pennylane.measurements import MidMeasureMP +from pennylane.ops import Controlled from pennylane.tape import QuantumScript from pennylane.wires import Wires @@ -131,11 +132,12 @@ def _apply_basis_state(self, state, wires): self._qubit_state.setBasisState(list(state), list(wires)) @abstractmethod - def _apply_lightning_controlled(self, operation): + def _apply_lightning_controlled(self, operation: Controlled, adjoint: bool): """Apply an arbitrary controlled operation to the state tensor. Args: operation (~pennylane.operation.Operation): controlled operation to apply + adjoint (bool): Apply the adjoint of the operation if True Returns: None diff --git a/pennylane_lightning/core/_version.py b/pennylane_lightning/core/_version.py index 2f93c37d80..49b5a80118 100644 --- a/pennylane_lightning/core/_version.py +++ b/pennylane_lightning/core/_version.py @@ -16,4 +16,4 @@ Version number (major.minor.patch[-label]) """ -__version__ = "0.41.0-dev24" +__version__ = "0.41.0-dev25" diff --git a/pennylane_lightning/lightning_gpu/_state_vector.py b/pennylane_lightning/lightning_gpu/_state_vector.py index cbf617af73..afa75387d9 100644 --- a/pennylane_lightning/lightning_gpu/_state_vector.py +++ b/pennylane_lightning/lightning_gpu/_state_vector.py @@ -229,34 +229,39 @@ def _apply_state_vector(self, state, device_wires, use_async: bool = False): # set the state vector on GPU with provided state and their corresponding wires self._qubit_state.setStateVector(state, list(device_wires), use_async) - def _apply_lightning_controlled(self, operation): + def _apply_lightning_controlled(self, operation, adjoint): """Apply an arbitrary controlled operation to the state tensor. Args: operation (~pennylane.operation.Operation): controlled operation to apply + adjoint (bool): Apply the adjoint of the operation if True Returns: None """ state = self.state_vector - basename = operation.base.name - method = getattr(state, f"{basename}", None) + if isinstance(operation.base, Adjoint): + base_operation = operation.base.base + adjoint = not adjoint + else: + base_operation = operation.base + + method = getattr(state, f"{base_operation.name}", None) control_wires = list(operation.control_wires) control_values = operation.control_values target_wires = list(operation.target_wires) if method: # apply n-controlled specialized gate - inv = False param = operation.parameters - method(control_wires, control_values, target_wires, inv, param) + method(control_wires, control_values, target_wires, adjoint, param) else: # apply gate as an n-controlled matrix method = getattr(state, "applyControlledMatrix") method( - qml.matrix(operation.base), + qml.matrix(base_operation), control_wires, control_values, target_wires, - False, + adjoint, ) def _apply_lightning_midmeasure( @@ -300,6 +305,7 @@ def _apply_lightning( postselection. Use ``"hw-like"`` to discard invalid shots and ``"fill-shots"`` to keep the same number of shots. Default is ``None``. + Returns: None """ @@ -311,11 +317,12 @@ def _apply_lightning( if isinstance(operation, qml.Identity): continue if isinstance(operation, Adjoint): - name = operation.base.name + op_adjoint_base = operation.base invert_param = True else: - name = operation.name + op_adjoint_base = operation invert_param = False + name = op_adjoint_base.name method = getattr(state, name, None) wires = list(operation.wires) @@ -330,13 +337,13 @@ def _apply_lightning( param = operation.parameters method(wires, invert_param, param) elif ( - isinstance(operation, qml.ops.Controlled) and not self._mpi_handler.use_mpi + isinstance(op_adjoint_base, qml.ops.Controlled) and not self._mpi_handler.use_mpi ): # MPI backend does not have native controlled gates support - self._apply_lightning_controlled(operation) + self._apply_lightning_controlled(op_adjoint_base, invert_param) elif ( self._mpi_handler.use_mpi - and isinstance(operation, qml.ops.Controlled) - and isinstance(operation.base, qml.GlobalPhase) + and isinstance(op_adjoint_base, qml.ops.Controlled) + and isinstance(op_adjoint_base.base, qml.GlobalPhase) ): # TODO: To move this line to the _apply_lightning_controlled method once the MPI backend supports controlled gates natively raise DeviceError( @@ -348,7 +355,6 @@ def _apply_lightning( except AttributeError: # pragma: no cover # To support older versions of PL mat = operation.matrix - r_dtype = np.float32 if self.dtype == np.complex64 else np.float64 param = ( [[r_dtype(operation.hash)]] diff --git a/pennylane_lightning/lightning_kokkos/_state_vector.py b/pennylane_lightning/lightning_kokkos/_state_vector.py index 062284def4..922895b899 100644 --- a/pennylane_lightning/lightning_kokkos/_state_vector.py +++ b/pennylane_lightning/lightning_kokkos/_state_vector.py @@ -181,34 +181,39 @@ def _apply_state_vector(self, state, device_wires: Wires): # This operate on device self._qubit_state.setStateVector(state, list(device_wires)) - def _apply_lightning_controlled(self, operation): + def _apply_lightning_controlled(self, operation, adjoint): """Apply an arbitrary controlled operation to the state tensor. Args: operation (~pennylane.operation.Operation): controlled operation to apply + adjoint (bool): Apply the adjoint of the operation if True Returns: None """ state = self.state_vector - basename = operation.base.name - method = getattr(state, f"{basename}", None) + if isinstance(operation.base, Adjoint): + base_operation = operation.base.base + adjoint = not adjoint + else: + base_operation = operation.base + + method = getattr(state, f"{base_operation.name}", None) control_wires = list(operation.control_wires) control_values = operation.control_values target_wires = list(operation.target_wires) - inv = False # TODO: update to use recursive _apply_lightning to handle nested adjoint/ctrl if method is not None: # apply n-controlled specialized gate param = operation.parameters - method(control_wires, control_values, target_wires, inv, param) + method(control_wires, control_values, target_wires, adjoint, param) else: # apply gate as an n-controlled matrix method = getattr(state, "applyControlledMatrix") method( - qml.matrix(operation.base), + qml.matrix(base_operation), control_wires, control_values, target_wires, - inv, + adjoint, ) def _apply_lightning_midmeasure( @@ -262,11 +267,12 @@ def _apply_lightning( if isinstance(operation, qml.Identity): continue if isinstance(operation, Adjoint): - name = operation.base.name + op_adjoint_base = operation.base invert_param = True else: - name = operation.name + op_adjoint_base = operation invert_param = False + name = op_adjoint_base.name method = getattr(state, name, None) wires = list(operation.wires) @@ -279,18 +285,17 @@ def _apply_lightning( ) elif isinstance(operation, qml.PauliRot): method = getattr(state, "applyPauliRot") - # pylint: disable=protected-access - paulis = operation._hyperparameters[ + paulis = operation._hyperparameters[ # pylint: disable=protected-access "pauli_word" - ] # pylint: disable=protected-access + ] wires = [i for i, w in zip(wires, paulis) if w != "I"] word = "".join(p for p in paulis if p != "I") method(wires, invert_param, operation.parameters, word) elif method is not None: # apply specialized gate param = operation.parameters method(wires, invert_param, param) - elif isinstance(operation, qml.ops.Controlled): # apply n-controlled gate - self._apply_lightning_controlled(operation) + elif isinstance(op_adjoint_base, qml.ops.Controlled): # apply n-controlled gate + self._apply_lightning_controlled(op_adjoint_base, invert_param) else: # apply gate as a matrix # Inverse can be set to False since qml.matrix(operation) is already in # inverted form diff --git a/pennylane_lightning/lightning_qubit/_state_vector.py b/pennylane_lightning/lightning_qubit/_state_vector.py index 62068dcbd7..1742fda3c1 100644 --- a/pennylane_lightning/lightning_qubit/_state_vector.py +++ b/pennylane_lightning/lightning_qubit/_state_vector.py @@ -106,34 +106,39 @@ def _apply_state_vector(self, state, device_wires: Wires): self._qubit_state.setStateVector(state, list(device_wires)) - def _apply_lightning_controlled(self, operation): + def _apply_lightning_controlled(self, operation, adjoint): """Apply an arbitrary controlled operation to the state tensor. Args: operation (~pennylane.operation.Operation): controlled operation to apply + adjoint (bool): Apply the adjoint of the operation if True Returns: None """ state = self.state_vector - basename = operation.base.name - method = getattr(state, f"{basename}", None) + if isinstance(operation.base, Adjoint): + base_operation = operation.base.base + adjoint = not adjoint + else: + base_operation = operation.base + + method = getattr(state, f"{base_operation.name}", None) control_wires = list(operation.control_wires) control_values = operation.control_values target_wires = list(operation.target_wires) if method is not None: # apply n-controlled specialized gate - inv = False param = operation.parameters - method(control_wires, control_values, target_wires, inv, param) + method(control_wires, control_values, target_wires, adjoint, param) else: # apply gate as an n-controlled matrix method = getattr(state, "applyControlledMatrix") method( - qml.matrix(operation.base), + qml.matrix(base_operation), control_wires, control_values, target_wires, - False, + adjoint, ) def _apply_lightning_midmeasure( @@ -187,11 +192,12 @@ def _apply_lightning( if isinstance(operation, qml.Identity): continue if isinstance(operation, Adjoint): - name = operation.base.name + op_adjoint_base = operation.base invert_param = True else: - name = operation.name + op_adjoint_base = operation invert_param = False + name = op_adjoint_base.name method = getattr(state, name, None) wires = list(operation.wires) @@ -213,8 +219,8 @@ def _apply_lightning( elif method is not None: # apply specialized gate param = operation.parameters method(wires, invert_param, param) - elif isinstance(operation, qml.ops.Controlled): # apply n-controlled gate - self._apply_lightning_controlled(operation) + elif isinstance(op_adjoint_base, qml.ops.Controlled): # apply n-controlled gate + self._apply_lightning_controlled(op_adjoint_base, invert_param) else: # apply gate as a matrix # Inverse can be set to False since qml.matrix(operation) is already in # inverted form diff --git a/tests/test_gates.py b/tests/test_gates.py index 85efe13f40..d6c2236c57 100644 --- a/tests/test_gates.py +++ b/tests/test_gates.py @@ -506,14 +506,16 @@ def circuit(): qml.GlobalPhase, ], ) +@pytest.mark.parametrize("adjoint", [False, True]) @pytest.mark.parametrize("control_value", [False, True]) @pytest.mark.parametrize("n_qubits", list(range(2, 8))) -def test_controlled_qubit_gates(operation, n_qubits, control_value, tol): +def test_controlled_qubit_gates(operation, n_qubits, control_value, adjoint, tol): """Test that multi-controlled gates are correctly applied to a state""" dev_def = qml.device("default.qubit", wires=n_qubits) dev = qml.device(device_name, wires=n_qubits) threshold = 5 if device_name == "lightning.tensor" else 250 num_wires = max(operation.num_wires, 1) + operation = qml.adjoint(operation) if adjoint else operation for n_wires in range(num_wires + 1, num_wires + 4): wire_lists = list(itertools.permutations(range(0, n_qubits), n_wires)) @@ -676,3 +678,161 @@ def circuit(): circ = qml.QNode(circuit, dev) circ_def = qml.QNode(circuit, dev_def) assert np.allclose(circ(), circ_def(), tol) + + +@pytest.mark.parametrize( + "operation", + [ + qml.PauliX, + qml.PauliY, + qml.PauliZ, + qml.Hadamard, + qml.S, + qml.SX, + qml.T, + qml.PhaseShift, + qml.RX, + qml.RY, + qml.RZ, + qml.Rot, + qml.SWAP, + qml.IsingXX, + qml.IsingXY, + qml.IsingYY, + qml.IsingZZ, + qml.SingleExcitation, + qml.SingleExcitationMinus, + qml.SingleExcitationPlus, + qml.DoubleExcitation, + qml.DoubleExcitationMinus, + qml.DoubleExcitationPlus, + qml.MultiRZ, + qml.GlobalPhase, + ], +) +@pytest.mark.parametrize("adjoint", [False, True]) +@pytest.mark.parametrize("control_value", [False, True]) +@pytest.mark.parametrize("n_qubits", list(range(2, 8))) +def test_adjoint_controlled_qubit_gates(operation, n_qubits, control_value, tol, adjoint): + """Test that adjoint of multi-controlled gates are correctly applied to a state""" + dev_def = qml.device("default.qubit", wires=n_qubits) + dev = qml.device(device_name, wires=n_qubits) + threshold = 5 if device_name == "lightning.tensor" else 250 + num_wires = max(operation.num_wires, 1) + operation = qml.adjoint(operation) if adjoint else operation + + for n_wires in range(num_wires + 1, num_wires + 4): + wire_lists = list(itertools.permutations(range(0, n_qubits), n_wires)) + n_perms = len(wire_lists) * n_wires + if n_perms > threshold: + wire_lists = wire_lists[0 :: (n_perms // threshold)] + for all_wires in wire_lists: + target_wires = all_wires[0:num_wires] + control_wires = all_wires[num_wires:] + init_state = np.random.rand(2**n_qubits) + 1.0j * np.random.rand(2**n_qubits) + init_state /= np.linalg.norm(init_state) + + def circuit(): + qml.StatePrep(init_state, wires=range(n_qubits)) + qml.adjoint( + qml.ctrl( + ( + operation(target_wires) + if operation.num_params == 0 + else operation(*tuple([0.1234] * operation.num_params), target_wires) + ), + control_wires, + control_values=( + [control_value or bool(i % 2) for i, _ in enumerate(control_wires)] + if device_name != "lightning.tensor" + else [control_value for _ in control_wires] + ), + ) + ) + return qml.state() + + circ = qml.QNode(circuit, dev) + circ_def = qml.QNode(circuit, dev_def) + assert np.allclose(circ(), circ_def(), tol) + + +@pytest.mark.parametrize("control_value", [False, True]) +@pytest.mark.parametrize("n_qubits", list(range(2, 8))) +def test_adjoint_controlled_qubit_unitary(n_qubits, control_value, tol): + """Test that Adjoint of ControlledQubitUnitary is correctly applied to a state""" + dev_def = qml.device("default.qubit", wires=n_qubits) + dev = qml.device(device_name, wires=n_qubits) + threshold = 500 + for n_wires in range(1, 5): + wire_lists = list(itertools.permutations(range(0, n_qubits), n_wires)) + n_perms = len(wire_lists) * (n_wires) ** 2 + if n_perms > threshold: + wire_lists = wire_lists[0 :: (n_perms // threshold)] + for all_wires in wire_lists: + for i in range(1, len(all_wires)): + control_wires = all_wires[0:i] + target_wires = all_wires[i:] + m = 2 ** len(target_wires) + U = np.random.rand(m, m) + 1.0j * np.random.rand(m, m) + U, _ = np.linalg.qr(U) + init_state = np.random.rand(2**n_qubits) + 1.0j * np.random.rand(2**n_qubits) + init_state /= np.linalg.norm(init_state) + + def circuit(): + qml.StatePrep(init_state, wires=range(n_qubits)) + qml.adjoint( + qml.ControlledQubitUnitary( + U, + wires=control_wires + target_wires, + control_values=( + [control_value or bool(i % 2) for i, _ in enumerate(control_wires)] + if device_name != "lightning.tensor" + else [control_value for _ in control_wires] + ), + ) + ) + return qml.state() + + circ = qml.QNode(circuit, dev) + circ_def = qml.QNode(circuit, dev_def) + assert np.allclose(circ(), circ_def(), tol) + + +@pytest.mark.parametrize("control_value", [False, True]) +@pytest.mark.parametrize("n_qubits", list(range(2, 8))) +def test_controlled_adjoint_qubit_unitary(n_qubits, control_value, tol): + """Test that controlled adjoint(QubitUnitary) is correctly applied to a state""" + dev_def = qml.device("default.qubit", wires=n_qubits) + dev = qml.device(device_name, wires=n_qubits) + threshold = 500 + for n_wires in range(1, 5): + wire_lists = list(itertools.permutations(range(0, n_qubits), n_wires)) + n_perms = len(wire_lists) * (n_wires) ** 2 + if n_perms > threshold: + wire_lists = wire_lists[0 :: (n_perms // threshold)] + for all_wires in wire_lists: + for i in range(1, len(all_wires)): + control_wires = all_wires[0:i] + target_wires = all_wires[i:] + m = 2 ** len(target_wires) + U = np.random.rand(m, m) + 1.0j * np.random.rand(m, m) + U, _ = np.linalg.qr(U) + init_state = np.random.rand(2**n_qubits) + 1.0j * np.random.rand(2**n_qubits) + init_state /= np.linalg.norm(init_state) + + def circuit(): + qml.StatePrep(init_state, wires=range(n_qubits)) + qml.ctrl( + qml.adjoint(qml.QubitUnitary(U, wires=target_wires)), + control=control_wires, + control_values=( + [control_value or bool(i % 2) for i, _ in enumerate(control_wires)] + if device_name != "lightning.tensor" + else [control_value for _ in control_wires] + ), + ) + return qml.state() + + circ = qml.QNode(circuit, dev) + circ_def = qml.QNode(circuit, dev_def) + assert np.allclose(circ(), circ_def(), tol) diff --git a/tests/test_serialize.py b/tests/test_serialize.py index ccf66640d3..976b1a5bf9 100644 --- a/tests/test_serialize.py +++ b/tests/test_serialize.py @@ -650,7 +650,7 @@ def test_Rot_in_circuit(self, wires_map): @pytest.mark.parametrize("wires_map", [wires_dict, None]) def test_basic_circuit_not_implemented_ctrl_ops(self, wires_map): - """Test expected serialization for a simple circuit""" + """Test expected serialization for circuit with a controlled operation that is not implemented""" ops = qml.OrbitalRotation(0.1234, wires=range(4)) with qml.tape.QuantumTape() as tape: qml.RX(0.4, wires=0) @@ -679,7 +679,7 @@ def test_basic_circuit_not_implemented_ctrl_ops(self, wires_map): @pytest.mark.parametrize("wires_map", [wires_dict, None]) def test_multicontrolledx(self, wires_map): - """Test expected serialization for a simple circuit""" + """Test expected serialization for a circuit with MultiControlledX""" with qml.tape.QuantumTape() as tape: qml.RX(0.4, wires=0) qml.RY(0.6, wires=1) @@ -757,6 +757,7 @@ def test_custom_wires_circuit(self): qml.SingleExcitation(0.5, wires=["a", 3.2]) qml.SingleExcitationPlus(0.4, wires=["a", 3.2]) qml.adjoint(qml.SingleExcitationMinus(0.5, wires=["a", 3.2]), lazy=False) + qml.adjoint(qml.SingleExcitationMinus(0.5, wires=["a", 3.2]), lazy=True) s = QuantumScriptSerializer(device_name).serialize_ops(tape, wires_dict) s_expected = ( @@ -768,18 +769,142 @@ def test_custom_wires_circuit(self): "SingleExcitation", "SingleExcitationPlus", "SingleExcitationMinus", + "SingleExcitationMinus", ], - [[0.4], [0.6], [], [0.5], [0.4], [-0.5]], - [[0], [1], [0, 1], [0, 1], [0, 1], [0, 1]], - [False, False, False, False, False, False], - [[], [], [], [], [], []], - [[], [], [], [], [], []], - [[], [], [], [], [], []], + [[0.4], [0.6], [], [0.5], [0.4], [-0.5], [0.5]], + [[0], [1], [0, 1], [0, 1], [0, 1], [0, 1], [0, 1]], + [False, False, False, False, False, False, True], + [[], [], [], [], [], [], []], + [[], [], [], [], [], [], []], + [[], [], [], [], [], [], []], ), False, ) assert s == s_expected + @pytest.mark.parametrize("wires_map", [wires_dict, None]) + def test_ctrl_inverse(self, wires_map): + """Test expected serialization for nested control adjoint operations""" + ops = qml.OrbitalRotation(0.1234, wires=range(4)) + with qml.tape.QuantumTape() as tape: + qml.ctrl(qml.RX(0.4, wires=0), [2, 3]) + qml.ctrl(qml.adjoint(qml.RX(0.4, wires=0), lazy=False), [2, 3]) + qml.ctrl(qml.adjoint(qml.RX(0.4, wires=0), lazy=True), [2, 3]) + qml.adjoint(qml.ctrl(qml.RX(0.4, wires=0), [2, 3])) + qml.adjoint(qml.ctrl(qml.adjoint(qml.RX(0.4, wires=0)), [2, 3])) + qml.ctrl(qml.adjoint(ops), [4, 5]) + qml.adjoint(qml.ctrl(ops, [4, 5])) + qml.adjoint(qml.ctrl(qml.adjoint(ops), [4, 5])) + + s = QuantumScriptSerializer(device_name).serialize_ops(tape, wires_map) + s_expected = ( + ( + ["RX", "RX", "RX", "RX", "RX", "QubitUnitary", "QubitUnitary", "QubitUnitary"], + [ + np.array([0.4]), + np.array([-0.4]), + np.array([0.4]), + np.array([0.4]), + np.array([0.4]), + [0.0], + [0.0], + [0.0], + ], + [[0], [0], [0], [0], [0], list(ops.wires), list(ops.wires), list(ops.wires)], + [False, False, True, True, False, False, True, True], + [ + [], + [], + [], + [], + [], + [qml.matrix(qml.adjoint(ops))], + [qml.matrix(ops)], + [qml.matrix(qml.adjoint(ops))], + ], + [[2, 3], [2, 3], [2, 3], [2, 3], [2, 3], [4, 5], [4, 5], [4, 5]], + ), + False, + ) + assert s[0][0] == s_expected[0][0] + assert s[0][1] == s_expected[0][1] + assert s[0][2] == s_expected[0][2] + assert s[0][3] == s_expected[0][3] + assert all(np.allclose(s0, s1) for s0, s1 in zip(s[0][4], s_expected[0][4])) + assert s[0][5] == s_expected[0][5] + assert s[1] == s_expected[1] + + @pytest.mark.parametrize("wires_map", [wires_dict, None]) + def test_ctrl_qubitunitary_inverse(self, wires_map): + """Test expected serialization for controlled qubit unitary with and without inverse""" + mat = qml.matrix(qml.RX(0.1234, wires=[0])) + op = qml.QubitUnitary(mat, wires=[0]) + with qml.tape.QuantumTape() as tape: + qml.ctrl(op, [4, 5]) + qml.adjoint(qml.ctrl(op, [4, 5])) + qml.ctrl(qml.adjoint(op, lazy=True), [4, 5]) + qml.adjoint(qml.ctrl(qml.adjoint(op, lazy=True), [4, 5]), lazy=True) + + s = QuantumScriptSerializer(device_name).serialize_ops(tape, wires_map) + s_expected = ( + ( + ["QubitUnitary", "QubitUnitary", "QubitUnitary", "QubitUnitary"], + [[0.0], [0.0], [0.0], [0.0]], + [list(op.wires), list(op.wires), list(op.wires), list(op.wires)], + [False, True, False, True], + [ + [qml.matrix(op)], + [qml.matrix(op)], + [qml.matrix(qml.adjoint(op))], + [qml.matrix(qml.adjoint(op))], + ], + [[4, 5], [4, 5], [4, 5], [4, 5]], + ), + False, + ) + assert s[0][0] == s_expected[0][0] + assert s[0][1] == s_expected[0][1] + assert s[0][2] == s_expected[0][2] + assert s[0][3] == s_expected[0][3] + assert all(np.allclose(s0, s1) for s0, s1 in zip(s[0][4], s_expected[0][4])) + assert s[0][5] == s_expected[0][5] + assert s[1] == s_expected[1] + + @pytest.mark.parametrize("wires_map", [wires_dict, None]) + def test_inverse(self, wires_map): + """Test expected serialization for adjoint gates and qubitunitary""" + mat = qml.matrix(qml.OrbitalRotation(0.1234, wires=range(4))) + with qml.tape.QuantumTape() as tape: + qml.adjoint(qml.SingleExcitationMinus(0.5, wires=[0, 1]), lazy=False) + qml.adjoint(qml.SingleExcitationMinus(0.5, wires=[0, 1]), lazy=True) + qml.adjoint(qml.QubitUnitary(mat, wires=range(4)), lazy=True) + + s = QuantumScriptSerializer(device_name).serialize_ops(tape, wires_map) + s_expected = ( + ( + [ + "SingleExcitationMinus", + "SingleExcitationMinus", + "QubitUnitary", + ], + [[-0.5], [0.5], [0.0]], + [[0, 1], [0, 1], list(range(4))], + [False, True, True], + [[], [], [mat]], + [[], [], []], + [[], [], []], + ), + False, + ) + + assert s[0][0] == s_expected[0][0] + assert s[0][1] == s_expected[0][1] + assert s[0][2] == s_expected[0][2] + assert s[0][3] == s_expected[0][3] + assert all(np.allclose(s0, s1) for s0, s1 in zip(s[0][4], s_expected[0][4])) + assert s[0][5] == s_expected[0][5] + assert s[1] == s_expected[1] + @pytest.mark.parametrize("wires_map", [wires_dict, None]) @pytest.mark.parametrize("C", [True, False]) def test_integration(self, C, wires_map):