Skip to content

Commit

Permalink
More fermionic dunders (#4209)
Browse files Browse the repository at this point in the history
* create fermiword class

* create fermisentence class

* [skip ci] modify order

* [skip ci] remove unused import

* [skip ci] add deepcopy method

* [skip ci] replace list with generator

* [skip ci] add tests

* [skip ci] add tests

* [skip ci] add tests

* add tests

* [skip ci] run black

* [skip ci] copy fermiword

* [skip ci] rebase and fix conflict

* [skip ci] run black

* [skip ci] copy fermiword

* [skip ci] add tests

* add test to tests_passing_pylint

* add sum mult test

* [skip ci] add tests

* [skip ci] add pow tests

* add tests

* fix pylint

* add error tests

* fix pylint

* add TODO

* modify docstrings

* update changelog

* update changelog

* modify docstrings

* fix codecov

* add code review comments

* add test for indices

* fix codefactor

* multiply FermiWord and FermiSentence

* add tests for fw * fs

* add and test multiplying fermiword with  integer or float

* allow number times fermi sentence

* update tests

* Allow subtracting one FermiSentence from another

* Allow substraction

* clean up formatting and tests

* Add multiplication by complex

* Add more tests and reorganize

* update change log

* add test

* black formatting

* Add constants to FermiWord and FermiSentence

* add tests

* radd and rsub methods plus tests

* missing lines for codecov

* start adding support for tensors

* numpy and pennylane numpy tensors plus tests for __add__ and __sub__

* add tests for multiplying by tensor

* fix formatting issues post merge

* add comment regarding overriding numpy dunders

* Apply suggestions from code review

Co-authored-by: soranjh <40344468+soranjh@users.noreply.github.com>

* check len of array before adding to FeriSentence

* Raise error if array with len>2 is passed to arithmetic dunders

* Apply suggestions from code review

Co-authored-by: Jay Soni <jbsoni@uwaterloo.ca>

* Update doc/releases/changelog-dev.md

* round to 10 digits

---------

Co-authored-by: soranjh <soranjh@yahoo.com>
Co-authored-by: soranjh <40344468+soranjh@users.noreply.github.com>
Co-authored-by: Jay Soni <jbsoni@uwaterloo.ca>
  • Loading branch information
4 people authored Jun 15, 2023
1 parent 3fffe83 commit 5824a8e
Show file tree
Hide file tree
Showing 3 changed files with 825 additions and 80 deletions.
5 changes: 5 additions & 0 deletions doc/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@
* Added the `QutritBasisState` operator to support qutrit state preparation for the `default.qutrit` device
[(#4185)](https://github.com/PennyLaneAI/pennylane/pull/4185)

* Added dunder methods to `FermiWord` and `FermiSentence` to allow arithmetic operations
using `+`, `-` and `*` between
`FermiWord`, `FermiSentence` and `int`, `float` and `complex` objects.
[(#4209)](https://github.com/PennyLaneAI/pennylane/pull/4209)

* Added the `one_qubit_decomposition` function to provide a unified interface for all one qubit decompositions. All
decompositions simplify the rotations angles to be between `0` and `4` pi.
[(#4210)](https://github.com/PennyLaneAI/pennylane/pull/4210)
Expand Down
254 changes: 231 additions & 23 deletions pennylane/fermi/fermionic.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
"""The Fermionic representation classes."""
import re
from copy import copy
from numbers import Number
from numpy import ndarray


class FermiWord(dict):
Expand All @@ -28,6 +30,12 @@ class FermiWord(dict):
<FermiWord = '0+ 1-'>
"""

# override the arithmetic dunder methods for numpy arrays so that the
# methods defined on this class are used instead
# (i.e. ensure `np.array + FermiWord` uses `FermiWord.__radd__` instead of `np.array.__add__`)
__numpy_ufunc__ = None
__array_ufunc__ = None

def __init__(self, operator):
self.sorted_dic = dict(sorted(operator.items()))

Expand Down Expand Up @@ -109,8 +117,77 @@ def __repr__(self):
r"""Terminal representation of a FermiWord"""
return str(self)

def __add__(self, other):
"""Add a FermiSentence, FermiWord or constant to a FermiWord. Converts both
elements into FermiSentences, and uses the FermiSentence __add__
method"""

self_fs = FermiSentence({self: 1.0})

if isinstance(other, FermiSentence):
return self_fs + other

if isinstance(other, FermiWord):
return self_fs + FermiSentence({other: 1.0})

if isinstance(other, (Number, ndarray)):
if isinstance(other, ndarray) and len(other) > 1:
raise ValueError(
f"Arithmetic Fermi operations can only accept an array of length 1, "
f"but received {other} of length {len(other)}"
)
return self_fs + FermiSentence({FermiWord({}): other})

raise TypeError(f"Cannot add {type(other)} to a FermiWord.")

def __radd__(self, other):
"""Add a FermiWord to a constant, i.e. `2 + FermiWord({...})`"""

if isinstance(other, (Number, ndarray)):
return self.__add__(other)

raise TypeError(f"Cannot add a FermiWord to {type(other)}.")

def __sub__(self, other):
"""Subtract a FermiSentence, FermiWord or constant from a FermiWord. Converts both
elements into FermiSentences (with negative coefficient for `other`), and
uses the FermiSentence __add__ method"""

self_fs = FermiSentence({self: 1.0})

if isinstance(other, FermiWord):
return self_fs + FermiSentence({other: -1.0})

if isinstance(other, FermiSentence):
other_fs = FermiSentence(dict(zip(other.keys(), [-v for v in other.values()])))
return self_fs + other_fs

if isinstance(other, (Number, ndarray)):
if isinstance(other, ndarray) and len(other) > 1:
raise ValueError(
f"Arithmetic Fermi operations can only accept an array of length 1, "
f"but received {other} of length {len(other)}"
)
return self_fs + FermiSentence({FermiWord({}): -1 * other}) # -constant * I

raise TypeError(f"Cannot subtract {type(other)} from a FermiWord.")

def __rsub__(self, other):
"""Subtract a FermiWord to a constant, i.e. `2 - FermiWord({...})`"""
if isinstance(other, (Number, ndarray)):
if isinstance(other, ndarray) and len(other) > 1:
raise ValueError(
f"Arithmetic Fermi operations can only accept an array of length 1, "
f"but received {other} of length {len(other)}"
)
self_fs = FermiSentence({self: -1.0})
other_fs = FermiSentence({FermiWord({}): other})
return self_fs + other_fs

raise TypeError(f"Cannot subtract a FermiWord from {type(other)}.")

def __mul__(self, other):
r"""Multiply two Fermi words together.
r"""Multiply a FermiWord with another FermiWord, a FermiSentence, or a constant.
>>> w = FermiWord({(0, 0) : '+', (1, 1) : '-'})
>>> w * w
Expand Down Expand Up @@ -139,6 +216,35 @@ def __mul__(self, other):

return FermiWord(dict_self)

if isinstance(other, FermiSentence):
return FermiSentence({self: 1}) * other

if isinstance(other, (Number, ndarray)):
if isinstance(other, ndarray) and len(other) > 1:
raise ValueError(
f"Arithmetic Fermi operations can only accept an array of length 1, "
f"but received {other} of length {len(other)}"
)
return FermiSentence({self: other})

raise TypeError(f"Cannot multiply FermiWord by {type(other)}.")

def __rmul__(self, other):
r"""Reverse multiply a FermiWord
Multiplies a FermiWord "from the left" with an object that can't be modified
to support __mul__ for FermiWord. Will be defaulted in for example
``2 * FermiWord({(0, 0): "+"})``, where the ``__mul__`` operator on an integer
will fail to multiply with a FermiWord"""

if isinstance(other, (Number, ndarray)):
if isinstance(other, ndarray) and len(other) > 1:
raise ValueError(
f"Arithmetic Fermi operations can only accept an array of length 1, "
f"but received {other} of length {len(other)}"
)
return FermiSentence({self: other})

raise TypeError(f"Cannot multiply FermiWord by {type(other)}.")

def __pow__(self, value):
Expand All @@ -159,12 +265,6 @@ def __pow__(self, value):

return operator

# TODO: create __add__ and __iadd__ method when FermiSentence is merged.
# TODO: create __sub__ and __isub__ method when FermiSentence is merged.
# TODO: create __imul__ method.
# TODO: support multiply by number in __mul__ when FermiSentence is merged.
# TODO: allow multiplication of a FermiWord with a FermiSentence and vice versa


class FermiSentence(dict):
r"""Immutable dictionary used to represent a Fermi sentence, a linear combination of Fermi words, with the keys
Expand All @@ -177,6 +277,11 @@ class FermiSentence(dict):
1.2 * '0+ 1-'
+ 3.1 * '1+ 2-'
"""
# override the arithmetic dunder methods for numpy arrays so that the
# methods defined on this class are used instead
# (i.e. ensure `np.array + FermiSentence` uses `FermiSentence.__radd__` instead of `np.array.__add__`)
__numpy_ufunc__ = None
__array_ufunc__ = None

@property
def wires(self):
Expand All @@ -187,6 +292,7 @@ def __str__(self):
r"""String representation of a FermiSentence."""
if len(self) == 0:
return "0 * 'I'"

return "\n+ ".join(f"{coeff} * '{fw.to_string()}'" for fw, coeff in self.items())

def __repr__(self):
Expand All @@ -198,30 +304,132 @@ def __missing__(self, key):
return 0.0

def __add__(self, other):
r"""Add two Fermi sentence together by iterating over the smaller one and adding its terms
to the larger one."""
smaller_fs, larger_fs = (
(self, copy(other)) if len(self) < len(other) else (other, copy(self))
)
for key in smaller_fs:
larger_fs[key] += smaller_fs[key]
r"""Add a FermiSentence, FermiWord or constant to a FermiSentence by iterating over the
smaller one and adding its terms to the larger one."""

# ensure other is FermiSentence
if isinstance(other, FermiWord):
other = FermiSentence({other: 1})
if isinstance(other, Number):
other = FermiSentence({FermiWord({}): other})
if isinstance(other, ndarray):
if len(other) > 1:
raise ValueError(
f"Arithmetic Fermi operations can only accept an array of length 1, "
f"but received {other} of length {len(other)}"
)
other = FermiSentence({FermiWord({}): other})

if isinstance(other, FermiSentence):
smaller_fs, larger_fs = (
(self, copy(other)) if len(self) < len(other) else (other, copy(self))
)
for key in smaller_fs:
larger_fs[key] += smaller_fs[key]

return larger_fs

raise TypeError(f"Cannot add {type(other)} to a FermiSentence.")

def __radd__(self, other):
"""Add a FermiSentence to a constant, i.e. `2 + FermiSentence({...})`"""

if isinstance(other, (Number, ndarray)):
return self.__add__(other)

raise TypeError(f"Cannot add a FermiSentence to {type(other)}.")

def __sub__(self, other):
r"""Subtract a FermiSentence, FermiWord or constant from a FermiSentence"""
if isinstance(other, FermiWord):
other = FermiSentence({other: -1})
return self.__add__(other)

if isinstance(other, Number):
other = FermiSentence({FermiWord({}): -1 * other}) # -constant * I
return self.__add__(other)

if isinstance(other, ndarray):
if len(other) > 1:
raise ValueError(
f"Arithmetic Fermi operations can only accept an array of length 1, "
f"but received {other} of length {len(other)}"
)
other = FermiSentence({FermiWord({}): -1 * other}) # -constant * I
return self.__add__(other)

if isinstance(other, FermiSentence):
other = FermiSentence(dict(zip(other.keys(), [-1 * v for v in other.values()])))
return self.__add__(other)

raise TypeError(f"Cannot subtract {type(other)} from a FermiSentence.")

return larger_fs
def __rsub__(self, other):
"""Subtract a FermiSentence to a constant, i.e.
>>> 2 - FermiSentence({...})
"""

if isinstance(other, (Number, ndarray)):
if isinstance(other, ndarray) and len(other) > 1:
raise ValueError(
f"Arithmetic Fermi operations can only accept an array of length 1, "
f"but received {other} of length {len(other)}"
)
self_fs = FermiSentence(dict(zip(self.keys(), [-1 * v for v in self.values()])))
other_fs = FermiSentence({FermiWord({}): other}) # constant * I
return self_fs + other_fs

raise TypeError(f"Cannot subtract a FermiSentence from {type(other)}.")

def __mul__(self, other):
r"""Multiply two Fermi sentences by iterating over each sentence and multiplying the Fermi
words pair-wise"""
# an empty FermiSentence represents the Null operator
if (len(self) == 0) or (len(other) == 0):
return FermiSentence({FermiWord({}): 0})

product = FermiSentence({})
if isinstance(other, FermiWord):
other = FermiSentence({other: 1})

if isinstance(other, FermiSentence):
if (len(self) == 0) or (len(other) == 0):
return FermiSentence({FermiWord({}): 0})

product = FermiSentence({})

for fw1, coeff1 in self.items():
for fw2, coeff2 in other.items():
product[fw1 * fw2] += coeff1 * coeff2

for fw1, coeff1 in self.items():
for fw2, coeff2 in other.items():
product[fw1 * fw2] += coeff1 * coeff2
return product

if isinstance(other, (Number, ndarray)):
if isinstance(other, ndarray) and len(other) > 1:
raise ValueError(
f"Arithmetic Fermi operations can only accept an array of length 1, "
f"but received {other} of length {len(other)}"
)
vals = [i * other for i in self.values()]
return FermiSentence(dict(zip(self.keys(), vals)))

raise TypeError(f"Cannot multiply FermiSentence by {type(other)}.")

def __rmul__(self, other):
r"""Reverse multiply a FermiSentence
Multiplies a FermiSentence "from the left" with an object that can't be modified
to support __mul__ for FermiSentence. Will be defaulted in for example when
multiplying ``2 * fermi_sentence``, since the ``__mul__`` operator on an integer
will fail to multiply with a FermiSentence"""

if isinstance(other, (Number, ndarray)):
if isinstance(other, ndarray) and len(other) > 1:
raise ValueError(
f"Arithmetic Fermi operations can only accept an array of length 1, "
f"but received {other} of length {len(other)}"
)
vals = [i * other for i in self.values()]
return FermiSentence(dict(zip(self.keys(), vals)))

return product
raise TypeError(f"Cannot multiply {type(other)} by FermiSentence.")

def __pow__(self, value):
r"""Exponentiate a Fermi sentence to an integer power."""
Expand Down
Loading

0 comments on commit 5824a8e

Please sign in to comment.