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

Improve Parameter handling in SparsePauliOp #9796

Merged
merged 8 commits into from
Mar 24, 2023
Merged
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
23 changes: 15 additions & 8 deletions qiskit/algorithms/time_evolvers/trotterization/trotter_qrte.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@
from qiskit.quantum_info import Pauli, SparsePauliOp
from qiskit.synthesis import ProductFormula, LieTrotter

from qiskit.algorithms.utils.assign_params import _assign_parameters, _get_parameters


class TrotterQRTE(RealTimeEvolver):
"""Quantum Real Time Evolution using Trotterization.
Expand Down Expand Up @@ -165,16 +163,25 @@ def evolve(self, evolution_problem: TimeEvolutionProblem) -> TimeEvolutionResult
"The time evolution problem contained ``aux_operators`` but no estimator was "
"provided. The algorithm continues without calculating these quantities. "
)

# ensure the hamiltonian is a sparse pauli op
hamiltonian = evolution_problem.hamiltonian
if not isinstance(hamiltonian, (Pauli, PauliSumOp, SparsePauliOp)):
raise ValueError(
f"TrotterQRTE only accepts Pauli | PauliSumOp, {type(hamiltonian)} provided."
f"TrotterQRTE only accepts Pauli | PauliSumOp | SparsePauliOp, {type(hamiltonian)} "
"provided."
)
if isinstance(hamiltonian, PauliSumOp):
hamiltonian = hamiltonian.primitive * hamiltonian.coeff
elif isinstance(hamiltonian, Pauli):
hamiltonian = SparsePauliOp(hamiltonian)

t_param = evolution_problem.t_param
if t_param is not None and _get_parameters(hamiltonian.coeffs) != ParameterView([t_param]):
free_parameters = hamiltonian.parameters
if t_param is not None and free_parameters != ParameterView([t_param]):
raise ValueError(
"Hamiltonian time parameter does not match evolution_problem.t_param "
"or contains multiple parameters"
f"Hamiltonian time parameters ({free_parameters}) do not match "
f"evolution_problem.t_param ({t_param})."
)

# make sure PauliEvolutionGate does not implement more than one Trotter step
Expand Down Expand Up @@ -213,9 +220,9 @@ def evolve(self, evolution_problem: TimeEvolutionProblem) -> TimeEvolutionResult
# evolution for next step
if t_param is not None:
time_value = (n + 1) * dt
bound_coeffs = _assign_parameters(hamiltonian.coeffs, [time_value])
bound_hamiltonian = hamiltonian.assign_parameters([time_value])
single_step_evolution_gate = PauliEvolutionGate(
SparsePauliOp(hamiltonian.paulis, bound_coeffs),
bound_hamiltonian,
dt,
synthesis=self.product_formula,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@
from qiskit.quantum_info import SparsePauliOp
from qiskit.quantum_info.operators.base_operator import BaseOperator

from qiskit.algorithms.utils.assign_params import _assign_parameters

from ..variational_principles import VariationalPrinciple


Expand Down Expand Up @@ -115,13 +113,12 @@ def solve_lse(

if self._time_param is not None:
if time_value is not None:
bound_params_array = _assign_parameters(self._hamiltonian.coeffs, [time_value])
hamiltonian = SparsePauliOp(self._hamiltonian.paulis, bound_params_array)
hamiltonian = hamiltonian.assign_parameters([time_value])
else:
raise ValueError(
f"Providing a time_value is required for time-dependant hamiltonians, "
"Providing a time_value is required for time-dependent hamiltonians, "
f"but got time_value = {time_value}. "
f"Please provide a time_value to the solve_lse method."
"Please provide a time_value to the solve_lse method."
)

evolution_grad_lse_rhs = self._var_principle.evolution_gradient(
Expand Down
62 changes: 0 additions & 62 deletions qiskit/algorithms/utils/assign_params.py

This file was deleted.

71 changes: 69 additions & 2 deletions qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,21 @@
N-Qubit Sparse Pauli Operator class.
"""

from __future__ import annotations

from collections import defaultdict
from collections.abc import Mapping, Sequence
from numbers import Number
from typing import Dict, Optional
from copy import deepcopy

import numpy as np
import rustworkx as rx

from qiskit._accelerate.sparse_pauli_op import unordered_unique
from qiskit.circuit.parameter import Parameter
from qiskit.circuit.parameterexpression import ParameterExpression
from qiskit.circuit.parametertable import ParameterView
from qiskit.exceptions import QiskitError
from qiskit.quantum_info.operators.custom_iterator import CustomIterator
from qiskit.quantum_info.operators.linear_op import LinearOp
Expand Down Expand Up @@ -112,10 +119,18 @@ def __init__(self, data, coeffs=None, *, ignore_pauli_phase=False, copy=True):

pauli_list = PauliList(data.copy() if copy and hasattr(data, "copy") else data)

dtype = object if isinstance(coeffs, np.ndarray) and coeffs.dtype == object else complex
if isinstance(coeffs, np.ndarray) and coeffs.dtype == object:
dtype = object
elif coeffs is not None:
if not isinstance(coeffs, (np.ndarray, Sequence)):
coeffs = [coeffs]
if any(isinstance(coeff, ParameterExpression) for coeff in coeffs):
dtype = object
else:
dtype = complex

if coeffs is None:
coeffs = np.ones(pauli_list.size, dtype=dtype)
coeffs = np.ones(pauli_list.size, dtype=complex)
Cryoris marked this conversation as resolved.
Show resolved Hide resolved
else:
coeffs = np.array(coeffs, copy=copy, dtype=dtype)

Expand Down Expand Up @@ -997,6 +1012,58 @@ def group_commuting(self, qubit_wise=False):
groups[color].append(idx)
return [self[group] for group in groups.values()]

@property
def parameters(self) -> ParameterView:
r"""Return the free ``Parameter``\s in the coefficients."""
ret = set()
for coeff in self.coeffs:
if isinstance(coeff, ParameterExpression):
ret |= coeff.parameters
return ParameterView(ret)

def assign_parameters(
self,
parameters: Mapping[Parameter, complex | ParameterExpression]
| Sequence[complex | ParameterExpression],
inplace: bool = False,
) -> SparsePauliOp | None:
r"""Bind the free ``Parameter``\s in the coefficients to provided values.

Args:
parameters: The values to bind the parameters to.
inplace: If ``False``, a copy of the operator with the bound parameters is returned.
If ``True`` the operator itself is modified.

Returns:
A copy of the operator with bound parameters, if ``inplace`` is ``False``, otherwise
``None``.
"""
if inplace:
bound = self
else:
bound = deepcopy(self)

# turn the parameters to a dictionary
if isinstance(parameters, Sequence):
free_parameters = bound.parameters
if len(parameters) != len(free_parameters):
raise ValueError(
f"Mismatching number of values ({len(parameters)}) and parameters "
f"({len(free_parameters)}). For partial binding please pass a dictionary of "
"{parameter: value} pairs."
)
parameters = dict(zip(free_parameters, parameters))

for i, coeff in enumerate(bound.coeffs):
if isinstance(coeff, ParameterExpression):
for key in coeff.parameters & parameters.keys():
coeff = coeff.assign(key, parameters[key])
if len(coeff.parameters) == 0:
coeff = complex(coeff)
bound.coeffs[i] = coeff

Copy link
Contributor

Choose a reason for hiding this comment

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

If there are no free parameters for coeffs, should the dtype of coeffs be np.complex128 not object?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That would be nicer, although we'll have to copy the array in that case. I couldn't think of a place where it would be a problem to have an object-array so I didn't add it, but we can also change it 🙂

Copy link
Contributor

Choose a reason for hiding this comment

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

OK, It is true that copying is not a good for the performance, so let's merge with this. If necessary, the user can make change.

return None if inplace else bound


# Update docstrings for API docs
generate_apidocs(SparsePauliOp)
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
features:
- |
Natively support the construction of :class:`.SparsePauliOp` objects with
:class:`.ParameterExpression` coefficients, without requiring the explicit construction
of an object-array. Now the following is supported::

from qiskit.circuit import Parameter
from qiskit.quantum_info import SparsePauliOp

x = Parameter("x")
op = SparsePauliOp(["Z", "X"], coeffs=[1, x])

- |
Added the :meth:`.SparsePauliOp.assign_parameters` method and
:attr:`.SparsePauliOp.parameters` attribute to assign and query unbound parameters
inside a :class:`.SparsePauliOp`. This function can for example be used as::

from qiskit.circuit import Parameter
from qiskit.quantum_info import SparsePauliOp

x = Parameter("x")
op = SparsePauliOp(["Z", "X"], coeffs=[1, x])

# free_params will be: ParameterView([x])
free_params = op.parameters

# assign the value 2 to the parameter x
bound = op.assign_parameters([2])
8 changes: 2 additions & 6 deletions test/python/algorithms/time_evolvers/test_trotter_qrte.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
from numpy.testing import assert_raises

from qiskit.algorithms.time_evolvers import TimeEvolutionProblem, TrotterQRTE
from qiskit.algorithms.utils.assign_params import _assign_parameters
from qiskit.primitives import Estimator
from qiskit import QuantumCircuit
from qiskit.circuit.library import ZGate
Expand Down Expand Up @@ -245,11 +244,8 @@ def _get_expected_trotter_qrte(operator, time, num_timesteps, init_state, observ
for n in range(num_timesteps):
if t_param is not None:
time_value = (n + 1) * dt
bound_coeffs = _assign_parameters(operator.coeffs, [time_value])
ops = [
Pauli(op).to_matrix() * np.real(coeff)
for op, coeff in SparsePauliOp(operator.paulis, bound_coeffs).to_list()
]
bound = operator.assign_parameters([time_value])
ops = [Pauli(op).to_matrix() * np.real(coeff) for op, coeff in bound.to_list()]
for op in ops:
psi = expm(-1j * op * dt).dot(psi)
observable_results.append(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

from qiskit import QiskitError
from qiskit.circuit import Parameter, ParameterVector
from qiskit.circuit.parametertable import ParameterView
from qiskit.quantum_info.operators import Operator, Pauli, PauliList, PauliTable, SparsePauliOp
from qiskit.test import QiskitTestCase

Expand Down Expand Up @@ -947,6 +948,33 @@ def test_dot_real(self):
iz = SparsePauliOp("Z", 1j)
self.assertEqual(x.dot(y), iz)

def test_get_parameters(self):
"""Test getting the parameters."""
x, y = Parameter("x"), Parameter("y")
op = SparsePauliOp(["X", "Y", "Z"], coeffs=[1, x, x * y])

with self.subTest(msg="all parameters"):
self.assertEqual(ParameterView([x, y]), op.parameters)

op.assign_parameters({y: 2}, inplace=True)
with self.subTest(msg="after partial binding"):
self.assertEqual(ParameterView([x]), op.parameters)

def test_assign_parameters(self):
"""Test assign parameters."""
x, y = Parameter("x"), Parameter("y")
op = SparsePauliOp(["X", "Y", "Z"], coeffs=[1, x, x * y])

# partial binding inplace
op.assign_parameters({y: 2}, inplace=True)
with self.subTest(msg="partial binding"):
self.assertListEqual(op.coeffs.tolist(), [1, x, 2 * x])

# bind via array
bound = op.assign_parameters([3])
with self.subTest(msg="fully bound"):
self.assertTrue(np.allclose(bound.coeffs.astype(complex), [1, 3, 6]))


if __name__ == "__main__":
unittest.main()