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

SparseObservable evolution #13836

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
354 changes: 260 additions & 94 deletions crates/accelerate/src/circuit_library/pauli_evolution.rs

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions crates/accelerate/src/circuit_library/pauli_feature_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,17 +178,17 @@ fn _get_evolution_layer<'a>(
// to call CircuitData::from_packed_operations. This is needed since we might
// have to interject barriers, which are not a standard gate and prevents us
// from using CircuitData::from_standard_gates.
let evo = pauli_evolution::pauli_evolution(
let evo = pauli_evolution::sparse_term_evolution(
pauli,
indices.into_iter().rev().collect(),
multiply_param(&angle, alpha, py),
true,
false,
)
.map(|(gate, params, qargs)| {
(gate.into(), params, qargs.to_vec(), vec![] as Vec<Clbit>)
})
.collect::<Vec<Instruction>>();
);
// .map(|(gate, params, qargs)| {
// (gate.into(), params, qargs.to_vec(), vec![] as Vec<Clbit>)
// })
// .collect::<Vec<Instruction>>();
Comment on lines +188 to +191
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// .map(|(gate, params, qargs)| {
// (gate.into(), params, qargs.to_vec(), vec![] as Vec<Clbit>)
// })
// .collect::<Vec<Instruction>>();

insts.extend(evo);
}
}
Expand Down
8 changes: 6 additions & 2 deletions crates/accelerate/src/sparse_observable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,8 +198,8 @@ fn bit_term_as_pauli(bit: &BitTerm) -> &'static [(bool, Option<BitTerm>)] {
BitTerm::Z => &[(true, Some(BitTerm::Z))],
BitTerm::Plus => &[(true, None), (true, Some(BitTerm::X))],
BitTerm::Minus => &[(true, None), (false, Some(BitTerm::X))],
BitTerm::Left => &[(true, None), (true, Some(BitTerm::Y))],
BitTerm::Right => &[(true, None), (false, Some(BitTerm::Y))],
BitTerm::Right => &[(true, None), (true, Some(BitTerm::Y))],
BitTerm::Left => &[(true, None), (false, Some(BitTerm::Y))],
BitTerm::Zero => &[(true, None), (true, Some(BitTerm::Z))],
BitTerm::One => &[(true, None), (false, Some(BitTerm::Z))],
}
Expand Down Expand Up @@ -1430,6 +1430,10 @@ impl PySparseTerm {
Ok(obs.into())
}

fn to_label(&self) -> PyResult<String> {
Ok(self.inner.view().to_sparse_str())
}

fn __eq__(slf: Bound<Self>, other: Bound<PyAny>) -> PyResult<bool> {
if slf.is(&other) {
return Ok(true);
Expand Down
69 changes: 48 additions & 21 deletions qiskit/circuit/library/pauli_evolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,23 @@
from qiskit.circuit.gate import Gate
from qiskit.circuit.quantumcircuit import ParameterValueType
from qiskit.circuit.parameterexpression import ParameterExpression
from qiskit.quantum_info import Pauli, SparsePauliOp
from qiskit.quantum_info import Pauli, SparsePauliOp, SparseObservable

if TYPE_CHECKING:
from qiskit.synthesis.evolution import EvolutionSynthesis

BIT_LABELS = {
0b0001: "Z",
0b1001: "0",
0b0101: "1",
0b0010: "X",
0b1010: "+",
0b0110: "-",
0b0011: "Y",
0b1011: "r",
0b0111: "l",
}


class PauliEvolutionGate(Gate):
r"""Time-evolution of an operator consisting of Paulis.
Expand Down Expand Up @@ -90,15 +102,19 @@ class PauliEvolutionGate(Gate):

def __init__(
self,
operator: Pauli | SparsePauliOp | list[Pauli | SparsePauliOp],
operator: (
Pauli
| SparsePauliOp
| SparseObservable
| list[Pauli | SparsePauliOp | SparseObservable]
),
time: ParameterValueType = 1.0,
label: str | None = None,
synthesis: EvolutionSynthesis | None = None,
) -> None:
"""
Args:
operator (Pauli | SparsePauliOp | list):
The operator to evolve. Can also be provided as list of non-commuting
operator: The operator to evolve. Can also be provided as list of non-commuting
operators where the elements are sums of commuting operators.
For example: ``[XY + YX, ZZ + ZI + IZ, YY]``.
time: The evolution time.
Expand All @@ -110,9 +126,9 @@ class docstring for an example.
product formula with a single repetition.
"""
if isinstance(operator, list):
operator = [_to_sparse_pauli_op(op) for op in operator]
operator = [_to_sparse_op(op) for op in operator]
else:
operator = _to_sparse_pauli_op(operator)
operator = _to_sparse_op(operator)

if label is None:
label = _get_default_label(operator)
Expand Down Expand Up @@ -159,32 +175,43 @@ def validate_parameter(self, parameter: ParameterValueType) -> ParameterValueTyp
return super().validate_parameter(parameter)


def _to_sparse_pauli_op(operator):
def _to_sparse_op(
operator: Pauli | SparsePauliOp | SparseObservable,
) -> SparsePauliOp | SparseObservable:
"""Cast the operator to a SparsePauliOp."""

if isinstance(operator, Pauli):
sparse_pauli = SparsePauliOp(operator)
elif isinstance(operator, SparsePauliOp):
sparse_pauli = operator
sparse = SparsePauliOp(operator)
elif isinstance(operator, (SparseObservable, SparsePauliOp)):
sparse = operator
else:
raise ValueError(f"Unsupported operator type for evolution: {type(operator)}.")

if any(np.iscomplex(sparse_pauli.coeffs)):
if any(np.iscomplex(sparse.coeffs)):
raise ValueError("Operator contains complex coefficients, which are not supported.")
if any(isinstance(coeff, ParameterExpression) for coeff in sparse_pauli.coeffs):
if any(isinstance(coeff, ParameterExpression) for coeff in sparse.coeffs):
raise ValueError("Operator contains ParameterExpression, which are not supported.")

return sparse_pauli
return sparse


def _operator_label(operator):
if isinstance(operator, SparseObservable):
if len(operator) == 1:
return _sparse_term_label(operator[0])
return "(" + " + ".join(_sparse_term_label(term) for term in operator) + ")"

# else: is a SparsePauliOp
if len(operator.paulis) == 1:
return operator.paulis.to_labels()[0]
return "(" + " + ".join(operator.paulis.to_labels()) + ")"


def _get_default_label(operator):
if isinstance(operator, list):
label = f"exp(-it ({[' + '.join(op.paulis.to_labels()) for op in operator]}))"
else:
if len(operator.paulis) == 1:
label = f"exp(-it {operator.paulis.to_labels()[0]})"
# for just a single Pauli don't add brackets around the sum
else:
label = f"exp(-it ({' + '.join(operator.paulis.to_labels())}))"
return f"exp(-it ({[_operator_label(op) for op in operator]}))"
return f"exp(-it {_operator_label(operator)})"


return label
def _sparse_term_label(term):
return "".join(BIT_LABELS[bit] for bit in reversed(term.bit_terms))
Comment on lines +216 to +217
Copy link
Member

Choose a reason for hiding this comment

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

I feel like we should do this in Rust as a pyfunction because it'd be faster and we could use the enum variants for BitTerm directly. Not that it really matters, but it feels more natural in rust.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was thinking of adding a method to SparseTerm, maybe something like bit_labels() or so. But a standalone function would ofc also work

10 changes: 9 additions & 1 deletion qiskit/synthesis/evolution/lie_trotter.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,10 @@ def __init__(
) = None,
wrap: bool = False,
preserve_order: bool = True,
*,
atomic_evolution_sparse_observable: bool = False,
) -> None:
"""
r"""
Args:
reps: The number of time steps.
insert_barriers: Whether to insert barriers between the atomic evolutions.
Expand All @@ -83,6 +85,11 @@ def __init__(
preserve_order: If ``False``, allows reordering the terms of the operator to
potentially yield a shallower evolution circuit. Not relevant
when synthesizing operator with a single term.
atomic_evolution_sparse_observable: If a custom ``atomic_evolution`` is passed,
which does not yet support :class:`.SparseObservable`\ s as input, set this
argument to ``False`` to automatically apply a conversion to :class:`.SparsePauliOp`.
This argument is supported until Qiskit 2.2, at which point all atomic evolutions
are required to support :class:`.SparseObservable`\ s as input.
"""
super().__init__(
1,
Expand All @@ -92,6 +99,7 @@ def __init__(
atomic_evolution,
wrap,
preserve_order=preserve_order,
atomic_evolution_sparse_observable=atomic_evolution_sparse_observable,
)

@property
Expand Down
66 changes: 53 additions & 13 deletions qiskit/synthesis/evolution/product_formula.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from __future__ import annotations

import warnings
import inspect
import itertools
from collections.abc import Callable, Sequence
Expand All @@ -24,7 +25,7 @@
import rustworkx as rx
from qiskit.circuit.parameterexpression import ParameterExpression
from qiskit.circuit.quantumcircuit import QuantumCircuit, ParameterValueType
from qiskit.quantum_info import SparsePauliOp, Pauli
from qiskit.quantum_info import SparsePauliOp, Pauli, SparseObservable
from qiskit.utils.deprecation import deprecate_arg
from qiskit._accelerate.circuit_library import pauli_evolution

Expand Down Expand Up @@ -70,8 +71,10 @@ def __init__(
) = None,
wrap: bool = False,
preserve_order: bool = True,
*,
atomic_evolution_sparse_observable: bool = False,
) -> None:
"""
r"""
Args:
order: The order of the product formula.
reps: The number of time steps.
Expand All @@ -94,6 +97,11 @@ def __init__(
preserve_order: If ``False``, allows reordering the terms of the operator to
potentially yield a shallower evolution circuit. Not relevant
when synthesizing operator with a single term.
atomic_evolution_sparse_observable: If a custom ``atomic_evolution`` is passed,
which does not yet support :class:`.SparseObservable`\ s as input, set this
argument to ``False`` to automatically apply a conversion to :class:`.SparsePauliOp`.
This argument is supported until Qiskit 2.2, at which point all atomic evolutions
are required to support :class:`.SparseObservable`\ s as input.
"""
super().__init__()
self.order = order
Expand All @@ -113,17 +121,10 @@ def __init__(
# if atomic evolution is not provided, set a default
if atomic_evolution is None:
self.atomic_evolution = None

elif len(inspect.signature(atomic_evolution).parameters) == 2:

def wrap_atomic_evolution(output, operator, time):
definition = atomic_evolution(operator, time)
output.compose(definition, wrap=wrap, inplace=True)

self.atomic_evolution = wrap_atomic_evolution

else:
self.atomic_evolution = atomic_evolution
self.atomic_evolution = wrap_custom_atomic_evolution(
atomic_evolution, wrap, atomic_evolution_sparse_observable
)

def expand(
self, evolution: PauliEvolutionGate
Expand Down Expand Up @@ -204,7 +205,7 @@ def _custom_evolution(self, num_qubits, pauli_rotations):
for i, pauli_rotation in enumerate(pauli_rotations):
if self._atomic_evolution is not None:
# use the user-provided evolution with a global operator
operator = SparsePauliOp.from_sparse_list([pauli_rotation], num_qubits)
operator = SparseObservable.from_sparse_list([pauli_rotation], num_qubits)
self.atomic_evolution(circuit, operator, time=1) # time is inside the Pauli coeff

else: # this means self._wrap is True
Expand Down Expand Up @@ -303,3 +304,42 @@ def _term_sort_key(term: SparsePauliLabel) -> typing.Any:

terms = list(itertools.chain(*terms_by_color.values()))
return terms


def wrap_custom_atomic_evolution(atomic_evolution, wrap, support_sparse_observable):
r"""Wrap a custom atomic evolution into compatible format for the product formula.

This includes an inplace action, i.e. the signature is (circuit, operator, time) and
ensuring that ``SparseObservable``\ s are supported.
"""
# first, ensure that the atomic evolution works in-place
if len(inspect.signature(atomic_evolution).parameters) == 2:

def inplace_atomic_evolution(output, operator, time):
definition = atomic_evolution(operator, time)
output.compose(definition, wrap=wrap, inplace=True)

else:
inplace_atomic_evolution = atomic_evolution

# next, enable backward compatible use of atomic evolutions, that did not support
# SparseObservable inputs
if support_sparse_observable is False:
warnings.warn(
"The atomic_evolution should support SparseObservables as operator input. "
"Until Qiskit 2.2, an automatic conversion to SparsePauliOp is done, which can "
"be turned off by passing the argument atomic_evolution_sparse_observable=True.",
category=PendingDeprecationWarning,
stacklevel=2,
)

def sparseobs_atomic_evolution(output, operator, time):
if isinstance(operator, SparseObservable):
operator = SparsePauliOp.from_sparse_observable(operator)

inplace_atomic_evolution(output, operator, time)

else:
sparseobs_atomic_evolution = inplace_atomic_evolution

return sparseobs_atomic_evolution
16 changes: 15 additions & 1 deletion qiskit/synthesis/evolution/qdrift.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ def __init__(
seed: int | None = None,
wrap: bool = False,
preserve_order: bool = True,
*,
atomic_evolution_sparse_observable: bool = False,
) -> None:
r"""
Args:
Expand All @@ -92,9 +94,21 @@ def __init__(
preserve_order: If ``False``, allows reordering the terms of the operator to
potentially yield a shallower evolution circuit. Not relevant
when synthesizing operator with a single term.
atomic_evolution_sparse_observable: If a custom ``atomic_evolution`` is passed,
which does not yet support :class:`.SparseObservable`\ s as input, set this
argument to ``False`` to automatically apply a conversion to :class:`.SparsePauliOp`.
This argument is supported until Qiskit 2.2, at which point all atomic evolutions
are required to support :class:`.SparseObservable`\ s as input.
"""
super().__init__(
1, reps, insert_barriers, cx_structure, atomic_evolution, wrap, preserve_order
1,
reps,
insert_barriers,
cx_structure,
atomic_evolution,
wrap,
preserve_order,
atomic_evolution_sparse_observable=atomic_evolution_sparse_observable,
)
self.sampled_ops = None
self.rng = np.random.default_rng(seed)
Expand Down
Loading
Loading