Repository: quantumlib/OpenFermion
Pull request #868 (2024-01-19 01:43:30)

MajoranaOperator supports operation with numbers.
Addition and subtraction in MajoranaOperator accept a number as an input.
Bugs in `__isub__` and the corresponding testcases are fixed.

Related to [this issue](https://github.com/quantumlib/OpenFermion/issues/862#issue-2084048358).


Patch:
diff --git a/src/openfermion/ops/operators/majorana_operator.py b/src/openfermion/ops/operators/majorana_operator.py
index fcb1e56..f5065ff 100644
--- a/src/openfermion/ops/operators/majorana_operator.py
+++ b/src/openfermion/ops/operators/majorana_operator.py
@@ -10,7 +10,7 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 """The MajoranaOperator data structure."""
-
+import copy
 import itertools
 import numpy
 
@@ -77,6 +77,9 @@ class MajoranaOperator:
 
     def commutes_with(self, other):
         """Test commutation with another MajoranaOperator"""
+        if isinstance(other, (int, float, complex)):
+            return True
+
         if not isinstance(other, type(self)):
             raise TypeError('Can only test commutation with another MajoranaOperator.')
 
@@ -114,57 +117,61 @@ class MajoranaOperator:
             rotated_op += rotated_term
         return rotated_op
 
-    def __iadd__(self, other):
-        if not isinstance(other, type(self)):
-            return NotImplemented
+    @property
+    def constant(self):
+        """The value of the constant term."""
+        return self.terms.get((), 0.0)
 
-        for term, coefficient in other.terms.items():
-            if term in self.terms:
-                self.terms[term] += coefficient
-            else:
-                self.terms[term] = coefficient
+    @constant.setter
+    def constant(self, value):
+        """Set the value of the constant term"""
+        self.terms[()] = value
 
-        return self
+    @classmethod
+    def zero(cls):
+        """Returns additive_identity."""
+        return cls(term=None)
 
-    def __add__(self, other):
-        if not isinstance(other, type(self)):
-            return NotImplemented
-
-        terms = {}
-        terms.update(self.terms)
+    @classmethod
+    def identity(cls):
+        """Returns multiplicative_identity."""
+        return cls(term=(), coefficient=1.0)
 
-        for term, coefficient in other.terms.items():
-            if term in terms:
-                terms[term] += coefficient
-            else:
-                terms[term] = coefficient
+    def __iadd__(self, other):
+        if isinstance(other, type(self)):
+            for term, coefficient in other.terms.items():
+                if term in self.terms:
+                    self.terms[term] += coefficient
+                else:
+                    self.terms[term] = coefficient
+        elif isinstance(other, (int, float, complex)):
+            self.constant += other
+        else:
+            raise TypeError("Cannot add invalid type to {}".format(type(self)))
+        return self
 
-        return MajoranaOperator.from_dict(terms)
+    def __add__(self, other):
+        summand = copy.deepcopy(self)
+        summand += other
+        return summand
 
     def __isub__(self, other):
-        if not isinstance(other, type(self)):
-            return NotImplemented
-
-        for term, coefficient in other.terms.items():
-            if term in self.terms:
-                self.terms[term] -= coefficient
-            else:
-                self.terms[term] = coefficient
-
+        if isinstance(other, type(self)):
+            for term, coefficient in other.terms.items():
+                if term in self.terms:
+                    self.terms[term] -= coefficient
+                else:
+                    self.terms[term] = -coefficient
+        elif isinstance(other, (int, float, complex)):
+            self.constant -= other
+        else:
+            raise TypeError("Cannot subtract invalid type from {}".format(type(self)))
         return self
 
     def __sub__(self, other):
-        if not isinstance(other, type(self)):
-            return NotImplemented
-
-        terms = {}
-        terms.update(self.terms)
-        for term, coefficient in other.terms.items():
-            if term in terms:
-                terms[term] -= coefficient
-            else:
-                terms[term] = -coefficient
-        return MajoranaOperator.from_dict(terms)
+        minuend = copy.deepcopy(self)
+        minuend -= other
+        return minuend
 
     def __mul__(self, other):
         if not isinstance(other, (type(self), int, float, complex)):


Test patch:
diff --git a/src/openfermion/ops/operators/majorana_operator_test.py b/src/openfermion/ops/operators/majorana_operator_test.py
index 8ad523c..3ea1b01 100644
--- a/src/openfermion/ops/operators/majorana_operator_test.py
+++ b/src/openfermion/ops/operators/majorana_operator_test.py
@@ -29,6 +29,12 @@ def test_majorana_operator_init():
     op = MajoranaOperator((5, 10, 4, 3, 6, 9, 6))
     assert op.terms == {(3, 4, 5, 9, 10): -1.0}
 
+    op = MajoranaOperator((), 3.0)
+    assert op.terms == {(): 3.0}
+    assert op.constant == 3.0
+    op.constant += 1.0
+    assert op.constant == 4.0
+
 
 def test_majorana_operator_commutes_with():
     a = MajoranaOperator((0, 1, 5))
@@ -49,8 +55,9 @@ def test_majorana_operator_commutes_with():
 
     assert e.commutes_with(f)
 
-    with pytest.raises(TypeError):
-        _ = e.commutes_with(0)
+    assert e.commutes_with(0)
+    assert e.commutes_with(2.0)
+    assert e.commutes_with(4.0j)
 
 
 def test_majorana_operator_with_basis_rotated_by():
@@ -83,34 +90,48 @@ def test_majorana_operator_add_subtract():
     b = MajoranaOperator((0, 2, 3), -0.5)
     c = MajoranaOperator((1, 5, 7), 4.75)
     d = MajoranaOperator((3, 5, 7), 2.25)
+    const = MajoranaOperator((), 5.25)
+    o = MajoranaOperator.zero()
+    identity = MajoranaOperator.identity()
 
     a += c
     assert a.terms == {(0, 2, 3): -1.25, (1, 5, 7): 4.75}
 
     a -= d
-    assert a.terms == {(0, 2, 3): -1.25, (1, 5, 7): 4.75, (3, 5, 7): 2.25}
+    assert a.terms == {(0, 2, 3): -1.25, (1, 5, 7): 4.75, (3, 5, 7): -2.25}
 
     a += MajoranaOperator((0, 2, 3), 0.5)
-    assert a.terms == {(0, 2, 3): -0.75, (1, 5, 7): 4.75, (3, 5, 7): 2.25}
+    assert a.terms == {(0, 2, 3): -0.75, (1, 5, 7): 4.75, (3, 5, 7): -2.25}
 
     a -= MajoranaOperator((0, 2, 3), 0.25)
-    assert a.terms == {(0, 2, 3): -1.0, (1, 5, 7): 4.75, (3, 5, 7): 2.25}
+    assert a.terms == {(0, 2, 3): -1.0, (1, 5, 7): 4.75, (3, 5, 7): -2.25}
 
-    assert (a + b).terms == {(0, 2, 3): -1.5, (1, 5, 7): 4.75, (3, 5, 7): 2.25}
+    assert (a + b).terms == {(0, 2, 3): -1.5, (1, 5, 7): 4.75, (3, 5, 7): -2.25}
 
-    assert (a - b).terms == {(0, 2, 3): -0.5, (1, 5, 7): 4.75, (3, 5, 7): 2.25}
+    assert (a - b).terms == {(0, 2, 3): -0.5, (1, 5, 7): 4.75, (3, 5, 7): -2.25}
 
-    with pytest.raises(TypeError):
-        _ = a + 0
+    assert (a + const).terms == {(0, 2, 3): -1.0, (1, 5, 7): 4.75, (3, 5, 7): -2.25, (): 5.25}
+    assert (a + 5.25).terms == {(0, 2, 3): -1.0, (1, 5, 7): 4.75, (3, 5, 7): -2.25, (): 5.25}
+    assert (a - const).terms == {(0, 2, 3): -1.0, (1, 5, 7): 4.75, (3, 5, 7): -2.25, (): -5.25}
+    assert (a - 5.25).terms == {(0, 2, 3): -1.0, (1, 5, 7): 4.75, (3, 5, 7): -2.25, (): -5.25}
 
-    with pytest.raises(TypeError):
-        a += 0
+    a += 5.25
+    assert a.constant == 5.25
+    a -= 10.5
+    assert a.constant == -5.25
 
-    with pytest.raises(TypeError):
-        _ = a - 0
+    assert a == a + 0
+    assert a == a - 0
 
+    assert a == a + o
+    assert a == a - o
+    assert a + 1.0 == a + identity
+    assert a - 1.0 == a - identity
+
+    with pytest.raises(TypeError):
+        a -= 'a'
     with pytest.raises(TypeError):
-        a -= 0
+        a += 'a'
 
 
 def test_majorana_operator_multiply():
@@ -129,6 +150,12 @@ def test_majorana_operator_multiply():
     a *= MajoranaOperator(())
     assert a.terms == {(0, 1, 5): 3.0, (1, 2, 7): -1.0}
 
+    zero = MajoranaOperator.zero()
+    assert a * zero == zero
+
+    identity = MajoranaOperator.identity()
+    assert a * identity == a
+
     with pytest.raises(TypeError):
         _ = a * 'a'
 
@@ -165,8 +192,8 @@ def test_majorana_operator_divide():
 
 
 def test_majorana_operator_neg():
-    a = MajoranaOperator((0, 1, 5), 1.5) + MajoranaOperator((1, 2, 7), -0.5)
-    assert (-a).terms == {(0, 1, 5): -1.5, (1, 2, 7): 0.5}
+    a = MajoranaOperator((0, 1, 5), 1.5) + MajoranaOperator((1, 2, 7), -0.5) + 2.5
+    assert (-a).terms == {(0, 1, 5): -1.5, (1, 2, 7): 0.5, (): -2.5}
 
 
 def test_majorana_operator_eq():