Repository: dwavesystems/dimod
Pull request #1365 (2024-02-19 14:33:51)

Addressing issue #1364: option for a variable check in add_discrete method
As discussed in the issue #1364, the variable check in `ConstrainedQuadraticModel.add_discrete` method  can be skipped in some cases and the user can decide if this is truly necessary. 

This is implemented by adding a `do_var_check` boolean variable (default: `True`), which enables the check if it's true, in the methods `ConstrainedQuadraticModel.add_discrete_from_model`, `ConstrainedQuadraticModel.add_discrete_from_comparison`,  and `ConstrainedQuadraticModel.add_discrete_from_iterable`.  





Patch:
diff --git a/dimod/constrained/constrained.py b/dimod/constrained/constrained.py
index 86c4fc93..3704a8a2 100644
--- a/dimod/constrained/constrained.py
+++ b/dimod/constrained/constrained.py
@@ -494,7 +494,7 @@ class ConstrainedQuadraticModel(cyConstrainedQuadraticModel):
 
             >>> d, e, f = dimod.Binaries(['d', 'e', 'f'])
             >>> cqm.add_discrete(d + e + f == 1, label='discrete-def')
-            'discrete-def'
+            'discrete-def'            
 
         """
         # in python 3.8+ we can use singledispatchmethod
@@ -510,7 +510,8 @@ class ConstrainedQuadraticModel(cyConstrainedQuadraticModel):
     def add_discrete_from_comparison(self,
                                      comp: Comparison,
                                      label: Optional[Hashable] = None,
-                                     copy: bool = True) -> Hashable:
+                                     copy: bool = True,
+                                     check_overlaps: bool = True) -> Hashable:
         """Add a one-hot constraint from a comparison.
 
         One-hot constraints can represent discrete variables (for example a
@@ -539,6 +540,10 @@ class ConstrainedQuadraticModel(cyConstrainedQuadraticModel):
                 set to `False` to improve performance, but subsequently mutating
                 the model can cause issues.
 
+            check_overlaps: If `True` we perform a variable overlap check.
+                In particular, if the variables already exist, we make sure
+                they're not used in another discrete constraint.
+
         Returns:
             Label of the added constraint.
 
@@ -555,11 +560,13 @@ class ConstrainedQuadraticModel(cyConstrainedQuadraticModel):
         if comp.rhs != 1:
             # could scale, but let's keep it simple for now
             raise ValueError("the right-hand side of a discrete constraint must be 1")
-        return self.add_discrete_from_model(comp.lhs, label=label, copy=copy)
+        return self.add_discrete_from_model(comp.lhs, label=label,
+                                            copy=copy, check_overlaps=check_overlaps)
 
     def add_discrete_from_iterable(self,
                                    variables: Iterable[Variable],
-                                   label: Optional[Hashable] = None) -> Hashable:
+                                   label: Optional[Hashable] = None,
+                                   check_overlaps: bool = True) -> Hashable:
         """Add a one-hot constraint from an iterable.
 
         One-hot constraints can represent discrete variables (for example a
@@ -581,6 +588,10 @@ class ConstrainedQuadraticModel(cyConstrainedQuadraticModel):
             label: Label for the constraint. Must be unique. If no label
                 is provided, then one is generated using :mod:`uuid`.
 
+            check_overlaps: If `True` we perform a variable overlap check.
+                In particular, if the variables already exist, we make sure
+                they're not used in another discrete constraint.
+
         Returns:
             Label of the added constraint.
 
@@ -607,7 +618,7 @@ class ConstrainedQuadraticModel(cyConstrainedQuadraticModel):
         for v in variables:
             if v in self.variables:
                 # it already exists, let's make sure it's not already used
-                if any(v in self.constraints[label].lhs.variables for label in self.discrete):
+                if check_overlaps and any(v in self.constraints[label].lhs.variables for label in self.discrete):
                     raise ValueError(f"variable {v!r} is already used in a discrete variable")
                 if self.vartype(v) is not Vartype.BINARY:
                     raise ValueError(f"variable {v!r} has already been added but is not BINARY")
@@ -621,7 +632,8 @@ class ConstrainedQuadraticModel(cyConstrainedQuadraticModel):
     def add_discrete_from_model(self,
                                 qm: Union[BinaryQuadraticModel, QuadraticModel],
                                 label: Optional[Hashable] = None,
-                                copy: bool = True) -> Hashable:
+                                copy: bool = True,
+                                check_overlaps: bool = True) -> Hashable:
         """Add a one-hot constraint from a model.
 
         One-hot constraints can represent discrete variables (for example a
@@ -650,6 +662,10 @@ class ConstrainedQuadraticModel(cyConstrainedQuadraticModel):
                 improve performance, but subsequently mutating the model can
                 cause issues.
 
+            check_overlaps: If `True` we perform a variable overlap check.
+                In particular, if the variables already exist, we make sure
+                they're not used in another discrete constraint.
+
         Returns:
             Label of the added constraint.
 
@@ -671,7 +687,7 @@ class ConstrainedQuadraticModel(cyConstrainedQuadraticModel):
         for v, bias in qm.iter_linear():
             if v in self.variables:
                 # it already exists, let's make sure it's not already used
-                if any(v in self.constraints[label].lhs.variables for label in self.discrete):
+                if check_overlaps and any(v in self.constraints[label].lhs.variables for label in self.discrete):
                     raise ValueError(f"variable {v!r} is already used in a discrete variable")
                 if self.vartype(v) is not Vartype.BINARY:
                     raise ValueError(f"variable {v!r} has already been added but is not BINARY")
diff --git a/releasenotes/notes/check-overlaps-keyword-in-add-discrete-f078c11e943cfb41.yaml b/releasenotes/notes/check-overlaps-keyword-in-add-discrete-f078c11e943cfb41.yaml
new file mode 100644
index 00000000..422df03e
--- /dev/null
+++ b/releasenotes/notes/check-overlaps-keyword-in-add-discrete-f078c11e943cfb41.yaml
@@ -0,0 +1,5 @@
+---
+features:
+  - |
+    Add ``check_overlaps`` keyword argument to ``ConstrainedQuadraticModel.add_discrete()``,
+    see `#1364 <https://github.com/dwavesystems/dimod/issues/1364>`_.


Test patch:
diff --git a/tests/test_constrained.py b/tests/test_constrained.py
index 54941b01..8387196e 100644
--- a/tests/test_constrained.py
+++ b/tests/test_constrained.py
@@ -172,6 +172,31 @@ class TestAddDiscrete(unittest.TestCase):
         c = cqm.add_discrete(qm, label='hello')
         self.assertIn(c, cqm.discrete)
         self.assertTrue(cqm.constraints[c].lhs.is_equal(qm))
+        
+    def test_check_overlaps_keyword(self):
+        # If check_overlaps=False, we do not check for overlaps
+        # and there is no ValueError.
+        # This is expected behavior.
+        cqm = CQM()
+        cqm.add_discrete("xyz")
+        cqm.add_discrete("yzw", check_overlaps=False)
+
+        # Other exceptions should not be affected
+        cqm = CQM()
+        x,y = dimod.Binaries('xy')
+        with self.subTest("wrong sense"):
+            with self.assertRaises(ValueError):
+                cqm.add_discrete(x + y <= 1, check_overlaps=False)
+        with self.subTest("wrong rhs"):
+            with self.assertRaises(ValueError):
+                cqm.add_discrete(x + y == 2, check_overlaps=False)
+        with self.subTest("wrong vartype"):
+            s = dimod.Spin('s')
+            with self.assertRaises(ValueError):
+                cqm.add_discrete(x + s == 1, check_overlaps=False)
+        with self.subTest("quadratic"):
+            with self.assertRaises(ValueError):
+                cqm.add_discrete(x + y + x*y== 1, check_overlaps=False)
 
     def test_comparison(self):
         cqm = CQM()