Repository: dwavesystems/dimod
Pull request #1282 (2022-10-31 19:31:54)

Fix CQM serialization to correctly encode all variables in the objective


Patch:
diff --git a/dimod/constrained/constrained.py b/dimod/constrained/constrained.py
index 6f62d037..838873ee 100644
--- a/dimod/constrained/constrained.py
+++ b/dimod/constrained/constrained.py
@@ -1747,14 +1747,36 @@ class ConstrainedQuadraticModel(cyConstrainedQuadraticModel):
             >>> print(cqm2.objective.to_polystring())
             -2*x + 2*x*y
         """
+        # developer note: this function needs a refactor
+
         file = SpooledTemporaryFile(max_size=spool_size)
 
+        # For legacy reasons, the variable info is encoded in the objective,
+        # so our serialized model will be a bit more verbose
+        if self.objective.variables != self.variables:
+            objective = QuadraticModel()
+
+            for v in self.variables:
+                objective.add_variable(self.vartype(v), v,
+                                       lower_bound=self.lower_bound(v),
+                                       upper_bound=self.upper_bound(v))
+
+            objective.update(self.objective)
+
+            surplus_num_biases = objective.num_variables - self.objective.num_variables
+            surplus_num_real_biases = sum(self.vartype(v) is Vartype.REAL
+                                          for v in objective.variables - self.objective.variables)
+        else:
+            objective = self.objective
+            surplus_num_biases = 0
+            surplus_num_real_biases = 0
+
         data = dict(num_variables=len(self.variables),
                     num_constraints=len(self.constraints),
-                    num_biases=self.num_biases(),
+                    num_biases=self.num_biases() + surplus_num_biases,
                     num_quadratic_variables=self.num_quadratic_variables(include_objective=False),
                     num_quadratic_variables_real=self.num_quadratic_variables(Vartype.REAL, include_objective=True),
-                    num_linear_biases_real=self.num_biases(Vartype.REAL, linear_only=True),
+                    num_linear_biases_real=self.num_biases(Vartype.REAL, linear_only=True) + surplus_num_real_biases,
                     num_weighted_constraints=sum(comp.lhs.is_soft() for comp in self.constraints.values()),
                     )
 
@@ -1762,7 +1784,7 @@ class ConstrainedQuadraticModel(cyConstrainedQuadraticModel):
 
         # write the values
         with zipfile.ZipFile(file, mode='a') as zf:
-            with self.objective.to_file(spool_size=int(1e12)) as f:
+            with objective.to_file(spool_size=int(1e12)) as f:
                 # we can avoid a copy by trying to read from the underlying buffer
                 obj = f._file.getbuffer() if isinstance(f._file, io.BytesIO) else f.read()
                 zf.writestr('objective', obj)
diff --git a/releasenotes/notes/fix-sparse-cqm-serialization-62653ce59533514c.yaml b/releasenotes/notes/fix-sparse-cqm-serialization-62653ce59533514c.yaml
new file mode 100644
index 00000000..ce4e3bdb
--- /dev/null
+++ b/releasenotes/notes/fix-sparse-cqm-serialization-62653ce59533514c.yaml
@@ -0,0 +1,3 @@
+---
+fixes:
+  - Fix constrained quadratic model serialziation to correctly encode all variables in the objective.


Test patch:
diff --git a/tests/test_constrained.py b/tests/test_constrained.py
index 17e4e44a..6579eb75 100644
--- a/tests/test_constrained.py
+++ b/tests/test_constrained.py
@@ -980,7 +980,13 @@ class TestSerialization(unittest.TestCase):
         with cqm.to_file() as f:
             new = CQM.from_file(f)
 
-        self.assertTrue(cqm.objective.is_equal(new.objective))
+        self.assertTrue(new.objective.variables >= cqm.objective.variables)
+        for v, bias in cqm.objective.iter_linear():
+            self.assertEqual(new.objective.get_linear(v), bias)
+        for u, v, bias in cqm.objective.iter_quadratic():
+            self.assertEqual(new.objective.get_quadratic(u, v), bias)
+        self.assertEqual(new.objective.offset, cqm.objective.offset)
+
         self.assertEqual(set(cqm.constraints), set(new.constraints))
         for label, constraint in cqm.constraints.items():
             self.assertTrue(constraint.lhs.is_equal(new.constraints[label].lhs))
@@ -1005,7 +1011,13 @@ class TestSerialization(unittest.TestCase):
         with cqm.to_file() as f:
             new = CQM.from_file(f)
 
-        self.assertTrue(cqm.objective.is_equal(new.objective))
+        self.assertTrue(new.objective.variables >= cqm.objective.variables)
+        for v, bias in cqm.objective.iter_linear():
+            self.assertEqual(new.objective.get_linear(v), bias)
+        for u, v, bias in cqm.objective.iter_quadratic():
+            self.assertEqual(new.objective.get_quadratic(u, v), bias)
+        self.assertEqual(new.objective.offset, cqm.objective.offset)
+
         self.assertEqual(set(cqm.constraints), set(new.constraints))
         for label, constraint in cqm.constraints.items():
             self.assertTrue(constraint.lhs.is_equal(new.constraints[label].lhs))
@@ -1021,7 +1033,13 @@ class TestSerialization(unittest.TestCase):
 
         new = CQM.from_file(cqm.to_file())
 
-        self.assertTrue(cqm.objective.is_equal(new.objective))
+        self.assertTrue(new.objective.variables >= cqm.objective.variables)
+        for v, bias in cqm.objective.iter_linear():
+            self.assertEqual(new.objective.get_linear(v), bias)
+        for u, v, bias in cqm.objective.iter_quadratic():
+            self.assertEqual(new.objective.get_quadratic(u, v), bias)
+        self.assertEqual(new.objective.offset, cqm.objective.offset)
+
         self.assertEqual(set(cqm.constraints), set(new.constraints))
         for label, constraint in cqm.constraints.items():
             self.assertTrue(constraint.lhs.is_equal(new.constraints[label].lhs))
@@ -1075,7 +1093,7 @@ class TestSerialization(unittest.TestCase):
 
         with cqm.to_file() as f:
             self.assertEqual(read_header(f, b'DIMODCQM').data,
-                             dict(num_biases=13,
+                             dict(num_biases=14,
                                   num_constraints=3,
                                   num_quadratic_variables=2,
                                   num_variables=6,
@@ -1089,7 +1107,7 @@ class TestSerialization(unittest.TestCase):
 
         with cqm.to_file() as f:
             self.assertEqual(read_header(f, b'DIMODCQM').data,
-                             dict(num_biases=17,
+                             dict(num_biases=18,
                                   num_constraints=4,
                                   num_quadratic_variables=4,
                                   num_variables=6,
@@ -1098,6 +1116,15 @@ class TestSerialization(unittest.TestCase):
                                   num_weighted_constraints=0,
                                   ))
 
+    def test_unused_variable(self):
+        cqm = dimod.CQM()
+        cqm.add_variable('BINARY', 'x')
+
+        with cqm.to_file() as f:
+            new = CQM.from_file(f)
+
+        self.assertEqual(new.variables, cqm.variables)
+
 
 class TestSetObjective(unittest.TestCase):
     def test_bqm(self):
diff --git a/tests/test_serialization_fileview.py b/tests/test_serialization_fileview.py
index c4a37c24..e1b0da49 100644
--- a/tests/test_serialization_fileview.py
+++ b/tests/test_serialization_fileview.py
@@ -191,7 +191,13 @@ class TestLoad(unittest.TestCase):
         with cqm.to_file() as f:
             new = load(f)
 
-        self.assertTrue(cqm.objective.is_equal(new.objective))
+        self.assertTrue(new.objective.variables >= cqm.objective.variables)
+        for v, bias in cqm.objective.iter_linear():
+            self.assertEqual(new.objective.get_linear(v), bias)
+        for u, v, bias in cqm.objective.iter_quadratic():
+            self.assertEqual(new.objective.get_quadratic(u, v), bias)
+        self.assertEqual(new.objective.offset, cqm.objective.offset)
+
         self.assertEqual(set(cqm.constraints), set(new.constraints))
         for label, constraint in cqm.constraints.items():
             self.assertTrue(constraint.lhs.is_equal(new.constraints[label].lhs))