diff --git a/.pylintdict b/.pylintdict index 72b7ab6fde..978c65f70d 100644 --- a/.pylintdict +++ b/.pylintdict @@ -381,6 +381,7 @@ physrevd physik plesset pmatrix +polynomialtensor polypeptides popovas pos diff --git a/qiskit_nature/second_q/operators/__init__.py b/qiskit_nature/second_q/operators/__init__.py index 1fd179b201..b1f682b063 100644 --- a/qiskit_nature/second_q/operators/__init__.py +++ b/qiskit_nature/second_q/operators/__init__.py @@ -26,12 +26,14 @@ SpinOp SecondQuantizedOp VibrationalOp + PolynomialTensor """ from .fermionic_op import FermionicOp from .second_quantized_op import SecondQuantizedOp from .spin_op import SpinOp from .vibrational_op import VibrationalOp +from .polynomial_tensor import PolynomialTensor from .sparse_label_op import SparseLabelOp __all__ = [ @@ -39,5 +41,6 @@ "SecondQuantizedOp", "SpinOp", "VibrationalOp", + "PolynomialTensor", "SparseLabelOp", ] diff --git a/qiskit_nature/second_q/operators/polynomial_tensor.py b/qiskit_nature/second_q/operators/polynomial_tensor.py new file mode 100644 index 0000000000..f04a2fa1de --- /dev/null +++ b/qiskit_nature/second_q/operators/polynomial_tensor.py @@ -0,0 +1,208 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +"""PolynomialTensor class""" + +from __future__ import annotations +from typing import Dict, Iterator +from collections.abc import Mapping +from numbers import Number +import numpy as np +from qiskit.quantum_info.operators.mixins import ( + LinearMixin, + AdjointMixin, + TolerancesMixin, +) + + +class PolynomialTensor(LinearMixin, AdjointMixin, TolerancesMixin, Mapping): + """PolynomialTensor class""" + + def __init__(self, data: Mapping[str, np.ndarray | Number], register_length: int) -> None: + """ + Args: + data: mapping of string-based operator keys to coefficient matrix values. + register_length: dimensions of the value matrices in data mapping. + Raises: + ValueError: when length of operator key does not match dimensions of value matrix. + ValueError: when value matrix does not have consistent dimensions. + ValueError: when some or all value matrices in ``data`` have different dimensions. + """ + copy_dict: Dict[str, np.ndarray] = {} + + for key, value in data.items(): + if isinstance(value, Number): + value = np.asarray(value) + + if len(value.shape) != len(key): + raise ValueError( + f"Data key {key} of length {len(key)} does not match " + f"data value matrix of dimensions {value.shape}" + ) + + dims = set(value.shape) + + if len(dims) > 1: + raise ValueError( + f"For key {key}: dimensions of value matrix are not identical {value.shape}" + ) + if len(dims) == 1 and dims.pop() != register_length: + raise ValueError( + f"Dimensions of value matrices in data dictionary do not match the provided " + f"register length, {register_length}" + ) + + copy_dict[key] = value + + self._data = copy_dict + self._register_length = register_length + + @property + def register_length(self) -> int: + """Returns register length of the operator key in `PolynomialTensor`.""" + return self._register_length + + def __getitem__(self, __k: str) -> (np.ndarray | Number): + """ + Returns value matrix in the `PolynomialTensor`. + + Args: + __k: operator key string in the `PolynomialTensor`. + Returns: + Value matrix corresponding to the operator key `__k` + """ + return self._data.__getitem__(__k) + + def __len__(self) -> int: + """ + Returns length of `PolynomialTensor`. + """ + return self._data.__len__() + + def __iter__(self) -> Iterator[str]: + """ + Returns iterator of the `PolynomialTensor`. + """ + return self._data.__iter__() + + def _multiply(self, other: complex) -> PolynomialTensor: + """Scalar multiplication of PolynomialTensor with complex + + Args: + other: scalar to be multiplied with the ``PolynomialTensor``. + Returns: + the new ``PolynomialTensor`` product object. + Raises: + TypeError: if ``other`` is not a ``Number``. + """ + if not isinstance(other, Number): + raise TypeError(f"other {other} must be a number") + + prod_dict: Dict[str, np.ndarray] = {} + for key, matrix in self._data.items(): + prod_dict[key] = np.multiply(matrix, other) + return PolynomialTensor(prod_dict, self._register_length) + + def _add(self, other: PolynomialTensor, qargs=None) -> PolynomialTensor: + """Addition of PolynomialTensors + + Args: + other: second``PolynomialTensor`` object to be added to the first. + Returns: + the new summed ``PolynomialTensor``. + Raises: + TypeError: when ``other`` is not a ``PolynomialTensor``. + ValueError: when values corresponding to keys in ``other`` and + the first ``PolynomialTensor`` object do not match. + """ + if not isinstance(other, PolynomialTensor): + raise TypeError("Incorrect argument type: other should be PolynomialTensor") + + if self._register_length != other._register_length: + raise ValueError( + "The dimensions of the PolynomialTensors which are to be added together, do not " + f"match: {self._register_length} != {other._register_length}" + ) + + sum_dict = {key: value + other._data.get(key, 0) for key, value in self._data.items()} + other_unique = {key: other._data[key] for key in other._data.keys() - self._data.keys()} + sum_dict.update(other_unique) + + return PolynomialTensor(sum_dict, self._register_length) + + def __eq__(self, other: object) -> bool: + """Check equality of first PolynomialTensor with other + + Args: + other: second``PolynomialTensor`` object to be compared with the first. + Returns: + True when ``PolynomialTensor`` objects are equal, False when unequal. + """ + if not isinstance(other, PolynomialTensor): + return False + + if self._register_length != other._register_length: + return False + + if self._data.keys() != other._data.keys(): + return False + + for key, value in self._data.items(): + if not np.array_equal(value, other._data[key]): + return False + return True + + def equiv(self, other: object) -> bool: + """Check equivalence of first PolynomialTensor with other + + Args: + other: second``PolynomialTensor`` object to be compared with the first. + Returns: + True when ``PolynomialTensor`` objects are equivalent, False when not. + """ + if not isinstance(other, PolynomialTensor): + return False + + if self._register_length != other._register_length: + return False + + if self._data.keys() != other._data.keys(): + return False + + for key, value in self._data.items(): + if not np.allclose(value, other._data[key], atol=self.atol, rtol=self.rtol): + return False + return True + + def conjugate(self) -> PolynomialTensor: + """Conjugate of PolynomialTensors + + Returns: + the complex conjugate of the ``PolynomialTensor``. + """ + conj_dict: Dict[str, np.ndarray] = {} + for key, value in self._data.items(): + conj_dict[key] = np.conjugate(value) + + return PolynomialTensor(conj_dict, self._register_length) + + def transpose(self) -> PolynomialTensor: + """Transpose of PolynomialTensor + + Returns: + the transpose of the ``PolynomialTensor``. + """ + transpose_dict: Dict[str, np.ndarray] = {} + for key, value in self._data.items(): + transpose_dict[key] = np.transpose(value) + + return PolynomialTensor(transpose_dict, self._register_length) diff --git a/test/second_q/operators/test_polynomial_tensor.py b/test/second_q/operators/test_polynomial_tensor.py new file mode 100644 index 0000000000..3ac3ba99ea --- /dev/null +++ b/test/second_q/operators/test_polynomial_tensor.py @@ -0,0 +1,180 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +"""Test for PolynomialTensor class""" + +from __future__ import annotations +import unittest +from test import QiskitNatureTestCase +from ddt import ddt, idata +import numpy as np +from qiskit_nature.second_q.operators import PolynomialTensor + + +@ddt +class TestPolynomialTensor(QiskitNatureTestCase): + """Tests for PolynomialTensor class""" + + def setUp(self) -> None: + super().setUp() + + self.og_poly = { + "": 1.0, + "+": self.build_matrix(4, 1), + "+-": self.build_matrix(4, 2), + "++--": self.build_matrix(4, 4), + } + + self.sample_poly_1 = { + "": 1.0, + "++": self.build_matrix(4, 1), + "+-": self.build_matrix(4, 2), + "++--": self.build_matrix(4, 4), + } + + self.sample_poly_2 = { + "": 1.0, + "+": self.build_matrix(4, 1), + "+-": self.build_matrix(4, 2), + "++--": np.arange(1, 13).reshape(1, 2, 3, 2), + } + + self.sample_poly_3 = { + "": 1.0, + "+": self.build_matrix(4, 1), + "+-": self.build_matrix(2, 2), + "++--": self.build_matrix(4, 4), + } + + self.sample_poly_4 = { + "": 1.0, + "+": self.build_matrix(2, 1), + "+-": self.build_matrix(2, 2), + "++--": self.build_matrix(2, 4), + } + + self.sample_poly_5 = { + "": 1.0, + "+": self.build_matrix(2, 1), + "+-": self.build_matrix(2, 2), + } + + self.expected_conjugate_poly = { + "": 1.0, + "+": self.build_matrix(4, 1).conjugate(), + "+-": self.build_matrix(4, 2).conjugate(), + "++--": self.build_matrix(4, 4).conjugate(), + } + + self.expected_transpose_poly = { + "": 1.0, + "+": self.build_matrix(4, 1).transpose(), + "+-": self.build_matrix(4, 2).transpose(), + "++--": self.build_matrix(4, 4).transpose(), + } + + self.expected_sum_poly = { + "": 2.0, + "+": np.add(self.build_matrix(4, 1), self.build_matrix(4, 1)), + "+-": np.add(self.build_matrix(4, 2), self.build_matrix(4, 2)), + "++--": np.add(self.build_matrix(4, 4), self.build_matrix(4, 4)), + } + + @staticmethod + def build_matrix(dim_size, num_dim, val=1): + """Build dictionary value matrix""" + return (np.arange(1, dim_size**num_dim + 1) * val).reshape((dim_size,) * num_dim) + + def test_init(self): + """Test for errors in constructor for PolynomialTensor""" + with self.assertRaisesRegex( + ValueError, + r"Data key .* of length \d does not match data value matrix of dimensions \(\d+, *\)", + ): + _ = PolynomialTensor(self.sample_poly_1, register_length=4) + + with self.assertRaisesRegex( + ValueError, r"For key (.*): dimensions of value matrix are not identical \(\d+, .*\)" + ): + _ = PolynomialTensor(self.sample_poly_2, register_length=4) + + with self.assertRaisesRegex( + ValueError, + r"Dimensions of value matrices in data dictionary " + r"do not match the provided register length, \d", + ): + _ = PolynomialTensor(self.sample_poly_3, register_length=4) + + def test_get_item(self): + """Test for getting value matrices corresponding to keys in PolynomialTensor""" + og_poly_tensor = PolynomialTensor(self.og_poly, 4) + for key, value in self.og_poly.items(): + np.testing.assert_array_equal(value, og_poly_tensor[key]) + + def test_len(self): + """Test for the length of PolynomialTensor""" + length = len(PolynomialTensor(self.sample_poly_4, 2)) + exp_len = 4 + self.assertEqual(exp_len, length) + + def test_iter(self): + """Test for the iterator of PolynomialTensor""" + og_poly_tensor = PolynomialTensor(self.og_poly, 4) + exp_iter = [key for key, _ in self.og_poly.items()] + self.assertEqual(exp_iter, list(iter(og_poly_tensor))) + + @idata(np.linspace(0, 3, 5)) + def test_mul(self, other): + """Test for scalar multiplication""" + expected_prod_poly = { + "": 1.0 * other, + "+": self.build_matrix(4, 1, other), + "+-": self.build_matrix(4, 2, other), + "++--": self.build_matrix(4, 4, other), + } + + result = PolynomialTensor(self.og_poly, 4) * other + self.assertEqual(result, PolynomialTensor(expected_prod_poly, 4)) + + with self.assertRaisesRegex(TypeError, r"other .* must be a number"): + _ = PolynomialTensor(self.og_poly, 4) * PolynomialTensor(self.og_poly, 4) + + def test_add(self): + """Test for addition of PolynomialTensor""" + result = PolynomialTensor(self.og_poly, 4) + PolynomialTensor(self.og_poly, 4) + self.assertEqual(result, PolynomialTensor(self.expected_sum_poly, 4)) + + with self.assertRaisesRegex( + TypeError, "Incorrect argument type: other should be PolynomialTensor" + ): + _ = PolynomialTensor(self.og_poly, 4) + 5 + + with self.assertRaisesRegex( + ValueError, + r"The dimensions of the PolynomialTensors which are to be added together, do not " + r"match: \d+ != \d+", + ): + _ = PolynomialTensor(self.og_poly, 4) + PolynomialTensor(self.sample_poly_4, 2) + + def test_conjugate(self): + """Test for conjugate of PolynomialTensor""" + result = PolynomialTensor(self.og_poly, 4).conjugate() + self.assertEqual(result, PolynomialTensor(self.expected_conjugate_poly, 4)) + + def test_transpose(self): + """Test for transpose of PolynomialTensor""" + result = PolynomialTensor(self.og_poly, 4).transpose() + self.assertEqual(result, PolynomialTensor(self.expected_transpose_poly, 4)) + + +if __name__ == "__main__": + unittest.main()