From 74d9adf0b6b92dd72555c085d143877a3e69829c Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Fri, 24 Mar 2023 08:46:38 +0100 Subject: [PATCH] Improve ``Parameter`` handling in ``SparsePauliOp`` (#9796) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * 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 Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> --- .../trotterization/trotter_qrte.py | 23 +++--- .../solvers/var_qte_linear_solver.py | 9 +-- qiskit/algorithms/utils/assign_params.py | 62 ---------------- .../operators/symplectic/sparse_pauli_op.py | 71 ++++++++++++++++++- ...ed-parameter-support-413f7598bac72166.yaml | 28 ++++++++ .../time_evolvers/test_trotter_qrte.py | 8 +-- .../symplectic/test_sparse_pauli_op.py | 28 ++++++++ 7 files changed, 145 insertions(+), 84 deletions(-) delete mode 100644 qiskit/algorithms/utils/assign_params.py create mode 100644 releasenotes/notes/sparsepauliop-improved-parameter-support-413f7598bac72166.yaml diff --git a/qiskit/algorithms/time_evolvers/trotterization/trotter_qrte.py b/qiskit/algorithms/time_evolvers/trotterization/trotter_qrte.py index 3e2c17870959..cb43e297aed2 100644 --- a/qiskit/algorithms/time_evolvers/trotterization/trotter_qrte.py +++ b/qiskit/algorithms/time_evolvers/trotterization/trotter_qrte.py @@ -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. @@ -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 @@ -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, ) diff --git a/qiskit/algorithms/time_evolvers/variational/solvers/var_qte_linear_solver.py b/qiskit/algorithms/time_evolvers/variational/solvers/var_qte_linear_solver.py index 4c99d853b640..12ff0c56c841 100644 --- a/qiskit/algorithms/time_evolvers/variational/solvers/var_qte_linear_solver.py +++ b/qiskit/algorithms/time_evolvers/variational/solvers/var_qte_linear_solver.py @@ -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 @@ -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( diff --git a/qiskit/algorithms/utils/assign_params.py b/qiskit/algorithms/utils/assign_params.py deleted file mode 100644 index 622d3f87ac97..000000000000 --- a/qiskit/algorithms/utils/assign_params.py +++ /dev/null @@ -1,62 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Helper class for assigning values to parameters.""" -from __future__ import annotations - -from collections.abc import Sequence - -import copy -import numpy as np - -from qiskit.circuit import ParameterExpression -from qiskit.circuit.parametertable import ParameterView - - -def _get_parameters(array: np.ndarray) -> ParameterView: - """Retrieves parameters from a numpy array as a ``ParameterView``.""" - ret = set() - for a in array: - if isinstance(a, ParameterExpression): - ret |= a.parameters - return ParameterView(ret) - - -def _assign_parameters( - array: np.ndarray, parameter_values: Sequence[float], inplace: bool = False -) -> np.ndarray: - """Binds ``ParameterExpression``s in a numpy array to provided values. - - Args: - array: array of ``ParameterExpression`` - parameter_values: array of values to bind to parameters - inplace: If ``False``, a copy of the array with the bound parameters is returned. - If True the array itself is modified. - - Returns: - A copy of the array with bound parameters, if ``inplace`` is False, otherwise None. - """ - if inplace: - bound_array = array - else: - bound_array = copy.deepcopy(array) - - parameter_dict = dict(zip(_get_parameters(bound_array), parameter_values)) - for i, a in enumerate(bound_array): - if isinstance(a, ParameterExpression): - for key in a.parameters & parameter_dict.keys(): - a = a.assign(key, parameter_dict[key]) - if not a.parameters: - a = complex(a) - bound_array[i] = a - - return None if inplace else bound_array diff --git a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py index 7a4de23eb64e..503c29f94689 100644 --- a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py +++ b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py @@ -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 @@ -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) @@ -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) diff --git a/releasenotes/notes/sparsepauliop-improved-parameter-support-413f7598bac72166.yaml b/releasenotes/notes/sparsepauliop-improved-parameter-support-413f7598bac72166.yaml new file mode 100644 index 000000000000..46c223aa67c7 --- /dev/null +++ b/releasenotes/notes/sparsepauliop-improved-parameter-support-413f7598bac72166.yaml @@ -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]) diff --git a/test/python/algorithms/time_evolvers/test_trotter_qrte.py b/test/python/algorithms/time_evolvers/test_trotter_qrte.py index 38e0bb939e37..e5d07a972c75 100644 --- a/test/python/algorithms/time_evolvers/test_trotter_qrte.py +++ b/test/python/algorithms/time_evolvers/test_trotter_qrte.py @@ -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 @@ -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( diff --git a/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py b/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py index bd494c1e15ce..e9c5d6741bd2 100644 --- a/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py +++ b/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py @@ -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 @@ -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()