Repository: dwavesystems/dwave-optimization (language: python)

Issue:
Consider adding a method to check feasibility
We currently have a [`Graph::feasible()`](https://github.com/dwavesystems/dwave-optimization/blob/6c626f52371eea3419015d0efbaa027255dcef10/dwave/optimization/include/dwave-optimization/graph.hpp#L152) method at the C++ level.

We could expose something similar at the Python level

Something like
```Python
class Model:
    ...

    def feasible(state_index: int) -> bool:
        with self.lock():
            return all(constraint.state(state_index) for constraint in self.iter_constraints())
```
or could wrap the C++ method directly, but that would be a bit more involved.


Discussion / hints:

Fix patch:
diff --git a/dwave/optimization/model.pyi b/dwave/optimization/model.pyi
index cf8ce815..e92f8cd4 100644
--- a/dwave/optimization/model.pyi
+++ b/dwave/optimization/model.pyi
@@ -45,6 +45,8 @@ class Model:
         self, primary_set_size: int, num_disjoint_lists: int,
         ) -> typing.Tuple[DisjointLists, typing.Tuple[DisjointList, ...]]: ...
 
+    def feasible(self, index: int = 0) -> bool: ...
+
     @classmethod
     def from_file(
         cls,
diff --git a/dwave/optimization/model.pyx b/dwave/optimization/model.pyx
index 9f954b29..34c8f7bf 100644
--- a/dwave/optimization/model.pyx
+++ b/dwave/optimization/model.pyx
@@ -245,6 +245,36 @@ cdef class Model:
         lists = [DisjointList(main, i) for i in range(num_disjoint_lists)]
         return main, lists
 
+    def feasible(self, int index = 0):
+        """Check the feasibility of the state at the input index.
+
+        Args:
+            index: index of the state to check for feasibility.
+
+        Returns:
+            Feasibility of the state.
+
+        Examples:
+            This example demonstrates checking the feasibility of a simple model with
+            feasible and infeasible states.
+
+            >>> from dwave.optimization.model import Model
+            >>> model = Model()
+            >>> b = model.binary()
+            >>> model.add_constraint(b) # doctest: +ELLIPSIS
+            <dwave.optimization.BinaryVariable at ...>
+            >>> model.states.resize(2)
+            >>> b.set_state(0, 1) # Feasible
+            >>> b.set_state(1, 0) # Infeasible
+            >>> with model.lock():
+            ...     model.feasible(0)
+            True
+            >>> with model.lock():
+            ...     model.feasible(1)
+            False
+        """
+        return all(sym.state(index) for sym in self.iter_constraints())
+
     @classmethod
     def from_file(cls, file, *,
                   check_header = True,


Test patch:
diff --git a/tests/test_model.py b/tests/test_model.py
index 896b25a3..d1573cd1 100644
--- a/tests/test_model.py
+++ b/tests/test_model.py
@@ -201,6 +201,32 @@ def test_add_constraint(self):
         self.assertTrue(c_iter.state(0))
         self.assertTrue(c_direct.state(0))
 
+    def test_feasible(self):
+        model = Model()
+        i = model.integer()
+        c = model.constant(5)
+        model.add_constraint(i <= c)
+
+        with model.lock():
+            # Check expected exception if no states initialize
+            with self.assertRaises(ValueError):
+                model.feasible(0)
+
+            model.states.resize(1)
+            i.set_state(0, 1)
+
+            # Check that True is returned for feasible state
+            self.assertTrue(model.feasible(0))
+
+            # Check expected exception for index out of range with initialized state
+            with self.assertRaises(ValueError):
+                model.feasible(1)
+
+            i.set_state(0, 6)
+
+            # Check that False is returned for infeasible state
+            self.assertFalse(model.feasible(0))
+
     def test_lock(self):
         model = Model()