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)