Skip to content

Commit

Permalink
Merge branch 'evolve-sparse-observable' into c-api-demo
Browse files Browse the repository at this point in the history
  • Loading branch information
Cryoris committed Feb 16, 2025
2 parents 5ef3735 + 1b8c9f7 commit 6a26224
Show file tree
Hide file tree
Showing 10 changed files with 641 additions and 175 deletions.
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>>();
insts.extend(evo);
}
}
Expand Down
4 changes: 4 additions & 0 deletions crates/accelerate/src/sparse_observable/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1837,6 +1837,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))
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

0 comments on commit 6a26224

Please sign in to comment.