Repository: Qiskit/qiskit-terra
Pull request #5552 (2020-12-21 18:10:23)

Add ``PauliTwoDesign`` to the circuit library
<!--
⚠️ If you do not respect this template, your pull request will be closed.
⚠️ Your pull request title should be short detailed and understandable for all.
⚠️ Also, please add a release note file using reno if the change needs to be
  documented in the release notes.
⚠️ If your pull request fixes an open issue, please link to the issue.

- [ ] I have added the tests to cover my changes.
- [ ] I have updated the documentation accordingly.
- [ ] I have read the CONTRIBUTING document.
-->

### Summary

Adds the benchmark circuit from https://arxiv.org/pdf/1803.11173.pdf to the circuit library and also includes some minor fixes around the n-local circuits. Name is open for other suggestions!

### Details and comments

* add the ansatz from https://arxiv.org/pdf/1803.11173.pdf, which looks like this:
```
     ┌─────────┐┌──────────┐       ░ ┌──────────┐       ░  ┌──────────┐
q_0: ┤ RY(π/4) ├┤ RZ(θ[0]) ├─■─────░─┤ RY(θ[4]) ├─■─────░──┤ RZ(θ[8]) ├
     ├─────────┤├──────────┤ │     ░ ├──────────┤ │     ░  ├──────────┤
q_1: ┤ RY(π/4) ├┤ RZ(θ[1]) ├─■──■──░─┤ RY(θ[5]) ├─■──■──░──┤ RX(θ[9]) ├
     ├─────────┤├──────────┤    │  ░ ├──────────┤    │  ░ ┌┴──────────┤
q_2: ┤ RY(π/4) ├┤ RX(θ[2]) ├─■──■──░─┤ RY(θ[6]) ├─■──■──░─┤ RX(θ[10]) ├
     ├─────────┤├──────────┤ │     ░ ├──────────┤ │     ░ ├───────────┤
q_3: ┤ RY(π/4) ├┤ RZ(θ[3]) ├─■─────░─┤ RX(θ[7]) ├─■─────░─┤ RY(θ[11]) ├
     └─────────┘└──────────┘       ░ └──────────┘       ░ └───────────┘
```
* adds a `pairwise` entanglement option (which is the entanglement above)
* fixes the `ParameterVector.resize` method
* fixes the rotation layer index for the last rotation block in the `NLocal` class





Patch:
diff --git a/qiskit/circuit/library/__init__.py b/qiskit/circuit/library/__init__.py
index 703cd7d68..f91f3b090 100644
--- a/qiskit/circuit/library/__init__.py
+++ b/qiskit/circuit/library/__init__.py
@@ -200,6 +200,7 @@ N-local circuits
 
    NLocal
    TwoLocal
+   PauliTwoDesign
    RealAmplitudes
    EfficientSU2
    ExcitationPreserving
@@ -312,6 +313,7 @@ from .arithmetic import (
 from .n_local import (
     NLocal,
     TwoLocal,
+    PauliTwoDesign,
     RealAmplitudes,
     EfficientSU2,
     ExcitationPreserving,
diff --git a/qiskit/circuit/library/n_local/__init__.py b/qiskit/circuit/library/n_local/__init__.py
index 499f2ead4..c81bbc206 100644
--- a/qiskit/circuit/library/n_local/__init__.py
+++ b/qiskit/circuit/library/n_local/__init__.py
@@ -14,6 +14,7 @@
 
 from .n_local import NLocal
 from .two_local import TwoLocal
+from .pauli_two_design import PauliTwoDesign
 from .real_amplitudes import RealAmplitudes
 from .efficient_su2 import EfficientSU2
 from .excitation_preserving import ExcitationPreserving
@@ -22,6 +23,7 @@ __all__ = [
     'NLocal',
     'TwoLocal',
     'RealAmplitudes',
+    'PauliTwoDesign',
     'EfficientSU2',
     'ExcitationPreserving'
 ]
diff --git a/qiskit/circuit/library/n_local/n_local.py b/qiskit/circuit/library/n_local/n_local.py
index 47a30a136..c56d31056 100644
--- a/qiskit/circuit/library/n_local/n_local.py
+++ b/qiskit/circuit/library/n_local/n_local.py
@@ -905,7 +905,7 @@ class NLocal(BlueprintCircuit):
         if not self._skip_final_rotation_layer:
             if self.insert_barriers:
                 self.barrier()
-            self._build_rotation_layer(param_iter, i)
+            self._build_rotation_layer(param_iter, self.reps)
 
         # add the appended layers
         self._build_additional_layers('appended')
@@ -965,14 +965,20 @@ def get_entangler_map(num_block_qubits: int, num_circuit_qubits: int, entangleme
         raise ValueError('The number of block qubits must be smaller or equal to the number of '
                          'qubits in the circuit.')
 
+    if entanglement == 'pairwise' and num_block_qubits != 2:
+        raise ValueError('Pairwise entanglement is only defined for blocks of 2 qubits.')
+
     if entanglement == 'full':
         return list(combinations(list(range(n)), m))
-    if entanglement in ['linear', 'circular', 'sca']:
+    if entanglement in ['linear', 'circular', 'sca', 'pairwise']:
         linear = [tuple(range(i, i + m)) for i in range(n - m + 1)]
         # if the number of block qubits is 1, we don't have to add the 'circular' part
         if entanglement == 'linear' or m == 1:
             return linear
 
+        if entanglement == 'pairwise':
+            return linear[::2] + linear[1::2]
+
         # circular equals linear plus top-bottom entanglement
         circular = [tuple(range(n - m + 1, n)) + (0,)] + linear
         if entanglement == 'circular':
diff --git a/qiskit/circuit/library/n_local/pauli_two_design.py b/qiskit/circuit/library/n_local/pauli_two_design.py
new file mode 100644
index 000000000..37dfbfd49
--- /dev/null
+++ b/qiskit/circuit/library/n_local/pauli_two_design.py
@@ -0,0 +1,118 @@
+# This code is part of Qiskit.
+#
+# (C) Copyright IBM 2017, 2020.
+#
+# This code is licensed under the Apache License, Version 2.0. You may
+# obtain a copy of this license in the LICENSE.txt file in the root directory
+# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
+#
+# Any modifications or derivative works of this code must retain this
+# copyright notice, and modified files need to carry a notice indicating
+# that they have been altered from the originals.
+
+"""The Random Pauli circuit class."""
+
+from typing import Optional
+import numpy as np
+
+from qiskit.circuit import QuantumCircuit
+
+
+from .two_local import TwoLocal
+
+
+class PauliTwoDesign(TwoLocal):
+    r"""The Pauli Two-Design ansatz.
+
+    This class implements a particular form of a 2-design circuit [1], which is frequently studied
+    in quantum machine learning literature, such as e.g. the investigating of Barren plateaus in
+    variational algorithms [2].
+
+    The circuit consists of alternating rotation and entanglement layers with
+    an initial layer of :math:`\sqrt{H} = RY(\pi/4)` gates.
+    The rotation layers contain single qubit Pauli rotations, where the axis is chosen uniformly
+    at random to be X, Y or Z. The entanglement layers is compromised of pairwise CZ gates
+    with a total depth of 2.
+
+    For instance, the circuit could look like this (but note that choosing a different seed
+    yields different Pauli rotations).
+
+    .. code-block::
+
+             ┌─────────┐┌──────────┐       ░ ┌──────────┐       ░  ┌──────────┐
+        q_0: ┤ RY(π/4) ├┤ RZ(θ[0]) ├─■─────░─┤ RY(θ[4]) ├─■─────░──┤ RZ(θ[8]) ├
+             ├─────────┤├──────────┤ │     ░ ├──────────┤ │     ░  ├──────────┤
+        q_1: ┤ RY(π/4) ├┤ RZ(θ[1]) ├─■──■──░─┤ RY(θ[5]) ├─■──■──░──┤ RX(θ[9]) ├
+             ├─────────┤├──────────┤    │  ░ ├──────────┤    │  ░ ┌┴──────────┤
+        q_2: ┤ RY(π/4) ├┤ RX(θ[2]) ├─■──■──░─┤ RY(θ[6]) ├─■──■──░─┤ RX(θ[10]) ├
+             ├─────────┤├──────────┤ │     ░ ├──────────┤ │     ░ ├───────────┤
+        q_3: ┤ RY(π/4) ├┤ RZ(θ[3]) ├─■─────░─┤ RX(θ[7]) ├─■─────░─┤ RY(θ[11]) ├
+             └─────────┘└──────────┘       ░ └──────────┘       ░ └───────────┘
+
+    Examples:
+
+        .. jupyter-execute:
+
+            from qiskit.circuit.library import PauliTwoDesign
+            circuit = PauliTwoDesign(4, reps=2, seed=5, insert_barriers=True)
+            print(circuit.draw())
+
+    References:
+
+        [1]: Nakata et al., Unitary 2-designs from random X- and Z-diagonal unitaries.
+            `arXiv:1502.07514 <https://arxiv.org/pdf/1502.07514.pdf>`_
+
+        [2]: McClean et al., Barren plateaus in quantum neural network training landscapes.
+             `arXiv:1803.11173 <https://arxiv.org/pdf/1803.11173.pdf>`_
+    """
+
+    def __init__(self, num_qubits: Optional[int] = None, reps: int = 3,
+                 seed: Optional[int] = None, insert_barriers: bool = False):
+        # store a random number generator
+        self._seed = seed
+        self._rng = np.random.default_rng(seed)
+
+        # store a dict to keep track of the random gates
+        self._gates = dict()
+
+        super().__init__(num_qubits, reps=reps, entanglement_blocks='cz', entanglement='pairwise',
+                         insert_barriers=insert_barriers)
+
+    def _invalidate(self):
+        self._rng = np.random.default_rng(self._seed)  # reset number generator
+        super()._invalidate()
+
+    def _build_rotation_layer(self, param_iter, i):
+        """Build a rotation layer."""
+        layer = QuantumCircuit(*self.qregs)
+        qubits = range(self.num_qubits)
+
+        # if no gates for this layer were generated, generate them
+        if i not in self._gates.keys():
+            self._gates[i] = list(self._rng.choice(['rx', 'ry', 'rz'], self.num_qubits))
+        # if not enough gates exist, add more
+        elif len(self._gates[i]) < self.num_qubits:
+            num_missing = self.num_qubits - len(self._gates[i])
+            self._gates[i] += list(self._rng.choice(['rx', 'ry', 'rz'], num_missing))
+
+        for j in qubits:
+            getattr(layer, self._gates[i][j])(next(param_iter), j)
+
+        # add the layer to the circuit
+        self.compose(layer, inplace=True)
+
+    @property
+    def num_parameters_settable(self) -> int:
+        """Return the number of settable parameters.
+
+        Returns:
+            The number of possibly distinct parameters.
+        """
+        return (self.reps + 1) * self.num_qubits
+
+    def _build(self):
+        super()._build()
+        initial_layer = QuantumCircuit(self.num_qubits)
+        initial_layer.ry(np.pi / 4, range(self.num_qubits))
+
+        self.compose(initial_layer, front=True, inplace=True)
diff --git a/qiskit/circuit/parametervector.py b/qiskit/circuit/parametervector.py
index e8ff52503..98b743d39 100644
--- a/qiskit/circuit/parametervector.py
+++ b/qiskit/circuit/parametervector.py
@@ -67,7 +67,7 @@ class ParameterVector:
         previous elements are cached and not re-generated if the vector is enlargened again.
         This is to ensure that the parameter instances do not change.
         """
-        if length > self._size:
-            for i in range(self._size, length):
+        if length > len(self._params):
+            for i in range(len(self._params), length):
                 self._params += [Parameter('{}[{}]'.format(self._name, i))]
         self._size = length
diff --git a/releasenotes/notes/randompauli-76928cc17c840aea.yaml b/releasenotes/notes/randompauli-76928cc17c840aea.yaml
new file mode 100644
index 000000000..59c6b6079
--- /dev/null
+++ b/releasenotes/notes/randompauli-76928cc17c840aea.yaml
@@ -0,0 +1,17 @@
+---
+features:
+  - |
+    Adds the benchmark circuit from arxiv.org/pdf/1803.11173.pdf to the circuit library.
+    For instance, this circuit can look like
+    
+    .. code-block::
+
+             ┌─────────┐┌──────────┐       ░ ┌──────────┐       ░  ┌──────────┐
+        q_0: ┤ RY(π/4) ├┤ RZ(θ[0]) ├─■─────░─┤ RY(θ[4]) ├─■─────░──┤ RZ(θ[8]) ├
+             ├─────────┤├──────────┤ │     ░ ├──────────┤ │     ░  ├──────────┤
+        q_1: ┤ RY(π/4) ├┤ RZ(θ[1]) ├─■──■──░─┤ RY(θ[5]) ├─■──■──░──┤ RX(θ[9]) ├
+             ├─────────┤├──────────┤    │  ░ ├──────────┤    │  ░ ┌┴──────────┤
+        q_2: ┤ RY(π/4) ├┤ RX(θ[2]) ├─■──■──░─┤ RY(θ[6]) ├─■──■──░─┤ RX(θ[10]) ├
+             ├─────────┤├──────────┤ │     ░ ├──────────┤ │     ░ ├───────────┤
+        q_3: ┤ RY(π/4) ├┤ RZ(θ[3]) ├─■─────░─┤ RX(θ[7]) ├─■─────░─┤ RY(θ[11]) ├
+             └─────────┘└──────────┘       ░ └──────────┘       ░ └───────────┘


Test patch:
diff --git a/test/python/circuit/library/test_nlocal.py b/test/python/circuit/library/test_nlocal.py
index af1f418a1..e467036f9 100644
--- a/test/python/circuit/library/test_nlocal.py
+++ b/test/python/circuit/library/test_nlocal.py
@@ -23,7 +23,7 @@ from qiskit import transpile
 from qiskit.circuit import QuantumCircuit, Parameter, ParameterVector, ParameterExpression
 from qiskit.circuit.library import (
     NLocal, TwoLocal, RealAmplitudes, ExcitationPreserving, XGate, CRXGate, CCXGate,
-    SwapGate, RXGate, RYGate, EfficientSU2, RZGate, RXXGate, RYYGate
+    SwapGate, RXGate, RYGate, EfficientSU2, RZGate, RXXGate, RYYGate, CXGate
 )
 from qiskit.circuit.random.utils import random_circuit
 from qiskit.converters.circuit_to_dag import circuit_to_dag
@@ -292,6 +292,23 @@ class TestNLocal(QiskitTestCase):
                 # using a set here since the order does not matter
                 self.assertEqual(set(entangler_map), set(expected))
 
+    def test_pairwise_entanglement(self):
+        """Test pairwise entanglement."""
+        nlocal = NLocal(5, rotation_blocks=XGate(), entanglement_blocks=CXGate(),
+                        entanglement='pairwise', reps=1)
+        entangler_map = nlocal.get_entangler_map(0, 0, 2)
+        pairwise = [(0, 1), (2, 3), (1, 2), (3, 4)]
+
+        self.assertEqual(pairwise, entangler_map)
+
+    def test_pairwise_entanglement_raises(self):
+        """Test choosing pairwise entanglement raises an error for too large blocks."""
+        nlocal = NLocal(3, XGate(), CCXGate(), entanglement='pairwise', reps=1)
+
+        # pairwise entanglement is only defined if the entangling gate has 2 qubits
+        with self.assertRaises(ValueError):
+            print(nlocal.draw())
+
     def test_entanglement_by_list(self):
         """Test setting the entanglement by list.
 
diff --git a/test/python/circuit/library/test_random_pauli.py b/test/python/circuit/library/test_random_pauli.py
new file mode 100644
index 000000000..d3024d3b6
--- /dev/null
+++ b/test/python/circuit/library/test_random_pauli.py
@@ -0,0 +1,75 @@
+# This code is part of Qiskit.
+#
+# (C) Copyright IBM 2017, 2020.
+#
+# This code is licensed under the Apache License, Version 2.0. You may
+# obtain a copy of this license in the LICENSE.txt file in the root directory
+# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
+#
+# Any modifications or derivative works of this code must retain this
+# copyright notice, and modified files need to carry a notice indicating
+# that they have been altered from the originals.
+
+"""Test library of quantum volume circuits."""
+
+import unittest
+
+import numpy as np
+
+from qiskit.test.base import QiskitTestCase
+from qiskit.circuit import QuantumCircuit, QuantumRegister
+from qiskit.circuit.library import PauliTwoDesign
+
+
+class TestPauliTwoDesign(QiskitTestCase):
+    """Test the Random Pauli circuit."""
+
+    def test_random_pauli(self):
+        """Test the Random Pauli circuit."""
+        circuit = PauliTwoDesign(4, seed=12, reps=1)
+
+        qr = QuantumRegister(4, 'q')
+        params = circuit.ordered_parameters
+
+        # expected circuit for the random seed 12
+        expected = QuantumCircuit(qr)
+
+        # initial RYs
+        expected.ry(np.pi / 4, qr)
+
+        # first random Pauli layer
+        expected.ry(params[0], 0)
+        expected.rx(params[1], 1)
+        expected.rz(params[2], 2)
+        expected.rz(params[3], 3)
+
+        # entanglement
+        expected.cz(0, 1)
+        expected.cz(2, 3)
+        expected.cz(1, 2)
+
+        # second random Pauli layer
+        expected.rx(params[4], 0)
+        expected.rx(params[5], 1)
+        expected.rx(params[6], 2)
+        expected.rx(params[7], 3)
+
+        self.assertEqual(circuit, expected)
+
+    def test_resize(self):
+        """Test resizing the Random Pauli circuit preserves the gates."""
+        circuit = PauliTwoDesign(1)
+        top_gates = [op.name for op, _, _ in circuit.data]
+
+        circuit.num_qubits = 3
+        with self.subTest('assert existing gates remain'):
+            new_top_gates = []
+            for op, qargs, _ in circuit:
+                if qargs == [circuit.qubits[0]]:  # if top qubit
+                    new_top_gates.append(op.name)
+
+            self.assertEqual(top_gates, new_top_gates)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/test/python/circuit/test_parameters.py b/test/python/circuit/test_parameters.py
index f6227d961..11bd9bc0e 100644
--- a/test/python/circuit/test_parameters.py
+++ b/test/python/circuit/test_parameters.py
@@ -952,6 +952,26 @@ class TestParameters(QiskitTestCase):
         qc_aer = transpile(qc, backend)
         self.assertIn(phi, qc_aer.parameters)
 
+    def test_parametervector_resize(self):
+        """Test the resize method of the parameter vector."""
+
+        vec = ParameterVector('x', 2)
+        element = vec[1]  # store an entry for instancecheck later on
+
+        with self.subTest('shorten'):
+            vec.resize(1)
+            self.assertEqual(len(vec), 1)
+            self.assertListEqual([param.name for param in vec], _paramvec_names('x', 1))
+
+        with self.subTest('enlargen'):
+            vec.resize(3)
+            self.assertEqual(len(vec), 3)
+            # ensure we still have the same instance not a copy with the same name
+            # this is crucial for adding parameters to circuits since we cannot use the same
+            # name if the instance is not the same
+            self.assertIs(element, vec[1])
+            self.assertListEqual([param.name for param in vec], _paramvec_names('x', 3))
+
 
 def _construct_circuit(param, qr):
     qc = QuantumCircuit(qr)
@@ -959,6 +979,10 @@ def _construct_circuit(param, qr):
     return qc
 
 
+def _paramvec_names(prefix, length):
+    return [f'{prefix}[{i}]' for i in range(length)]
+
+
 @ddt
 class TestParameterExpressions(QiskitTestCase):
     """Test expressions of Parameters."""
diff --git a/test/python/quantum_info/operators/test_random.py b/test/python/quantum_info/operators/test_random.py
index 53db7b6e5..e200e363b 100644
--- a/test/python/quantum_info/operators/test_random.py
+++ b/test/python/quantum_info/operators/test_random.py
@@ -191,7 +191,7 @@ class TestRandomClifford(QiskitTestCase):
 
 
 @ddt
-class TestRandomPauliTable(QiskitTestCase):
+class TestPauliTwoDesignTable(QiskitTestCase):
     """Testing random_pauli_table function."""
 
     @combine(num_qubits=[1, 2, 3, 4, 5, 10, 50, 100, 200, 250],