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

add support for Parameters to SparseLabelOp #891

Merged
merged 26 commits into from
Nov 1, 2022
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d9d3475
add support for Parameters to SparseLabelOp
kevinsung Oct 7, 2022
0b223b8
fix isinstance for python < 3.10
kevinsung Oct 17, 2022
557e4be
improve docs
kevinsung Oct 17, 2022
442a1c5
chop zero Parameter
kevinsung Oct 17, 2022
30b0d55
Update test/second_q/operators/test_sparse_label_op.py
kevinsung Oct 17, 2022
cc3b1f2
Merge branch 'main' into qiskit-nature-parameters
mrossinek Oct 18, 2022
74bc55b
Merge remote-tracking branch 'upstream/main' into qiskit-nature-param…
kevinsung Oct 18, 2022
805f793
fix test
kevinsung Oct 18, 2022
006753e
add is_parameterized and check it for unsupported operations
kevinsung Oct 20, 2022
8d84809
Merge remote-tracking branch 'upstream/main' into qiskit-nature-param…
kevinsung Oct 20, 2022
8c5eaa9
add assign_parameters
kevinsung Oct 21, 2022
b83381e
Merge remote-tracking branch 'upstream/main' into qiskit-nature-param…
kevinsung Oct 21, 2022
e811bbd
fix chop doc
kevinsung Oct 21, 2022
8644ce5
lint
kevinsung Oct 23, 2022
a8f926b
Merge remote-tracking branch 'upstream/main' into qiskit-nature-param…
kevinsung Oct 23, 2022
f172b4c
add parameters method
kevinsung Oct 23, 2022
3480c79
use dict instead of Mapping for data
kevinsung Oct 24, 2022
a4ad6ae
return self instead of None for inplace
kevinsung Oct 24, 2022
c2d9f2a
Revert "use dict instead of Mapping for data"
kevinsung Oct 26, 2022
4bba597
remove inplace option for assign_parameters
kevinsung Oct 26, 2022
04ff77f
address comments
kevinsung Oct 27, 2022
30e8824
Update qiskit_nature/second_q/operators/sparse_label_op.py
mrossinek Oct 28, 2022
6ea18f3
Merge branch 'main' into qiskit-nature-parameters
mrossinek Oct 28, 2022
853e730
fix equiv
kevinsung Oct 28, 2022
68a69a8
Merge remote-tracking branch 'upstream/main' into qiskit-nature-param…
kevinsung Oct 28, 2022
a56898c
Merge remote-tracking branch 'upstream/main' into qiskit-nature-param…
kevinsung Oct 31, 2022
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
5 changes: 3 additions & 2 deletions qiskit_nature/second_q/mappers/qubit_mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from abc import ABC, abstractmethod
from functools import lru_cache

import numpy as np
from qiskit.opflow import PauliSumOp
from qiskit.quantum_info.operators import Pauli, SparsePauliOp

Expand Down Expand Up @@ -182,7 +183,7 @@ def mode_based_mapping(
else:
for terms, coeff in second_q_op.terms():
# 1. Initialize an operator list with the identity scaled by the `coeff`
ret_op = SparsePauliOp("I" * nmodes, coeffs=[coeff])
ret_op = SparsePauliOp("I" * nmodes, coeffs=np.array([coeff]))

# Go through the label and replace the fermion operators by their qubit-equivalent, then
# save the respective Pauli string in the pauli_str list.
Expand All @@ -202,4 +203,4 @@ def mode_based_mapping(
)
ret_op_list.append(ret_op)

return PauliSumOp(SparsePauliOp.sum(ret_op_list).simplify().chop())
return PauliSumOp(SparsePauliOp.sum(ret_op_list).simplify())
22 changes: 15 additions & 7 deletions qiskit_nature/second_q/operators/fermionic_op.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
import qiskit_nature.optionals as _optionals

from .polynomial_tensor import PolynomialTensor
from .sparse_label_op import SparseLabelOp
from .sparse_label_op import _TCoeff, SparseLabelOp, _to_number


class FermionicOp(SparseLabelOp):
Expand Down Expand Up @@ -148,6 +148,12 @@ class FermionicOp(SparseLabelOp):
considered a lower bound, which means that mathematical operations acting on two or more
operators will result in a new operator with the maximum number of spin orbitals of any
of the involved operators.
.. note::

A FermionicOp can contain :class:`qiskit.circuit.Parameter` objects as coefficients.
However, a FermionicOp containing Parameters does not support the following methods:
- ``is_hermitian``
- ``to_matrix``
"""

_OPERATION_REGEX = re.compile(r"([\+\-]_\d+\s)*[\+\-]_\d+")
Expand Down Expand Up @@ -284,7 +290,7 @@ def __str__(self) -> str:
)
return pre + ret

def terms(self) -> Iterator[tuple[list[tuple[str, int]], complex]]:
def terms(self) -> Iterator[tuple[list[tuple[str, int]], _TCoeff]]:
"""Provides an iterator analogous to :meth:`items` but with the labels already split into
pairs of operation characters and indices.

Expand Down Expand Up @@ -441,11 +447,11 @@ def normal_ordered(self) -> FermionicOp:
{
label: coeff
for label, coeff in ordered_op.items()
if not np.isclose(coeff, 0.0, atol=self.atol)
if not np.isclose(_to_number(coeff), 0.0, atol=self.atol)
}
)

def _normal_ordered(self, terms: list[tuple[str, int]], coeff: complex) -> FermionicOp:
def _normal_ordered(self, terms: list[tuple[str, int]], coeff: _TCoeff) -> FermionicOp:
if not terms:
return self._new_instance({"": coeff})

Expand Down Expand Up @@ -505,17 +511,19 @@ def is_hermitian(self, *, atol: float | None = None) -> bool:
def simplify(self, *, atol: float | None = None) -> FermionicOp:
atol = self.atol if atol is None else atol

data = defaultdict(complex) # type: dict[str, complex]
data = defaultdict(complex) # type: dict[str, _TCoeff]
# TODO: use parallel_map to make this more efficient (?)
for label, coeff in self.items():
label, coeff = self._simplify_label(label, coeff)
data[label] += coeff
simplified_data = {
label: coeff for label, coeff in data.items() if not np.isclose(coeff, 0.0, atol=atol)
label: coeff
for label, coeff in data.items()
if not np.isclose(_to_number(coeff), 0.0, atol=atol)
}
return self._new_instance(simplified_data)

def _simplify_label(self, label: str, coeff: complex) -> tuple[str, complex]:
def _simplify_label(self, label: str, coeff: _TCoeff) -> tuple[str, _TCoeff]:
bits = _BitsContainer()

# Since Python 3.7, dictionaries are guaranteed to be insert-order preserving. We use this
Expand Down
65 changes: 42 additions & 23 deletions qiskit_nature/second_q/operators/sparse_label_op.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@

from abc import ABC, abstractmethod
from collections.abc import Collection, Mapping
from numbers import Number
from typing import Iterator, Sequence
from numbers import Complex
from typing import Iterator, Sequence, SupportsComplex, Union

import cmath
import numpy as np

from qiskit.circuit import ParameterExpression
from qiskit.quantum_info.operators.mixins import (
AdjointMixin,
GroupMixin,
Expand All @@ -32,6 +32,16 @@
from .polynomial_tensor import PolynomialTensor


_TCoeff = Union[complex, ParameterExpression]


def _to_number(a: SupportsComplex | ParameterExpression) -> complex | float:
if isinstance(a, ParameterExpression):
sympified = a.sympify()
return complex(sympified) if sympified.is_Number else np.nan
return complex(a)


class SparseLabelOp(LinearMixin, AdjointMixin, GroupMixin, TolerancesMixin, ABC, Mapping):
"""The base class for sparse second-quantized operators.

Expand All @@ -48,11 +58,18 @@ class SparseLabelOp(LinearMixin, AdjointMixin, GroupMixin, TolerancesMixin, ABC,
- equality and equivalence (using the :attr:`atol` and :attr:`rtol` tolerances) comparisons

Furthermore, several general utility methods exist which are documented below.

.. note::

A SparseLabelOp can contain Parameters. However, a SparseLabelOp containing Parameters
does not support the following methods:
- ``equiv``
- ``induced_norm``
"""

def __init__(
self,
data: Mapping[str, complex],
data: Mapping[str, _TCoeff],
*,
copy: bool = True,
validate: bool = True,
Expand All @@ -72,7 +89,7 @@ def __init__(
Raises:
QiskitNatureError: when an invalid key is encountered during validation.
"""
self._data: Mapping[str, complex] = {}
self._data: Mapping[str, _TCoeff] = {}
if copy:
if validate:
self._validate_keys(data.keys())
Expand Down Expand Up @@ -191,7 +208,7 @@ def _add(self, other: SparseLabelOp, qargs: None = None) -> SparseLabelOp:

return self._new_instance(new_data, other=other)

def _multiply(self, other: complex) -> SparseLabelOp:
def _multiply(self, other: _TCoeff) -> SparseLabelOp:
"""Return scalar multiplication of self and other.

Args:
Expand All @@ -203,7 +220,7 @@ def _multiply(self, other: complex) -> SparseLabelOp:
Raises:
TypeError: if ``other`` is not compatible type (int, float or complex)
"""
if not isinstance(other, Number):
if not isinstance(other, (Complex, ParameterExpression)):
raise TypeError(
f"Unsupported operand type(s) for *: 'SparseLabelOp' and '{type(other).__name__}'"
)
Expand Down Expand Up @@ -296,8 +313,8 @@ def equiv(
) -> bool:
"""Check equivalence of two ``SparseLabelOp`` instances up to an accepted tolerance.

The absolute and relative tolerances can be changed via the `atol` and `rtol` attributes,
respectively.
The default absolute and relative tolerances can be changed via the `atol` and `rtol`
attributes, respectively.

Args:
other: the second ``SparseLabelOp`` to compare with this instance.
Expand Down Expand Up @@ -335,7 +352,7 @@ def __eq__(self, other: object) -> bool:

return self._data == other._data

def __getitem__(self, __k: str) -> complex:
def __getitem__(self, __k: str) -> _TCoeff:
"""Get the requested element of the ``SparseLabelOp``."""
return self._data.__getitem__(__k)

Expand All @@ -348,7 +365,7 @@ def __iter__(self) -> Iterator[str]:
return self._data.__iter__()

@abstractmethod
def terms(self) -> Iterator[tuple[list[tuple[str, int]], complex]]:
def terms(self) -> Iterator[tuple[list[tuple[str, int]], _TCoeff]]:
"""Provides an iterator analogous to :meth:`items` but with the labels already split into
pairs of operation characters and indices.

Expand Down Expand Up @@ -394,10 +411,10 @@ def sort(self, *, weight: bool = False) -> SparseLabelOp:
return self._new_instance({ind: self[ind] for ind in indices})

def chop(self, tol: float | None = None) -> SparseLabelOp:
"""Chops the real and imaginary phases of the operator coefficients.
"""Chops the real and imaginary parts of the operator coefficients.

This function separately chops the real and imaginary phase of all coefficients to the
provided tolerance.
This function separately chops the real and imaginary parts of all coefficients to the
provided tolerance. Parameters are ignored.

Args:
tol: the tolerance to which to chop. If ``None``, :attr:`atol` will be used.
Expand All @@ -409,16 +426,18 @@ def chop(self, tol: float | None = None) -> SparseLabelOp:

new_data = {}
for key, value in self.items():
zero_real = cmath.isclose(value.real, 0.0, abs_tol=tol)
zero_imag = cmath.isclose(value.imag, 0.0, abs_tol=tol)
if zero_real and zero_imag:
if _to_number(value) == 0:
continue
if zero_imag:
new_data[key] = value.real
elif zero_real:
new_data[key] = value.imag
else:
new_data[key] = value
if not isinstance(value, ParameterExpression):
zero_real = cmath.isclose(value.real, 0.0, abs_tol=tol)
zero_imag = cmath.isclose(value.imag, 0.0, abs_tol=tol)
if zero_real and zero_imag:
continue
if zero_imag:
value = value.real
elif zero_real:
value = value.imag * 1j
new_data[key] = value

return self._new_instance(new_data)

Expand Down
8 changes: 8 additions & 0 deletions test/second_q/mappers/test_jordan_wigner_mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import unittest
from test import QiskitNatureTestCase

from qiskit.circuit import Parameter
from qiskit.quantum_info import SparsePauliOp
from qiskit.opflow import I, PauliSumOp, X, Y, Z

import qiskit_nature.optionals as _optionals
Expand Down Expand Up @@ -92,6 +94,12 @@ def test_mapping_for_single_op(self):
expected = PauliSumOp.from_list([("I", 1)])
self.assertEqual(JordanWignerMapper().map(op), expected)

with self.subTest("test parameters"):
a = Parameter("a")
op = FermionicOp({"+_0": a})
expected = SparsePauliOp.from_list([("X", 0.5 * a), ("Y", -0.5j * a)], dtype=object)
self.assertEqual(JordanWignerMapper().map(op).primitive, expected)


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