Fixed ZZFeatureMap not accepting a list of entanglement (#12767)

* Added a get_entangler_map() and _selective_entangler_map() functions to PauliFeatureMap

* Added some logic to handle the List[List[List[List[int]]]] case. Refer to the comments in the code.

* `entanglement` can now be specified as dictionary `{num_qubits: entanglement}`

* Added logic to handle cases when entanglement is not specified for some pauli blocks.

* Added tests cases for `PauliFeatureMap`

* Added test and releasenotes. Also modified the typing hints and doc strings to reflect new changes.

* Replaced all tab characters with spaces to fix the linting error.

* Formatted releasenotes as per the suggestion to fix the issue in reno.

* Added `Callable[[int], Union[str | Dict[...]]]` to the typing hints and included some logic to handle that.

* Removed `List[List[int]]` from typing hints as this is equivalent to List[Tuple[int]]

Co-authored-by: Julien Gacon <gaconju@gmail.com>

* updated the release notes

Co-authored-by: Julien Gacon <gaconju@gmail.com>

* Updated typing hints in `ZZFeatureMap`

---------

Co-authored-by: Julien Gacon <gaconju@gmail.com>
This commit is contained in:
Shravan Patel 2024-09-03 02:51:38 -05:00 committed by GitHub
parent 3fac5da41e
commit f30f851c98
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 158 additions and 8 deletions

View File

@ -11,8 +11,7 @@
# that they have been altered from the originals. # that they have been altered from the originals.
"""The Pauli expansion circuit module.""" """The Pauli expansion circuit module."""
from typing import Optional, Callable, List, Union, Sequence, Dict, Tuple
from typing import Optional, Callable, List, Union
from functools import reduce from functools import reduce
import numpy as np import numpy as np
@ -116,7 +115,11 @@ class PauliFeatureMap(NLocal):
self, self,
feature_dimension: Optional[int] = None, feature_dimension: Optional[int] = None,
reps: int = 2, reps: int = 2,
entanglement: Union[str, List[List[int]], Callable[[int], List[int]]] = "full", entanglement: Union[
str,
Dict[int, List[Tuple[int]]],
Callable[[int], Union[str, Dict[int, List[Tuple[int]]]]],
] = "full",
alpha: float = 2.0, alpha: float = 2.0,
paulis: Optional[List[str]] = None, paulis: Optional[List[str]] = None,
data_map_func: Optional[Callable[[np.ndarray], float]] = None, data_map_func: Optional[Callable[[np.ndarray], float]] = None,
@ -129,8 +132,13 @@ class PauliFeatureMap(NLocal):
Args: Args:
feature_dimension: Number of qubits in the circuit. feature_dimension: Number of qubits in the circuit.
reps: The number of repeated circuits. reps: The number of repeated circuits.
entanglement: Specifies the entanglement structure. Refer to entanglement: Specifies the entanglement structure. Can be a string (``'full'``,
:class:`~qiskit.circuit.library.NLocal` for detail. ``'linear'``, ``'reverse_linear'``, ``'circular'`` or ``'sca'``) or can be a
dictionary where the keys represent the number of qubits and the values are list
of integer-pairs specifying the indices of qubits that are entangled with one
another, for example: ``{1: [(0,), (2,)], 2: [(0,1), (2,0)]}`` or can be a
``Callable[[int], Union[str | Dict[...]]]`` to return an entanglement specific for
a repetition
alpha: The Pauli rotation factor, multiplicative to the pauli rotations alpha: The Pauli rotation factor, multiplicative to the pauli rotations
paulis: A list of strings for to-be-used paulis. If None are provided, ``['Z', 'ZZ']`` paulis: A list of strings for to-be-used paulis. If None are provided, ``['Z', 'ZZ']``
will be used. will be used.
@ -281,6 +289,45 @@ class PauliFeatureMap(NLocal):
basis_change(evo, inverse=True) basis_change(evo, inverse=True)
return evo return evo
def get_entangler_map(
self, rep_num: int, block_num: int, num_block_qubits: int
) -> Sequence[Sequence[int]]:
# if entanglement is a Callable[[int], Union[str | Dict[...]]]
if callable(self._entanglement):
entanglement = self._entanglement(rep_num)
else:
entanglement = self._entanglement
# entanglement is Dict[int, List[List[int]]]
if isinstance(entanglement, dict):
if all(
isinstance(e2, (int, np.int32, np.int64))
for key in entanglement.keys()
for en in entanglement[key]
for e2 in en
):
for qb, ent in entanglement.items():
for en in ent:
if len(en) != qb:
raise ValueError(
f"For num_qubits = {qb}, entanglement must be a "
f"tuple of length {qb}. You specified {en}."
)
# Check if the entanglement is specified for all the pauli blocks being used
for pauli in self.paulis:
if len(pauli) not in entanglement.keys():
raise ValueError(f"No entanglement specified for {pauli} pauli.")
return entanglement[num_block_qubits]
else:
# if the entanglement is not Dict[int, List[List[int]]] or
# Dict[int, List[Tuple[int]]] then we fall back on the original
# `get_entangler_map()` method from NLocal
return super().get_entangler_map(rep_num, block_num, num_block_qubits)
def self_product(x: np.ndarray) -> float: def self_product(x: np.ndarray) -> float:
""" """

View File

@ -12,7 +12,7 @@
"""Second-order Pauli-Z expansion circuit.""" """Second-order Pauli-Z expansion circuit."""
from typing import Callable, List, Union, Optional from typing import Callable, List, Union, Optional, Dict, Tuple
import numpy as np import numpy as np
from .pauli_feature_map import PauliFeatureMap from .pauli_feature_map import PauliFeatureMap
@ -75,7 +75,11 @@ class ZZFeatureMap(PauliFeatureMap):
self, self,
feature_dimension: int, feature_dimension: int,
reps: int = 2, reps: int = 2,
entanglement: Union[str, List[List[int]], Callable[[int], List[int]]] = "full", entanglement: Union[
str,
Dict[int, List[Tuple[int]]],
Callable[[int], Union[str, Dict[int, List[Tuple[int]]]]],
] = "full",
data_map_func: Optional[Callable[[np.ndarray], float]] = None, data_map_func: Optional[Callable[[np.ndarray], float]] = None,
parameter_prefix: str = "x", parameter_prefix: str = "x",
insert_barriers: bool = False, insert_barriers: bool = False,
@ -87,7 +91,7 @@ class ZZFeatureMap(PauliFeatureMap):
feature_dimension: Number of features. feature_dimension: Number of features.
reps: The number of repeated circuits, has a min. value of 1. reps: The number of repeated circuits, has a min. value of 1.
entanglement: Specifies the entanglement structure. Refer to entanglement: Specifies the entanglement structure. Refer to
:class:`~qiskit.circuit.library.NLocal` for detail. :class:`~qiskit.circuit.library.PauliFeatureMap` for detail.
data_map_func: A mapping function for data x. data_map_func: A mapping function for data x.
parameter_prefix: The prefix used if default parameters are generated. parameter_prefix: The prefix used if default parameters are generated.
insert_barriers: If True, barriers are inserted in between the evolution instructions insert_barriers: If True, barriers are inserted in between the evolution instructions

View File

@ -0,0 +1,26 @@
---
fixes:
- |
Fixed that the entanglement in :class:`.PauliFeatureMap` and :class:`.ZZFeatureMap`
could be given as ``List[int]`` or ``List[List[int]]``, which was incompatible with the fact
that entanglement blocks of different sizes are used. Instead, the entanglement can be
given as dictionary with ``{block_size: entanglement}`` pairs.
features_circuits:
- |
:class:`.PauliFeatureMap` and :class:`.ZZFeatureMap` now support specifying the
entanglement as a dictionary where the keys represent the number of qubits, and
the values are lists of integer tuples that define which qubits are entangled with one another. This
allows for more flexibility in constructing feature maps tailored to specific quantum algorithms.
Example usage::
from qiskit.circuit.library import PauliFeatureMap
entanglement = {
1: [(0,), (2,)],
2: [(0, 1), (1, 2)],
3: [(0, 1, 2)],
}
qc = PauliFeatureMap(3, reps=2, paulis=['Z', 'ZZ', 'ZZZ'], entanglement=entanglement, insert_barriers=True)
qc.decompose().draw('mpl')

View File

@ -221,6 +221,79 @@ class TestDataPreparation(QiskitTestCase):
self.assertEqual(str(encoding_z_param_y.parameters), "ParameterView([Parameter(y)])") self.assertEqual(str(encoding_z_param_y.parameters), "ParameterView([Parameter(y)])")
self.assertEqual(str(encoding_zz_param_y.parameters), "ParameterView([Parameter(y)])") self.assertEqual(str(encoding_zz_param_y.parameters), "ParameterView([Parameter(y)])")
def test_entanglement_as_dictionary(self):
"""Test whether PauliFeatureMap accepts entanglement as a dictionary and generates
correct feature map circuit"""
n_qubits = 3
entanglement = {
1: [(0,), (2,)],
2: [(0, 1), (1, 2)],
3: [(0, 1, 2)],
}
params = [np.pi / 4, np.pi / 2, np.pi]
def z_block(circuit, q1):
circuit.p(2 * params[q1], q1)
def zz_block(circuit, q1, q2):
param = (np.pi - params[q1]) * (np.pi - params[q2])
circuit.cx(q1, q2)
circuit.p(2 * param, q2)
circuit.cx(q1, q2)
def zzz_block(circuit, q1, q2, q3):
param = (np.pi - params[q1]) * (np.pi - params[q2]) * (np.pi - params[q3])
circuit.cx(q1, q2)
circuit.cx(q2, q3)
circuit.p(2 * param, q3)
circuit.cx(q2, q3)
circuit.cx(q1, q2)
feat_map = PauliFeatureMap(
n_qubits, reps=2, paulis=["Z", "ZZ", "ZZZ"], entanglement=entanglement
).assign_parameters(params)
qc = QuantumCircuit(n_qubits)
for _ in range(2):
qc.h([0, 1, 2])
for e1 in entanglement[1]:
z_block(qc, *e1)
for e2 in entanglement[2]:
zz_block(qc, *e2)
for e3 in entanglement[3]:
zzz_block(qc, *e3)
self.assertTrue(Operator(feat_map).equiv(qc))
def test_invalid_entanglement(self):
"""Test if a ValueError is raised when an invalid entanglement is passed"""
n_qubits = 3
entanglement = {
1: [(0, 1), (2,)],
2: [(0, 1), (1, 2)],
3: [(0, 1, 2)],
}
with self.assertRaises(ValueError):
feat_map = PauliFeatureMap(
n_qubits, reps=2, paulis=["Z", "ZZ", "ZZZ"], entanglement=entanglement
)
feat_map.count_ops()
def test_entanglement_not_specified(self):
"""Test if an error is raised when entanglement is not explicitly specified for
all n-qubit pauli blocks"""
n_qubits = 3
entanglement = {
1: [(0, 1), (2,)],
3: [(0, 1, 2)],
}
with self.assertRaises(ValueError):
feat_map = PauliFeatureMap(
n_qubits, reps=2, paulis=["Z", "ZZ", "ZZZ"], entanglement=entanglement
)
feat_map.count_ops()
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()