diff --git a/doc/_static/templates/subroutines/kupccgsd.png b/doc/_static/templates/subroutines/kupccgsd.png new file mode 100644 index 00000000000..a3e4c13e76d Binary files /dev/null and b/doc/_static/templates/subroutines/kupccgsd.png differ diff --git a/doc/introduction/templates.rst b/doc/introduction/templates.rst index 87ee7276b6b..b33a714a307 100644 --- a/doc/introduction/templates.rst +++ b/doc/introduction/templates.rst @@ -178,6 +178,11 @@ Other useful templates which do not belong to the previous categories can be fou :description: UCCSD :figure: ../_static/templates/subroutines/uccsd.png +.. customgalleryitem:: + :link: ../code/api/pennylane.templates.subroutines.kUpCCGSD.html + :description: k-UpCCGSD + :figure: ../_static/templates/subroutines/kupccgsd.png + .. customgalleryitem:: :link: ../code/api/pennylane.templates.subroutines.ArbitraryUnitary.html :description: ArbitraryUnitary diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 5711d3c7b86..b18750e46e3 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -365,6 +365,25 @@ For more details, see the [GateFabric documentation](../code/api/pennylane.templates.layers.GateFabric.html). +* Added a new template `kUpCCGSD`, which implements a unitary coupled cluster ansatz with + generalized singles and pair doubles excitation operators, proposed by Joonho Lee *et al.* + in [arXiv:1810.02327](https://arxiv.org/abs/1810.02327). + + An example of a circuit using `kUpCCGSD` template is: + + ```python + coordinates = np.array([0.0, 0.0, -0.6614, 0.0, 0.0, 0.6614]) + H, qubits = qml.qchem.molecular_hamiltonian(["H", "H"], coordinates) + ref_state = qml.qchem.hf_state(electrons=2, qubits) + + dev = qml.device('default.qubit', wires=qubits) + @qml.qnode(dev) + def ansatz(weights): + qml.templates.kUpCCGSD(weights, wires=[0,1,2,3], k=0, delta_sz=0, + init_state=ref_state) + return qml.expval(H) + ``` +

Improvements

diff --git a/pennylane/templates/subroutines/__init__.py b/pennylane/templates/subroutines/__init__.py index 91fabc6bc2d..b4c5cdd90ba 100644 --- a/pennylane/templates/subroutines/__init__.py +++ b/pennylane/templates/subroutines/__init__.py @@ -28,3 +28,4 @@ from .all_singles_doubles import AllSinglesDoubles from .grover import GroverOperator from .qft import QFT +from .kupccgsd import kUpCCGSD diff --git a/pennylane/templates/subroutines/kupccgsd.py b/pennylane/templates/subroutines/kupccgsd.py new file mode 100644 index 00000000000..10ab230b4cc --- /dev/null +++ b/pennylane/templates/subroutines/kupccgsd.py @@ -0,0 +1,289 @@ +# 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. +r""" +Contains the k-UpCCGSD template. +""" +# pylint: disable-msg=too-many-branches,too-many-arguments,protected-access +import numpy as np +import pennylane as qml +from pennylane.operation import Operation, AnyWires + + +def generalized_singles(wires, delta_sz): + r"""Return generalized single excitation terms + + .. math:: + \hat{T_1} = \sum_{pq} t_{p}^{q} \hat{c}^{\dagger}_{q} \hat{c}_{p} + + """ + sz = np.array( + [0.5 if (i % 2 == 0) else -0.5 for i in range(len(wires))] + ) # alpha-beta electrons + gen_singles_wires = [] + for r in range(len(wires)): + for p in range(len(wires)): + if sz[p] - sz[r] == delta_sz and p != r: + if r < p: + gen_singles_wires.append(wires[r : p + 1]) + else: + gen_singles_wires.append(wires[p : r + 1][::-1]) + return gen_singles_wires + + +def generalized_pair_doubles(wires): + r"""Return pair coupled-cluster double excitations + + .. math:: + \hat{T_2} = \sum_{pq} t_{p_\alpha p_\beta}^{q_\alpha, q_\beta} + \hat{c}^{\dagger}_{q_\alpha} \hat{c}^{\dagger}_{q_\beta} \hat{c}_{p_\beta} \hat{c}_{p_\alpha} + + """ + pair_gen_doubles_wires = [ + [ + wires[r : r + 2], + wires[p : p + 2], + ] # wires for [wires[r], wires[r+1], wires[p], wires[p+1]] terms + for r in range(0, len(wires) - 1, 2) + for p in range(0, len(wires) - 1, 2) + if p != r # remove redundant terms + ] + return pair_gen_doubles_wires + + +class kUpCCGSD(Operation): + r"""Implements the k-Unitary Pair Coupled-Cluster Generalized Singles and Doubles (k-UpCCGSD) ansatz. + + The k-UpCCGSD ansatz calls the :func:`~.SingleExcitationUnitary` and :func:`~.DoubleExcitationUnitary` + templates to exponentiate the product of :math:`k` generalized singles and pair coupled-cluster doubles + excitation operators. Here, "generalized" means that the single and double excitation terms do not + distinguish between occupied and unoccupied orbitals. Additionally, the term "pair coupled-cluster" + refers to the fact that the double excitations contain only those two-body excitations that move a + pair of electrons from one spatial orbital to another. This k-UpCCGSD belongs to the family of Unitary + Coupled Cluster (UCC) based ansätze, commonly used to solve quantum chemistry problems on quantum computers. + + The k-UpCCGSD unitary, within the first-order Trotter approximation for a given integer :math:`k`, is given by: + + .. math:: + + \hat{U}(\vec{\theta}) = + \prod_{l=1}^{k} \bigg(\prod_{p,r}\exp{\Big\{ + \theta_{r}^{p}(\hat{c}^{\dagger}_p\hat{c}_r - \text{H.c.})\Big\}} + \ \prod_{i,j} \Big\{\exp{\theta_{j_\alpha j_\beta}^{i_\alpha i_\beta} + (\hat{c}^{\dagger}_{i_\alpha}\hat{c}^{\dagger}_{i_\beta} + \hat{c}_{j_\alpha}\hat{c}_{j_\beta} - \text{H.c.}) \Big\}}\bigg) + + where :math:`\hat{c}` and :math:`\hat{c}^{\dagger}` are the fermionic annihilation and creation operators. + The indices :math:`p, q` run over the spin orbitals and :math:`i, j` run over the spatial orbitals. The + singles and paired doubles amplitudes :math:`\theta_{r}^{p}` and + :math:`\theta_{j_\alpha j_\beta}^{i_\alpha i_\beta}` represent the set of variational parameters. + + Args: + weights (tensor_like): Tensor containing the parameters :math:`\theta_{pr}` and :math:`\theta_{pqrs}` + entering the Z rotation in :func:`~.SingleExcitationUnitary` and :func:`~.DoubleExcitationUnitary`. + These parameters are the coupled-cluster amplitudes that need to be optimized for each generalized + single and pair double excitation terms. + wires (Iterable): wires that the template acts on + k (int): Number of times UpCCGSD unitary is repeated. + delta_sz (int): Specifies the selection rule ``sz[p] - sz[r] = delta_sz`` + for the spin-projection ``sz`` of the orbitals involved in the generalized single excitations. + ``delta_sz`` can take the values :math:`0` and :math:`\pm 1`. + init_state (array[int]): Length ``len(wires)`` occupation-number vector representing the + HF state. ``init_state`` is used to initialize the wires. + + .. UsageDetails:: + + #. The number of wires has to be equal to the number of + spin-orbitals included in the active space, and should be even. + + #. The number of trainable parameters scales linearly with the number of layers as + :math:`2 k n`, where :math:`n` is the total number of + generalized singles and paired doubles excitation terms. + + An example of how to use this template is shown below: + + .. code-block:: python + + import numpy as np + import pennylane as qml + + # Build the electronic Hamiltonian + symbols = ["H", "H"] + coordinates = np.array([0.0, 0.0, -0.6614, 0.0, 0.0, 0.6614]) + H, qubits = qml.qchem.molecular_hamiltonian(symbols, coordinates) + + # Define the Hartree-Fock state + electrons = 2 + ref_state = qml.qchem.hf_state(electrons, qubits) + + # Define the device + dev = qml.device('default.qubit', wires=qubits) + + # Define the ansatz + @qml.qnode(dev) + def ansatz(weights): + qml.templates.kUpCCGSD(weights, wires=[0, 1, 2, 3], + k=1, delta_sz=0, init_state=ref_state) + return qml.expval(H) + + # Get the shape of the weights for this template + layers = 1 + shape = qml.templates.kUpCCGSD.shape(k=layers, + n_wires=qubits, delta_sz=0) + + # Initialize the weight tensors + np.random.seed(24) + weights = np.random.random(size=shape) + + # Define the optimizer + opt = qml.GradientDescentOptimizer(stepsize=0.4) + + # Store the values of the cost function + energy = [ansatz(weights)] + + # Store the values of the circuit weights + angle = [weights] + max_iterations = 100 + conv_tol = 1e-06 + for n in range(max_iterations): + weights, prev_energy = opt.step_and_cost(ansatz, weights) + energy.append(ansatz(weights)) + angle.append(weights) + conv = np.abs(energy[-1] - prev_energy) + if n % 4 == 0: + print(f"Step = {n}, Energy = {energy[-1]:.8f} Ha") + if conv <= conv_tol: + break + + print("\n" f"Final value of the ground-state energy = {energy[-1]:.8f} Ha") + print("\n" f"Optimal value of the circuit parameters = {angle[-1]}") + + .. code-block:: none + + Step = 0, Energy = 0.18046117 Ha + Step = 4, Energy = -1.06545723 Ha + Step = 8, Energy = -1.13028734 Ha + Step = 12, Energy = -1.13528393 Ha + Step = 16, Energy = -1.13604954 Ha + Step = 20, Energy = -1.13616762 Ha + Step = 24, Energy = -1.13618584 Ha + + Final value of the ground-state energy = -1.13618786 Ha + + Optimal value of the circuit parameters = [[ 0.97882258 0.46090942 0.98106201 + 0.45866993 -0.91548184 2.01637919]] + + + **Parameter shape** + + The shape of the weights argument can be computed by the static method + :meth:`~.kUpCCGSD.shape` and used when creating randomly + initialised weight tensors: + + .. code-block:: python + + shape = qml.templates.kUpCCGSD.shape(n_layers=2, n_wires=4) + weights = np.random.random(size=shape) + + >>> weights.shape + (2, 6) + + """ + + num_params = 1 + num_wires = AnyWires + par_domain = "A" + + def __init__(self, weights, wires, k=1, delta_sz=0, init_state=None, do_queue=True, id=None): + + if len(wires) < 4: + raise ValueError(f"Requires at least four wires; got {len(wires)} wires.") + if len(wires) % 2: + raise ValueError(f"Requires even number of wires; got {len(wires)} wires.") + + if k < 1: + raise ValueError(f"Requires k to be at least 1; got {k}.") + + if delta_sz not in [-1, 0, 1]: + raise ValueError(f"Requires delta_sz to be one of ±1 or 0; got {delta_sz}.") + + self.k = k + + self.s_wires = generalized_singles(list(wires), delta_sz) + self.d_wires = generalized_pair_doubles(list(wires)) + + shape = qml.math.shape(weights) + if shape != ( + k, + len(self.s_wires) + len(self.d_wires), + ): + raise ValueError( + f"Weights tensor must be of shape {(k, len(self.s_wires) + len(self.d_wires),)}; got {shape}." + ) + + # we can extract the numpy representation here + # since init_state can never be differentiable + self.init_state = qml.math.toarray(init_state) + + if init_state.dtype != np.dtype("int"): + raise ValueError(f"Elements of 'init_state' must be integers; got {init_state.dtype}") + + self.init_state_flipped = np.flip(init_state) + + super().__init__(weights, wires=wires, do_queue=do_queue, id=id) + + def expand(self): + + with qml.tape.QuantumTape() as tape: + + qml.templates.BasisEmbedding(self.init_state_flipped, wires=self.wires) + weights = self.parameters[0] + + for layer in range(self.k): + for i, (w1, w2) in enumerate(self.d_wires): + qml.templates.DoubleExcitationUnitary( + weights[layer][len(self.s_wires) + i], wires1=w1, wires2=w2 + ) + + for j, s_wires_ in enumerate(self.s_wires): + qml.templates.SingleExcitationUnitary(weights[layer][j], wires=s_wires_) + + return tape + + @staticmethod + def shape(k, n_wires, delta_sz): + r"""Returns the shape of the weight tensor required for this template. + Args: + k (int): Number of layers + n_wires (int): Number of qubits + delta_sz (int): Specifies the selection rules ``sz[p] - sz[r] = delta_sz`` + for the spin-projection ``sz`` of the orbitals involved in the single excitations. + ``delta_sz`` can take the values :math:`0` and :math:`\pm 1`. + Returns: + tuple[int]: shape + """ + + if n_wires < 4: + raise ValueError( + f"This template requires the number of qubits to be greater than four; got 'n_wires' = {n_wires}" + ) + + if n_wires % 2: + raise ValueError( + f"This template requires an even number of qubits; got 'n_wires' = {n_wires}" + ) + + s_wires = generalized_singles(range(n_wires), delta_sz) + d_wires = generalized_pair_doubles(range(n_wires)) + + return k, len(s_wires) + len(d_wires) diff --git a/tests/templates/test_layers/test_gate_fabric.py b/tests/templates/test_layers/test_gate_fabric.py index f6949377bc5..a68b06b4c15 100644 --- a/tests/templates/test_layers/test_gate_fabric.py +++ b/tests/templates/test_layers/test_gate_fabric.py @@ -56,7 +56,6 @@ def test_operations(self, layers, qubits, init_state, include_pi): weights, wires=range(qubits), init_state=init_state, include_pi=include_pi ) queue = op.expand().operations - print(op, n_gates, queue) # number of gates assert len(queue) == n_gates diff --git a/tests/templates/test_subroutines/test_kupccgsd.py b/tests/templates/test_subroutines/test_kupccgsd.py new file mode 100644 index 00000000000..d42123f301c --- /dev/null +++ b/tests/templates/test_subroutines/test_kupccgsd.py @@ -0,0 +1,623 @@ +# 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 k-UpCCGSD template. +""" +from os import killpg +import pytest +import numpy as np +import pennylane as qml + + +class TestDecomposition: + """Test that the template defines the correct decomposition.""" + + @pytest.mark.parametrize( + ("k", "delta_sz", "init_state", "wires"), + [ + ( + 1, + 0, + qml.math.array([1, 1, 0, 0]), + qml.math.array([0, 1, 2, 3]), + ), + ( + 1, + -1, + qml.math.array([1, 1, 0, 0]), + qml.math.array([0, 1, 2, 3]), + ), + ( + 2, + 1, + qml.math.array([1, 1, 0, 0]), + qml.math.array([0, 1, 2, 3]), + ), + ( + 2, + 0, + qml.math.array([1, 1, 0, 0, 0, 0]), + qml.math.array([0, 1, 2, 3, 4, 5]), + ), + ( + 2, + 1, + qml.math.array([1, 1, 0, 0, 0, 0, 0, 0]), + qml.math.array([0, 1, 2, 3, 4, 5, 6, 7]), + ), + ], + ) + def test_kupccgsd_operations(self, k, delta_sz, init_state, wires): + """Test the correctness of the k-UpCCGSD template including the gate count + and order, the wires the operation acts on and the correct use of parameters + in the circuit.""" + + # wires for generalized single excitation terms + sz = np.array([0.5 if (i % 2 == 0) else -0.5 for i in range(len(wires))]) + gen_single_terms_wires = [ + wires[r : p + 1] if r < p else wires[p : r + 1][::-1] + for r in range(len(wires)) + for p in range(len(wires)) + if sz[p] - sz[r] == delta_sz and p != r + ] + + # wires for generalized pair coupled cluser double exictation terms + pair_double_terms_wires = [ + [wires[r : r + 2], wires[p : p + 2]] + for r in range(0, len(wires) - 1, 2) + for p in range(0, len(wires) - 1, 2) + if p != r + ] + + n_excit_terms = len(gen_single_terms_wires) + len(pair_double_terms_wires) + weights = np.random.normal(0, 2 * np.pi, (k, n_excit_terms)) + + n_gates = 1 + n_excit_terms * k + exp_unitary = [qml.templates.DoubleExcitationUnitary] * len(pair_double_terms_wires) + exp_unitary += [qml.templates.SingleExcitationUnitary] * len(gen_single_terms_wires) + + op = qml.templates.kUpCCGSD( + weights, wires=wires, k=k, delta_sz=delta_sz, init_state=init_state + ) + queue = op.expand().operations + + # number of gates + assert len(queue) == n_gates + + # initialization + assert isinstance(queue[0], qml.templates.BasisEmbedding) + + # order of gates + for op1, op2 in zip(queue[1:], exp_unitary): + assert isinstance(op1, op2) + + # gate parameter + params = np.zeros((k, n_excit_terms)) + for i in range(1, n_gates): + gate_index = (i - 1) % n_excit_terms + if gate_index < len(pair_double_terms_wires): + gate_index += len(gen_single_terms_wires) + else: + gate_index -= len(pair_double_terms_wires) + params[(i - 1) // n_excit_terms][gate_index] = queue[i].parameters[0] + + assert qml.math.allclose(params.flatten(), weights.flatten()) + + # gate wires + exp_wires = ( + [np.concatenate(w) for w in pair_double_terms_wires] + gen_single_terms_wires + ) * k + res_wires = [queue[i].wires.tolist() for i in range(1, n_gates)] + for wires1, wires2 in zip(exp_wires, res_wires): + assert np.all(wires1 == wires2) + + def test_custom_wire_labels(self, tol): + """Test that template can deal with non-numeric, nonconsecutive wire labels.""" + + weights = np.random.random(size=(1, 6)) + + dev = qml.device("default.qubit", wires=4) + dev2 = qml.device("default.qubit", wires=["z", "a", "k", "e"]) + + @qml.qnode(dev) + def circuit(): + qml.templates.kUpCCGSD( + weights, + wires=range(4), + k=1, + delta_sz=0, + init_state=np.array([0, 1, 0, 1]), + ) + return qml.expval(qml.Identity(0)) + + @qml.qnode(dev2) + def circuit2(): + qml.templates.kUpCCGSD( + weights, + wires=["z", "a", "k", "e"], + k=1, + delta_sz=0, + init_state=np.array([0, 1, 0, 1]), + ) + return qml.expval(qml.Identity("z")) + + circuit() + circuit2() + + assert np.allclose(dev.state, dev2.state, atol=tol, rtol=0) + + @pytest.mark.parametrize( + ("num_qubits", "k", "exp_state"), + [ + ( + 4, + 4, + qml.math.array( + [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + ), + ), + ( + 6, + 6, + qml.math.array( + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.1077, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.686, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -0.0429, + 0.0, + 0.0, + -0.0956, + 0.0, + 0.0, + 0.2733, + 0.0, + 0.0, + -0.6089, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -0.1777, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0708, + 0.0, + 0.0, + -0.1577, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ] + ), + ), + ], + ) + def test_k_layers_upccgsd(self, num_qubits, k, exp_state, tol): + """Test that the k-UpCCGSD template with multiple layers works correctly asserting the prepared state.""" + + wires = range(num_qubits) + + shape = qml.templates.kUpCCGSD.shape(k=k, n_wires=num_qubits, delta_sz=0) + weight = np.pi / 2 * qml.math.ones(shape) + + dev = qml.device("default.qubit", wires=wires) + + init_state = qml.math.array([1 if x < num_qubits // 2 else 0 for x in wires]) + + @qml.qnode(dev) + def circuit(weight): + qml.templates.kUpCCGSD(weight, wires=wires, k=k, delta_sz=0, init_state=init_state) + return qml.state() + + circuit(weight) + + assert qml.math.allclose(circuit.device.state, exp_state, atol=tol) + + @pytest.mark.parametrize( + ("wires", "delta_sz", "generalized_singles_wires", "generalized_pair_doubles_wires"), + [ + ( + [0, 1, 2, 3], + 0, + [[0, 1, 2], [1, 2, 3], [2, 1, 0], [3, 2, 1]], + [[[0, 1], [2, 3]], [[2, 3], [0, 1]]], + ), + ( + [0, 1, 2, 3], + 1, + [[1, 0], [1, 2], [3, 2, 1, 0], [3, 2]], + [[[0, 1], [2, 3]], [[2, 3], [0, 1]]], + ), + ( + [0, 1, 2, 3, 4, 5], + -1, + [ + [0, 1], + [0, 1, 2, 3], + [0, 1, 2, 3, 4, 5], + [2, 1], + [2, 3], + [2, 3, 4, 5], + [4, 3, 2, 1], + [4, 3], + [4, 5], + ], + [ + [[0, 1], [2, 3]], + [[0, 1], [4, 5]], + [[2, 3], [0, 1]], + [[2, 3], [4, 5]], + [[4, 5], [0, 1]], + [[4, 5], [2, 3]], + ], + ), + ( + ["a0", "b1", "c2", "d3", "e4", "f5"], + 1, + [ + ["b1", "a0"], + ["b1", "c2"], + ["b1", "c2", "d3", "e4"], + ["d3", "c2", "b1", "a0"], + ["d3", "c2"], + ["d3", "e4"], + ["f5", "e4", "d3", "c2", "b1", "a0"], + ["f5", "e4", "d3", "c2"], + ["f5", "e4"], + ], + [ + [["a0", "b1"], ["c2", "d3"]], + [["a0", "b1"], ["e4", "f5"]], + [["c2", "d3"], ["a0", "b1"]], + [["c2", "d3"], ["e4", "f5"]], + [["e4", "f5"], ["a0", "b1"]], + [["e4", "f5"], ["c2", "d3"]], + ], + ), + ], + ) + def test_excitations_wires_kupccgsd( + self, wires, delta_sz, generalized_singles_wires, generalized_pair_doubles_wires + ): + """Test the correctness of the wire indices for the generalized singles and paired doubles excitaitons + used by the template.""" + + shape = qml.templates.kUpCCGSD.shape(k=1, n_wires=len(wires), delta_sz=delta_sz) + weights = np.pi / 2 * qml.math.ones(shape) + + ref_state = qml.math.array([1, 1, 0, 0]) + + op = qml.templates.kUpCCGSD( + weights, wires=wires, k=1, delta_sz=delta_sz, init_state=ref_state + ) + gen_singles_wires, gen_doubles_wires = op.s_wires, op.d_wires + + assert gen_singles_wires == generalized_singles_wires + assert gen_doubles_wires == generalized_pair_doubles_wires + + +class TestInputs: + """Test inputs and pre-processing.""" + + @pytest.mark.parametrize( + ("weights", "wires", "k", "delta_sz", "init_state", "msg_match"), + [ + ( + np.array([[0.55, 0.72, 0.6, 0.54, 0.42, 0.65]]), + [0, 1, 2], + 1, + 0, + np.array([1, 1, 0, 0]), + "Requires at least four wires", + ), + ( + np.array([[0.55, 0.72, 0.6, 0.54, 0.42, 0.65]]), + [0, 1, 2, 3, 4], + 1, + 0, + np.array([1, 1, 0, 0]), + "Requires even number of wires", + ), + ( + np.array([[0.55, 0.72, 0.6, 0.54, 0.42, 0.65]]), + [0, 1, 2, 3], + 0, + 0, + np.array([1, 1, 0, 0]), + "Requires k to be at least 1", + ), + ( + np.array([[0.55, 0.72, 0.6, 0.54, 0.42, 0.65]]), + [0, 1, 2, 3], + 1, + -2, + np.array([1, 1, 0, 0]), + "Requires delta_sz to be one of ±1 or 0", + ), + ( + np.array([-2.8, 1.6]), + [0, 1, 2, 3], + 1, + 0, + np.array([1, 1, 0, 0]), + "Weights tensor must be of", + ), + ( + np.array([-2.8, 1.6]), + [0, 1, 2, 3, 4, 5], + 2, + -1, + np.array([1, 1, 0, 0]), + "Weights tensor must be of", + ), + ( + np.array([[0.55, 0.72, 0.6, 0.54, 0.42, 0.65]]), + [0, 1, 2, 3], + 1, + 0, + np.array([1.4, 1.3, 0.0, 0.0]), + "Elements of 'init_state' must be integers", + ), + ], + ) + def test_kupccgsd_exceptions(self, wires, weights, k, delta_sz, init_state, msg_match): + """Test that k-UpCCGSD throws an exception if the parameters have illegal + shapes, types or values.""" + + dev = qml.device("default.qubit", wires=len(wires)) + + @qml.qnode(dev) + def circuit(): + qml.templates.kUpCCGSD( + weights=weights, + wires=wires, + k=k, + delta_sz=delta_sz, + init_state=init_state, + ) + return qml.expval(qml.PauliZ(0)) + + qnode = qml.QNode(circuit, dev) + + with pytest.raises(ValueError, match=msg_match): + circuit() + + def test_id(self): + """Test that the id attribute can be set.""" + template = qml.templates.kUpCCGSD( + qml.math.array([[0.55, 0.72, 0.6, 0.54, 0.42, 0.65]]), + wires=range(4), + k=1, + delta_sz=0, + init_state=qml.math.array([1, 1, 0, 0]), + id="a", + ) + assert template.id == "a" + + +class TestAttributes: + """Test additional methods and attributes""" + + @pytest.mark.parametrize( + "k, n_wires, delta_sz, expected_shape", + [ + (2, 4, 0, (2, 6)), + (2, 6, 0, (2, 18)), + (2, 8, 0, (2, 36)), + (2, 4, 1, (2, 6)), + (2, 6, 1, (2, 15)), + (2, 8, 1, (2, 28)), + ], + ) + def test_shape(self, k, n_wires, delta_sz, expected_shape): + """Test that the shape method returns the correct shape of the weights tensor.""" + + shape = qml.templates.kUpCCGSD.shape(k, n_wires, delta_sz) + assert shape == expected_shape + + def test_shape_exception_not_enough_qubits(self): + """Test that the shape function warns if there are not enough qubits.""" + + with pytest.raises( + ValueError, match="This template requires the number of qubits to be greater than four" + ): + qml.templates.kUpCCGSD.shape(k=2, n_wires=1, delta_sz=0) + + def test_shape_exception_not_even_qubits(self): + """Test that the shape function warns if the number of qubits are not even.""" + + with pytest.raises(ValueError, match="This template requires an even number of qubits"): + qml.templates.kUpCCGSD.shape(k=2, n_wires=5, delta_sz=0) + + +def circuit_template(weights): + qml.templates.kUpCCGSD( + weights, + wires=range(4), + k=1, + delta_sz=0, + init_state=np.array([1, 1, 0, 0]), + ) + return qml.expval(qml.PauliZ(0)) + + +def circuit_decomposed(weights): + qml.BasisState(np.array([0, 0, 1, 1]), wires=[0, 1, 2, 3]) + qml.templates.DoubleExcitationUnitary(weights[0][4], wires1=[0, 1], wires2=[2, 3]) + qml.templates.DoubleExcitationUnitary(weights[0][5], wires1=[2, 3], wires2=[0, 1]) + qml.templates.SingleExcitationUnitary(weights[0][0], wires=[0, 1, 2]) + qml.templates.SingleExcitationUnitary(weights[0][1], wires=[1, 2, 3]) + qml.templates.SingleExcitationUnitary(weights[0][2], wires=[2, 1, 0]) + qml.templates.SingleExcitationUnitary(weights[0][3], wires=[3, 2, 1]) + return qml.expval(qml.PauliZ(0)) + + +class TestInterfaces: + """Test that the template is compatible with all interfaces, including the computation + of gradients.""" + + def test_list_and_tuples(self, tol): + """Test common iterables as inputs.""" + + dev = qml.device("default.qubit", wires=4) + + circuit = qml.QNode(circuit_template, dev) + circuit2 = qml.QNode(circuit_decomposed, dev) + + weights = [[0.55, 0.72, 0.6, 0.54, 0.42, 0.65]] + res = circuit(weights) + res2 = circuit2(weights) + assert qml.math.allclose(res, res2, atol=tol, rtol=0) + + weights_tuple = [((0.55, 0.72, 0.6, 0.54, 0.42, 0.65))] + res = circuit(weights_tuple) + res2 = circuit2(weights_tuple) + assert qml.math.allclose(res, res2, atol=tol, rtol=0) + + def test_autograd(self, tol): + """Test the autograd interface.""" + + weights = qml.math.array(np.random.random(size=(1, 6))) + + dev = qml.device("default.qubit", wires=4) + + circuit = qml.QNode(circuit_template, dev, interface="autograd") + circuit2 = qml.QNode(circuit_decomposed, dev, interface="autograd") + + res = circuit(weights) + res2 = circuit2(weights) + assert qml.math.allclose(res, res2, atol=tol, rtol=0) + + grad_fn = qml.grad(circuit) + grads = grad_fn(weights) + + grad_fn2 = qml.grad(circuit2) + grads2 = grad_fn2(weights) + + assert np.allclose(grads, grads2, atol=tol, rtol=0) + + def test_jax(self, tol): + """Test the jax interface.""" + + jax = pytest.importorskip("jax") + import jax.numpy as jnp + + weights = jnp.array(np.random.random(size=(1, 6))) + + dev = qml.device("default.qubit", wires=4) + + circuit = qml.QNode(circuit_template, dev, interface="jax") + circuit2 = qml.QNode(circuit_decomposed, dev, interface="jax") + + res = circuit(weights) + res2 = circuit2(weights) + assert qml.math.allclose(res, res2, atol=tol, rtol=0) + + grad_fn = jax.grad(circuit) + grads = grad_fn(weights) + + grad_fn2 = jax.grad(circuit2) + grads2 = grad_fn2(weights) + + assert np.allclose(grads[0], grads2[0], atol=tol, rtol=0) + + def test_tf(self, tol): + """Test the tf interface.""" + + tf = pytest.importorskip("tensorflow") + + weights = tf.Variable(np.random.random(size=(1, 6))) + + dev = qml.device("default.qubit", wires=4) + + circuit = qml.QNode(circuit_template, dev, interface="tf") + circuit2 = qml.QNode(circuit_decomposed, dev, interface="tf") + + res = circuit(weights) + res2 = circuit2(weights) + assert qml.math.allclose(res, res2, atol=tol, rtol=0) + + with tf.GradientTape() as tape: + res = circuit(weights) + grads = tape.gradient(res, [weights]) + + with tf.GradientTape() as tape2: + res2 = circuit2(weights) + grads2 = tape2.gradient(res2, [weights]) + + assert np.allclose(grads[0], grads2[0], atol=tol, rtol=0) + + def test_torch(self, tol): + """Test the torch interface.""" + + torch = pytest.importorskip("torch") + + weights = torch.tensor(np.random.random(size=(1, 6)), requires_grad=True) + + dev = qml.device("default.qubit", wires=4) + + circuit = qml.QNode(circuit_template, dev, interface="torch") + circuit2 = qml.QNode(circuit_decomposed, dev, interface="torch") + + res = circuit(weights) + res2 = circuit2(weights) + assert qml.math.allclose(res, res2, atol=tol, rtol=0) + + res = circuit(weights) + res.backward() + grads = [weights.grad] + + res2 = circuit2(weights) + res2.backward() + grads2 = [weights.grad] + + assert np.allclose(grads[0], grads2[0], atol=tol, rtol=0)