Add flatten option to the NLocal family (#10269)

* Add flatten option to the NLocal family

This commit adds a new flag `flatten` to the circuit library elements in
the NLocal family. This flag is used to avoid the nested wrapping of the
output that is done by default in the output of these gates. This is
done primarily in the interest of performance. The runtime
performance of bind_parameters() is vastly improved with a flattened
output compared to the nested object. For example, running:

```
qc = EfficientSU2(100, entanglement="linear", reps=100)
qc.measure_all()
qc.bind_parameters({x: math.pi / 2 for x in qc.parameters})
```

the runtime of `bind_parameters()` went from ~390 seconds with the
wrapped output to ~0.5 seconds with the flattened output. I think given
these results longer term we might want to flip the default behavior to
only wrap on user request (likely for visualization as that's the only
use case I can think of for the wrapped output). The default value
is set to `None` in this PR to facilitate this change. In 0.26/0.45 we
can emit a warning if `flatten` is `None` to warn that the default will
change in a future release.

* Add missing nlocal subclasses

* Add setter

* Fix lint

* Fix flatten for EvolvedOperatorAnsatz
This commit is contained in:
Matthew Treinish 2023-07-20 12:08:56 -04:00 committed by GitHub
parent 7c1b8ee4fe
commit fcd7766fec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 121 additions and 10 deletions

View File

@ -14,6 +14,7 @@
from __future__ import annotations
from collections.abc import Sequence
from typing import Optional
import numpy as np
@ -40,6 +41,7 @@ class EvolvedOperatorAnsatz(NLocal):
name: str = "EvolvedOps",
parameter_prefix: str | Sequence[str] = "t",
initial_state: QuantumCircuit | None = None,
flatten: Optional[bool] = None,
):
"""
Args:
@ -59,6 +61,13 @@ class EvolvedOperatorAnsatz(NLocal):
will be used for each parameters. Can also be a list to specify a prefix per
operator.
initial_state: A :class:`.QuantumCircuit` object to prepend to the circuit.
flatten: Set this to ``True`` to output a flat circuit instead of nesting it inside multiple
layers of gate objects. By default currently the contents of
the output circuit will be wrapped in nested objects for
cleaner visualization. However, if you're using this circuit
for anything besides visualization its **strongly** recommended
to set this flag to ``True`` to avoid a large performance
overhead for parameter binding.
"""
super().__init__(
initial_state=initial_state,
@ -66,6 +75,7 @@ class EvolvedOperatorAnsatz(NLocal):
reps=reps,
insert_barriers=insert_barriers,
name=name,
flatten=flatten,
)
self._operators = None
@ -187,7 +197,10 @@ class EvolvedOperatorAnsatz(NLocal):
gate = PauliEvolutionGate(operator, time, synthesis=evolution)
evolved = QuantumCircuit(operator.num_qubits)
evolved.append(gate, evolved.qubits)
if not self.flatten:
evolved.append(gate, evolved.qubits)
else:
evolved.compose(gate.definition, evolved.qubits, inplace=True)
return evolved
def _build(self):

View File

@ -94,6 +94,7 @@ class EfficientSU2(TwoLocal):
insert_barriers: bool = False,
initial_state: Optional[Any] = None,
name: str = "EfficientSU2",
flatten: Optional[bool] = None,
) -> None:
"""Create a new EfficientSU2 2-local circuit.
@ -124,7 +125,13 @@ class EfficientSU2(TwoLocal):
we use :class:`~qiskit.circuit.ParameterVector`.
insert_barriers: If True, barriers are inserted in between each layer. If False,
no barriers are inserted.
flatten: Set this to ``True`` to output a flat circuit instead of nesting it inside multiple
layers of gate objects. By default currently the contents of
the output circuit will be wrapped in nested objects for
cleaner visualization. However, if you're using this circuit
for anything besides visualization its **strongly** recommended
to set this flag to ``True`` to avoid a large performance
overhead for parameter binding.
"""
if su2_gates is None:
su2_gates = [RYGate, RZGate]
@ -140,6 +147,7 @@ class EfficientSU2(TwoLocal):
insert_barriers=insert_barriers,
initial_state=initial_state,
name=name,
flatten=flatten,
)
@property

View File

@ -100,6 +100,7 @@ class ExcitationPreserving(TwoLocal):
insert_barriers: bool = False,
initial_state: Optional[Any] = None,
name: str = "ExcitationPreserving",
flatten: Optional[bool] = None,
) -> None:
"""Create a new ExcitationPreserving 2-local circuit.
@ -127,6 +128,13 @@ class ExcitationPreserving(TwoLocal):
we use :class:`~qiskit.circuit.ParameterVector`.
insert_barriers: If True, barriers are inserted in between each layer. If False,
no barriers are inserted.
flatten: Set this to ``True`` to output a flat circuit instead of nesting it inside multiple
layers of gate objects. By default currently the contents of
the output circuit will be wrapped in nested objects for
cleaner visualization. However, if you're using this circuit
for anything besides visualization its **strongly** recommended
to set this flag to ``True`` to avoid a large performance
overhead for parameter binding.
Raises:
ValueError: If the selected mode is not supported.
@ -155,6 +163,7 @@ class ExcitationPreserving(TwoLocal):
insert_barriers=insert_barriers,
initial_state=initial_state,
name=name,
flatten=flatten,
)
@property

View File

@ -90,6 +90,7 @@ class NLocal(BlueprintCircuit):
skip_unentangled_qubits: bool = False,
initial_state: QuantumCircuit | None = None,
name: str | None = "nlocal",
flatten: Optional[bool] = None,
) -> None:
"""Create a new n-local circuit.
@ -114,6 +115,13 @@ class NLocal(BlueprintCircuit):
initial_state: A :class:`.QuantumCircuit` object which can be used to describe an initial
state prepended to the NLocal circuit.
name: The name of the circuit.
flatten: Set this to ``True`` to output a flat circuit instead of nesting it inside multiple
layers of gate objects. By default currently the contents of
the output circuit will be wrapped in nested objects for
cleaner visualization. However, if you're using this circuit
for anything besides visualization its **strongly** recommended
to set this flag to ``True`` to avoid a large performance
overhead for parameter binding.
Examples:
TODO
@ -144,6 +152,7 @@ class NLocal(BlueprintCircuit):
self._initial_state: QuantumCircuit | None = None
self._initial_state_circuit: QuantumCircuit | None = None
self._bounds: list[tuple[float | None, float | None]] | None = None
self._flatten = flatten
if int(reps) != reps:
raise TypeError("The value of reps should be int")
@ -188,6 +197,16 @@ class NLocal(BlueprintCircuit):
self._num_qubits = num_qubits
self.qregs = [QuantumRegister(num_qubits, name="q")]
@property
def flatten(self) -> bool:
"""Returns whether the circuit is wrapped in nested gates/instructions or flattened."""
return bool(self._flatten)
@flatten.setter
def flatten(self, flatten: bool) -> None:
self._invalidate()
self._flatten = flatten
def _convert_to_block(self, layer: Any) -> QuantumCircuit:
"""Try to convert ``layer`` to a QuantumCircuit.
@ -900,7 +919,10 @@ class NLocal(BlueprintCircuit):
if self.num_qubits == 0:
return
circuit = QuantumCircuit(*self.qregs, name=self.name)
if not self._flatten:
circuit = QuantumCircuit(*self.qregs, name=self.name)
else:
circuit = self
# use the initial state as starting circuit, if it is set
if self.initial_state:
@ -944,12 +966,13 @@ class NLocal(BlueprintCircuit):
# expression contains free parameters
pass
try:
block = circuit.to_gate()
except QiskitError:
block = circuit.to_instruction()
if not self._flatten:
try:
block = circuit.to_gate()
except QiskitError:
block = circuit.to_instruction()
self.append(block, self.qubits)
self.append(block, self.qubits)
# pylint: disable=unused-argument
def _parameter_generator(self, rep: int, block: int, indices: list[int]) -> Parameter | None:

View File

@ -14,6 +14,8 @@
# pylint: disable=cyclic-import
from __future__ import annotations
from typing import Optional
import numpy as np
from qiskit.circuit.library.evolved_operator_ansatz import EvolvedOperatorAnsatz, _is_pauli_identity
@ -39,6 +41,7 @@ class QAOAAnsatz(EvolvedOperatorAnsatz):
initial_state: QuantumCircuit | None = None,
mixer_operator=None,
name: str = "QAOA",
flatten: Optional[bool] = None,
):
r"""
Args:
@ -55,8 +58,15 @@ class QAOAAnsatz(EvolvedOperatorAnsatz):
in the original paper. Can be an operator or an optionally parameterized quantum
circuit.
name (str): A name of the circuit, default 'qaoa'
flatten: Set this to ``True`` to output a flat circuit instead of nesting it inside multiple
layers of gate objects. By default currently the contents of
the output circuit will be wrapped in nested objects for
cleaner visualization. However, if you're using this circuit
for anything besides visualization its **strongly** recommended
to set this flag to ``True`` to avoid a large performance
overhead for parameter binding.
"""
super().__init__(reps=reps, name=name)
super().__init__(reps=reps, name=name, flatten=flatten)
self._cost_operator = None
self._reps = reps

View File

@ -124,6 +124,7 @@ class RealAmplitudes(TwoLocal):
insert_barriers: bool = False,
initial_state: Optional[Any] = None,
name: str = "RealAmplitudes",
flatten: Optional[bool] = None,
) -> None:
"""Create a new RealAmplitudes 2-local circuit.
@ -153,7 +154,13 @@ class RealAmplitudes(TwoLocal):
we use :class:`~qiskit.circuit.ParameterVector`.
insert_barriers: If True, barriers are inserted in between each layer. If False,
no barriers are inserted.
flatten: Set this to ``True`` to output a flat circuit instead of nesting it inside multiple
layers of gate objects. By default currently the contents of
the output circuit will be wrapped in nested objects for
cleaner visualization. However, if you're using this circuit
for anything besides visualization its **strongly** recommended
to set this flag to ``True`` to avoid a large performance
overhead for parameter binding.
"""
super().__init__(
num_qubits=num_qubits,
@ -167,6 +174,7 @@ class RealAmplitudes(TwoLocal):
parameter_prefix=parameter_prefix,
insert_barriers=insert_barriers,
name=name,
flatten=flatten,
)
@property

View File

@ -173,6 +173,7 @@ class TwoLocal(NLocal):
insert_barriers: bool = False,
initial_state: Optional[Any] = None,
name: str = "TwoLocal",
flatten: Optional[bool] = None,
) -> None:
"""Construct a new two-local circuit.
@ -208,6 +209,13 @@ class TwoLocal(NLocal):
insert_barriers: If ``True``, barriers are inserted in between each layer. If ``False``,
no barriers are inserted. Defaults to ``False``.
initial_state: A :class:`.QuantumCircuit` object to prepend to the circuit.
flatten: Set this to ``True`` to output a flat circuit instead of nesting it inside multiple
layers of gate objects. By default currently the contents of
the output circuit will be wrapped in nested objects for
cleaner visualization. However, if you're using this circuit
for anything besides visualization its **strongly** recommended
to set this flag to ``True`` to avoid a large performance
overhead for parameter binding.
"""
super().__init__(
@ -222,6 +230,7 @@ class TwoLocal(NLocal):
initial_state=initial_state,
parameter_prefix=parameter_prefix,
name=name,
flatten=flatten,
)
def _convert_to_block(self, layer: Union[str, type, Gate, QuantumCircuit]) -> QuantumCircuit:

View File

@ -0,0 +1,22 @@
---
features:
- |
Added a new keyword argument ``flatten`` to the constructor for the
following classes:
* :class:`~.EfficientSU2`
* :class:`~.ExcitationPreserving`
* :class:`~.NLocal`
* :class:`~.RealAmplitudes`
* :class:`~.TwoLocal`
* :class:`~.EvolvedOperatorAnsatz`
* :class:`~.QAOAAnsatz`
If this argument is set to ``True`` the :class:`~.QuantumCircuit` subclass
generated will not wrap the implementation into :class:`~.Gate` or
:class:`~.circuit.Instruction` objects. While this isn't optimal for visualization
it typically results in much better runtime performance, especially with
:meth:`.QuantumCircuit.bind_parameters` and
:meth:`.QuantumCircuit.assign_parameters` which can see a substatial
runtime improvement with a flattened output compared to the nested
wrapped default output.

View File

@ -119,6 +119,15 @@ class TestEvolvedOperatorAnsatz(QiskitTestCase):
evo = EvolvedOperatorAnsatz(unitary, reps=3).decompose()
self.assertEqual(evo.count_ops()["hamiltonian"], 3)
def test_flattened(self):
"""Test flatten option is actually flattened."""
num_qubits = 3
ops = [Pauli("Z" * num_qubits), Pauli("Y" * num_qubits), Pauli("X" * num_qubits)]
evo = EvolvedOperatorAnsatz(ops, reps=3, flatten=True)
self.assertNotIn("hamiltonian", evo.count_ops())
self.assertNotIn("EvolvedOps", evo.count_ops())
self.assertNotIn("PauliEvolution", evo.count_ops())
def evolve(pauli_string, time):
"""Get the reference evolution circuit for a single Pauli string."""