Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed ZZFeatureMap not accepting a list of entanglement #12767

Merged
merged 12 commits into from
Sep 3, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 42 additions & 5 deletions qiskit/circuit/library/data_preparation/pauli_feature_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@
# that they have been altered from the originals.

"""The Pauli expansion circuit module."""

from typing import Optional, Callable, List, Union
from typing import Optional, Callable, List, Union, Sequence, Dict, Tuple
from functools import reduce
import numpy as np

Expand Down Expand Up @@ -116,7 +115,7 @@ def __init__(
self,
feature_dimension: Optional[int] = None,
reps: int = 2,
entanglement: Union[str, List[List[int]], Callable[[int], List[int]]] = "full",
entanglement: Union[str, Dict[int, List[Tuple[int]]], Dict[int, List[List[int]]]] = "full",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should still accept a callable, which can return an entanglement specific for a repetition. So the signature should be: Union[str, Dict[...], Callable[[int], Union[str | Dict[...]].

alpha: float = 2.0,
paulis: Optional[List[str]] = None,
data_map_func: Optional[Callable[[np.ndarray], float]] = None,
Expand All @@ -129,8 +128,11 @@ def __init__(
Args:
feature_dimension: Number of qubits in the circuit.
reps: The number of repeated circuits.
entanglement: Specifies the entanglement structure. Refer to
:class:`~qiskit.circuit.library.NLocal` for detail.
entanglement: Specifies the entanglement structure. Can be a string (``'full'``,
``'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)]}``
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']``
will be used.
Expand All @@ -156,6 +158,7 @@ def __init__(
self._data_map_func = data_map_func or self_product
self._paulis = paulis or ["Z", "ZZ"]
self._alpha = alpha
self.entanglement = entanglement
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this necessary? This attribute should already be accessible via self._entanglement.


def _parameter_generator(
self, rep: int, block: int, indices: List[int]
Expand Down Expand Up @@ -281,6 +284,40 @@ def cx_chain(circuit, inverse=False):
basis_change(evo, inverse=True)
return evo

def get_entangler_map(
self, rep_num: int, block_num: int, num_block_qubits: int
) -> Sequence[Sequence[int]]:

# entanglement is Dict[int, List[List[int]]]
if isinstance(self.entanglement, dict):
if all(
isinstance(e2, (int, np.int32, np.int64))
for key in self.entanglement.keys()
for en in self.entanglement[key]
for e2 in en
):
for qb, ent in self.entanglement.items():
for ind, en in enumerate(ent):
if len(en) != qb:
raise ValueError(
f"For num_qubits = {qb}, entanglement must be a "
f"tuple of length {qb}. You specified {en}."
)
self.entanglement[qb][ind] = tuple(map(int, en))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we need this; users should already provide integers (as per the type hint) and I think the code works with both tuples and lists.


# Check if the entanglement is specified for all the pauli blocks being used
for pauli in self.paulis:
if len(pauli) not in self.entanglement.keys():
raise ValueError(f"No entanglement specified for {pauli} pauli.")

return self.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:
"""
Expand Down
6 changes: 3 additions & 3 deletions qiskit/circuit/library/data_preparation/zz_feature_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

"""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
from .pauli_feature_map import PauliFeatureMap

Expand Down Expand Up @@ -75,7 +75,7 @@ def __init__(
self,
feature_dimension: int,
reps: int = 2,
entanglement: Union[str, List[List[int]], Callable[[int], List[int]]] = "full",
entanglement: Union[str, Dict[int, List[Tuple[int]]], Dict[int, List[List[int]]]] = "full",
data_map_func: Optional[Callable[[np.ndarray], float]] = None,
parameter_prefix: str = "x",
insert_barriers: bool = False,
Expand All @@ -87,7 +87,7 @@ def __init__(
feature_dimension: Number of features.
reps: The number of repeated circuits, has a min. value of 1.
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.
parameter_prefix: The prefix used if default parameters are generated.
insert_barriers: If True, barriers are inserted in between the evolution instructions
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
features_circuits:
- |
The entanglement argument in :class:`.PauliFeatureMap` and :class:`.ZZFeatureMap` will no longer support ``List[int]`` or ``List[List[int]]``. Instead,
the entanglement structure in :class:`.PauliFeatureMap` and :class:`.ZZFeatureMap` can now be specified 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')

shravanpatel30 marked this conversation as resolved.
Show resolved Hide resolved


73 changes: 73 additions & 0 deletions test/python/circuit/library/test_pauli_feature_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,79 @@ def test_parameter_prefix(self):
self.assertEqual(str(encoding_z_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__":
unittest.main()
Loading