# Portions Copyright (c) 2020 Huawei Technologies Co.,ltd.
# Portions Copyright 2017 The OpenFermion Developers.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# You may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# This module we develop is default being licensed under Apache 2.0 license,
# and also uses or refactor Fermilib and OpenFermion licensed under
# Apache 2.0 license.
"""This is the base class that to represent fermionic molecular or Hamiltonian."""
import copy
import itertools
import numpy
EQ_TOLERANCE = 1e-8
class PolynomialTensorError(Exception):
r"""Exception raised in methods from the PolynomialTensor class."""
[文档]class PolynomialTensor:
r"""
Class to store the coefficient of the fermionic ladder operators in a tensor form.
For instance, in a molecular Hamiltonian (degree 4 polynomial) which
conserves particle number, there are only three kinds of terms,
namely constant term, single excitation :math:`a^\dagger_p a_q` and
double excitation terms :math:`a^\dagger_p a^\dagger_q a_r a_s`,
and their corresponding coefficients can be stored in an scalar,
:math:`n_\text{qubits}\times n_\text{qubits}` matrix and
:math:`n_\text{qubits}\times n_\text{qubits} n_\text{qubits}\times n_\text{qubits}` matrix.
Note that each tensor must have an even number of dimensions due to
the parity conservation.
Much of the functionality of this class is similar to that of
FermionOperator.
Args:
n_body_tensors(dict): A dictionary storing the tensors describing
n-body interactions. The keys are tuples that indicate the
type of tensor.
For instance, n_body_tensors[()] would return a constant,
while a n_body_tensors[(1, 0)] would be an
:math:`n_\text{qubits}\times n_\text{qubits}` numpy
array, and n_body_tensors[(1,1,0,0)]
would return a
:math:`n_\text{qubits}\times n_\text{qubits} n_\text{qubits}\times n_\text{qubits}`
numpy array
and those constant and array represent the coefficients of terms of
the form identity, :math:`a^\dagger_p a_q`,
:math:`a^\dagger_p a^\dagger_q a_r a_s`, respectively. Default: None.
Note:
Here '1' represents :math:`a^\dagger`, while '0' represent :math:`a`.
Examples:
>>> import numpy as np
>>> from mindquantum.core.operators import PolynomialTensor
>>> constant = 1
>>> one_body_term = np.array([[1,0],[0,1]])
>>> two_body_term = two_body_term = np.array([[[[1,0],[0,1]],[[1,0],[0,1]]],[[[1,0],[0,1]],[[1,0],[0,1]]]])
>>> n_body_tensors = {(): 1, (1,0): one_body_term,(1,1,0,0):two_body_term}
>>> poly_op = PolynomialTensor(n_body_tensors)
>>> poly_op
() 1
((0, 1), (0, 0)) 1
((1, 1), (1, 0)) 1
((0, 1), (0, 1), (0, 0), (0, 0)) 1
((0, 1), (0, 1), (1, 0), (1, 0)) 1
((0, 1), (1, 1), (0, 0), (0, 0)) 1
((0, 1), (1, 1), (1, 0), (1, 0)) 1
((1, 1), (0, 1), (0, 0), (0, 0)) 1
((1, 1), (0, 1), (1, 0), (1, 0)) 1
((1, 1), (1, 1), (0, 0), (0, 0)) 1
((1, 1), (1, 1), (1, 0), (1, 0)) 1
>>> # get the constant
>>> poly_op.constant
1
>>> # set the constant
>>> poly_op.constant = 2
>>> poly_op.constant
2
>>> poly_op.n_qubits
2
>>> poly_op.one_body_tensor
array([[1, 0],
[0, 1]])
>>> poly_op.two_body_tensor
array([[[[1, 0],
[0, 1]],
[[1, 0],
[0, 1]]],
[[[1, 0],
[0, 1]],
[[1, 0],
[0, 1]]]])
"""
__hash__ = None
def __init__(self, n_body_tensors=None):
"""Initialize a PolynomialTensor object."""
self.n_body_tensors = n_body_tensors
self.n_qubits = 0
for key, _ in self.n_body_tensors.items():
if key == ():
pass
elif len(key) == 2 or len(key) == 4: # one body tensors
self.n_qubits = self.n_body_tensors[key].shape[0]
else:
PolynomialTensorError("Unexpected type of n-body-tensors!")
@property
def constant(self):
"""Get the value of the identity term."""
return self.n_body_tensors.get(())
@constant.setter
def constant(self, value):
"""Set the value of the identity term."""
self.n_body_tensors[()] = value
@property
def one_body_tensor(self):
"""Get the one-body term."""
if (1, 0) in self.n_body_tensors:
return self.n_body_tensors[(1, 0)]
return 0
@one_body_tensor.setter
def one_body_tensor(self, value):
"""
Set the value of the one-body term.
The value should numpy array with size n_qubits x n_qubits.
"""
self.n_body_tensors[(1, 0)] = value
@property
def two_body_tensor(self):
"""Get the two-body term."""
if (1, 1, 0, 0) in self.n_body_tensors:
return self.n_body_tensors[(1, 1, 0, 0)]
return 0
@two_body_tensor.setter
def two_body_tensor(self, value):
"""
Set the two-body term.
The value should be of numpy array with size n_qubits x n_qubits x n_qubits x n_qubits.
"""
self.n_body_tensors[(1, 1, 0, 0)] = value
def __getitem__(self, args):
r"""
Look up the matrix table.
Args:
args(tuples): Tuples indicating which coefficient to get.
For instance, `my_tensor[(3, 1), (4, 1), (2, 0)]` means look for the coefficient of fermionic ladder
operator (a^\dagger_3 a^\dagger_4 a_2 ) returns `my_tensor.n_body_tensors[1, 1, 0][3, 4, 2]`
Note: this supports single element extraction
"""
if args == ():
return self.constant
# change it into array
index, key = tuple(zip(*args))[0], tuple(zip(*args))[1]
return self.n_body_tensors[key][index]
def __setitem__(self, args, value):
"""
Set matrix element.
Args:
args(tuples): Tuples indicating which terms to set the corresponding coefficient.
value: ???
"""
if args == ():
self.constant = value
else:
# handle with the case (1,0) or ((1,0),(2,1)) they both have the
# length 2
index, key = tuple(zip(*args))[0], tuple(zip(*args))[1]
self.n_body_tensors[key][index] = value
def __eq__(self, other):
"""Equality comparison operator."""
# first check qubits number
if self.n_qubits != other.n_qubits:
return False
# then check the maximum difference whether within the EQ_TOLERANCE
diff = 0.0
self_keys = set(self.n_body_tensors.keys())
other_keys = set(other.n_body_tensors.keys())
# check the intersection part
for key in self_keys.intersection(other_keys):
if key == () or key is not None:
self_tensor = self.n_body_tensors[key]
other_tensor = other.n_body_tensors[key]
discrepancy = numpy.amax(numpy.absolute(self_tensor - other_tensor))
diff = max(diff, discrepancy)
# check the difference part
for key in self_keys.symmetric_difference(other_keys):
if key == () or key is not None:
tensor = self.n_body_tensors[key] if self.n_body_tensors[key] is not None else other.n_body_tensors[key]
discrepancy = numpy.amax(numpy.abs(tensor))
diff = max(diff, discrepancy)
return diff < EQ_TOLERANCE
def __ne__(self, other):
"""Inequality comparison operator."""
return not self == other
def __iadd__(self, addend):
"""
In-place method for += addition of PolynomialTensor.
Args:
addend (PolynomialTensor): The addend.
Returns:
sum (PolynomialTensor), Mutated self.
Raises:
TypeError: Cannot add invalid addend type.
"""
if not isinstance(addend, type(self)):
raise PolynomialTensorError(f"Cannot add invalid type! \n Expect {type(self)}")
# check dimension, self.n_qubits
if self.n_qubits != addend.n_qubits:
raise PolynomialTensorError("Can not add invalid type, the shape does not match!")
# add the common part
self_keys = set(self.n_body_tensors.keys())
addend_keys = set(addend.n_body_tensors.keys())
for key in self_keys.intersection(addend_keys):
self.n_body_tensors[key] = numpy.add(self.n_body_tensors[key], addend.n_body_tensors[key])
for key in addend_keys.difference(self_keys): # the term in added but not in self
if key:
self.n_body_tensors[key] = addend.n_body_tensors[key]
return self
def __add__(self, addend):
"""
Addition of PolynomialTensor.
Args:
added(PolynomialTensor): The addend.
Returns:
sum (PolynomialTensor), un-mutated self, but has new instance
Raises:
TypeError: Cannot add invalid operator type.
"""
sum_addend = copy.deepcopy(self)
sum_addend += addend
return sum_addend
def __neg__(self):
"""Return negation of the PolynomialTensor,mutated itself."""
for key in self.n_body_tensors:
self.n_body_tensors[key] = numpy.negative(self.n_body_tensors[key])
return self
def __isub__(self, subtracted):
"""
In-place method for -= subtraction of PolynomialTensor.
Args:
subtracted (PolynomialTensor): subtracted.
Returns:
subtract (PolynomialTensor), Mutated self.
Raises:
TypeError: Cannot sub invalid addend type.
"""
if not isinstance(subtracted, type(self)):
raise PolynomialTensorError(f"Cannot sub invalid type! \n Expect {type(self)}")
# check dimension, self.n_qubits
if self.n_qubits != subtracted.n_qubits:
raise PolynomialTensorError("Cannot sub invalid type, the shape does not match!")
# sub the common part
self_keys = set(self.n_body_tensors.keys())
sub_keys = set(subtracted.n_body_tensors.keys())
for key in self_keys.intersection(sub_keys):
self.n_body_tensors[key] = numpy.subtract(self.n_body_tensors[key], subtracted.n_body_tensors[key])
for key in sub_keys.difference(self_keys): # the term in sub but not in self
if key:
self.n_body_tensors[key] = numpy.negative(subtracted.n_body_tensors[key])
return self
def __sub__(self, subtracted):
"""
Subtraction of PolynomialTensor.
Args:
subtracted(PolynomialTensor): The subtracted.
Returns:
subtracted (PolynomialTensor), un-mutated self,
but has new instance
Raises:
TypeError: Cannot sub invalid operator type.
"""
res = copy.deepcopy(self)
res -= subtracted
return res
def __imul__(self, multiplier):
"""
In-place multiply (*=) with scalar or operator of the same type.
Default implementation is to multiply coefficients and concatenate terms (same operator).
Args:
multiplier(complex, float, or PolynomialTensor): multiplier
Returns:
products(PolynomialTensor)
Raises:
TypeError: cannot multiply invalid type of multiplier.
"""
# hand with scalar
if isinstance(multiplier, (int, float, complex)):
for key in self.n_body_tensors:
self.n_body_tensors[key] *= multiplier
elif isinstance(multiplier, type(self)):
if self.n_qubits != multiplier.n_qubits:
raise PolynomialTensorError("Cannot multiply invalid type, the shape does not match!")
# note we do not deal with the key multiplication,
# unlike that in FermionOperator, which is possible
self_keys = set(self.n_body_tensors.keys())
multiply_keys = set(multiplier.n_body_tensors.keys())
for key in self_keys.intersection(multiply_keys):
self.n_body_tensors[key] = numpy.multiply(self.n_body_tensors[key], multiplier.n_body_tensors[key])
for key in self_keys.difference(multiply_keys): # the term in added but not in self
if key == ():
self.constant = 0
else:
self.n_body_tensors[key] = numpy.zeros(self.n_body_tensors[key].shape)
else:
raise PolynomialTensorError("Cannot multiply invalid type!")
return self
def __mul__(self, multiplier):
"""
Multiplication for PolynomialTensor.
Args:
multiplier (PolynomialTensor): The multiplier to multiply.
Returns:
multiply (PolynomialTensor), un-Mutated self.
Raises:
TypeError: Cannot multiply invalid type.
"""
if isinstance(multiplier, (int, float, complex, type(self))):
# make use of the *= method
product_results = copy.deepcopy(self)
product_results *= multiplier
else:
raise PolynomialTensorError(f'Cannot multiply invalid type to {type(self)}.')
return product_results
def __rmul__(self, multiplier):
"""
Return multiplier * self.
Args:
multiplier: The operator to multiply.
Returns:
a new instance of PolynomialTensor
Raises:
TypeError: Cannot multiply invalid type.
"""
if isinstance(multiplier, (int, float, complex)): # make use of the * method, basically scalar
return self * multiplier
raise PolynomialTensorError(f'Cannot multiply invalid operator type to {type(self)}.')
def __itruediv__(self, divisor):
"""
Return self/divisor for the scalar.
Args:
divisor(int, float, complex): scalar
Returns:
a new instance of PolynomialTensor
Raises:
TypeError: cannot divide non-numeric type.
"""
if isinstance(divisor, (int, float, complex)) and divisor != 0:
for key in self.n_body_tensors:
self.n_body_tensors[key] /= divisor
else:
raise PolynomialTensorError(f'Cannot divide the {type(self)} by non_numeric type or the divisor is 0.')
return self
def __truediv__(self, divisor):
"""Implement division."""
if isinstance(divisor, (int, float, complex)) and divisor != 0:
quotient = copy.deepcopy(self)
quotient /= divisor
else:
raise PolynomialTensorError(f'Cannot divide the {type(self)} by non_numeric type or the divisor is 0.')
return quotient
# be careful with this function
def __div__(self, divisor):
"""For compatibility with Python 2."""
return self.__truediv__(divisor)
def __iter__(self):
"""Iterate over non-zero elements in the PolynomialTensor."""
def sort_key(key):
"""Determine how the keys to n_body_tensors should be sorted by mapping it to the corresponding integer."""
if key == ():
return 0
return int(''.join(map(str, key)))
for key in sorted(self.n_body_tensors, key=sort_key):
if key == ():
yield ()
else:
n_body_tensors = self.n_body_tensors[key] # get the matrix
# look up the non-zero elements in the n_body_tensors
for index in itertools.product(range(self.n_qubits), repeat=len(key)):
if n_body_tensors[index]:
yield tuple(zip(index, key))
def __str__(self):
"""Print out the non-zero elements of PolynomialTensor."""
strings = []
for key in self:
strings.append(f'{key} {self[key]}\n')
return ''.join(strings) if strings else '0'
def __repr__(self):
"""Return a string representation of the object."""
return str(self)
__all__ = ['PolynomialTensor']