Skip to content

Commit

Permalink
Improve Parameter handling in SparsePauliOp (Qiskit#9796)
Browse files Browse the repository at this point in the history
* add reno

* Add assign_parameters and parameter in init

* add SPO.parameters and remove utils

* fix ParameterValueType typehint

* Update qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py

Co-authored-by: Ikko Hamamura <ikkoham@users.noreply.github.com>

* remove trailing print

* Elena's comments

Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com>

* fix line length

---------

Co-authored-by: Ikko Hamamura <ikkoham@users.noreply.github.com>
Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com>
  • Loading branch information
3 people authored Mar 24, 2023
1 parent 7bb4af9 commit 9c8eb06
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 84 deletions.
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)
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

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 @@ -961,6 +962,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()

0 comments on commit 9c8eb06

Please sign in to comment.