Repository: rigetticomputing/pyquil
Pull request #92 (2017-07-28 00:23:59)

deepcopy / np.isclose dependencies removed --> PauliTerm/Sum speed 2x-10x improvement
**Profiling revealed most of time spent in np.isclose compare-with-zero and deepcopy() calls.**

Added PauliTerm.copy() method, directly copying the immutable terms (coefficient and id) and using the mutable copy methods (_ops dict). I have proof-of-validity code for the dict.copy() method, can link if necessary.

Replaced np.isclose compare-with-zero calls with Python real/imag compare-with-zero checks.


Patch:
diff --git a/pyquil/paulis.py b/pyquil/paulis.py
index 2892785..8ea8d5d 100644
--- a/pyquil/paulis.py
+++ b/pyquil/paulis.py
@@ -97,6 +97,22 @@ class PauliTerm(object):
         """
         return len(self._ops)
 
+    def copy(self):
+        """
+        Properly creates a new PauliTerm, with a completely new dictionary
+        of operators
+        """
+        new_term = PauliTerm("I", 0, 1.0)  # create new object
+        # manually copy all attributes over
+        for key in self.__dict__.keys():
+            val = self.__dict__[key]
+            if isinstance(val, (dict, list, set)):  # mutable types
+                new_term.__dict__[key] = copy.copy(val)
+            else:  # immutable types
+                new_term.__dict__[key] = val
+
+        return new_term
+
     def get_qubits(self):
         """Gets all the qubits that this PauliTerm operates on.
         """
@@ -261,11 +277,10 @@ def term_with_coeff(term, coeff):
     :returns: A new PauliTerm that duplicates term but sets coeff
     :rtype: PauliTerm
     """
-    new_pauli = copy.copy(term)
+    new_pauli = term.copy()
     new_pauli.coefficient = coeff
     return new_pauli
 
-
 class PauliSum(object):
     """A sum of one or more PauliTerms.
     """
@@ -328,7 +343,7 @@ class PauliSum(object):
         :rtype: PauliSum
         """
         assert isinstance(other, Number)
-        new_terms = copy.deepcopy(self.terms)
+        new_terms = [term.copy() for term in self.terms]
         for term in new_terms:
             term.coefficient *= other
         return PauliSum(new_terms).simplify()
@@ -346,7 +361,7 @@ class PauliSum(object):
             other = PauliSum([other])
         elif isinstance(other, Number):
             other = PauliSum([other * ID])
-        new_terms = copy.deepcopy(self.terms)
+        new_terms = [term.copy() for term in self.terms]
         new_terms.extend(other.terms)
         new_sum = PauliSum(new_terms)
         return new_sum.simplify()
@@ -402,8 +417,8 @@ class PauliSum(object):
             terms = []
             for k in sorted(d):
                 term_list = d[k]
-                if (len(term_list) == 1 and not
-                   np.isclose(term_list[0].coefficient, 0.0)):
+                if (len(term_list) == 1 and not \
+                    np.isclose(term_list[0].coefficient, 0.0)):
                     terms.append(term_list[0])
                 else:
                     coeff = sum(t.coefficient for t in term_list)


Test patch:
diff --git a/pyquil/tests/test_paulis.py b/pyquil/tests/test_paulis.py
index ac4d66c..d2240d8 100644
--- a/pyquil/tests/test_paulis.py
+++ b/pyquil/tests/test_paulis.py
@@ -16,9 +16,9 @@
 ##############################################################################
 
 import pytest
-from pyquil.paulis import (PauliTerm, PauliSum, exponential_map, ID, exponentiate,
-                           trotterize, is_zero, check_commutation, commuting_sets, sZ, sX
-                           )
+from pyquil.paulis import (PauliTerm, PauliSum, exponential_map, ID,
+                           exponentiate, trotterize, is_zero, check_commutation, 
+                           commuting_sets, sZ, sX)
 from pyquil.quil import Program
 from pyquil.gates import RX, RZ, CNOT, H, X, PHASE
 
@@ -124,6 +124,24 @@ def test_simplify_sum_terms():
     assert str(sum_term * sum_term) == '1.0*I'
 
 
+def test_copy():
+    term = PauliTerm('X', 0, 0.5) * PauliTerm('X', 1, 0.5)
+    new_term = term.copy()
+
+    term = term * PauliTerm('X', 2, 0.5)
+    new_term = new_term * PauliTerm('X', 2, 0.5)
+
+    assert term == new_term  # value equality
+    assert not term is new_term  # ref inequality
+    assert not term._ops is new_term._ops
+
+    term = PauliTerm('X', 0, 0.5) * PauliTerm('X', 1, 0.5)
+    new_term = term * PauliTerm('X', 2, 0.5)
+    assert term != new_term
+    assert not term is new_term
+    assert not term._ops is new_term._ops
+
+
 def test_len():
     term = PauliTerm("Z", 0, 1.0) * PauliTerm("Z", 1, 1.0)
     assert len(term) == 2