diff --git a/dimod/binary/binary_quadratic_model.py b/dimod/binary/binary_quadratic_model.py index 69ace3ef4..cbea23fc8 100644 --- a/dimod/binary/binary_quadratic_model.py +++ b/dimod/binary/binary_quadratic_model.py @@ -46,7 +46,7 @@ from dimod.quadratic.quadratic_model import _VariableArray from dimod.serialization.fileview import SpooledTemporaryFile, _BytesIO, VariablesSection from dimod.serialization.fileview import load, read_header, write_header -from dimod.sym import Eq, Ge, Le +from dimod.sym import Eq, Ge, Le, Ne from dimod.typing import (Bias, BQMVectors, LabelledBQMVectors, QuadraticVectors, Variable, VartypeLike) from dimod.variables import Variables, iter_deserialize_variables @@ -482,6 +482,8 @@ def __le__(self, other: Bias): return NotImplemented def __ne__(self, other): + if isinstance(other, Number): + return Ne(self, other) return not self.is_equal(other) @property diff --git a/dimod/constrained/cyconstrained.pyx b/dimod/constrained/cyconstrained.pyx index b8d265516..e77233f34 100644 --- a/dimod/constrained/cyconstrained.pyx +++ b/dimod/constrained/cyconstrained.pyx @@ -38,7 +38,7 @@ from dimod.libcpp.abc cimport QuadraticModelBase as cppQuadraticModelBase from dimod.libcpp.constrained_quadratic_model cimport Sense as cppSense, Penalty as cppPenalty, Constraint as cppConstraint from dimod.libcpp.vartypes cimport Vartype as cppVartype, vartype_info as cppvartype_info -from dimod.sym import Sense, Eq, Ge, Le +from dimod.sym import Sense, Eq, Ge, Le, Ne from dimod.variables import Variables from dimod.vartypes import as_vartype, Vartype from dimod.views.quadratic import QuadraticViewsMixin @@ -55,6 +55,8 @@ cdef cppSense cppsense(object sense) except? cppSense.GE: return cppSense.LE elif sense is Sense.Ge: return cppSense.GE + elif sense is Sense.Ne: + return cppSense.NE else: raise RuntimeError(f"unexpected sense: {sense!r}") @@ -85,6 +87,8 @@ cdef class cyConstraintsView: return Le(lhs, rhs) elif self.parent.cppcqm.constraint_ref(vi).sense() == cppSense.GE: return Ge(lhs, rhs) + elif self.parent.cppcqm.constraint_ref(vi).sense() == cppSense.NE: + return Ne(lhs, rhs) else: raise RuntimeError("unexpected Sense") diff --git a/dimod/include/dimod/constraint.h b/dimod/include/dimod/constraint.h index d32c64e9f..6ad99c59c 100644 --- a/dimod/include/dimod/constraint.h +++ b/dimod/include/dimod/constraint.h @@ -25,7 +25,7 @@ namespace dimod { -enum Sense { LE, GE, EQ }; +enum Sense { LE, GE, EQ, NE }; enum Penalty { LINEAR, QUADRATIC, CONSTANT }; @@ -144,10 +144,16 @@ void Constraint::scale(bias_type scalar) { base_type::scale(scalar); rhs_ *= scalar; if (scalar < 0) { - if (sense_ == Sense::LE) { - sense_ = Sense::GE; - } else if (sense_ == Sense::GE) { - sense_ = Sense::LE; + switch (sense_) { + case Sense::LE : + sense_ = Sense::GE; + break; + case Sense::GE : + sense_ = Sense::LE; + break; + case Sense::EQ: + case Sense::NE: + break; } } } diff --git a/dimod/libcpp/constrained_quadratic_model.pxd b/dimod/libcpp/constrained_quadratic_model.pxd index 4d294d0e1..971786be8 100644 --- a/dimod/libcpp/constrained_quadratic_model.pxd +++ b/dimod/libcpp/constrained_quadratic_model.pxd @@ -28,6 +28,7 @@ cdef extern from "dimod/constrained_quadratic_model.h" namespace "dimod" nogil: EQ LE GE + NE enum Penalty: LINEAR diff --git a/dimod/quadratic/quadratic_model.py b/dimod/quadratic/quadratic_model.py index 99a9d73e0..38d90d95d 100644 --- a/dimod/quadratic/quadratic_model.py +++ b/dimod/quadratic/quadratic_model.py @@ -38,7 +38,7 @@ from dimod.serialization.fileview import SpooledTemporaryFile, _BytesIO from dimod.serialization.fileview import VariablesSection, Section from dimod.serialization.fileview import load, read_header, write_header -from dimod.sym import Eq, Ge, Le, Comparison +from dimod.sym import Eq, Ge, Le, Ne, Comparison from dimod.typing import Variable, Bias, VartypeLike from dimod.variables import Variables from dimod.vartypes import Vartype, as_vartype @@ -347,6 +347,11 @@ def __le__(self, other: Bias) -> Comparison: return Le(self, other) return NotImplemented + def __ne__(self, other: Number) -> Comparison: + if isinstance(other, Number): + return Ne(self, other) + return NotImplemented + @property def dtype(self) -> np.dtype: """Data-type of the model's biases.""" diff --git a/dimod/sym.py b/dimod/sym.py index 999776d2d..d53f042f0 100644 --- a/dimod/sym.py +++ b/dimod/sym.py @@ -29,6 +29,7 @@ class Sense(enum.Enum): * ``Le``: less or equal (:math:`\le`) * ``Ge``: greater or equal (:math:`\ge`) * ``Eq``: equal (:math:`=`) + * ``Ne``: not equal( :math:`\ne`) Example: @@ -43,6 +44,7 @@ class Sense(enum.Enum): Le = '<=' Ge = '>=' Eq = '==' + Ne = '!=' class Comparison(abc.ABC): @@ -96,3 +98,11 @@ class Ge(Comparison, sense='>='): class Le(Comparison, sense='<='): pass + + +class Ne(Comparison, sense='!='): + def __bool__(self): + try: + return not self.lhs.is_equal(self.rhs) + except AttributeError: + return False diff --git a/tests/test_constrained.py b/tests/test_constrained.py index 6579eb758..c39a0c9cd 100644 --- a/tests/test_constrained.py +++ b/tests/test_constrained.py @@ -77,6 +77,17 @@ def test_duplicate(self): with self.assertRaises(ValueError): cqm.add_constraint(bqm <= 5, label='hello') + def test_ne(self): + cqm = dimod.CQM() + + x = dimod.Binary('x') + i = dimod.Integer('i') + + c0 = cqm.add_constraint(x != 1) + cqm.add_constraint(i != 1) + + self.assertIs(cqm.constraints[c0].sense, dimod.sym.Sense.Ne) + def test_symbolic(self): cqm = CQM() diff --git a/testscpp/tests/test_constrained_quadratic_model.cpp b/testscpp/tests/test_constrained_quadratic_model.cpp index fd9f8200d..082c26053 100644 --- a/testscpp/tests/test_constrained_quadratic_model.cpp +++ b/testscpp/tests/test_constrained_quadratic_model.cpp @@ -468,6 +468,56 @@ TEST_CASE("Bug 0") { } } +TEST_CASE("Test Constraint::scale()") { + GIVEN("A CQM with several constraints") { + auto cqm = dimod::ConstrainedQuadraticModel(); + cqm.add_variables(Vartype::BINARY, 1); + auto c0 = cqm.add_linear_constraint({0}, {4}, Sense::EQ, 2); + auto c1 = cqm.add_linear_constraint({0}, {4}, Sense::LE, 2); + auto c2 = cqm.add_linear_constraint({0}, {4}, Sense::GE, 2); + + cqm.constraint_ref(c0).set_offset(8); + cqm.constraint_ref(c1).set_offset(8); + cqm.constraint_ref(c2).set_offset(8); + + WHEN("we scale the constraints by a positive number") { + cqm.constraint_ref(c0).scale(.5); + cqm.constraint_ref(c1).scale(.5); + cqm.constraint_ref(c2).scale(.5); + + THEN("the biases are scaled and everything else stays the same") { + for (auto c = c0; c <= c2; ++c) { + CHECK(cqm.constraint_ref(c).offset() == 4); + CHECK(cqm.constraint_ref(c).linear(0) == 2); + CHECK(cqm.constraint_ref(c).rhs() == 1); + } + + CHECK(cqm.constraint_ref(c0).sense() == Sense::EQ); + CHECK(cqm.constraint_ref(c1).sense() == Sense::LE); + CHECK(cqm.constraint_ref(c2).sense() == Sense::GE); + } + } + + WHEN("we scale the constraints by a negative number") { + cqm.constraint_ref(c0).scale(-.5); + cqm.constraint_ref(c1).scale(-.5); + cqm.constraint_ref(c2).scale(-.5); + + THEN("the biases are scaled and some of the signs flip") { + for (auto c = c0; c <= c2; ++c) { + CHECK(cqm.constraint_ref(c).offset() == -4); + CHECK(cqm.constraint_ref(c).linear(0) == -2); + CHECK(cqm.constraint_ref(c).rhs() == -1); + } + + CHECK(cqm.constraint_ref(c0).sense() == Sense::EQ); + CHECK(cqm.constraint_ref(c1).sense() == Sense::GE); // flipped + CHECK(cqm.constraint_ref(c2).sense() == Sense::LE); // flipped + } + } + } +} + TEST_CASE("Test CQM.add_constraint()") { GIVEN("A CQM and a BQM") { auto cqm = dimod::ConstrainedQuadraticModel();