From 98a9292f711573a8aae32978ad0e59e8ba18d261 Mon Sep 17 00:00:00 2001 From: Joseph Lee <40768758+josephleekl@users.noreply.github.com> Date: Wed, 26 Feb 2025 17:49:48 -0500 Subject: [PATCH] Use native implementation for adjoints in (control) operations (#1063) ### Before submitting Please complete the following checklist when submitting a PR: - [ ] All new features must include a unit test. If you've fixed a bug or added code that should be tested, add a test to the [`tests`](../tests) directory! - [ ] All new functions and code must be clearly commented and documented. If you do make documentation changes, make sure that the docs build and render correctly by running `make docs`. - [ ] Ensure that the test suite passes, by running `make test`. - [ ] Add a new entry to the `.github/CHANGELOG.md` file, summarizing the change, and including a link back to the PR. - [ ] Ensure that code is properly formatted by running `make format`. When all the above are checked, delete everything above the dashed line and fill in the pull request template. ------------------------------------------------------------------------------------------------------------ **Context:** Currently in `_apply_lightning`, we check for whether an operation is `Adjoint`, then we apply the operation with an adjoint (`inv_param`) flag. However, in cases where we have: - adjoint(s) within control - e.g. `control(adjoint(gate))` - control within adjoint - e.g. `adjoint(control(gate))`, these are all applied as matrices. **Description of the Change:** `_apply_lightning` and `_apply_lightning_controlled` checks for adjoint in an operation, and if it's an adjoint it applies the base operation with an adjoint flag, instead of treating everything as a matrix. So in effect we have: `control(adjoint(gate))` -> `control(gate with adjoint)` `adjoint(control(gate))` -> `control(gate with adjoint)` which are implemented natively in C++ (if the `gate` is supported), yielding better performance **Benefits:** adjoint(ctrl()) will see the most speedup, especially with large number of control wires, since we use native control operation which contains less wires than the equivalent matrix, and needs to be operated on less wires. adjoint(ctrl()) will see some speed-up, since we are now able to use the native named gate implementation in C++. Example timing improvement: 4 ctrl wires LQ: | LQ, 25 qubits, 500 repeats | master | branch | |-------------------------------------|--------|-------| | ctrl(adjoint(IsingXX)) | 9.6s | 6.0s | | ctrl(adjoint(DoubleExcitationPlus)) | 27.6s | 9.2s | | LQ, 25 qubits, 100 repeats | master | branch | |-------------------------------------|------------------|--------| | adjoint(ctrl(IsingXX)) | 267s | 2.9s| | adjoint(ctrl(DoubleExcitationPlus)) | 1002s| 3.6s | Baseline: | LQ, 25 qubits, 500 repeats | master | branch | |-------------------------------------|--------|--------| | ctrl(IsingXX) | 6.1s |6.1s | | ctrl(DoubleExcitationPlus)| 9.1s | 9.1s | LG: | LG, 31 qubits, 1000 repeats | master | branch | |-------------------------------------|--------|--------| | ctrl(adjoint(IsingXX)) | 4.9s | 4.8s | | ctrl(adjoint(DoubleExcitationPlus)) | 5.0s | 4.9s | | LG, 31 qubits, 1000 repeats | master | branch | |-------------------------------------|-------------|--------| | adjoint(ctrl(IsingXX)) | 119s | 4.8s| | adjoint(ctrl(DoubleExcitationPlus)) | 208s | 4.9s | Baseline: | LG, 31 qubits, 1000 repeats | master | branch | |-------------------------------------|-------------------|--------| | ctrl(IsingXX) | 4.8s | 4.8s | | ctrl(DoubleExcitationPlus)| 4.9s | 4.9s | LK: | LK, 25 qubits, 500 repeats | master | branch | |-------------------------------------|--------|--------| | ctrl(adjoint(IsingXX)) | 8.5s |5.7s | | ctrl(adjoint(DoubleExcitationPlus)) | 24.5s | 7.6s | | LK, 25 qubits, 100 repeats | master | branch | |-------------------------------------|-----|--------| | adjoint(ctrl(IsingXX)) | 235s | 2.6s | | adjoint(ctrl(DoubleExcitationPlus)) | 867s | 2.9s | Baseline: | LK, 25 qubits, 500 repeats | master |branch | |-------------------------------------|-------------|--------| | ctrl(IsingXX) | 5.6s |5.8s | | ctrl(DoubleExcitationPlus)| 7.7s | 7.6 s | **Possible Drawbacks:** **Related GitHub Issues:** [sc-79430] --------- Co-authored-by: ringo-but-quantum Co-authored-by: Christina Lee Co-authored-by: Amintor Dusko <87949283+AmintorDusko@users.noreply.github.com> --- .github/CHANGELOG.md | 3 + pennylane_lightning/core/_serialize.py | 71 +++++--- .../core/_state_vector_base.py | 4 +- pennylane_lightning/core/_version.py | 2 +- .../lightning_gpu/_state_vector.py | 34 ++-- .../lightning_kokkos/_state_vector.py | 33 ++-- .../lightning_qubit/_state_vector.py | 28 +-- tests/test_gates.py | 162 +++++++++++++++++- tests/test_serialize.py | 141 ++++++++++++++- 9 files changed, 408 insertions(+), 70 deletions(-) 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):