diff --git a/qiskit_optimization/translators/docplex_mp.py b/qiskit_optimization/translators/docplex_mp.py index f96320683..34c7f020d 100644 --- a/qiskit_optimization/translators/docplex_mp.py +++ b/qiskit_optimization/translators/docplex_mp.py @@ -12,12 +12,17 @@ """Translator between a docplex.mp model and a quadratic program""" -from typing import cast - -from docplex.mp.constr import LinearConstraint as DocplexLinearConstraint -from docplex.mp.constr import NotEqualConstraint -from docplex.mp.constr import QuadraticConstraint as DocplexQuadraticConstraint +from typing import Dict, Optional, Tuple, cast + +from docplex.mp.constants import ComparisonType +from docplex.mp.constr import ( + IndicatorConstraint, + LinearConstraint, + NotEqualConstraint, + QuadraticConstraint, +) from docplex.mp.dvar import Var +from docplex.mp.linear import AbstractLinearExpr from docplex.mp.model import Model from docplex.mp.quad import QuadExpr from docplex.mp.vartype import BinaryVarType, ContinuousVarType, IntegerVarType @@ -25,9 +30,8 @@ from qiskit_optimization.exceptions import QiskitOptimizationError from qiskit_optimization.problems.constraint import Constraint from qiskit_optimization.problems.quadratic_objective import QuadraticObjective -from qiskit_optimization.problems.variable import Variable - from qiskit_optimization.problems.quadratic_program import QuadraticProgram +from qiskit_optimization.problems.variable import Variable def to_docplex_mp(quadratic_program: QuadraticProgram) -> Model: @@ -56,9 +60,7 @@ def to_docplex_mp(quadratic_program: QuadraticProgram) -> Model: var[idx] = mdl.integer_var(lb=x.lowerbound, ub=x.upperbound, name=x.name) else: # should never happen - raise QiskitOptimizationError( - "Internal error: unsupported variable type: {}".format(x.vartype) - ) + raise QiskitOptimizationError(f"Internal error: unsupported variable type: {x.vartype}") # add objective objective = quadratic_program.objective.constant @@ -89,9 +91,7 @@ def to_docplex_mp(quadratic_program: QuadraticProgram) -> Model: mdl.add_constraint(linear_expr <= rhs, ctname=name) else: # should never happen - raise QiskitOptimizationError( - "Internal error: unsupported constraint sense: {}".format(sense) - ) + raise QiskitOptimizationError(f"Internal error: unsupported constraint sense: {sense}") # add quadratic constraints for i, q_constraint in enumerate(quadratic_program.quadratic_constraints): @@ -117,23 +117,173 @@ def to_docplex_mp(quadratic_program: QuadraticProgram) -> Model: mdl.add_constraint(quadratic_expr <= rhs, ctname=name) else: # should never happen + raise QiskitOptimizationError(f"Internal error: unsupported constraint sense: {sense}") + + return mdl + + +# from_docplex_mp + + +class _FromDocplexMp: + _sense_dict = {ComparisonType.EQ: "==", ComparisonType.LE: "<=", ComparisonType.GE: ">="} + + @classmethod + def _linear_constraint( + cls, var_names: Dict[Var, str], constraint: LinearConstraint + ) -> Tuple[Dict[str, float], str, float]: + left_expr = constraint.get_left_expr() + right_expr = constraint.get_right_expr() + # for linear constraints we may get an instance of Var instead of expression, + # e.g. x + y = z + if not isinstance(left_expr, (AbstractLinearExpr, Var)): + raise QiskitOptimizationError(f"Unsupported expression: {left_expr} {type(left_expr)}") + if not isinstance(right_expr, (AbstractLinearExpr, Var)): raise QiskitOptimizationError( - "Internal error: unsupported constraint sense: {}".format(sense) + f"Unsupported expression: {right_expr} {type(right_expr)}" ) + if isinstance(left_expr, Var): + left_expr = left_expr + 0 + if isinstance(right_expr, Var): + right_expr = right_expr + 0 - return mdl + linear = {} + for x in left_expr.iter_variables(): + linear[var_names[x]] = left_expr.get_coef(x) + for x in right_expr.iter_variables(): + linear[var_names[x]] = linear.get(var_names[x], 0.0) - right_expr.get_coef(x) + + rhs = right_expr.constant - left_expr.constant + + if constraint.sense not in cls._sense_dict: + raise QiskitOptimizationError(f"Unsupported constraint sense: {constraint}") + + return linear, cls._sense_dict[constraint.sense], rhs + + @classmethod + def _quadratic_constraint( + cls, var_names: Dict[Var, str], constraint: QuadraticConstraint + ) -> Tuple[Dict[str, float], Dict[Tuple[str, str], float], str, float]: + left_expr = constraint.get_left_expr() + right_expr = constraint.get_right_expr() + if not isinstance(left_expr, (QuadExpr, AbstractLinearExpr, Var)): + raise QiskitOptimizationError(f"Unsupported expression: {left_expr} {type(left_expr)}") + if not isinstance(right_expr, (QuadExpr, AbstractLinearExpr, Var)): + raise QiskitOptimizationError( + f"Unsupported expression: {right_expr} {type(right_expr)}" + ) + + lin = {} + quad = {} + + if left_expr.is_quad_expr(): + for x in left_expr.linear_part.iter_variables(): + lin[var_names[x]] = left_expr.linear_part.get_coef(x) + for quad_triplet in left_expr.iter_quad_triplets(): + i = var_names[quad_triplet[0]] + j = var_names[quad_triplet[1]] + v = quad_triplet[2] + quad[i, j] = v + else: + for x in left_expr.iter_variables(): + lin[var_names[x]] = left_expr.get_coef(x) + + if right_expr.is_quad_expr(): + for x in right_expr.linear_part.iter_variables(): + lin[var_names[x]] = lin.get(var_names[x], 0.0) - right_expr.linear_part.get_coef(x) + for quad_triplet in right_expr.iter_quad_triplets(): + i = var_names[quad_triplet[0]] + j = var_names[quad_triplet[1]] + v = quad_triplet[2] + quad[i, j] = quad.get((i, j), 0.0) - v + else: + for x in right_expr.iter_variables(): + lin[var_names[x]] = lin.get(var_names[x], 0.0) - right_expr.get_coef(x) + rhs = right_expr.constant - left_expr.constant -def from_docplex_mp(model: Model) -> QuadraticProgram: + if constraint.sense not in cls._sense_dict: + raise QiskitOptimizationError(f"Unsupported constraint sense: {constraint}") + + return lin, quad, cls._sense_dict[constraint.sense], rhs + + @staticmethod + def _linear_bounds(var_bounds: Dict[str, Tuple[float, float]], linear: Dict[str, float]): + linear_lb = 0.0 + linear_ub = 0.0 + for var_name, val in linear.items(): + x_lb, x_ub = var_bounds[var_name] + x_lb *= val + x_ub *= val + linear_lb += min(x_lb, x_ub) + linear_ub += max(x_lb, x_ub) + return linear_lb, linear_ub + + @classmethod + def _indicator_constraints( + cls, + var_names: Dict[Var, str], + var_bounds: Dict[str, Tuple[float, float]], + constraint: IndicatorConstraint, + indicator_big_m: Optional[float] = None, + ): + name = constraint.name + binary_var = constraint.binary_var + active_value = constraint.active_value + linear_constraint = constraint.linear_constraint + linear, sense, rhs = cls._linear_constraint(var_names, linear_constraint) + linear_lb, linear_ub = cls._linear_bounds(var_bounds, linear) + if sense == "<=": + big_m = max(0.0, linear_ub - rhs) if indicator_big_m is None else indicator_big_m + if active_value: + linear[binary_var.name] = big_m + rhs += big_m + else: + linear[binary_var.name] = -big_m + return [(linear, sense, rhs, name)] + elif sense == ">=": + big_m = max(0.0, rhs - linear_lb) if indicator_big_m is None else indicator_big_m + if active_value: + linear[binary_var.name] = -big_m + rhs -= big_m + else: + linear[binary_var.name] = big_m + return [(linear, sense, rhs, name)] + elif sense == "==": + # for equality constraints, add both GE and LE constraints. + # linear2, rhs2, and big_m2 are for the GE constraint. + linear2 = linear.copy() + rhs2 = rhs + big_m = max(0.0, linear_ub - rhs) if indicator_big_m is None else indicator_big_m + big_m2 = max(0.0, rhs - linear_lb) if indicator_big_m is None else indicator_big_m + if active_value: + linear[binary_var.name] = big_m + rhs += big_m + linear2[binary_var.name] = -big_m2 + rhs2 -= big_m2 + else: + linear[binary_var.name] = -big_m + linear2[binary_var.name] = big_m2 + return [(linear, "<=", rhs, name + "_LE"), (linear2, ">=", rhs2, name + "_GE")] + else: + raise QiskitOptimizationError( + f"Internal error: invalid sense of indicator constraint: {sense}" + ) + + +def from_docplex_mp(model: Model, indicator_big_m: Optional[float] = None) -> QuadraticProgram: """Translate a docplex.mp model into a quadratic program. Note that this supports only basic functions of docplex as follows: - quadratic objective function - - linear / quadratic constraints + - linear / quadratic / indicator constraints - binary / integer / continuous variables Args: model: The docplex.mp model to be loaded. + indicator_big_m: The big-M value used for the big-M formulation to convert + indicator constraints into linear constraints. + If ``None``, it is automatically derived from the model. Returns: The quadratic program corresponding to the model. @@ -144,14 +294,22 @@ def from_docplex_mp(model: Model) -> QuadraticProgram: if not isinstance(model, Model): raise QiskitOptimizationError(f"The model is not compatible: {model}") - quadratic_program = QuadraticProgram() + if model.number_of_user_cut_constraints > 0: + raise QiskitOptimizationError("User cut constraints are not supported") + + if model.number_of_lazy_constraints > 0: + raise QiskitOptimizationError("Lazy constraints are not supported") + + if model.number_of_sos > 0: + raise QiskitOptimizationError("SOS sets are not supported") # get name - quadratic_program.name = model.name + quadratic_program = QuadraticProgram(model.name) # get variables # keep track of names separately, since docplex allows to have None names. var_names = {} + var_bounds = {} for x in model.iter_variables(): if isinstance(x.vartype, ContinuousVarType): x_new = quadratic_program.continuous_var(x.lb, x.ub, x.name) @@ -160,10 +318,9 @@ def from_docplex_mp(model: Model) -> QuadraticProgram: elif isinstance(x.vartype, IntegerVarType): x_new = quadratic_program.integer_var(x.lb, x.ub, x.name) else: - raise QiskitOptimizationError( - "Unsupported variable type: {} {}".format(x.name, x.vartype) - ) + raise QiskitOptimizationError(f"Unsupported variable type: {x.name} {x.vartype}") var_names[x] = x_new.name + var_bounds[x.name] = (x_new.lowerbound, x_new.upperbound) # objective sense minimize = model.objective_sense.is_minimize() @@ -196,92 +353,33 @@ def from_docplex_mp(model: Model) -> QuadraticProgram: else: quadratic_program.maximize(constant, linear, quadratic) - # get linear constraints + # check constraint type for constraint in model.iter_constraints(): - if isinstance(constraint, DocplexQuadraticConstraint): - # ignore quadratic constraints here and process them later - continue - if not isinstance(constraint, DocplexLinearConstraint) or isinstance( - constraint, NotEqualConstraint - ): - # If any constraint is not linear/quadratic constraints, it raises an error. - # Notice that NotEqualConstraint is a subclass of Docplex's LinearConstraint, - # but it cannot be handled by optimization. - raise QiskitOptimizationError("Unsupported constraint: {}".format(constraint)) - name = constraint.name - sense = constraint.sense + # If any constraint is not linear/quadratic/indicator constraints, it raises an error. + if isinstance(constraint, LinearConstraint): + if isinstance(constraint, NotEqualConstraint): + # Notice that NotEqualConstraint is a subclass of Docplex's LinearConstraint, + # but it cannot be handled by optimization. + raise QiskitOptimizationError(f"Unsupported constraint: {constraint}") + elif not isinstance(constraint, (QuadraticConstraint, IndicatorConstraint)): + raise QiskitOptimizationError(f"Unsupported constraint: {constraint}") - left_expr = constraint.get_left_expr() - right_expr = constraint.get_right_expr() - # for linear constraints we may get an instance of Var instead of expression, - # e.g. x + y = z - if isinstance(left_expr, Var): - left_expr = left_expr + 0 - if isinstance(right_expr, Var): - right_expr = right_expr + 0 - - rhs = right_expr.constant - left_expr.constant - - lhs = {} - for x in left_expr.iter_variables(): - lhs[var_names[x]] = left_expr.get_coef(x) - for x in right_expr.iter_variables(): - lhs[var_names[x]] = lhs.get(var_names[x], 0.0) - right_expr.get_coef(x) - - if sense == sense.EQ: - quadratic_program.linear_constraint(lhs, "==", rhs, name) - elif sense == sense.GE: - quadratic_program.linear_constraint(lhs, ">=", rhs, name) - elif sense == sense.LE: - quadratic_program.linear_constraint(lhs, "<=", rhs, name) - else: - raise QiskitOptimizationError("Unsupported constraint sense: {}".format(constraint)) + # get linear constraints + for constraint in model.iter_linear_constraints(): + lhs, sense, rhs = _FromDocplexMp._linear_constraint(var_names, constraint) + quadratic_program.linear_constraint(lhs, sense, rhs, constraint.name) # get quadratic constraints for constraint in model.iter_quadratic_constraints(): - name = constraint.name - sense = constraint.sense - - left_expr = constraint.get_left_expr() - right_expr = constraint.get_right_expr() - - rhs = right_expr.constant - left_expr.constant - linear = {} - quadratic = {} - - if left_expr.is_quad_expr(): - for x in left_expr.linear_part.iter_variables(): - linear[var_names[x]] = left_expr.linear_part.get_coef(x) - for quad_triplet in left_expr.iter_quad_triplets(): - i = var_names[quad_triplet[0]] - j = var_names[quad_triplet[1]] - v = quad_triplet[2] - quadratic[i, j] = v - else: - for x in left_expr.iter_variables(): - linear[var_names[x]] = left_expr.get_coef(x) - - if right_expr.is_quad_expr(): - for x in right_expr.linear_part.iter_variables(): - linear[var_names[x]] = linear.get( - var_names[x], 0.0 - ) - right_expr.linear_part.get_coef(x) - for quad_triplet in right_expr.iter_quad_triplets(): - i = var_names[quad_triplet[0]] - j = var_names[quad_triplet[1]] - v = quad_triplet[2] - quadratic[i, j] = quadratic.get((i, j), 0.0) - v - else: - for x in right_expr.iter_variables(): - linear[var_names[x]] = linear.get(var_names[x], 0.0) - right_expr.get_coef(x) - - if sense == sense.EQ: - quadratic_program.quadratic_constraint(linear, quadratic, "==", rhs, name) - elif sense == sense.GE: - quadratic_program.quadratic_constraint(linear, quadratic, ">=", rhs, name) - elif sense == sense.LE: - quadratic_program.quadratic_constraint(linear, quadratic, "<=", rhs, name) - else: - raise QiskitOptimizationError("Unsupported constraint sense: {}".format(constraint)) + linear, quadratic, sense, rhs = _FromDocplexMp._quadratic_constraint(var_names, constraint) + quadratic_program.quadratic_constraint(linear, quadratic, sense, rhs, constraint.name) + + # get indicator constraints + for constraint in model.iter_indicator_constraints(): + linear_constraints = _FromDocplexMp._indicator_constraints( + var_names, var_bounds, constraint, indicator_big_m + ) + for linear, sense, rhs, name in linear_constraints: + quadratic_program.linear_constraint(linear, sense, rhs, name) return quadratic_program diff --git a/releasenotes/notes/add-indicator-constraint-071b54d9ae1b042f.yaml b/releasenotes/notes/add-indicator-constraint-071b54d9ae1b042f.yaml new file mode 100644 index 000000000..9ce6409b5 --- /dev/null +++ b/releasenotes/notes/add-indicator-constraint-071b54d9ae1b042f.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Adds the support of indicator constraints (e.g. x=1 -> y+z=1) in + :meth:`~qiskit_optimization.translator.from_docplex_mp` using the big-M formulation. diff --git a/test/problems/test_quadratic_program.py b/test/problems/test_quadratic_program.py index 9c4ad2078..f9afcc6e5 100644 --- a/test/problems/test_quadratic_program.py +++ b/test/problems/test_quadratic_program.py @@ -951,15 +951,6 @@ def test_docplex(self): warnings.simplefilter("ignore") q_p.from_docplex(mod) - with self.assertRaises(QiskitOptimizationError): - mod = Model() - x = mod.binary_var("x") - y = mod.binary_var("y") - mod.add_indicator(x, x + y <= 1, 1) - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - q_p.from_docplex(mod) - with self.assertRaises(QiskitOptimizationError): mod = Model() x = mod.binary_var("x") @@ -973,7 +964,7 @@ def test_docplex(self): mod = Model() x = mod.binary_var("x") y = mod.binary_var("y") - mod.add(mod.not_equal_constraint(x, y + 1)) + mod.add(x != y) with warnings.catch_warnings(): warnings.simplefilter("ignore") q_p.from_docplex(mod) diff --git a/test/translators/test_docplex_mp.py b/test/translators/test_docplex_mp.py index 66fea9cdc..bf22c9ea1 100644 --- a/test/translators/test_docplex_mp.py +++ b/test/translators/test_docplex_mp.py @@ -49,39 +49,8 @@ def test_from_and_to(self): mod.add(2 * x - z + 3 * y * z == 1, "q0") self.assertEqual(q_p.export_as_lp_string(), mod.export_as_lp_string()) - with self.assertRaises(QiskitOptimizationError): - mod = Model() - mod.semiinteger_var(lb=1, name="x") - _ = from_docplex_mp(mod) - - with self.assertRaises(QiskitOptimizationError): - mod = Model() - x = mod.binary_var("x") - mod.add_range(0, 2 * x, 1) - _ = from_docplex_mp(mod) - - with self.assertRaises(QiskitOptimizationError): - mod = Model() - x = mod.binary_var("x") - y = mod.binary_var("y") - mod.add_indicator(x, x + y <= 1, 1) - _ = from_docplex_mp(mod) - - with self.assertRaises(QiskitOptimizationError): - mod = Model() - x = mod.binary_var("x") - y = mod.binary_var("y") - mod.add_equivalence(x, x + y <= 1, 1) - _ = from_docplex_mp(mod) - - with self.assertRaises(QiskitOptimizationError): - mod = Model() - x = mod.binary_var("x") - y = mod.binary_var("y") - mod.add(mod.not_equal_constraint(x, y + 1)) - _ = from_docplex_mp(mod) - - # test from_docplex without explicit variable names + def test_from_without_variable_names(self): + """test from_docplex_mp without explicit variable names""" mod = Model() x = mod.binary_var() y = mod.continuous_var() @@ -106,3 +75,438 @@ def test_from_and_to(self): self.assertDictEqual(c.linear.to_dict(use_name=True), {"x2": -1}) self.assertDictEqual(c.quadratic.to_dict(use_name=True), {("x0", "x1"): 1}) self.assertEqual(c.sense, senses[i]) + + def test_unsupported_features(self): + """Test unsupported features""" + with self.subTest("semiinteget_var"), self.assertRaises(QiskitOptimizationError): + mod = Model() + mod.semiinteger_var(lb=1, name="x") + _ = from_docplex_mp(mod) + + with self.subTest("range constraint"), self.assertRaises(QiskitOptimizationError): + mod = Model() + x = mod.binary_var("x") + mod.add_range(0, 2 * x, 1) + _ = from_docplex_mp(mod) + + with self.subTest("equivalence constraint"), self.assertRaises(QiskitOptimizationError): + mod = Model() + x = mod.binary_var("x") + y = mod.binary_var("y") + mod.add_equivalence(x, x + y <= 1, 1) + _ = from_docplex_mp(mod) + + with self.subTest("not equal constraint"), self.assertRaises(QiskitOptimizationError): + mod = Model() + x = mod.binary_var("x") + y = mod.binary_var("y") + mod.add(x != y) + _ = from_docplex_mp(mod) + + with self.subTest("logical expression"), self.assertRaises(QiskitOptimizationError): + mod = Model() + x = mod.binary_var("x") + y = mod.binary_var("y") + mod.add(mod.logical_and(x, y)) + _ = from_docplex_mp(mod) + + with self.subTest("PWL constraint"), self.assertRaises(QiskitOptimizationError): + mod = Model() + x = mod.binary_var("x") + mod.add(mod.piecewise(-1, [(0, 0)], 1)(x) <= 1) + _ = from_docplex_mp(mod) + + with self.subTest("lazy constraint"), self.assertRaises(QiskitOptimizationError): + mod = Model() + x = mod.binary_var("x") + y = mod.binary_var("y") + mod.add_lazy_constraint(x + y <= 1) + _ = from_docplex_mp(mod) + + with self.subTest("user cut constraint"), self.assertRaises(QiskitOptimizationError): + mod = Model() + x = mod.binary_var("x") + y = mod.binary_var("y") + mod.add_user_cut_constraint(x + y <= 1) + _ = from_docplex_mp(mod) + + with self.subTest("sos1"), self.assertRaises(QiskitOptimizationError): + mod = Model() + x = mod.binary_var("x") + y = mod.binary_var("y") + mod.add_sos1([x, y]) + _ = from_docplex_mp(mod) + + with self.subTest("sos2"), self.assertRaises(QiskitOptimizationError): + mod = Model() + x = mod.binary_var("x") + y = mod.binary_var("y") + z = mod.binary_var("z") + mod.add_sos2([x, y, z]) + _ = from_docplex_mp(mod) + + def test_indicator_constraints(self): + """Test indicator constraints""" + with self.subTest("active 0, sense <="): + mod = Model() + x = mod.binary_var("x") + y = mod.integer_var(lb=-1, ub=2, name="y") + z = mod.continuous_var(lb=-1, ub=2, name="z") + mod.add_indicator(binary_var=x, active_value=0, linear_ct=(y + 2 * z <= 1), name="ind") + quad_prog = from_docplex_mp(mod) + self.assertEqual(quad_prog.get_num_linear_constraints(), 1) + ind = quad_prog.get_linear_constraint(0) + self.assertEqual(ind.name, "ind") + self.assertEqual(ind.sense, Constraint.Sense.LE) + self.assertDictEqual(ind.linear.to_dict(use_name=True), {"x": -5.0, "y": 1.0, "z": 2.0}) + self.assertEqual(ind.rhs, 1) + + with self.subTest("active 0, sense >="): + mod = Model() + x = mod.binary_var("x") + y = mod.integer_var(lb=-1, ub=2, name="y") + z = mod.continuous_var(lb=-1, ub=2, name="z") + mod.add_indicator(binary_var=x, active_value=0, linear_ct=(y + 2 * z >= 1), name="ind") + quad_prog = from_docplex_mp(mod) + self.assertEqual(quad_prog.get_num_linear_constraints(), 1) + ind = quad_prog.get_linear_constraint(0) + self.assertEqual(ind.name, "ind") + self.assertEqual(ind.sense, Constraint.Sense.GE) + self.assertDictEqual(ind.linear.to_dict(use_name=True), {"x": 4.0, "y": 1.0, "z": 2.0}) + self.assertEqual(ind.rhs, 1) + + with self.subTest("active 1, sense <="): + mod = Model() + x = mod.binary_var("x") + y = mod.integer_var(lb=-1, ub=2, name="y") + z = mod.continuous_var(lb=-1, ub=2, name="z") + mod.add_indicator(binary_var=x, active_value=1, linear_ct=(y + 2 * z <= 1), name="ind") + quad_prog = from_docplex_mp(mod) + self.assertEqual(quad_prog.get_num_linear_constraints(), 1) + ind = quad_prog.get_linear_constraint(0) + self.assertEqual(ind.name, "ind") + self.assertEqual(ind.sense, Constraint.Sense.LE) + self.assertDictEqual(ind.linear.to_dict(use_name=True), {"x": 5.0, "y": 1.0, "z": 2.0}) + self.assertEqual(ind.rhs, 6) + + with self.subTest("active 1, sense >="): + mod = Model() + x = mod.binary_var("x") + y = mod.integer_var(lb=-1, ub=2, name="y") + z = mod.continuous_var(lb=-1, ub=2, name="z") + mod.add_indicator(binary_var=x, active_value=1, linear_ct=(y + 2 * z >= 1), name="ind") + quad_prog = from_docplex_mp(mod) + self.assertEqual(quad_prog.get_num_linear_constraints(), 1) + ind = quad_prog.get_linear_constraint(0) + self.assertEqual(ind.name, "ind") + self.assertEqual(ind.sense, Constraint.Sense.GE) + self.assertDictEqual(ind.linear.to_dict(use_name=True), {"x": -4.0, "y": 1.0, "z": 2.0}) + self.assertEqual(ind.rhs, -3) + + with self.subTest("active 0, sense =="): + mod = Model() + x = mod.binary_var("x") + y = mod.integer_var(lb=-1, ub=2, name="y") + z = mod.continuous_var(lb=-1, ub=2, name="z") + mod.add_indicator(binary_var=x, active_value=0, linear_ct=(y + 2 * z == 1), name="ind") + quad_prog = from_docplex_mp(mod) + self.assertEqual(quad_prog.get_num_linear_constraints(), 2) + + ind = quad_prog.get_linear_constraint(0) + self.assertEqual(ind.name, "ind_LE") + self.assertEqual(ind.sense, Constraint.Sense.LE) + self.assertDictEqual(ind.linear.to_dict(use_name=True), {"x": -5.0, "y": 1.0, "z": 2.0}) + self.assertEqual(ind.rhs, 1) + + ind = quad_prog.get_linear_constraint(1) + self.assertEqual(ind.name, "ind_GE") + self.assertEqual(ind.sense, Constraint.Sense.GE) + self.assertDictEqual(ind.linear.to_dict(use_name=True), {"x": 4.0, "y": 1.0, "z": 2.0}) + self.assertEqual(ind.rhs, 1) + + with self.subTest("active 1, sense =="): + mod = Model() + x = mod.binary_var("x") + y = mod.integer_var(lb=-1, ub=2, name="y") + z = mod.continuous_var(lb=-1, ub=2, name="z") + mod.add_indicator(binary_var=x, active_value=1, linear_ct=(y + 2 * z == 1), name="ind") + quad_prog = from_docplex_mp(mod) + self.assertEqual(quad_prog.get_num_linear_constraints(), 2) + + ind = quad_prog.get_linear_constraint(0) + self.assertEqual(ind.name, "ind_LE") + self.assertEqual(ind.sense, Constraint.Sense.LE) + self.assertDictEqual(ind.linear.to_dict(use_name=True), {"x": 5.0, "y": 1.0, "z": 2.0}) + self.assertEqual(ind.rhs, 6) + + ind = quad_prog.get_linear_constraint(1) + self.assertEqual(ind.name, "ind_GE") + self.assertEqual(ind.sense, Constraint.Sense.GE) + self.assertDictEqual(ind.linear.to_dict(use_name=True), {"x": -4.0, "y": 1.0, "z": 2.0}) + self.assertEqual(ind.rhs, -3) + + with self.subTest("active 0, sense <=, indicator_big_m"): + mod = Model() + x = mod.binary_var("x") + y = mod.integer_var(lb=-1, ub=2, name="y") + z = mod.continuous_var(lb=-1, ub=2, name="z") + mod.add_indicator(binary_var=x, active_value=0, linear_ct=(y + 2 * z <= 1), name="ind") + quad_prog = from_docplex_mp(mod, indicator_big_m=100) + self.assertEqual(quad_prog.get_num_linear_constraints(), 1) + ind = quad_prog.get_linear_constraint(0) + self.assertEqual(ind.name, "ind") + self.assertEqual(ind.sense, Constraint.Sense.LE) + self.assertDictEqual( + ind.linear.to_dict(use_name=True), {"x": -100.0, "y": 1.0, "z": 2.0} + ) + self.assertEqual(ind.rhs, 1) + + with self.subTest("active 0, sense >=, indicator_big_m"): + mod = Model() + x = mod.binary_var("x") + y = mod.integer_var(lb=-1, ub=2, name="y") + z = mod.continuous_var(lb=-1, ub=2, name="z") + mod.add_indicator(binary_var=x, active_value=0, linear_ct=(y + 2 * z >= 1), name="ind") + quad_prog = from_docplex_mp(mod, indicator_big_m=100) + self.assertEqual(quad_prog.get_num_linear_constraints(), 1) + ind = quad_prog.get_linear_constraint(0) + self.assertEqual(ind.name, "ind") + self.assertEqual(ind.sense, Constraint.Sense.GE) + self.assertDictEqual( + ind.linear.to_dict(use_name=True), {"x": 100.0, "y": 1.0, "z": 2.0} + ) + self.assertEqual(ind.rhs, 1) + + with self.subTest("active 1, sense <=, indicator_big_m"): + mod = Model() + x = mod.binary_var("x") + y = mod.integer_var(lb=-1, ub=2, name="y") + z = mod.continuous_var(lb=-1, ub=2, name="z") + mod.add_indicator(binary_var=x, active_value=1, linear_ct=(y + 2 * z <= 1), name="ind") + quad_prog = from_docplex_mp(mod, indicator_big_m=100) + self.assertEqual(quad_prog.get_num_linear_constraints(), 1) + ind = quad_prog.get_linear_constraint(0) + self.assertEqual(ind.name, "ind") + self.assertEqual(ind.sense, Constraint.Sense.LE) + self.assertDictEqual( + ind.linear.to_dict(use_name=True), {"x": 100.0, "y": 1.0, "z": 2.0} + ) + self.assertEqual(ind.rhs, 101) + + with self.subTest("active 1, sense >=, indicator_big_m"): + mod = Model() + x = mod.binary_var("x") + y = mod.integer_var(lb=-1, ub=2, name="y") + z = mod.continuous_var(lb=-1, ub=2, name="z") + mod.add_indicator(binary_var=x, active_value=1, linear_ct=(y + 2 * z >= 1), name="ind") + quad_prog = from_docplex_mp(mod, indicator_big_m=100) + self.assertEqual(quad_prog.get_num_linear_constraints(), 1) + ind = quad_prog.get_linear_constraint(0) + self.assertEqual(ind.name, "ind") + self.assertEqual(ind.sense, Constraint.Sense.GE) + self.assertDictEqual( + ind.linear.to_dict(use_name=True), {"x": -100.0, "y": 1.0, "z": 2.0} + ) + self.assertEqual(ind.rhs, -99) + + with self.subTest("active 0, sense ==, indicator_big_m"): + mod = Model() + x = mod.binary_var("x") + y = mod.integer_var(lb=-1, ub=2, name="y") + z = mod.continuous_var(lb=-1, ub=2, name="z") + mod.add_indicator(binary_var=x, active_value=0, linear_ct=(y + 2 * z == 1), name="ind") + quad_prog = from_docplex_mp(mod, indicator_big_m=100) + self.assertEqual(quad_prog.get_num_linear_constraints(), 2) + + ind = quad_prog.get_linear_constraint(0) + self.assertEqual(ind.name, "ind_LE") + self.assertEqual(ind.sense, Constraint.Sense.LE) + self.assertDictEqual( + ind.linear.to_dict(use_name=True), {"x": -100.0, "y": 1.0, "z": 2.0} + ) + self.assertEqual(ind.rhs, 1) + + ind = quad_prog.get_linear_constraint(1) + self.assertEqual(ind.name, "ind_GE") + self.assertEqual(ind.sense, Constraint.Sense.GE) + self.assertDictEqual( + ind.linear.to_dict(use_name=True), {"x": 100.0, "y": 1.0, "z": 2.0} + ) + self.assertEqual(ind.rhs, 1) + + with self.subTest("active 1, sense ==, indicator_big_m"): + mod = Model() + x = mod.binary_var("x") + y = mod.integer_var(lb=-1, ub=2, name="y") + z = mod.continuous_var(lb=-1, ub=2, name="z") + mod.add_indicator(binary_var=x, active_value=1, linear_ct=(y + 2 * z == 1), name="ind") + quad_prog = from_docplex_mp(mod, indicator_big_m=100) + self.assertEqual(quad_prog.get_num_linear_constraints(), 2) + + ind = quad_prog.get_linear_constraint(0) + self.assertEqual(ind.name, "ind_LE") + self.assertEqual(ind.sense, Constraint.Sense.LE) + self.assertDictEqual( + ind.linear.to_dict(use_name=True), {"x": 100.0, "y": 1.0, "z": 2.0} + ) + self.assertEqual(ind.rhs, 101) + + ind = quad_prog.get_linear_constraint(1) + self.assertEqual(ind.name, "ind_GE") + self.assertEqual(ind.sense, Constraint.Sense.GE) + self.assertDictEqual( + ind.linear.to_dict(use_name=True), {"x": -100.0, "y": 1.0, "z": 2.0} + ) + self.assertEqual(ind.rhs, -99) + + with self.subTest("active 0, sense <=, obvious bound"): + mod = Model() + x = mod.binary_var("x") + y = mod.integer_var(lb=-1, ub=2, name="y") + z = mod.continuous_var(lb=-1, ub=2, name="z") + mod.add_indicator(binary_var=x, active_value=0, linear_ct=(y + 2 * z <= 10), name="ind") + quad_prog = from_docplex_mp(mod) + self.assertEqual(quad_prog.get_num_linear_constraints(), 1) + ind = quad_prog.get_linear_constraint(0) + self.assertEqual(ind.name, "ind") + self.assertEqual(ind.sense, Constraint.Sense.LE) + self.assertDictEqual(ind.linear.to_dict(use_name=True), {"y": 1.0, "z": 2.0}) + self.assertEqual(ind.rhs, 10) + + with self.subTest("active 0, sense >=, obvious bound"): + mod = Model() + x = mod.binary_var("x") + y = mod.integer_var(lb=-1, ub=2, name="y") + z = mod.continuous_var(lb=-1, ub=2, name="z") + mod.add_indicator( + binary_var=x, active_value=0, linear_ct=(y + 2 * z >= -10), name="ind" + ) + quad_prog = from_docplex_mp(mod) + self.assertEqual(quad_prog.get_num_linear_constraints(), 1) + ind = quad_prog.get_linear_constraint(0) + self.assertEqual(ind.name, "ind") + self.assertEqual(ind.sense, Constraint.Sense.GE) + self.assertDictEqual(ind.linear.to_dict(use_name=True), {"y": 1.0, "z": 2.0}) + self.assertEqual(ind.rhs, -10) + + with self.subTest("active 1, sense <=, obvious bound"): + mod = Model() + x = mod.binary_var("x") + y = mod.integer_var(lb=-1, ub=2, name="y") + z = mod.continuous_var(lb=-1, ub=2, name="z") + mod.add_indicator(binary_var=x, active_value=1, linear_ct=(y + 2 * z <= 10), name="ind") + quad_prog = from_docplex_mp(mod) + self.assertEqual(quad_prog.get_num_linear_constraints(), 1) + ind = quad_prog.get_linear_constraint(0) + self.assertEqual(ind.name, "ind") + self.assertEqual(ind.sense, Constraint.Sense.LE) + self.assertDictEqual(ind.linear.to_dict(use_name=True), {"y": 1.0, "z": 2.0}) + self.assertEqual(ind.rhs, 10) + + with self.subTest("active 1, sense >=, obvious bound"): + mod = Model() + x = mod.binary_var("x") + y = mod.integer_var(lb=-1, ub=2, name="y") + z = mod.continuous_var(lb=-1, ub=2, name="z") + mod.add_indicator( + binary_var=x, active_value=1, linear_ct=(y + 2 * z >= -10), name="ind" + ) + quad_prog = from_docplex_mp(mod) + self.assertEqual(quad_prog.get_num_linear_constraints(), 1) + ind = quad_prog.get_linear_constraint(0) + self.assertEqual(ind.name, "ind") + self.assertEqual(ind.sense, Constraint.Sense.GE) + self.assertDictEqual(ind.linear.to_dict(use_name=True), {"y": 1.0, "z": 2.0}) + self.assertEqual(ind.rhs, -10) + + with self.subTest("active 0, sense ==, too small rhs"): + mod = Model() + x = mod.binary_var("x") + y = mod.integer_var(lb=-1, ub=2, name="y") + z = mod.continuous_var(lb=-1, ub=2, name="z") + mod.add_indicator( + binary_var=x, active_value=0, linear_ct=(y + 2 * z == -10), name="ind" + ) + quad_prog = from_docplex_mp(mod) + self.assertEqual(quad_prog.get_num_linear_constraints(), 2) + + ind = quad_prog.get_linear_constraint(0) + self.assertEqual(ind.name, "ind_LE") + self.assertEqual(ind.sense, Constraint.Sense.LE) + self.assertDictEqual( + ind.linear.to_dict(use_name=True), {"x": -16.0, "y": 1.0, "z": 2.0} + ) + self.assertEqual(ind.rhs, -10) + + ind = quad_prog.get_linear_constraint(1) + self.assertEqual(ind.name, "ind_GE") + self.assertEqual(ind.sense, Constraint.Sense.GE) + self.assertDictEqual(ind.linear.to_dict(use_name=True), {"y": 1.0, "z": 2.0}) + self.assertEqual(ind.rhs, -10) + + with self.subTest("active 0, sense ==, too large rhs"): + mod = Model() + x = mod.binary_var("x") + y = mod.integer_var(lb=-1, ub=2, name="y") + z = mod.continuous_var(lb=-1, ub=2, name="z") + mod.add_indicator(binary_var=x, active_value=0, linear_ct=(y + 2 * z == 10), name="ind") + quad_prog = from_docplex_mp(mod) + self.assertEqual(quad_prog.get_num_linear_constraints(), 2) + + ind = quad_prog.get_linear_constraint(0) + self.assertEqual(ind.name, "ind_LE") + self.assertEqual(ind.sense, Constraint.Sense.LE) + self.assertDictEqual(ind.linear.to_dict(use_name=True), {"y": 1.0, "z": 2.0}) + self.assertEqual(ind.rhs, 10) + + ind = quad_prog.get_linear_constraint(1) + self.assertEqual(ind.name, "ind_GE") + self.assertEqual(ind.sense, Constraint.Sense.GE) + self.assertDictEqual(ind.linear.to_dict(use_name=True), {"x": 13, "y": 1.0, "z": 2.0}) + self.assertEqual(ind.rhs, 10) + + with self.subTest("active 1, sense ==, too small rhs"): + mod = Model() + x = mod.binary_var("x") + y = mod.integer_var(lb=-1, ub=2, name="y") + z = mod.continuous_var(lb=-1, ub=2, name="z") + mod.add_indicator( + binary_var=x, active_value=1, linear_ct=(y + 2 * z == -10), name="ind" + ) + quad_prog = from_docplex_mp(mod) + self.assertEqual(quad_prog.get_num_linear_constraints(), 2) + + ind = quad_prog.get_linear_constraint(0) + self.assertEqual(ind.name, "ind_LE") + self.assertEqual(ind.sense, Constraint.Sense.LE) + self.assertDictEqual(ind.linear.to_dict(use_name=True), {"x": 16.0, "y": 1.0, "z": 2.0}) + self.assertEqual(ind.rhs, 6) + + ind = quad_prog.get_linear_constraint(1) + self.assertEqual(ind.name, "ind_GE") + self.assertEqual(ind.sense, Constraint.Sense.GE) + self.assertDictEqual(ind.linear.to_dict(use_name=True), {"y": 1.0, "z": 2.0}) + self.assertEqual(ind.rhs, -10) + + with self.subTest("active 1, sense ==, too large rhs"): + mod = Model() + x = mod.binary_var("x") + y = mod.integer_var(lb=-1, ub=2, name="y") + z = mod.continuous_var(lb=-1, ub=2, name="z") + mod.add_indicator(binary_var=x, active_value=1, linear_ct=(y + 2 * z == 10), name="ind") + quad_prog = from_docplex_mp(mod) + self.assertEqual(quad_prog.get_num_linear_constraints(), 2) + + ind = quad_prog.get_linear_constraint(0) + self.assertEqual(ind.name, "ind_LE") + self.assertEqual(ind.sense, Constraint.Sense.LE) + self.assertDictEqual(ind.linear.to_dict(use_name=True), {"y": 1.0, "z": 2.0}) + self.assertEqual(ind.rhs, 10) + + ind = quad_prog.get_linear_constraint(1) + self.assertEqual(ind.name, "ind_GE") + self.assertEqual(ind.sense, Constraint.Sense.GE) + self.assertDictEqual( + ind.linear.to_dict(use_name=True), {"x": -13.0, "y": 1.0, "z": 2.0} + ) + self.assertEqual(ind.rhs, -3)