ortools.math_opt.python.model
A solver independent library for modeling optimization problems.
Example use to model the optimization problem:
max 2.0 * x + y s.t. x + y <= 1.5 x in {0.0, 1.0} y in [0.0, 2.5]
model = mathopt.Model(name='my_model') x = model.add_binary_variable(name='x') y = model.add_variable(lb=0.0, ub=2.5, name='y')
We can directly use linear combinations of variables ...
model.add_linear_constraint(x + y <= 1.5, name='c')
... or build them incrementally.
objective_expression = 0 objective_expression += 2 * x objective_expression += y model.maximize(objective_expression)
May raise a RuntimeError on invalid input or internal solver errors.
result = mathopt.solve(model, mathopt.SolverType.GSCIP)
if result.termination.reason not in (mathopt.TerminationReason.OPTIMAL, mathopt.TerminationReason.FEASIBLE): raise RuntimeError(f'model failed to solve: {result.termination}')
print(f'Objective value: {result.objective_value()}') print(f'Value for variable x: {result.variable_values()[x]}')
1#!/usr/bin/env python3 2# Copyright 2010-2025 Google LLC 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15"""A solver independent library for modeling optimization problems. 16 17Example use to model the optimization problem: 18 max 2.0 * x + y 19 s.t. x + y <= 1.5 20 x in {0.0, 1.0} 21 y in [0.0, 2.5] 22 23 model = mathopt.Model(name='my_model') 24 x = model.add_binary_variable(name='x') 25 y = model.add_variable(lb=0.0, ub=2.5, name='y') 26 # We can directly use linear combinations of variables ... 27 model.add_linear_constraint(x + y <= 1.5, name='c') 28 # ... or build them incrementally. 29 objective_expression = 0 30 objective_expression += 2 * x 31 objective_expression += y 32 model.maximize(objective_expression) 33 34 # May raise a RuntimeError on invalid input or internal solver errors. 35 result = mathopt.solve(model, mathopt.SolverType.GSCIP) 36 37 if result.termination.reason not in (mathopt.TerminationReason.OPTIMAL, 38 mathopt.TerminationReason.FEASIBLE): 39 raise RuntimeError(f'model failed to solve: {result.termination}') 40 41 print(f'Objective value: {result.objective_value()}') 42 print(f'Value for variable x: {result.variable_values()[x]}') 43""" 44 45import math 46from typing import Iterator, Optional, Tuple, Union 47 48# typing.Self is only in python 3.11+, for OR-tools supports down to 3.8. 49from typing_extensions import Self 50 51from ortools.math_opt import model_pb2 52from ortools.math_opt import model_update_pb2 53from ortools.math_opt.elemental.python import cpp_elemental 54from ortools.math_opt.elemental.python import enums 55from ortools.math_opt.python import from_model 56from ortools.math_opt.python import indicator_constraints 57from ortools.math_opt.python import linear_constraints as linear_constraints_mod 58from ortools.math_opt.python import normalized_inequality 59from ortools.math_opt.python import objectives 60from ortools.math_opt.python import quadratic_constraints 61from ortools.math_opt.python import variables as variables_mod 62from ortools.math_opt.python.elemental import elemental 63 64 65class UpdateTracker: 66 """Tracks updates to an optimization model from a ModelStorage. 67 68 Do not instantiate directly, instead create through 69 ModelStorage.add_update_tracker(). 70 71 Querying an UpdateTracker after calling Model.remove_update_tracker will 72 result in a model_storage.UsedUpdateTrackerAfterRemovalError. 73 74 Example: 75 mod = Model() 76 x = mod.add_variable(0.0, 1.0, True, 'x') 77 y = mod.add_variable(0.0, 1.0, True, 'y') 78 tracker = mod.add_update_tracker() 79 mod.set_variable_ub(x, 3.0) 80 tracker.export_update() 81 => "variable_updates: {upper_bounds: {ids: [0], values[3.0] }" 82 mod.set_variable_ub(y, 2.0) 83 tracker.export_update() 84 => "variable_updates: {upper_bounds: {ids: [0, 1], values[3.0, 2.0] }" 85 tracker.advance_checkpoint() 86 tracker.export_update() 87 => None 88 mod.set_variable_ub(y, 4.0) 89 tracker.export_update() 90 => "variable_updates: {upper_bounds: {ids: [1], values[4.0] }" 91 tracker.advance_checkpoint() 92 mod.remove_update_tracker(tracker) 93 """ 94 95 def __init__( 96 self, 97 diff_id: int, 98 elem: elemental.Elemental, 99 ): 100 """Do not invoke directly, use Model.add_update_tracker() instead.""" 101 self._diff_id = diff_id 102 self._elemental = elem 103 104 def export_update( 105 self, *, remove_names: bool = False 106 ) -> Optional[model_update_pb2.ModelUpdateProto]: 107 """Returns changes to the model since last call to checkpoint/creation.""" 108 return self._elemental.export_model_update( 109 self._diff_id, remove_names=remove_names 110 ) 111 112 def advance_checkpoint(self) -> None: 113 """Track changes to the model only after this function call.""" 114 return self._elemental.advance_diff(self._diff_id) 115 116 @property 117 def diff_id(self) -> int: 118 return self._diff_id 119 120 121class Model: 122 """An optimization model. 123 124 The objective function of the model can be linear or quadratic, and some 125 solvers can only handle linear objective functions. For this reason Model has 126 three versions of all objective setting functions: 127 * A generic one (e.g. maximize()), which accepts linear or quadratic 128 expressions, 129 * a quadratic version (e.g. maximize_quadratic_objective()), which also 130 accepts linear or quadratic expressions and can be used to signal a 131 quadratic objective is possible, and 132 * a linear version (e.g. maximize_linear_objective()), which only accepts 133 linear expressions and can be used to avoid solve time errors for solvers 134 that do not accept quadratic objectives. 135 136 Attributes: 137 name: A description of the problem, can be empty. 138 objective: A function to maximize or minimize. 139 storage: Implementation detail, do not access directly. 140 _variable_ids: Maps variable ids to Variable objects. 141 _linear_constraint_ids: Maps linear constraint ids to LinearConstraint 142 objects. 143 """ 144 145 __slots__ = ("_elemental",) 146 147 def __init__( 148 self, 149 *, 150 name: str = "", # TODO(b/371236599): rename to model_name 151 primary_objective_name: str = "", 152 ) -> None: 153 self._elemental: elemental.Elemental = cpp_elemental.CppElemental( 154 model_name=name, primary_objective_name=primary_objective_name 155 ) 156 157 @property 158 def name(self) -> str: 159 return self._elemental.model_name 160 161 ############################################################################## 162 # Variables 163 ############################################################################## 164 165 def add_variable( 166 self, 167 *, 168 lb: float = -math.inf, 169 ub: float = math.inf, 170 is_integer: bool = False, 171 name: str = "", 172 ) -> variables_mod.Variable: 173 """Adds a decision variable to the optimization model. 174 175 Args: 176 lb: The new variable must take at least this value (a lower bound). 177 ub: The new variable must be at most this value (an upper bound). 178 is_integer: Indicates if the variable can only take integer values 179 (otherwise, the variable can take any continuous value). 180 name: For debugging purposes only, but nonempty names must be distinct. 181 182 Returns: 183 A reference to the new decision variable. 184 """ 185 186 variable_id = self._elemental.add_element(enums.ElementType.VARIABLE, name) 187 result = variables_mod.Variable(self._elemental, variable_id) 188 result.lower_bound = lb 189 result.upper_bound = ub 190 result.integer = is_integer 191 return result 192 193 def add_integer_variable( 194 self, *, lb: float = -math.inf, ub: float = math.inf, name: str = "" 195 ) -> variables_mod.Variable: 196 return self.add_variable(lb=lb, ub=ub, is_integer=True, name=name) 197 198 def add_binary_variable(self, *, name: str = "") -> variables_mod.Variable: 199 return self.add_variable(lb=0.0, ub=1.0, is_integer=True, name=name) 200 201 def get_variable( 202 self, var_id: int, *, validate: bool = True 203 ) -> variables_mod.Variable: 204 """Returns the Variable for the id var_id, or raises KeyError.""" 205 if validate and not self._elemental.element_exists( 206 enums.ElementType.VARIABLE, var_id 207 ): 208 raise KeyError(f"Variable does not exist with id {var_id}.") 209 return variables_mod.Variable(self._elemental, var_id) 210 211 def has_variable(self, var_id: int) -> bool: 212 """Returns true if a Variable with this id is in the model.""" 213 return self._elemental.element_exists(enums.ElementType.VARIABLE, var_id) 214 215 def get_num_variables(self) -> int: 216 """Returns the number of variables in the model.""" 217 return self._elemental.get_num_elements(enums.ElementType.VARIABLE) 218 219 def get_next_variable_id(self) -> int: 220 """Returns the id of the next variable created in the model.""" 221 return self._elemental.get_next_element_id(enums.ElementType.VARIABLE) 222 223 def ensure_next_variable_id_at_least(self, var_id: int) -> None: 224 """If the next variable id would be less than `var_id`, sets it to `var_id`.""" 225 self._elemental.ensure_next_element_id_at_least( 226 enums.ElementType.VARIABLE, var_id 227 ) 228 229 def delete_variable(self, var: variables_mod.Variable) -> None: 230 """Removes this variable from the model.""" 231 self.check_compatible(var) 232 if not self._elemental.delete_element(enums.ElementType.VARIABLE, var.id): 233 raise ValueError(f"Variable with id {var.id} was not in the model.") 234 235 def variables(self) -> Iterator[variables_mod.Variable]: 236 """Yields the variables in the order of creation.""" 237 var_ids = self._elemental.get_elements(enums.ElementType.VARIABLE) 238 var_ids.sort() 239 for var_id in var_ids: 240 yield variables_mod.Variable(self._elemental, int(var_id)) 241 242 ############################################################################## 243 # Objective 244 ############################################################################## 245 246 @property 247 def objective(self) -> objectives.Objective: 248 return objectives.PrimaryObjective(self._elemental) 249 250 def maximize(self, obj: variables_mod.QuadraticTypes) -> None: 251 """Sets the objective to maximize the provided expression `obj`.""" 252 self.set_objective(obj, is_maximize=True) 253 254 def maximize_linear_objective(self, obj: variables_mod.LinearTypes) -> None: 255 """Sets the objective to maximize the provided linear expression `obj`.""" 256 self.set_linear_objective(obj, is_maximize=True) 257 258 def maximize_quadratic_objective(self, obj: variables_mod.QuadraticTypes) -> None: 259 """Sets the objective to maximize the provided quadratic expression `obj`.""" 260 self.set_quadratic_objective(obj, is_maximize=True) 261 262 def minimize(self, obj: variables_mod.QuadraticTypes) -> None: 263 """Sets the objective to minimize the provided expression `obj`.""" 264 self.set_objective(obj, is_maximize=False) 265 266 def minimize_linear_objective(self, obj: variables_mod.LinearTypes) -> None: 267 """Sets the objective to minimize the provided linear expression `obj`.""" 268 self.set_linear_objective(obj, is_maximize=False) 269 270 def minimize_quadratic_objective(self, obj: variables_mod.QuadraticTypes) -> None: 271 """Sets the objective to minimize the provided quadratic expression `obj`.""" 272 self.set_quadratic_objective(obj, is_maximize=False) 273 274 def set_objective( 275 self, obj: variables_mod.QuadraticTypes, *, is_maximize: bool 276 ) -> None: 277 """Sets the objective to optimize the provided expression `obj`.""" 278 self.objective.set_to_expression(obj) 279 self.objective.is_maximize = is_maximize 280 281 def set_linear_objective( 282 self, obj: variables_mod.LinearTypes, *, is_maximize: bool 283 ) -> None: 284 """Sets the objective to optimize the provided linear expression `obj`.""" 285 self.objective.set_to_linear_expression(obj) 286 self.objective.is_maximize = is_maximize 287 288 def set_quadratic_objective( 289 self, obj: variables_mod.QuadraticTypes, *, is_maximize: bool 290 ) -> None: 291 """Sets the objective to optimize the provided quadratic expression `obj`.""" 292 self.objective.set_to_quadratic_expression(obj) 293 self.objective.is_maximize = is_maximize 294 295 def linear_objective_terms(self) -> Iterator[variables_mod.LinearTerm]: 296 """Yields variable coefficient pairs for variables with nonzero objective coefficient in undefined order.""" 297 yield from self.objective.linear_terms() 298 299 def quadratic_objective_terms(self) -> Iterator[variables_mod.QuadraticTerm]: 300 """Yields the quadratic terms with nonzero objective coefficient in undefined order.""" 301 yield from self.objective.quadratic_terms() 302 303 ############################################################################## 304 # Auxiliary Objectives 305 ############################################################################## 306 307 def add_auxiliary_objective( 308 self, 309 *, 310 priority: int, 311 name: str = "", 312 expr: Optional[variables_mod.LinearTypes] = None, 313 is_maximize: bool = False, 314 ) -> objectives.AuxiliaryObjective: 315 """Adds an additional objective to the model.""" 316 obj_id = self._elemental.add_element( 317 enums.ElementType.AUXILIARY_OBJECTIVE, name 318 ) 319 self._elemental.set_attr( 320 enums.IntAttr1.AUXILIARY_OBJECTIVE_PRIORITY, (obj_id,), priority 321 ) 322 result = objectives.AuxiliaryObjective(self._elemental, obj_id) 323 if expr is not None: 324 result.set_to_linear_expression(expr) 325 result.is_maximize = is_maximize 326 return result 327 328 def add_maximization_objective( 329 self, expr: variables_mod.LinearTypes, *, priority: int, name: str = "" 330 ) -> objectives.AuxiliaryObjective: 331 """Adds an additional objective to the model that is maximizaition.""" 332 result = self.add_auxiliary_objective( 333 priority=priority, name=name, expr=expr, is_maximize=True 334 ) 335 return result 336 337 def add_minimization_objective( 338 self, expr: variables_mod.LinearTypes, *, priority: int, name: str = "" 339 ) -> objectives.AuxiliaryObjective: 340 """Adds an additional objective to the model that is minimizaition.""" 341 result = self.add_auxiliary_objective( 342 priority=priority, name=name, expr=expr, is_maximize=False 343 ) 344 return result 345 346 def delete_auxiliary_objective(self, obj: objectives.AuxiliaryObjective) -> None: 347 """Removes an auxiliary objective from the model.""" 348 self.check_compatible(obj) 349 if not self._elemental.delete_element( 350 enums.ElementType.AUXILIARY_OBJECTIVE, obj.id 351 ): 352 raise ValueError( 353 f"Auxiliary objective with id {obj.id} is not in the model." 354 ) 355 356 def has_auxiliary_objective(self, obj_id: int) -> bool: 357 """Returns true if the model has an auxiliary objective with id `obj_id`.""" 358 return self._elemental.element_exists( 359 enums.ElementType.AUXILIARY_OBJECTIVE, obj_id 360 ) 361 362 def next_auxiliary_objective_id(self) -> int: 363 """Returns the id of the next auxiliary objective added to the model.""" 364 return self._elemental.get_next_element_id( 365 enums.ElementType.AUXILIARY_OBJECTIVE 366 ) 367 368 def num_auxiliary_objectives(self) -> int: 369 """Returns the number of auxiliary objectives in this model.""" 370 return self._elemental.get_num_elements(enums.ElementType.AUXILIARY_OBJECTIVE) 371 372 def ensure_next_auxiliary_objective_id_at_least(self, obj_id: int) -> None: 373 """If the next auxiliary objective id would be less than `obj_id`, sets it to `obj_id`.""" 374 self._elemental.ensure_next_element_id_at_least( 375 enums.ElementType.AUXILIARY_OBJECTIVE, obj_id 376 ) 377 378 def get_auxiliary_objective( 379 self, obj_id: int, *, validate: bool = True 380 ) -> objectives.AuxiliaryObjective: 381 """Returns the auxiliary objective with this id. 382 383 If there is no objective with this id, an exception is thrown if validate is 384 true, and an invalid AuxiliaryObjective is returned if validate is false 385 (later interactions with this object will cause unpredictable errors). Only 386 set validate=False if there is a known performance problem. 387 388 Args: 389 obj_id: The id of the auxiliary objective to look for. 390 validate: Set to false for more speed, but fails to raise an exception if 391 the objective is missing. 392 393 Raises: 394 KeyError: If `validate` is True and there is no objective with this id. 395 """ 396 if validate and not self.has_auxiliary_objective(obj_id): 397 raise KeyError(f"Model has no auxiliary objective with id {obj_id}") 398 return objectives.AuxiliaryObjective(self._elemental, obj_id) 399 400 def auxiliary_objectives(self) -> Iterator[objectives.AuxiliaryObjective]: 401 """Returns the auxiliary objectives in the model in the order of creation.""" 402 ids = self._elemental.get_elements(enums.ElementType.AUXILIARY_OBJECTIVE) 403 ids.sort() 404 for aux_obj_id in ids: 405 yield objectives.AuxiliaryObjective(self._elemental, int(aux_obj_id)) 406 407 ############################################################################## 408 # Linear Constraints 409 ############################################################################## 410 411 # TODO(b/227214976): Update the note below and link to pytype bug number. 412 # Note: bounded_expr's type includes bool only as a workaround to a pytype 413 # issue. Passing a bool for bounded_expr will raise an error in runtime. 414 def add_linear_constraint( 415 self, 416 bounded_expr: Optional[Union[bool, variables_mod.BoundedLinearTypes]] = None, 417 *, 418 lb: Optional[float] = None, 419 ub: Optional[float] = None, 420 expr: Optional[variables_mod.LinearTypes] = None, 421 name: str = "", 422 ) -> linear_constraints_mod.LinearConstraint: 423 """Adds a linear constraint to the optimization model. 424 425 The simplest way to specify the constraint is by passing a one-sided or 426 two-sided linear inequality as in: 427 * add_linear_constraint(x + y + 1.0 <= 2.0), 428 * add_linear_constraint(x + y >= 2.0), or 429 * add_linear_constraint((1.0 <= x + y) <= 2.0). 430 431 Note the extra parenthesis for two-sided linear inequalities, which is 432 required due to some language limitations (see 433 https://peps.python.org/pep-0335/ and https://peps.python.org/pep-0535/). 434 If the parenthesis are omitted, a TypeError will be raised explaining the 435 issue (if this error was not raised the first inequality would have been 436 silently ignored because of the noted language limitations). 437 438 The second way to specify the constraint is by setting lb, ub, and/or expr 439 as in: 440 * add_linear_constraint(expr=x + y + 1.0, ub=2.0), 441 * add_linear_constraint(expr=x + y, lb=2.0), 442 * add_linear_constraint(expr=x + y, lb=1.0, ub=2.0), or 443 * add_linear_constraint(lb=1.0). 444 Omitting lb is equivalent to setting it to -math.inf and omiting ub is 445 equivalent to setting it to math.inf. 446 447 These two alternatives are exclusive and a combined call like: 448 * add_linear_constraint(x + y <= 2.0, lb=1.0), or 449 * add_linear_constraint(x + y <= 2.0, ub=math.inf) 450 will raise a ValueError. A ValueError is also raised if expr's offset is 451 infinite. 452 453 Args: 454 bounded_expr: a linear inequality describing the constraint. Cannot be 455 specified together with lb, ub, or expr. 456 lb: The constraint's lower bound if bounded_expr is omitted (if both 457 bounder_expr and lb are omitted, the lower bound is -math.inf). 458 ub: The constraint's upper bound if bounded_expr is omitted (if both 459 bounder_expr and ub are omitted, the upper bound is math.inf). 460 expr: The constraint's linear expression if bounded_expr is omitted. 461 name: For debugging purposes only, but nonempty names must be distinct. 462 463 Returns: 464 A reference to the new linear constraint. 465 """ 466 norm_ineq = normalized_inequality.as_normalized_linear_inequality( 467 bounded_expr, lb=lb, ub=ub, expr=expr 468 ) 469 lin_con_id = self._elemental.add_element( 470 enums.ElementType.LINEAR_CONSTRAINT, name 471 ) 472 473 result = linear_constraints_mod.LinearConstraint(self._elemental, lin_con_id) 474 result.lower_bound = norm_ineq.lb 475 result.upper_bound = norm_ineq.ub 476 for var, coefficient in norm_ineq.coefficients.items(): 477 result.set_coefficient(var, coefficient) 478 return result 479 480 def has_linear_constraint(self, con_id: int) -> bool: 481 """Returns true if a linear constraint with this id is in the model.""" 482 return self._elemental.element_exists( 483 enums.ElementType.LINEAR_CONSTRAINT, con_id 484 ) 485 486 def get_num_linear_constraints(self) -> int: 487 """Returns the number of linear constraints in the model.""" 488 return self._elemental.get_num_elements(enums.ElementType.LINEAR_CONSTRAINT) 489 490 def get_next_linear_constraint_id(self) -> int: 491 """Returns the id of the next linear constraint created in the model.""" 492 return self._elemental.get_next_element_id(enums.ElementType.LINEAR_CONSTRAINT) 493 494 def ensure_next_linear_constraint_id_at_least(self, con_id: int) -> None: 495 """If the next linear constraint id would be less than `con_id`, sets it to `con_id`.""" 496 self._elemental.ensure_next_element_id_at_least( 497 enums.ElementType.LINEAR_CONSTRAINT, con_id 498 ) 499 500 def get_linear_constraint( 501 self, con_id: int, *, validate: bool = True 502 ) -> linear_constraints_mod.LinearConstraint: 503 """Returns the LinearConstraint for the id con_id.""" 504 if validate and not self._elemental.element_exists( 505 enums.ElementType.LINEAR_CONSTRAINT, con_id 506 ): 507 raise KeyError(f"Linear constraint does not exist with id {con_id}.") 508 return linear_constraints_mod.LinearConstraint(self._elemental, con_id) 509 510 def delete_linear_constraint( 511 self, lin_con: linear_constraints_mod.LinearConstraint 512 ) -> None: 513 self.check_compatible(lin_con) 514 if not self._elemental.delete_element( 515 enums.ElementType.LINEAR_CONSTRAINT, lin_con.id 516 ): 517 raise ValueError( 518 f"Linear constraint with id {lin_con.id} was not in the model." 519 ) 520 521 def linear_constraints( 522 self, 523 ) -> Iterator[linear_constraints_mod.LinearConstraint]: 524 """Yields the linear constraints in the order of creation.""" 525 lin_con_ids = self._elemental.get_elements(enums.ElementType.LINEAR_CONSTRAINT) 526 lin_con_ids.sort() 527 for lin_con_id in lin_con_ids: 528 yield linear_constraints_mod.LinearConstraint( 529 self._elemental, int(lin_con_id) 530 ) 531 532 def row_nonzeros( 533 self, lin_con: linear_constraints_mod.LinearConstraint 534 ) -> Iterator[variables_mod.Variable]: 535 """Yields the variables with nonzero coefficient for this linear constraint.""" 536 keys = self._elemental.slice_attr( 537 enums.DoubleAttr2.LINEAR_CONSTRAINT_COEFFICIENT, 0, lin_con.id 538 ) 539 for var_id in keys[:, 1]: 540 yield variables_mod.Variable(self._elemental, int(var_id)) 541 542 def column_nonzeros( 543 self, var: variables_mod.Variable 544 ) -> Iterator[linear_constraints_mod.LinearConstraint]: 545 """Yields the linear constraints with nonzero coefficient for this variable.""" 546 keys = self._elemental.slice_attr( 547 enums.DoubleAttr2.LINEAR_CONSTRAINT_COEFFICIENT, 1, var.id 548 ) 549 for lin_con_id in keys[:, 0]: 550 yield linear_constraints_mod.LinearConstraint( 551 self._elemental, int(lin_con_id) 552 ) 553 554 def linear_constraint_matrix_entries( 555 self, 556 ) -> Iterator[linear_constraints_mod.LinearConstraintMatrixEntry]: 557 """Yields the nonzero elements of the linear constraint matrix in undefined order.""" 558 keys = self._elemental.get_attr_non_defaults( 559 enums.DoubleAttr2.LINEAR_CONSTRAINT_COEFFICIENT 560 ) 561 coefs = self._elemental.get_attrs( 562 enums.DoubleAttr2.LINEAR_CONSTRAINT_COEFFICIENT, keys 563 ) 564 for i in range(len(keys)): 565 yield linear_constraints_mod.LinearConstraintMatrixEntry( 566 linear_constraint=linear_constraints_mod.LinearConstraint( 567 self._elemental, int(keys[i, 0]) 568 ), 569 variable=variables_mod.Variable(self._elemental, int(keys[i, 1])), 570 coefficient=float(coefs[i]), 571 ) 572 573 ############################################################################## 574 # Quadratic Constraints 575 ############################################################################## 576 577 def add_quadratic_constraint( 578 self, 579 bounded_expr: Optional[ 580 Union[ 581 bool, 582 variables_mod.BoundedLinearTypes, 583 variables_mod.BoundedQuadraticTypes, 584 ] 585 ] = None, 586 *, 587 lb: Optional[float] = None, 588 ub: Optional[float] = None, 589 expr: Optional[variables_mod.QuadraticTypes] = None, 590 name: str = "", 591 ) -> quadratic_constraints.QuadraticConstraint: 592 """Adds a quadratic constraint to the optimization model. 593 594 The simplest way to specify the constraint is by passing a one-sided or 595 two-sided quadratic inequality as in: 596 * add_quadratic_constraint(x * x + y + 1.0 <= 2.0), 597 * add_quadratic_constraint(x * x + y >= 2.0), or 598 * add_quadratic_constraint((1.0 <= x * x + y) <= 2.0). 599 600 Note the extra parenthesis for two-sided linear inequalities, which is 601 required due to some language limitations (see add_linear_constraint for 602 details). 603 604 The second way to specify the constraint is by setting lb, ub, and/or expr 605 as in: 606 * add_quadratic_constraint(expr=x * x + y + 1.0, ub=2.0), 607 * add_quadratic_constraint(expr=x * x + y, lb=2.0), 608 * add_quadratic_constraint(expr=x * x + y, lb=1.0, ub=2.0), or 609 * add_quadratic_constraint(lb=1.0). 610 Omitting lb is equivalent to setting it to -math.inf and omiting ub is 611 equivalent to setting it to math.inf. 612 613 These two alternatives are exclusive and a combined call like: 614 * add_quadratic_constraint(x * x + y <= 2.0, lb=1.0), or 615 * add_quadratic_constraint(x * x+ y <= 2.0, ub=math.inf) 616 will raise a ValueError. A ValueError is also raised if expr's offset is 617 infinite. 618 619 Args: 620 bounded_expr: a quadratic inequality describing the constraint. Cannot be 621 specified together with lb, ub, or expr. 622 lb: The constraint's lower bound if bounded_expr is omitted (if both 623 bounder_expr and lb are omitted, the lower bound is -math.inf). 624 ub: The constraint's upper bound if bounded_expr is omitted (if both 625 bounder_expr and ub are omitted, the upper bound is math.inf). 626 expr: The constraint's quadratic expression if bounded_expr is omitted. 627 name: For debugging purposes only, but nonempty names must be distinct. 628 629 Returns: 630 A reference to the new quadratic constraint. 631 """ 632 norm_quad = normalized_inequality.as_normalized_quadratic_inequality( 633 bounded_expr, lb=lb, ub=ub, expr=expr 634 ) 635 quad_con_id = self._elemental.add_element( 636 enums.ElementType.QUADRATIC_CONSTRAINT, name 637 ) 638 for var, coef in norm_quad.linear_coefficients.items(): 639 self._elemental.set_attr( 640 enums.DoubleAttr2.QUADRATIC_CONSTRAINT_LINEAR_COEFFICIENT, 641 (quad_con_id, var.id), 642 coef, 643 ) 644 for key, coef in norm_quad.quadratic_coefficients.items(): 645 self._elemental.set_attr( 646 enums.SymmetricDoubleAttr3.QUADRATIC_CONSTRAINT_QUADRATIC_COEFFICIENT, 647 (quad_con_id, key.first_var.id, key.second_var.id), 648 coef, 649 ) 650 if norm_quad.lb > -math.inf: 651 self._elemental.set_attr( 652 enums.DoubleAttr1.QUADRATIC_CONSTRAINT_LOWER_BOUND, 653 (quad_con_id,), 654 norm_quad.lb, 655 ) 656 if norm_quad.ub < math.inf: 657 self._elemental.set_attr( 658 enums.DoubleAttr1.QUADRATIC_CONSTRAINT_UPPER_BOUND, 659 (quad_con_id,), 660 norm_quad.ub, 661 ) 662 return quadratic_constraints.QuadraticConstraint(self._elemental, quad_con_id) 663 664 def has_quadratic_constraint(self, con_id: int) -> bool: 665 """Returns true if a quadratic constraint with this id is in the model.""" 666 return self._elemental.element_exists( 667 enums.ElementType.QUADRATIC_CONSTRAINT, con_id 668 ) 669 670 def get_num_quadratic_constraints(self) -> int: 671 """Returns the number of quadratic constraints in the model.""" 672 return self._elemental.get_num_elements(enums.ElementType.QUADRATIC_CONSTRAINT) 673 674 def get_next_quadratic_constraint_id(self) -> int: 675 """Returns the id of the next quadratic constraint created in the model.""" 676 return self._elemental.get_next_element_id( 677 enums.ElementType.QUADRATIC_CONSTRAINT 678 ) 679 680 def ensure_next_quadratic_constraint_id_at_least(self, con_id: int) -> None: 681 """If the next quadratic constraint id would be less than `con_id`, sets it to `con_id`.""" 682 self._elemental.ensure_next_element_id_at_least( 683 enums.ElementType.QUADRATIC_CONSTRAINT, con_id 684 ) 685 686 def get_quadratic_constraint( 687 self, con_id: int, *, validate: bool = True 688 ) -> quadratic_constraints.QuadraticConstraint: 689 """Returns the constraint for the id, or raises KeyError if not in model.""" 690 if validate and not self._elemental.element_exists( 691 enums.ElementType.QUADRATIC_CONSTRAINT, con_id 692 ): 693 raise KeyError(f"Quadratic constraint does not exist with id {con_id}.") 694 return quadratic_constraints.QuadraticConstraint(self._elemental, con_id) 695 696 def delete_quadratic_constraint( 697 self, quad_con: quadratic_constraints.QuadraticConstraint 698 ) -> None: 699 """Deletes the constraint with id, or raises ValueError if not in model.""" 700 self.check_compatible(quad_con) 701 if not self._elemental.delete_element( 702 enums.ElementType.QUADRATIC_CONSTRAINT, quad_con.id 703 ): 704 raise ValueError( 705 f"Quadratic constraint with id {quad_con.id} was not in the model." 706 ) 707 708 def get_quadratic_constraints( 709 self, 710 ) -> Iterator[quadratic_constraints.QuadraticConstraint]: 711 """Yields the quadratic constraints in the order of creation.""" 712 quad_con_ids = self._elemental.get_elements( 713 enums.ElementType.QUADRATIC_CONSTRAINT 714 ) 715 quad_con_ids.sort() 716 for quad_con_id in quad_con_ids: 717 yield quadratic_constraints.QuadraticConstraint( 718 self._elemental, int(quad_con_id) 719 ) 720 721 def quadratic_constraint_linear_nonzeros( 722 self, 723 ) -> Iterator[ 724 Tuple[ 725 quadratic_constraints.QuadraticConstraint, 726 variables_mod.Variable, 727 float, 728 ] 729 ]: 730 """Yields the linear coefficients for all quadratic constraints in the model.""" 731 keys = self._elemental.get_attr_non_defaults( 732 enums.DoubleAttr2.QUADRATIC_CONSTRAINT_LINEAR_COEFFICIENT 733 ) 734 coefs = self._elemental.get_attrs( 735 enums.DoubleAttr2.QUADRATIC_CONSTRAINT_LINEAR_COEFFICIENT, keys 736 ) 737 for i in range(len(keys)): 738 yield ( 739 quadratic_constraints.QuadraticConstraint( 740 self._elemental, int(keys[i, 0]) 741 ), 742 variables_mod.Variable(self._elemental, int(keys[i, 1])), 743 float(coefs[i]), 744 ) 745 746 def quadratic_constraint_quadratic_nonzeros( 747 self, 748 ) -> Iterator[ 749 Tuple[ 750 quadratic_constraints.QuadraticConstraint, 751 variables_mod.Variable, 752 variables_mod.Variable, 753 float, 754 ] 755 ]: 756 """Yields the quadratic coefficients for all quadratic constraints in the model.""" 757 keys = self._elemental.get_attr_non_defaults( 758 enums.SymmetricDoubleAttr3.QUADRATIC_CONSTRAINT_QUADRATIC_COEFFICIENT 759 ) 760 coefs = self._elemental.get_attrs( 761 enums.SymmetricDoubleAttr3.QUADRATIC_CONSTRAINT_QUADRATIC_COEFFICIENT, 762 keys, 763 ) 764 for i in range(len(keys)): 765 yield ( 766 quadratic_constraints.QuadraticConstraint( 767 self._elemental, int(keys[i, 0]) 768 ), 769 variables_mod.Variable(self._elemental, int(keys[i, 1])), 770 variables_mod.Variable(self._elemental, int(keys[i, 2])), 771 float(coefs[i]), 772 ) 773 774 ############################################################################## 775 # Indicator Constraints 776 ############################################################################## 777 778 def add_indicator_constraint( 779 self, 780 *, 781 indicator: Optional[variables_mod.Variable] = None, 782 activate_on_zero: bool = False, 783 implied_constraint: Optional[ 784 Union[bool, variables_mod.BoundedLinearTypes] 785 ] = None, 786 implied_lb: Optional[float] = None, 787 implied_ub: Optional[float] = None, 788 implied_expr: Optional[variables_mod.LinearTypes] = None, 789 name: str = "", 790 ) -> indicator_constraints.IndicatorConstraint: 791 """Adds an indicator constraint to the model. 792 793 If indicator is None or the variable equal to indicator is deleted from 794 the model, the model will be considered invalid at solve time (unless this 795 constraint is also deleted before solving). Likewise, the variable indicator 796 must be binary at solve time for the model to be valid. 797 798 If implied_constraint is set, you may not set implied_lb, implied_ub, or 799 implied_expr. 800 801 Args: 802 indicator: The variable whose value determines if implied_constraint must 803 be enforced. 804 activate_on_zero: If true, implied_constraint must hold when indicator is 805 zero, otherwise, the implied_constraint must hold when indicator is one. 806 implied_constraint: A linear constraint to conditionally enforce, if set. 807 If None, that information is instead passed via implied_lb, implied_ub, 808 and implied_expr. 809 implied_lb: The lower bound of the condtionally enforced linear constraint 810 (or -inf if None), used only when implied_constraint is None. 811 implied_ub: The upper bound of the condtionally enforced linear constraint 812 (or +inf if None), used only when implied_constraint is None. 813 implied_expr: The linear part of the condtionally enforced linear 814 constraint (or 0 if None), used only when implied_constraint is None. If 815 expr has a nonzero offset, it is subtracted from lb and ub. 816 name: For debugging purposes only, but nonempty names must be distinct. 817 818 Returns: 819 A reference to the new indicator constraint. 820 """ 821 ind_con_id = self._elemental.add_element( 822 enums.ElementType.INDICATOR_CONSTRAINT, name 823 ) 824 if indicator is not None: 825 self._elemental.set_attr( 826 enums.VariableAttr1.INDICATOR_CONSTRAINT_INDICATOR, 827 (ind_con_id,), 828 indicator.id, 829 ) 830 self._elemental.set_attr( 831 enums.BoolAttr1.INDICATOR_CONSTRAINT_ACTIVATE_ON_ZERO, 832 (ind_con_id,), 833 activate_on_zero, 834 ) 835 implied_inequality = normalized_inequality.as_normalized_linear_inequality( 836 implied_constraint, lb=implied_lb, ub=implied_ub, expr=implied_expr 837 ) 838 self._elemental.set_attr( 839 enums.DoubleAttr1.INDICATOR_CONSTRAINT_LOWER_BOUND, 840 (ind_con_id,), 841 implied_inequality.lb, 842 ) 843 self._elemental.set_attr( 844 enums.DoubleAttr1.INDICATOR_CONSTRAINT_UPPER_BOUND, 845 (ind_con_id,), 846 implied_inequality.ub, 847 ) 848 for var, coef in implied_inequality.coefficients.items(): 849 self._elemental.set_attr( 850 enums.DoubleAttr2.INDICATOR_CONSTRAINT_LINEAR_COEFFICIENT, 851 (ind_con_id, var.id), 852 coef, 853 ) 854 855 return indicator_constraints.IndicatorConstraint(self._elemental, ind_con_id) 856 857 def has_indicator_constraint(self, con_id: int) -> bool: 858 """Returns true if an indicator constraint with this id is in the model.""" 859 return self._elemental.element_exists( 860 enums.ElementType.INDICATOR_CONSTRAINT, con_id 861 ) 862 863 def get_num_indicator_constraints(self) -> int: 864 """Returns the number of indicator constraints in the model.""" 865 return self._elemental.get_num_elements(enums.ElementType.INDICATOR_CONSTRAINT) 866 867 def get_next_indicator_constraint_id(self) -> int: 868 """Returns the id of the next indicator constraint created in the model.""" 869 return self._elemental.get_next_element_id( 870 enums.ElementType.INDICATOR_CONSTRAINT 871 ) 872 873 def ensure_next_indicator_constraint_id_at_least(self, con_id: int) -> None: 874 """If the next indicator constraint id would be less than `con_id`, sets it to `con_id`.""" 875 self._elemental.ensure_next_element_id_at_least( 876 enums.ElementType.INDICATOR_CONSTRAINT, con_id 877 ) 878 879 def get_indicator_constraint( 880 self, con_id: int, *, validate: bool = True 881 ) -> indicator_constraints.IndicatorConstraint: 882 """Returns the IndicatorConstraint for the id con_id.""" 883 if validate and not self._elemental.element_exists( 884 enums.ElementType.INDICATOR_CONSTRAINT, con_id 885 ): 886 raise KeyError(f"Indicator constraint does not exist with id {con_id}.") 887 return indicator_constraints.IndicatorConstraint(self._elemental, con_id) 888 889 def delete_indicator_constraint( 890 self, ind_con: indicator_constraints.IndicatorConstraint 891 ) -> None: 892 self.check_compatible(ind_con) 893 if not self._elemental.delete_element( 894 enums.ElementType.INDICATOR_CONSTRAINT, ind_con.id 895 ): 896 raise ValueError( 897 f"Indicator constraint with id {ind_con.id} was not in the model." 898 ) 899 900 def get_indicator_constraints( 901 self, 902 ) -> Iterator[indicator_constraints.IndicatorConstraint]: 903 """Yields the indicator constraints in the order of creation.""" 904 ind_con_ids = self._elemental.get_elements( 905 enums.ElementType.INDICATOR_CONSTRAINT 906 ) 907 ind_con_ids.sort() 908 for ind_con_id in ind_con_ids: 909 yield indicator_constraints.IndicatorConstraint( 910 self._elemental, int(ind_con_id) 911 ) 912 913 ############################################################################## 914 # Proto import/export 915 ############################################################################## 916 917 @classmethod 918 def from_model_proto(cls, proto: model_pb2.ModelProto) -> Self: 919 """Returns a Model equivalent to the input model proto.""" 920 model = cls() 921 model._elemental = cpp_elemental.CppElemental.from_model_proto(proto) 922 return model 923 924 def export_model(self, *, remove_names: bool = False) -> model_pb2.ModelProto: 925 """Returns a protocol buffer equivalent to this model. 926 927 Args: 928 remove_names: When true, remove all names for the ModelProto. 929 930 Returns: 931 The model proto. 932 """ 933 return self._elemental.export_model(remove_names=remove_names) 934 935 def add_update_tracker(self) -> UpdateTracker: 936 """Creates an UpdateTracker registered on this model to view changes.""" 937 return UpdateTracker(self._elemental.add_diff(), self._elemental) 938 939 def remove_update_tracker(self, tracker: UpdateTracker): 940 """Stops tracker from getting updates on changes to this model. 941 942 An error will be raised if tracker was not created by this Model or if 943 tracker has been previously removed. 944 945 Using (via checkpoint or update) an UpdateTracker after it has been removed 946 will result in an error. 947 948 Args: 949 tracker: The UpdateTracker to unregister. 950 951 Raises: 952 KeyError: The tracker was created by another model or was already removed. 953 """ 954 self._elemental.delete_diff(tracker.diff_id) 955 956 def check_compatible(self, e: from_model.FromModel) -> None: 957 """Raises a ValueError if the model of var_or_constraint is not self.""" 958 if e.elemental is not self._elemental: 959 raise ValueError( 960 f"Expected element from model named: '{self._elemental.model_name}'," 961 f" but observed element {e} from model named:" 962 f" '{e.elemental.model_name}'." 963 )
66class UpdateTracker: 67 """Tracks updates to an optimization model from a ModelStorage. 68 69 Do not instantiate directly, instead create through 70 ModelStorage.add_update_tracker(). 71 72 Querying an UpdateTracker after calling Model.remove_update_tracker will 73 result in a model_storage.UsedUpdateTrackerAfterRemovalError. 74 75 Example: 76 mod = Model() 77 x = mod.add_variable(0.0, 1.0, True, 'x') 78 y = mod.add_variable(0.0, 1.0, True, 'y') 79 tracker = mod.add_update_tracker() 80 mod.set_variable_ub(x, 3.0) 81 tracker.export_update() 82 => "variable_updates: {upper_bounds: {ids: [0], values[3.0] }" 83 mod.set_variable_ub(y, 2.0) 84 tracker.export_update() 85 => "variable_updates: {upper_bounds: {ids: [0, 1], values[3.0, 2.0] }" 86 tracker.advance_checkpoint() 87 tracker.export_update() 88 => None 89 mod.set_variable_ub(y, 4.0) 90 tracker.export_update() 91 => "variable_updates: {upper_bounds: {ids: [1], values[4.0] }" 92 tracker.advance_checkpoint() 93 mod.remove_update_tracker(tracker) 94 """ 95 96 def __init__( 97 self, 98 diff_id: int, 99 elem: elemental.Elemental, 100 ): 101 """Do not invoke directly, use Model.add_update_tracker() instead.""" 102 self._diff_id = diff_id 103 self._elemental = elem 104 105 def export_update( 106 self, *, remove_names: bool = False 107 ) -> Optional[model_update_pb2.ModelUpdateProto]: 108 """Returns changes to the model since last call to checkpoint/creation.""" 109 return self._elemental.export_model_update( 110 self._diff_id, remove_names=remove_names 111 ) 112 113 def advance_checkpoint(self) -> None: 114 """Track changes to the model only after this function call.""" 115 return self._elemental.advance_diff(self._diff_id) 116 117 @property 118 def diff_id(self) -> int: 119 return self._diff_id
Tracks updates to an optimization model from a ModelStorage.
Do not instantiate directly, instead create through ModelStorage.add_update_tracker().
Querying an UpdateTracker after calling Model.remove_update_tracker will result in a model_storage.UsedUpdateTrackerAfterRemovalError.
Example:
mod = Model() x = mod.add_variable(0.0, 1.0, True, 'x') y = mod.add_variable(0.0, 1.0, True, 'y') tracker = mod.add_update_tracker() mod.set_variable_ub(x, 3.0) tracker.export_update() => "variable_updates: {upper_bounds: {ids: [0], values[3.0] }" mod.set_variable_ub(y, 2.0) tracker.export_update() => "variable_updates: {upper_bounds: {ids: [0, 1], values[3.0, 2.0] }" tracker.advance_checkpoint() tracker.export_update() => None mod.set_variable_ub(y, 4.0) tracker.export_update() => "variable_updates: {upper_bounds: {ids: [1], values[4.0] }" tracker.advance_checkpoint() mod.remove_update_tracker(tracker)
96 def __init__( 97 self, 98 diff_id: int, 99 elem: elemental.Elemental, 100 ): 101 """Do not invoke directly, use Model.add_update_tracker() instead.""" 102 self._diff_id = diff_id 103 self._elemental = elem
Do not invoke directly, use Model.add_update_tracker() instead.
105 def export_update( 106 self, *, remove_names: bool = False 107 ) -> Optional[model_update_pb2.ModelUpdateProto]: 108 """Returns changes to the model since last call to checkpoint/creation.""" 109 return self._elemental.export_model_update( 110 self._diff_id, remove_names=remove_names 111 )
Returns changes to the model since last call to checkpoint/creation.
122class Model: 123 """An optimization model. 124 125 The objective function of the model can be linear or quadratic, and some 126 solvers can only handle linear objective functions. For this reason Model has 127 three versions of all objective setting functions: 128 * A generic one (e.g. maximize()), which accepts linear or quadratic 129 expressions, 130 * a quadratic version (e.g. maximize_quadratic_objective()), which also 131 accepts linear or quadratic expressions and can be used to signal a 132 quadratic objective is possible, and 133 * a linear version (e.g. maximize_linear_objective()), which only accepts 134 linear expressions and can be used to avoid solve time errors for solvers 135 that do not accept quadratic objectives. 136 137 Attributes: 138 name: A description of the problem, can be empty. 139 objective: A function to maximize or minimize. 140 storage: Implementation detail, do not access directly. 141 _variable_ids: Maps variable ids to Variable objects. 142 _linear_constraint_ids: Maps linear constraint ids to LinearConstraint 143 objects. 144 """ 145 146 __slots__ = ("_elemental",) 147 148 def __init__( 149 self, 150 *, 151 name: str = "", # TODO(b/371236599): rename to model_name 152 primary_objective_name: str = "", 153 ) -> None: 154 self._elemental: elemental.Elemental = cpp_elemental.CppElemental( 155 model_name=name, primary_objective_name=primary_objective_name 156 ) 157 158 @property 159 def name(self) -> str: 160 return self._elemental.model_name 161 162 ############################################################################## 163 # Variables 164 ############################################################################## 165 166 def add_variable( 167 self, 168 *, 169 lb: float = -math.inf, 170 ub: float = math.inf, 171 is_integer: bool = False, 172 name: str = "", 173 ) -> variables_mod.Variable: 174 """Adds a decision variable to the optimization model. 175 176 Args: 177 lb: The new variable must take at least this value (a lower bound). 178 ub: The new variable must be at most this value (an upper bound). 179 is_integer: Indicates if the variable can only take integer values 180 (otherwise, the variable can take any continuous value). 181 name: For debugging purposes only, but nonempty names must be distinct. 182 183 Returns: 184 A reference to the new decision variable. 185 """ 186 187 variable_id = self._elemental.add_element(enums.ElementType.VARIABLE, name) 188 result = variables_mod.Variable(self._elemental, variable_id) 189 result.lower_bound = lb 190 result.upper_bound = ub 191 result.integer = is_integer 192 return result 193 194 def add_integer_variable( 195 self, *, lb: float = -math.inf, ub: float = math.inf, name: str = "" 196 ) -> variables_mod.Variable: 197 return self.add_variable(lb=lb, ub=ub, is_integer=True, name=name) 198 199 def add_binary_variable(self, *, name: str = "") -> variables_mod.Variable: 200 return self.add_variable(lb=0.0, ub=1.0, is_integer=True, name=name) 201 202 def get_variable( 203 self, var_id: int, *, validate: bool = True 204 ) -> variables_mod.Variable: 205 """Returns the Variable for the id var_id, or raises KeyError.""" 206 if validate and not self._elemental.element_exists( 207 enums.ElementType.VARIABLE, var_id 208 ): 209 raise KeyError(f"Variable does not exist with id {var_id}.") 210 return variables_mod.Variable(self._elemental, var_id) 211 212 def has_variable(self, var_id: int) -> bool: 213 """Returns true if a Variable with this id is in the model.""" 214 return self._elemental.element_exists(enums.ElementType.VARIABLE, var_id) 215 216 def get_num_variables(self) -> int: 217 """Returns the number of variables in the model.""" 218 return self._elemental.get_num_elements(enums.ElementType.VARIABLE) 219 220 def get_next_variable_id(self) -> int: 221 """Returns the id of the next variable created in the model.""" 222 return self._elemental.get_next_element_id(enums.ElementType.VARIABLE) 223 224 def ensure_next_variable_id_at_least(self, var_id: int) -> None: 225 """If the next variable id would be less than `var_id`, sets it to `var_id`.""" 226 self._elemental.ensure_next_element_id_at_least( 227 enums.ElementType.VARIABLE, var_id 228 ) 229 230 def delete_variable(self, var: variables_mod.Variable) -> None: 231 """Removes this variable from the model.""" 232 self.check_compatible(var) 233 if not self._elemental.delete_element(enums.ElementType.VARIABLE, var.id): 234 raise ValueError(f"Variable with id {var.id} was not in the model.") 235 236 def variables(self) -> Iterator[variables_mod.Variable]: 237 """Yields the variables in the order of creation.""" 238 var_ids = self._elemental.get_elements(enums.ElementType.VARIABLE) 239 var_ids.sort() 240 for var_id in var_ids: 241 yield variables_mod.Variable(self._elemental, int(var_id)) 242 243 ############################################################################## 244 # Objective 245 ############################################################################## 246 247 @property 248 def objective(self) -> objectives.Objective: 249 return objectives.PrimaryObjective(self._elemental) 250 251 def maximize(self, obj: variables_mod.QuadraticTypes) -> None: 252 """Sets the objective to maximize the provided expression `obj`.""" 253 self.set_objective(obj, is_maximize=True) 254 255 def maximize_linear_objective(self, obj: variables_mod.LinearTypes) -> None: 256 """Sets the objective to maximize the provided linear expression `obj`.""" 257 self.set_linear_objective(obj, is_maximize=True) 258 259 def maximize_quadratic_objective(self, obj: variables_mod.QuadraticTypes) -> None: 260 """Sets the objective to maximize the provided quadratic expression `obj`.""" 261 self.set_quadratic_objective(obj, is_maximize=True) 262 263 def minimize(self, obj: variables_mod.QuadraticTypes) -> None: 264 """Sets the objective to minimize the provided expression `obj`.""" 265 self.set_objective(obj, is_maximize=False) 266 267 def minimize_linear_objective(self, obj: variables_mod.LinearTypes) -> None: 268 """Sets the objective to minimize the provided linear expression `obj`.""" 269 self.set_linear_objective(obj, is_maximize=False) 270 271 def minimize_quadratic_objective(self, obj: variables_mod.QuadraticTypes) -> None: 272 """Sets the objective to minimize the provided quadratic expression `obj`.""" 273 self.set_quadratic_objective(obj, is_maximize=False) 274 275 def set_objective( 276 self, obj: variables_mod.QuadraticTypes, *, is_maximize: bool 277 ) -> None: 278 """Sets the objective to optimize the provided expression `obj`.""" 279 self.objective.set_to_expression(obj) 280 self.objective.is_maximize = is_maximize 281 282 def set_linear_objective( 283 self, obj: variables_mod.LinearTypes, *, is_maximize: bool 284 ) -> None: 285 """Sets the objective to optimize the provided linear expression `obj`.""" 286 self.objective.set_to_linear_expression(obj) 287 self.objective.is_maximize = is_maximize 288 289 def set_quadratic_objective( 290 self, obj: variables_mod.QuadraticTypes, *, is_maximize: bool 291 ) -> None: 292 """Sets the objective to optimize the provided quadratic expression `obj`.""" 293 self.objective.set_to_quadratic_expression(obj) 294 self.objective.is_maximize = is_maximize 295 296 def linear_objective_terms(self) -> Iterator[variables_mod.LinearTerm]: 297 """Yields variable coefficient pairs for variables with nonzero objective coefficient in undefined order.""" 298 yield from self.objective.linear_terms() 299 300 def quadratic_objective_terms(self) -> Iterator[variables_mod.QuadraticTerm]: 301 """Yields the quadratic terms with nonzero objective coefficient in undefined order.""" 302 yield from self.objective.quadratic_terms() 303 304 ############################################################################## 305 # Auxiliary Objectives 306 ############################################################################## 307 308 def add_auxiliary_objective( 309 self, 310 *, 311 priority: int, 312 name: str = "", 313 expr: Optional[variables_mod.LinearTypes] = None, 314 is_maximize: bool = False, 315 ) -> objectives.AuxiliaryObjective: 316 """Adds an additional objective to the model.""" 317 obj_id = self._elemental.add_element( 318 enums.ElementType.AUXILIARY_OBJECTIVE, name 319 ) 320 self._elemental.set_attr( 321 enums.IntAttr1.AUXILIARY_OBJECTIVE_PRIORITY, (obj_id,), priority 322 ) 323 result = objectives.AuxiliaryObjective(self._elemental, obj_id) 324 if expr is not None: 325 result.set_to_linear_expression(expr) 326 result.is_maximize = is_maximize 327 return result 328 329 def add_maximization_objective( 330 self, expr: variables_mod.LinearTypes, *, priority: int, name: str = "" 331 ) -> objectives.AuxiliaryObjective: 332 """Adds an additional objective to the model that is maximizaition.""" 333 result = self.add_auxiliary_objective( 334 priority=priority, name=name, expr=expr, is_maximize=True 335 ) 336 return result 337 338 def add_minimization_objective( 339 self, expr: variables_mod.LinearTypes, *, priority: int, name: str = "" 340 ) -> objectives.AuxiliaryObjective: 341 """Adds an additional objective to the model that is minimizaition.""" 342 result = self.add_auxiliary_objective( 343 priority=priority, name=name, expr=expr, is_maximize=False 344 ) 345 return result 346 347 def delete_auxiliary_objective(self, obj: objectives.AuxiliaryObjective) -> None: 348 """Removes an auxiliary objective from the model.""" 349 self.check_compatible(obj) 350 if not self._elemental.delete_element( 351 enums.ElementType.AUXILIARY_OBJECTIVE, obj.id 352 ): 353 raise ValueError( 354 f"Auxiliary objective with id {obj.id} is not in the model." 355 ) 356 357 def has_auxiliary_objective(self, obj_id: int) -> bool: 358 """Returns true if the model has an auxiliary objective with id `obj_id`.""" 359 return self._elemental.element_exists( 360 enums.ElementType.AUXILIARY_OBJECTIVE, obj_id 361 ) 362 363 def next_auxiliary_objective_id(self) -> int: 364 """Returns the id of the next auxiliary objective added to the model.""" 365 return self._elemental.get_next_element_id( 366 enums.ElementType.AUXILIARY_OBJECTIVE 367 ) 368 369 def num_auxiliary_objectives(self) -> int: 370 """Returns the number of auxiliary objectives in this model.""" 371 return self._elemental.get_num_elements(enums.ElementType.AUXILIARY_OBJECTIVE) 372 373 def ensure_next_auxiliary_objective_id_at_least(self, obj_id: int) -> None: 374 """If the next auxiliary objective id would be less than `obj_id`, sets it to `obj_id`.""" 375 self._elemental.ensure_next_element_id_at_least( 376 enums.ElementType.AUXILIARY_OBJECTIVE, obj_id 377 ) 378 379 def get_auxiliary_objective( 380 self, obj_id: int, *, validate: bool = True 381 ) -> objectives.AuxiliaryObjective: 382 """Returns the auxiliary objective with this id. 383 384 If there is no objective with this id, an exception is thrown if validate is 385 true, and an invalid AuxiliaryObjective is returned if validate is false 386 (later interactions with this object will cause unpredictable errors). Only 387 set validate=False if there is a known performance problem. 388 389 Args: 390 obj_id: The id of the auxiliary objective to look for. 391 validate: Set to false for more speed, but fails to raise an exception if 392 the objective is missing. 393 394 Raises: 395 KeyError: If `validate` is True and there is no objective with this id. 396 """ 397 if validate and not self.has_auxiliary_objective(obj_id): 398 raise KeyError(f"Model has no auxiliary objective with id {obj_id}") 399 return objectives.AuxiliaryObjective(self._elemental, obj_id) 400 401 def auxiliary_objectives(self) -> Iterator[objectives.AuxiliaryObjective]: 402 """Returns the auxiliary objectives in the model in the order of creation.""" 403 ids = self._elemental.get_elements(enums.ElementType.AUXILIARY_OBJECTIVE) 404 ids.sort() 405 for aux_obj_id in ids: 406 yield objectives.AuxiliaryObjective(self._elemental, int(aux_obj_id)) 407 408 ############################################################################## 409 # Linear Constraints 410 ############################################################################## 411 412 # TODO(b/227214976): Update the note below and link to pytype bug number. 413 # Note: bounded_expr's type includes bool only as a workaround to a pytype 414 # issue. Passing a bool for bounded_expr will raise an error in runtime. 415 def add_linear_constraint( 416 self, 417 bounded_expr: Optional[Union[bool, variables_mod.BoundedLinearTypes]] = None, 418 *, 419 lb: Optional[float] = None, 420 ub: Optional[float] = None, 421 expr: Optional[variables_mod.LinearTypes] = None, 422 name: str = "", 423 ) -> linear_constraints_mod.LinearConstraint: 424 """Adds a linear constraint to the optimization model. 425 426 The simplest way to specify the constraint is by passing a one-sided or 427 two-sided linear inequality as in: 428 * add_linear_constraint(x + y + 1.0 <= 2.0), 429 * add_linear_constraint(x + y >= 2.0), or 430 * add_linear_constraint((1.0 <= x + y) <= 2.0). 431 432 Note the extra parenthesis for two-sided linear inequalities, which is 433 required due to some language limitations (see 434 https://peps.python.org/pep-0335/ and https://peps.python.org/pep-0535/). 435 If the parenthesis are omitted, a TypeError will be raised explaining the 436 issue (if this error was not raised the first inequality would have been 437 silently ignored because of the noted language limitations). 438 439 The second way to specify the constraint is by setting lb, ub, and/or expr 440 as in: 441 * add_linear_constraint(expr=x + y + 1.0, ub=2.0), 442 * add_linear_constraint(expr=x + y, lb=2.0), 443 * add_linear_constraint(expr=x + y, lb=1.0, ub=2.0), or 444 * add_linear_constraint(lb=1.0). 445 Omitting lb is equivalent to setting it to -math.inf and omiting ub is 446 equivalent to setting it to math.inf. 447 448 These two alternatives are exclusive and a combined call like: 449 * add_linear_constraint(x + y <= 2.0, lb=1.0), or 450 * add_linear_constraint(x + y <= 2.0, ub=math.inf) 451 will raise a ValueError. A ValueError is also raised if expr's offset is 452 infinite. 453 454 Args: 455 bounded_expr: a linear inequality describing the constraint. Cannot be 456 specified together with lb, ub, or expr. 457 lb: The constraint's lower bound if bounded_expr is omitted (if both 458 bounder_expr and lb are omitted, the lower bound is -math.inf). 459 ub: The constraint's upper bound if bounded_expr is omitted (if both 460 bounder_expr and ub are omitted, the upper bound is math.inf). 461 expr: The constraint's linear expression if bounded_expr is omitted. 462 name: For debugging purposes only, but nonempty names must be distinct. 463 464 Returns: 465 A reference to the new linear constraint. 466 """ 467 norm_ineq = normalized_inequality.as_normalized_linear_inequality( 468 bounded_expr, lb=lb, ub=ub, expr=expr 469 ) 470 lin_con_id = self._elemental.add_element( 471 enums.ElementType.LINEAR_CONSTRAINT, name 472 ) 473 474 result = linear_constraints_mod.LinearConstraint(self._elemental, lin_con_id) 475 result.lower_bound = norm_ineq.lb 476 result.upper_bound = norm_ineq.ub 477 for var, coefficient in norm_ineq.coefficients.items(): 478 result.set_coefficient(var, coefficient) 479 return result 480 481 def has_linear_constraint(self, con_id: int) -> bool: 482 """Returns true if a linear constraint with this id is in the model.""" 483 return self._elemental.element_exists( 484 enums.ElementType.LINEAR_CONSTRAINT, con_id 485 ) 486 487 def get_num_linear_constraints(self) -> int: 488 """Returns the number of linear constraints in the model.""" 489 return self._elemental.get_num_elements(enums.ElementType.LINEAR_CONSTRAINT) 490 491 def get_next_linear_constraint_id(self) -> int: 492 """Returns the id of the next linear constraint created in the model.""" 493 return self._elemental.get_next_element_id(enums.ElementType.LINEAR_CONSTRAINT) 494 495 def ensure_next_linear_constraint_id_at_least(self, con_id: int) -> None: 496 """If the next linear constraint id would be less than `con_id`, sets it to `con_id`.""" 497 self._elemental.ensure_next_element_id_at_least( 498 enums.ElementType.LINEAR_CONSTRAINT, con_id 499 ) 500 501 def get_linear_constraint( 502 self, con_id: int, *, validate: bool = True 503 ) -> linear_constraints_mod.LinearConstraint: 504 """Returns the LinearConstraint for the id con_id.""" 505 if validate and not self._elemental.element_exists( 506 enums.ElementType.LINEAR_CONSTRAINT, con_id 507 ): 508 raise KeyError(f"Linear constraint does not exist with id {con_id}.") 509 return linear_constraints_mod.LinearConstraint(self._elemental, con_id) 510 511 def delete_linear_constraint( 512 self, lin_con: linear_constraints_mod.LinearConstraint 513 ) -> None: 514 self.check_compatible(lin_con) 515 if not self._elemental.delete_element( 516 enums.ElementType.LINEAR_CONSTRAINT, lin_con.id 517 ): 518 raise ValueError( 519 f"Linear constraint with id {lin_con.id} was not in the model." 520 ) 521 522 def linear_constraints( 523 self, 524 ) -> Iterator[linear_constraints_mod.LinearConstraint]: 525 """Yields the linear constraints in the order of creation.""" 526 lin_con_ids = self._elemental.get_elements(enums.ElementType.LINEAR_CONSTRAINT) 527 lin_con_ids.sort() 528 for lin_con_id in lin_con_ids: 529 yield linear_constraints_mod.LinearConstraint( 530 self._elemental, int(lin_con_id) 531 ) 532 533 def row_nonzeros( 534 self, lin_con: linear_constraints_mod.LinearConstraint 535 ) -> Iterator[variables_mod.Variable]: 536 """Yields the variables with nonzero coefficient for this linear constraint.""" 537 keys = self._elemental.slice_attr( 538 enums.DoubleAttr2.LINEAR_CONSTRAINT_COEFFICIENT, 0, lin_con.id 539 ) 540 for var_id in keys[:, 1]: 541 yield variables_mod.Variable(self._elemental, int(var_id)) 542 543 def column_nonzeros( 544 self, var: variables_mod.Variable 545 ) -> Iterator[linear_constraints_mod.LinearConstraint]: 546 """Yields the linear constraints with nonzero coefficient for this variable.""" 547 keys = self._elemental.slice_attr( 548 enums.DoubleAttr2.LINEAR_CONSTRAINT_COEFFICIENT, 1, var.id 549 ) 550 for lin_con_id in keys[:, 0]: 551 yield linear_constraints_mod.LinearConstraint( 552 self._elemental, int(lin_con_id) 553 ) 554 555 def linear_constraint_matrix_entries( 556 self, 557 ) -> Iterator[linear_constraints_mod.LinearConstraintMatrixEntry]: 558 """Yields the nonzero elements of the linear constraint matrix in undefined order.""" 559 keys = self._elemental.get_attr_non_defaults( 560 enums.DoubleAttr2.LINEAR_CONSTRAINT_COEFFICIENT 561 ) 562 coefs = self._elemental.get_attrs( 563 enums.DoubleAttr2.LINEAR_CONSTRAINT_COEFFICIENT, keys 564 ) 565 for i in range(len(keys)): 566 yield linear_constraints_mod.LinearConstraintMatrixEntry( 567 linear_constraint=linear_constraints_mod.LinearConstraint( 568 self._elemental, int(keys[i, 0]) 569 ), 570 variable=variables_mod.Variable(self._elemental, int(keys[i, 1])), 571 coefficient=float(coefs[i]), 572 ) 573 574 ############################################################################## 575 # Quadratic Constraints 576 ############################################################################## 577 578 def add_quadratic_constraint( 579 self, 580 bounded_expr: Optional[ 581 Union[ 582 bool, 583 variables_mod.BoundedLinearTypes, 584 variables_mod.BoundedQuadraticTypes, 585 ] 586 ] = None, 587 *, 588 lb: Optional[float] = None, 589 ub: Optional[float] = None, 590 expr: Optional[variables_mod.QuadraticTypes] = None, 591 name: str = "", 592 ) -> quadratic_constraints.QuadraticConstraint: 593 """Adds a quadratic constraint to the optimization model. 594 595 The simplest way to specify the constraint is by passing a one-sided or 596 two-sided quadratic inequality as in: 597 * add_quadratic_constraint(x * x + y + 1.0 <= 2.0), 598 * add_quadratic_constraint(x * x + y >= 2.0), or 599 * add_quadratic_constraint((1.0 <= x * x + y) <= 2.0). 600 601 Note the extra parenthesis for two-sided linear inequalities, which is 602 required due to some language limitations (see add_linear_constraint for 603 details). 604 605 The second way to specify the constraint is by setting lb, ub, and/or expr 606 as in: 607 * add_quadratic_constraint(expr=x * x + y + 1.0, ub=2.0), 608 * add_quadratic_constraint(expr=x * x + y, lb=2.0), 609 * add_quadratic_constraint(expr=x * x + y, lb=1.0, ub=2.0), or 610 * add_quadratic_constraint(lb=1.0). 611 Omitting lb is equivalent to setting it to -math.inf and omiting ub is 612 equivalent to setting it to math.inf. 613 614 These two alternatives are exclusive and a combined call like: 615 * add_quadratic_constraint(x * x + y <= 2.0, lb=1.0), or 616 * add_quadratic_constraint(x * x+ y <= 2.0, ub=math.inf) 617 will raise a ValueError. A ValueError is also raised if expr's offset is 618 infinite. 619 620 Args: 621 bounded_expr: a quadratic inequality describing the constraint. Cannot be 622 specified together with lb, ub, or expr. 623 lb: The constraint's lower bound if bounded_expr is omitted (if both 624 bounder_expr and lb are omitted, the lower bound is -math.inf). 625 ub: The constraint's upper bound if bounded_expr is omitted (if both 626 bounder_expr and ub are omitted, the upper bound is math.inf). 627 expr: The constraint's quadratic expression if bounded_expr is omitted. 628 name: For debugging purposes only, but nonempty names must be distinct. 629 630 Returns: 631 A reference to the new quadratic constraint. 632 """ 633 norm_quad = normalized_inequality.as_normalized_quadratic_inequality( 634 bounded_expr, lb=lb, ub=ub, expr=expr 635 ) 636 quad_con_id = self._elemental.add_element( 637 enums.ElementType.QUADRATIC_CONSTRAINT, name 638 ) 639 for var, coef in norm_quad.linear_coefficients.items(): 640 self._elemental.set_attr( 641 enums.DoubleAttr2.QUADRATIC_CONSTRAINT_LINEAR_COEFFICIENT, 642 (quad_con_id, var.id), 643 coef, 644 ) 645 for key, coef in norm_quad.quadratic_coefficients.items(): 646 self._elemental.set_attr( 647 enums.SymmetricDoubleAttr3.QUADRATIC_CONSTRAINT_QUADRATIC_COEFFICIENT, 648 (quad_con_id, key.first_var.id, key.second_var.id), 649 coef, 650 ) 651 if norm_quad.lb > -math.inf: 652 self._elemental.set_attr( 653 enums.DoubleAttr1.QUADRATIC_CONSTRAINT_LOWER_BOUND, 654 (quad_con_id,), 655 norm_quad.lb, 656 ) 657 if norm_quad.ub < math.inf: 658 self._elemental.set_attr( 659 enums.DoubleAttr1.QUADRATIC_CONSTRAINT_UPPER_BOUND, 660 (quad_con_id,), 661 norm_quad.ub, 662 ) 663 return quadratic_constraints.QuadraticConstraint(self._elemental, quad_con_id) 664 665 def has_quadratic_constraint(self, con_id: int) -> bool: 666 """Returns true if a quadratic constraint with this id is in the model.""" 667 return self._elemental.element_exists( 668 enums.ElementType.QUADRATIC_CONSTRAINT, con_id 669 ) 670 671 def get_num_quadratic_constraints(self) -> int: 672 """Returns the number of quadratic constraints in the model.""" 673 return self._elemental.get_num_elements(enums.ElementType.QUADRATIC_CONSTRAINT) 674 675 def get_next_quadratic_constraint_id(self) -> int: 676 """Returns the id of the next quadratic constraint created in the model.""" 677 return self._elemental.get_next_element_id( 678 enums.ElementType.QUADRATIC_CONSTRAINT 679 ) 680 681 def ensure_next_quadratic_constraint_id_at_least(self, con_id: int) -> None: 682 """If the next quadratic constraint id would be less than `con_id`, sets it to `con_id`.""" 683 self._elemental.ensure_next_element_id_at_least( 684 enums.ElementType.QUADRATIC_CONSTRAINT, con_id 685 ) 686 687 def get_quadratic_constraint( 688 self, con_id: int, *, validate: bool = True 689 ) -> quadratic_constraints.QuadraticConstraint: 690 """Returns the constraint for the id, or raises KeyError if not in model.""" 691 if validate and not self._elemental.element_exists( 692 enums.ElementType.QUADRATIC_CONSTRAINT, con_id 693 ): 694 raise KeyError(f"Quadratic constraint does not exist with id {con_id}.") 695 return quadratic_constraints.QuadraticConstraint(self._elemental, con_id) 696 697 def delete_quadratic_constraint( 698 self, quad_con: quadratic_constraints.QuadraticConstraint 699 ) -> None: 700 """Deletes the constraint with id, or raises ValueError if not in model.""" 701 self.check_compatible(quad_con) 702 if not self._elemental.delete_element( 703 enums.ElementType.QUADRATIC_CONSTRAINT, quad_con.id 704 ): 705 raise ValueError( 706 f"Quadratic constraint with id {quad_con.id} was not in the model." 707 ) 708 709 def get_quadratic_constraints( 710 self, 711 ) -> Iterator[quadratic_constraints.QuadraticConstraint]: 712 """Yields the quadratic constraints in the order of creation.""" 713 quad_con_ids = self._elemental.get_elements( 714 enums.ElementType.QUADRATIC_CONSTRAINT 715 ) 716 quad_con_ids.sort() 717 for quad_con_id in quad_con_ids: 718 yield quadratic_constraints.QuadraticConstraint( 719 self._elemental, int(quad_con_id) 720 ) 721 722 def quadratic_constraint_linear_nonzeros( 723 self, 724 ) -> Iterator[ 725 Tuple[ 726 quadratic_constraints.QuadraticConstraint, 727 variables_mod.Variable, 728 float, 729 ] 730 ]: 731 """Yields the linear coefficients for all quadratic constraints in the model.""" 732 keys = self._elemental.get_attr_non_defaults( 733 enums.DoubleAttr2.QUADRATIC_CONSTRAINT_LINEAR_COEFFICIENT 734 ) 735 coefs = self._elemental.get_attrs( 736 enums.DoubleAttr2.QUADRATIC_CONSTRAINT_LINEAR_COEFFICIENT, keys 737 ) 738 for i in range(len(keys)): 739 yield ( 740 quadratic_constraints.QuadraticConstraint( 741 self._elemental, int(keys[i, 0]) 742 ), 743 variables_mod.Variable(self._elemental, int(keys[i, 1])), 744 float(coefs[i]), 745 ) 746 747 def quadratic_constraint_quadratic_nonzeros( 748 self, 749 ) -> Iterator[ 750 Tuple[ 751 quadratic_constraints.QuadraticConstraint, 752 variables_mod.Variable, 753 variables_mod.Variable, 754 float, 755 ] 756 ]: 757 """Yields the quadratic coefficients for all quadratic constraints in the model.""" 758 keys = self._elemental.get_attr_non_defaults( 759 enums.SymmetricDoubleAttr3.QUADRATIC_CONSTRAINT_QUADRATIC_COEFFICIENT 760 ) 761 coefs = self._elemental.get_attrs( 762 enums.SymmetricDoubleAttr3.QUADRATIC_CONSTRAINT_QUADRATIC_COEFFICIENT, 763 keys, 764 ) 765 for i in range(len(keys)): 766 yield ( 767 quadratic_constraints.QuadraticConstraint( 768 self._elemental, int(keys[i, 0]) 769 ), 770 variables_mod.Variable(self._elemental, int(keys[i, 1])), 771 variables_mod.Variable(self._elemental, int(keys[i, 2])), 772 float(coefs[i]), 773 ) 774 775 ############################################################################## 776 # Indicator Constraints 777 ############################################################################## 778 779 def add_indicator_constraint( 780 self, 781 *, 782 indicator: Optional[variables_mod.Variable] = None, 783 activate_on_zero: bool = False, 784 implied_constraint: Optional[ 785 Union[bool, variables_mod.BoundedLinearTypes] 786 ] = None, 787 implied_lb: Optional[float] = None, 788 implied_ub: Optional[float] = None, 789 implied_expr: Optional[variables_mod.LinearTypes] = None, 790 name: str = "", 791 ) -> indicator_constraints.IndicatorConstraint: 792 """Adds an indicator constraint to the model. 793 794 If indicator is None or the variable equal to indicator is deleted from 795 the model, the model will be considered invalid at solve time (unless this 796 constraint is also deleted before solving). Likewise, the variable indicator 797 must be binary at solve time for the model to be valid. 798 799 If implied_constraint is set, you may not set implied_lb, implied_ub, or 800 implied_expr. 801 802 Args: 803 indicator: The variable whose value determines if implied_constraint must 804 be enforced. 805 activate_on_zero: If true, implied_constraint must hold when indicator is 806 zero, otherwise, the implied_constraint must hold when indicator is one. 807 implied_constraint: A linear constraint to conditionally enforce, if set. 808 If None, that information is instead passed via implied_lb, implied_ub, 809 and implied_expr. 810 implied_lb: The lower bound of the condtionally enforced linear constraint 811 (or -inf if None), used only when implied_constraint is None. 812 implied_ub: The upper bound of the condtionally enforced linear constraint 813 (or +inf if None), used only when implied_constraint is None. 814 implied_expr: The linear part of the condtionally enforced linear 815 constraint (or 0 if None), used only when implied_constraint is None. If 816 expr has a nonzero offset, it is subtracted from lb and ub. 817 name: For debugging purposes only, but nonempty names must be distinct. 818 819 Returns: 820 A reference to the new indicator constraint. 821 """ 822 ind_con_id = self._elemental.add_element( 823 enums.ElementType.INDICATOR_CONSTRAINT, name 824 ) 825 if indicator is not None: 826 self._elemental.set_attr( 827 enums.VariableAttr1.INDICATOR_CONSTRAINT_INDICATOR, 828 (ind_con_id,), 829 indicator.id, 830 ) 831 self._elemental.set_attr( 832 enums.BoolAttr1.INDICATOR_CONSTRAINT_ACTIVATE_ON_ZERO, 833 (ind_con_id,), 834 activate_on_zero, 835 ) 836 implied_inequality = normalized_inequality.as_normalized_linear_inequality( 837 implied_constraint, lb=implied_lb, ub=implied_ub, expr=implied_expr 838 ) 839 self._elemental.set_attr( 840 enums.DoubleAttr1.INDICATOR_CONSTRAINT_LOWER_BOUND, 841 (ind_con_id,), 842 implied_inequality.lb, 843 ) 844 self._elemental.set_attr( 845 enums.DoubleAttr1.INDICATOR_CONSTRAINT_UPPER_BOUND, 846 (ind_con_id,), 847 implied_inequality.ub, 848 ) 849 for var, coef in implied_inequality.coefficients.items(): 850 self._elemental.set_attr( 851 enums.DoubleAttr2.INDICATOR_CONSTRAINT_LINEAR_COEFFICIENT, 852 (ind_con_id, var.id), 853 coef, 854 ) 855 856 return indicator_constraints.IndicatorConstraint(self._elemental, ind_con_id) 857 858 def has_indicator_constraint(self, con_id: int) -> bool: 859 """Returns true if an indicator constraint with this id is in the model.""" 860 return self._elemental.element_exists( 861 enums.ElementType.INDICATOR_CONSTRAINT, con_id 862 ) 863 864 def get_num_indicator_constraints(self) -> int: 865 """Returns the number of indicator constraints in the model.""" 866 return self._elemental.get_num_elements(enums.ElementType.INDICATOR_CONSTRAINT) 867 868 def get_next_indicator_constraint_id(self) -> int: 869 """Returns the id of the next indicator constraint created in the model.""" 870 return self._elemental.get_next_element_id( 871 enums.ElementType.INDICATOR_CONSTRAINT 872 ) 873 874 def ensure_next_indicator_constraint_id_at_least(self, con_id: int) -> None: 875 """If the next indicator constraint id would be less than `con_id`, sets it to `con_id`.""" 876 self._elemental.ensure_next_element_id_at_least( 877 enums.ElementType.INDICATOR_CONSTRAINT, con_id 878 ) 879 880 def get_indicator_constraint( 881 self, con_id: int, *, validate: bool = True 882 ) -> indicator_constraints.IndicatorConstraint: 883 """Returns the IndicatorConstraint for the id con_id.""" 884 if validate and not self._elemental.element_exists( 885 enums.ElementType.INDICATOR_CONSTRAINT, con_id 886 ): 887 raise KeyError(f"Indicator constraint does not exist with id {con_id}.") 888 return indicator_constraints.IndicatorConstraint(self._elemental, con_id) 889 890 def delete_indicator_constraint( 891 self, ind_con: indicator_constraints.IndicatorConstraint 892 ) -> None: 893 self.check_compatible(ind_con) 894 if not self._elemental.delete_element( 895 enums.ElementType.INDICATOR_CONSTRAINT, ind_con.id 896 ): 897 raise ValueError( 898 f"Indicator constraint with id {ind_con.id} was not in the model." 899 ) 900 901 def get_indicator_constraints( 902 self, 903 ) -> Iterator[indicator_constraints.IndicatorConstraint]: 904 """Yields the indicator constraints in the order of creation.""" 905 ind_con_ids = self._elemental.get_elements( 906 enums.ElementType.INDICATOR_CONSTRAINT 907 ) 908 ind_con_ids.sort() 909 for ind_con_id in ind_con_ids: 910 yield indicator_constraints.IndicatorConstraint( 911 self._elemental, int(ind_con_id) 912 ) 913 914 ############################################################################## 915 # Proto import/export 916 ############################################################################## 917 918 @classmethod 919 def from_model_proto(cls, proto: model_pb2.ModelProto) -> Self: 920 """Returns a Model equivalent to the input model proto.""" 921 model = cls() 922 model._elemental = cpp_elemental.CppElemental.from_model_proto(proto) 923 return model 924 925 def export_model(self, *, remove_names: bool = False) -> model_pb2.ModelProto: 926 """Returns a protocol buffer equivalent to this model. 927 928 Args: 929 remove_names: When true, remove all names for the ModelProto. 930 931 Returns: 932 The model proto. 933 """ 934 return self._elemental.export_model(remove_names=remove_names) 935 936 def add_update_tracker(self) -> UpdateTracker: 937 """Creates an UpdateTracker registered on this model to view changes.""" 938 return UpdateTracker(self._elemental.add_diff(), self._elemental) 939 940 def remove_update_tracker(self, tracker: UpdateTracker): 941 """Stops tracker from getting updates on changes to this model. 942 943 An error will be raised if tracker was not created by this Model or if 944 tracker has been previously removed. 945 946 Using (via checkpoint or update) an UpdateTracker after it has been removed 947 will result in an error. 948 949 Args: 950 tracker: The UpdateTracker to unregister. 951 952 Raises: 953 KeyError: The tracker was created by another model or was already removed. 954 """ 955 self._elemental.delete_diff(tracker.diff_id) 956 957 def check_compatible(self, e: from_model.FromModel) -> None: 958 """Raises a ValueError if the model of var_or_constraint is not self.""" 959 if e.elemental is not self._elemental: 960 raise ValueError( 961 f"Expected element from model named: '{self._elemental.model_name}'," 962 f" but observed element {e} from model named:" 963 f" '{e.elemental.model_name}'." 964 )
An optimization model.
The objective function of the model can be linear or quadratic, and some solvers can only handle linear objective functions. For this reason Model has three versions of all objective setting functions:
- A generic one (e.g. maximize()), which accepts linear or quadratic expressions,
- a quadratic version (e.g. maximize_quadratic_objective()), which also accepts linear or quadratic expressions and can be used to signal a quadratic objective is possible, and
- a linear version (e.g. maximize_linear_objective()), which only accepts linear expressions and can be used to avoid solve time errors for solvers that do not accept quadratic objectives.
Attributes:
- name: A description of the problem, can be empty.
- objective: A function to maximize or minimize.
- storage: Implementation detail, do not access directly.
- _variable_ids: Maps variable ids to Variable objects.
- _linear_constraint_ids: Maps linear constraint ids to LinearConstraint objects.
166 def add_variable( 167 self, 168 *, 169 lb: float = -math.inf, 170 ub: float = math.inf, 171 is_integer: bool = False, 172 name: str = "", 173 ) -> variables_mod.Variable: 174 """Adds a decision variable to the optimization model. 175 176 Args: 177 lb: The new variable must take at least this value (a lower bound). 178 ub: The new variable must be at most this value (an upper bound). 179 is_integer: Indicates if the variable can only take integer values 180 (otherwise, the variable can take any continuous value). 181 name: For debugging purposes only, but nonempty names must be distinct. 182 183 Returns: 184 A reference to the new decision variable. 185 """ 186 187 variable_id = self._elemental.add_element(enums.ElementType.VARIABLE, name) 188 result = variables_mod.Variable(self._elemental, variable_id) 189 result.lower_bound = lb 190 result.upper_bound = ub 191 result.integer = is_integer 192 return result
Adds a decision variable to the optimization model.
Arguments:
- lb: The new variable must take at least this value (a lower bound).
- ub: The new variable must be at most this value (an upper bound).
- is_integer: Indicates if the variable can only take integer values (otherwise, the variable can take any continuous value).
- name: For debugging purposes only, but nonempty names must be distinct.
Returns:
A reference to the new decision variable.
202 def get_variable( 203 self, var_id: int, *, validate: bool = True 204 ) -> variables_mod.Variable: 205 """Returns the Variable for the id var_id, or raises KeyError.""" 206 if validate and not self._elemental.element_exists( 207 enums.ElementType.VARIABLE, var_id 208 ): 209 raise KeyError(f"Variable does not exist with id {var_id}.") 210 return variables_mod.Variable(self._elemental, var_id)
Returns the Variable for the id var_id, or raises KeyError.
212 def has_variable(self, var_id: int) -> bool: 213 """Returns true if a Variable with this id is in the model.""" 214 return self._elemental.element_exists(enums.ElementType.VARIABLE, var_id)
Returns true if a Variable with this id is in the model.
216 def get_num_variables(self) -> int: 217 """Returns the number of variables in the model.""" 218 return self._elemental.get_num_elements(enums.ElementType.VARIABLE)
Returns the number of variables in the model.
220 def get_next_variable_id(self) -> int: 221 """Returns the id of the next variable created in the model.""" 222 return self._elemental.get_next_element_id(enums.ElementType.VARIABLE)
Returns the id of the next variable created in the model.
224 def ensure_next_variable_id_at_least(self, var_id: int) -> None: 225 """If the next variable id would be less than `var_id`, sets it to `var_id`.""" 226 self._elemental.ensure_next_element_id_at_least( 227 enums.ElementType.VARIABLE, var_id 228 )
If the next variable id would be less than var_id, sets it to var_id.
230 def delete_variable(self, var: variables_mod.Variable) -> None: 231 """Removes this variable from the model.""" 232 self.check_compatible(var) 233 if not self._elemental.delete_element(enums.ElementType.VARIABLE, var.id): 234 raise ValueError(f"Variable with id {var.id} was not in the model.")
Removes this variable from the model.
236 def variables(self) -> Iterator[variables_mod.Variable]: 237 """Yields the variables in the order of creation.""" 238 var_ids = self._elemental.get_elements(enums.ElementType.VARIABLE) 239 var_ids.sort() 240 for var_id in var_ids: 241 yield variables_mod.Variable(self._elemental, int(var_id))
Yields the variables in the order of creation.
251 def maximize(self, obj: variables_mod.QuadraticTypes) -> None: 252 """Sets the objective to maximize the provided expression `obj`.""" 253 self.set_objective(obj, is_maximize=True)
Sets the objective to maximize the provided expression obj.
255 def maximize_linear_objective(self, obj: variables_mod.LinearTypes) -> None: 256 """Sets the objective to maximize the provided linear expression `obj`.""" 257 self.set_linear_objective(obj, is_maximize=True)
Sets the objective to maximize the provided linear expression obj.
259 def maximize_quadratic_objective(self, obj: variables_mod.QuadraticTypes) -> None: 260 """Sets the objective to maximize the provided quadratic expression `obj`.""" 261 self.set_quadratic_objective(obj, is_maximize=True)
Sets the objective to maximize the provided quadratic expression obj.
263 def minimize(self, obj: variables_mod.QuadraticTypes) -> None: 264 """Sets the objective to minimize the provided expression `obj`.""" 265 self.set_objective(obj, is_maximize=False)
Sets the objective to minimize the provided expression obj.
267 def minimize_linear_objective(self, obj: variables_mod.LinearTypes) -> None: 268 """Sets the objective to minimize the provided linear expression `obj`.""" 269 self.set_linear_objective(obj, is_maximize=False)
Sets the objective to minimize the provided linear expression obj.
271 def minimize_quadratic_objective(self, obj: variables_mod.QuadraticTypes) -> None: 272 """Sets the objective to minimize the provided quadratic expression `obj`.""" 273 self.set_quadratic_objective(obj, is_maximize=False)
Sets the objective to minimize the provided quadratic expression obj.
275 def set_objective( 276 self, obj: variables_mod.QuadraticTypes, *, is_maximize: bool 277 ) -> None: 278 """Sets the objective to optimize the provided expression `obj`.""" 279 self.objective.set_to_expression(obj) 280 self.objective.is_maximize = is_maximize
Sets the objective to optimize the provided expression obj.
282 def set_linear_objective( 283 self, obj: variables_mod.LinearTypes, *, is_maximize: bool 284 ) -> None: 285 """Sets the objective to optimize the provided linear expression `obj`.""" 286 self.objective.set_to_linear_expression(obj) 287 self.objective.is_maximize = is_maximize
Sets the objective to optimize the provided linear expression obj.
289 def set_quadratic_objective( 290 self, obj: variables_mod.QuadraticTypes, *, is_maximize: bool 291 ) -> None: 292 """Sets the objective to optimize the provided quadratic expression `obj`.""" 293 self.objective.set_to_quadratic_expression(obj) 294 self.objective.is_maximize = is_maximize
Sets the objective to optimize the provided quadratic expression obj.
296 def linear_objective_terms(self) -> Iterator[variables_mod.LinearTerm]: 297 """Yields variable coefficient pairs for variables with nonzero objective coefficient in undefined order.""" 298 yield from self.objective.linear_terms()
Yields variable coefficient pairs for variables with nonzero objective coefficient in undefined order.
300 def quadratic_objective_terms(self) -> Iterator[variables_mod.QuadraticTerm]: 301 """Yields the quadratic terms with nonzero objective coefficient in undefined order.""" 302 yield from self.objective.quadratic_terms()
Yields the quadratic terms with nonzero objective coefficient in undefined order.
308 def add_auxiliary_objective( 309 self, 310 *, 311 priority: int, 312 name: str = "", 313 expr: Optional[variables_mod.LinearTypes] = None, 314 is_maximize: bool = False, 315 ) -> objectives.AuxiliaryObjective: 316 """Adds an additional objective to the model.""" 317 obj_id = self._elemental.add_element( 318 enums.ElementType.AUXILIARY_OBJECTIVE, name 319 ) 320 self._elemental.set_attr( 321 enums.IntAttr1.AUXILIARY_OBJECTIVE_PRIORITY, (obj_id,), priority 322 ) 323 result = objectives.AuxiliaryObjective(self._elemental, obj_id) 324 if expr is not None: 325 result.set_to_linear_expression(expr) 326 result.is_maximize = is_maximize 327 return result
Adds an additional objective to the model.
329 def add_maximization_objective( 330 self, expr: variables_mod.LinearTypes, *, priority: int, name: str = "" 331 ) -> objectives.AuxiliaryObjective: 332 """Adds an additional objective to the model that is maximizaition.""" 333 result = self.add_auxiliary_objective( 334 priority=priority, name=name, expr=expr, is_maximize=True 335 ) 336 return result
Adds an additional objective to the model that is maximizaition.
338 def add_minimization_objective( 339 self, expr: variables_mod.LinearTypes, *, priority: int, name: str = "" 340 ) -> objectives.AuxiliaryObjective: 341 """Adds an additional objective to the model that is minimizaition.""" 342 result = self.add_auxiliary_objective( 343 priority=priority, name=name, expr=expr, is_maximize=False 344 ) 345 return result
Adds an additional objective to the model that is minimizaition.
347 def delete_auxiliary_objective(self, obj: objectives.AuxiliaryObjective) -> None: 348 """Removes an auxiliary objective from the model.""" 349 self.check_compatible(obj) 350 if not self._elemental.delete_element( 351 enums.ElementType.AUXILIARY_OBJECTIVE, obj.id 352 ): 353 raise ValueError( 354 f"Auxiliary objective with id {obj.id} is not in the model." 355 )
Removes an auxiliary objective from the model.
357 def has_auxiliary_objective(self, obj_id: int) -> bool: 358 """Returns true if the model has an auxiliary objective with id `obj_id`.""" 359 return self._elemental.element_exists( 360 enums.ElementType.AUXILIARY_OBJECTIVE, obj_id 361 )
Returns true if the model has an auxiliary objective with id obj_id.
363 def next_auxiliary_objective_id(self) -> int: 364 """Returns the id of the next auxiliary objective added to the model.""" 365 return self._elemental.get_next_element_id( 366 enums.ElementType.AUXILIARY_OBJECTIVE 367 )
Returns the id of the next auxiliary objective added to the model.
369 def num_auxiliary_objectives(self) -> int: 370 """Returns the number of auxiliary objectives in this model.""" 371 return self._elemental.get_num_elements(enums.ElementType.AUXILIARY_OBJECTIVE)
Returns the number of auxiliary objectives in this model.
373 def ensure_next_auxiliary_objective_id_at_least(self, obj_id: int) -> None: 374 """If the next auxiliary objective id would be less than `obj_id`, sets it to `obj_id`.""" 375 self._elemental.ensure_next_element_id_at_least( 376 enums.ElementType.AUXILIARY_OBJECTIVE, obj_id 377 )
If the next auxiliary objective id would be less than obj_id, sets it to obj_id.
379 def get_auxiliary_objective( 380 self, obj_id: int, *, validate: bool = True 381 ) -> objectives.AuxiliaryObjective: 382 """Returns the auxiliary objective with this id. 383 384 If there is no objective with this id, an exception is thrown if validate is 385 true, and an invalid AuxiliaryObjective is returned if validate is false 386 (later interactions with this object will cause unpredictable errors). Only 387 set validate=False if there is a known performance problem. 388 389 Args: 390 obj_id: The id of the auxiliary objective to look for. 391 validate: Set to false for more speed, but fails to raise an exception if 392 the objective is missing. 393 394 Raises: 395 KeyError: If `validate` is True and there is no objective with this id. 396 """ 397 if validate and not self.has_auxiliary_objective(obj_id): 398 raise KeyError(f"Model has no auxiliary objective with id {obj_id}") 399 return objectives.AuxiliaryObjective(self._elemental, obj_id)
Returns the auxiliary objective with this id.
If there is no objective with this id, an exception is thrown if validate is true, and an invalid AuxiliaryObjective is returned if validate is false (later interactions with this object will cause unpredictable errors). Only set validate=False if there is a known performance problem.
Arguments:
- obj_id: The id of the auxiliary objective to look for.
- validate: Set to false for more speed, but fails to raise an exception if the objective is missing.
Raises:
- KeyError: If
validateis True and there is no objective with this id.
401 def auxiliary_objectives(self) -> Iterator[objectives.AuxiliaryObjective]: 402 """Returns the auxiliary objectives in the model in the order of creation.""" 403 ids = self._elemental.get_elements(enums.ElementType.AUXILIARY_OBJECTIVE) 404 ids.sort() 405 for aux_obj_id in ids: 406 yield objectives.AuxiliaryObjective(self._elemental, int(aux_obj_id))
Returns the auxiliary objectives in the model in the order of creation.
415 def add_linear_constraint( 416 self, 417 bounded_expr: Optional[Union[bool, variables_mod.BoundedLinearTypes]] = None, 418 *, 419 lb: Optional[float] = None, 420 ub: Optional[float] = None, 421 expr: Optional[variables_mod.LinearTypes] = None, 422 name: str = "", 423 ) -> linear_constraints_mod.LinearConstraint: 424 """Adds a linear constraint to the optimization model. 425 426 The simplest way to specify the constraint is by passing a one-sided or 427 two-sided linear inequality as in: 428 * add_linear_constraint(x + y + 1.0 <= 2.0), 429 * add_linear_constraint(x + y >= 2.0), or 430 * add_linear_constraint((1.0 <= x + y) <= 2.0). 431 432 Note the extra parenthesis for two-sided linear inequalities, which is 433 required due to some language limitations (see 434 https://peps.python.org/pep-0335/ and https://peps.python.org/pep-0535/). 435 If the parenthesis are omitted, a TypeError will be raised explaining the 436 issue (if this error was not raised the first inequality would have been 437 silently ignored because of the noted language limitations). 438 439 The second way to specify the constraint is by setting lb, ub, and/or expr 440 as in: 441 * add_linear_constraint(expr=x + y + 1.0, ub=2.0), 442 * add_linear_constraint(expr=x + y, lb=2.0), 443 * add_linear_constraint(expr=x + y, lb=1.0, ub=2.0), or 444 * add_linear_constraint(lb=1.0). 445 Omitting lb is equivalent to setting it to -math.inf and omiting ub is 446 equivalent to setting it to math.inf. 447 448 These two alternatives are exclusive and a combined call like: 449 * add_linear_constraint(x + y <= 2.0, lb=1.0), or 450 * add_linear_constraint(x + y <= 2.0, ub=math.inf) 451 will raise a ValueError. A ValueError is also raised if expr's offset is 452 infinite. 453 454 Args: 455 bounded_expr: a linear inequality describing the constraint. Cannot be 456 specified together with lb, ub, or expr. 457 lb: The constraint's lower bound if bounded_expr is omitted (if both 458 bounder_expr and lb are omitted, the lower bound is -math.inf). 459 ub: The constraint's upper bound if bounded_expr is omitted (if both 460 bounder_expr and ub are omitted, the upper bound is math.inf). 461 expr: The constraint's linear expression if bounded_expr is omitted. 462 name: For debugging purposes only, but nonempty names must be distinct. 463 464 Returns: 465 A reference to the new linear constraint. 466 """ 467 norm_ineq = normalized_inequality.as_normalized_linear_inequality( 468 bounded_expr, lb=lb, ub=ub, expr=expr 469 ) 470 lin_con_id = self._elemental.add_element( 471 enums.ElementType.LINEAR_CONSTRAINT, name 472 ) 473 474 result = linear_constraints_mod.LinearConstraint(self._elemental, lin_con_id) 475 result.lower_bound = norm_ineq.lb 476 result.upper_bound = norm_ineq.ub 477 for var, coefficient in norm_ineq.coefficients.items(): 478 result.set_coefficient(var, coefficient) 479 return result
Adds a linear constraint to the optimization model.
The simplest way to specify the constraint is by passing a one-sided or two-sided linear inequality as in:
- add_linear_constraint(x + y + 1.0 <= 2.0),
- add_linear_constraint(x + y >= 2.0), or
- add_linear_constraint((1.0 <= x + y) <= 2.0).
Note the extra parenthesis for two-sided linear inequalities, which is required due to some language limitations (see https://peps.python.org/pep-0335/ and https://peps.python.org/pep-0535/). If the parenthesis are omitted, a TypeError will be raised explaining the issue (if this error was not raised the first inequality would have been silently ignored because of the noted language limitations).
The second way to specify the constraint is by setting lb, ub, and/or expr as in:
- add_linear_constraint(expr=x + y + 1.0, ub=2.0),
- add_linear_constraint(expr=x + y, lb=2.0),
- add_linear_constraint(expr=x + y, lb=1.0, ub=2.0), or
- add_linear_constraint(lb=1.0). Omitting lb is equivalent to setting it to -math.inf and omiting ub is equivalent to setting it to math.inf.
These two alternatives are exclusive and a combined call like:
- add_linear_constraint(x + y <= 2.0, lb=1.0), or
- add_linear_constraint(x + y <= 2.0, ub=math.inf)
will raise a ValueError. A ValueError is also raised if expr's offset is infinite.
Arguments:
- bounded_expr: a linear inequality describing the constraint. Cannot be specified together with lb, ub, or expr.
- lb: The constraint's lower bound if bounded_expr is omitted (if both bounder_expr and lb are omitted, the lower bound is -math.inf).
- ub: The constraint's upper bound if bounded_expr is omitted (if both bounder_expr and ub are omitted, the upper bound is math.inf).
- expr: The constraint's linear expression if bounded_expr is omitted.
- name: For debugging purposes only, but nonempty names must be distinct.
Returns:
A reference to the new linear constraint.
481 def has_linear_constraint(self, con_id: int) -> bool: 482 """Returns true if a linear constraint with this id is in the model.""" 483 return self._elemental.element_exists( 484 enums.ElementType.LINEAR_CONSTRAINT, con_id 485 )
Returns true if a linear constraint with this id is in the model.
487 def get_num_linear_constraints(self) -> int: 488 """Returns the number of linear constraints in the model.""" 489 return self._elemental.get_num_elements(enums.ElementType.LINEAR_CONSTRAINT)
Returns the number of linear constraints in the model.
491 def get_next_linear_constraint_id(self) -> int: 492 """Returns the id of the next linear constraint created in the model.""" 493 return self._elemental.get_next_element_id(enums.ElementType.LINEAR_CONSTRAINT)
Returns the id of the next linear constraint created in the model.
495 def ensure_next_linear_constraint_id_at_least(self, con_id: int) -> None: 496 """If the next linear constraint id would be less than `con_id`, sets it to `con_id`.""" 497 self._elemental.ensure_next_element_id_at_least( 498 enums.ElementType.LINEAR_CONSTRAINT, con_id 499 )
If the next linear constraint id would be less than con_id, sets it to con_id.
501 def get_linear_constraint( 502 self, con_id: int, *, validate: bool = True 503 ) -> linear_constraints_mod.LinearConstraint: 504 """Returns the LinearConstraint for the id con_id.""" 505 if validate and not self._elemental.element_exists( 506 enums.ElementType.LINEAR_CONSTRAINT, con_id 507 ): 508 raise KeyError(f"Linear constraint does not exist with id {con_id}.") 509 return linear_constraints_mod.LinearConstraint(self._elemental, con_id)
Returns the LinearConstraint for the id con_id.
511 def delete_linear_constraint( 512 self, lin_con: linear_constraints_mod.LinearConstraint 513 ) -> None: 514 self.check_compatible(lin_con) 515 if not self._elemental.delete_element( 516 enums.ElementType.LINEAR_CONSTRAINT, lin_con.id 517 ): 518 raise ValueError( 519 f"Linear constraint with id {lin_con.id} was not in the model." 520 )
522 def linear_constraints( 523 self, 524 ) -> Iterator[linear_constraints_mod.LinearConstraint]: 525 """Yields the linear constraints in the order of creation.""" 526 lin_con_ids = self._elemental.get_elements(enums.ElementType.LINEAR_CONSTRAINT) 527 lin_con_ids.sort() 528 for lin_con_id in lin_con_ids: 529 yield linear_constraints_mod.LinearConstraint( 530 self._elemental, int(lin_con_id) 531 )
Yields the linear constraints in the order of creation.
533 def row_nonzeros( 534 self, lin_con: linear_constraints_mod.LinearConstraint 535 ) -> Iterator[variables_mod.Variable]: 536 """Yields the variables with nonzero coefficient for this linear constraint.""" 537 keys = self._elemental.slice_attr( 538 enums.DoubleAttr2.LINEAR_CONSTRAINT_COEFFICIENT, 0, lin_con.id 539 ) 540 for var_id in keys[:, 1]: 541 yield variables_mod.Variable(self._elemental, int(var_id))
Yields the variables with nonzero coefficient for this linear constraint.
543 def column_nonzeros( 544 self, var: variables_mod.Variable 545 ) -> Iterator[linear_constraints_mod.LinearConstraint]: 546 """Yields the linear constraints with nonzero coefficient for this variable.""" 547 keys = self._elemental.slice_attr( 548 enums.DoubleAttr2.LINEAR_CONSTRAINT_COEFFICIENT, 1, var.id 549 ) 550 for lin_con_id in keys[:, 0]: 551 yield linear_constraints_mod.LinearConstraint( 552 self._elemental, int(lin_con_id) 553 )
Yields the linear constraints with nonzero coefficient for this variable.
555 def linear_constraint_matrix_entries( 556 self, 557 ) -> Iterator[linear_constraints_mod.LinearConstraintMatrixEntry]: 558 """Yields the nonzero elements of the linear constraint matrix in undefined order.""" 559 keys = self._elemental.get_attr_non_defaults( 560 enums.DoubleAttr2.LINEAR_CONSTRAINT_COEFFICIENT 561 ) 562 coefs = self._elemental.get_attrs( 563 enums.DoubleAttr2.LINEAR_CONSTRAINT_COEFFICIENT, keys 564 ) 565 for i in range(len(keys)): 566 yield linear_constraints_mod.LinearConstraintMatrixEntry( 567 linear_constraint=linear_constraints_mod.LinearConstraint( 568 self._elemental, int(keys[i, 0]) 569 ), 570 variable=variables_mod.Variable(self._elemental, int(keys[i, 1])), 571 coefficient=float(coefs[i]), 572 )
Yields the nonzero elements of the linear constraint matrix in undefined order.
578 def add_quadratic_constraint( 579 self, 580 bounded_expr: Optional[ 581 Union[ 582 bool, 583 variables_mod.BoundedLinearTypes, 584 variables_mod.BoundedQuadraticTypes, 585 ] 586 ] = None, 587 *, 588 lb: Optional[float] = None, 589 ub: Optional[float] = None, 590 expr: Optional[variables_mod.QuadraticTypes] = None, 591 name: str = "", 592 ) -> quadratic_constraints.QuadraticConstraint: 593 """Adds a quadratic constraint to the optimization model. 594 595 The simplest way to specify the constraint is by passing a one-sided or 596 two-sided quadratic inequality as in: 597 * add_quadratic_constraint(x * x + y + 1.0 <= 2.0), 598 * add_quadratic_constraint(x * x + y >= 2.0), or 599 * add_quadratic_constraint((1.0 <= x * x + y) <= 2.0). 600 601 Note the extra parenthesis for two-sided linear inequalities, which is 602 required due to some language limitations (see add_linear_constraint for 603 details). 604 605 The second way to specify the constraint is by setting lb, ub, and/or expr 606 as in: 607 * add_quadratic_constraint(expr=x * x + y + 1.0, ub=2.0), 608 * add_quadratic_constraint(expr=x * x + y, lb=2.0), 609 * add_quadratic_constraint(expr=x * x + y, lb=1.0, ub=2.0), or 610 * add_quadratic_constraint(lb=1.0). 611 Omitting lb is equivalent to setting it to -math.inf and omiting ub is 612 equivalent to setting it to math.inf. 613 614 These two alternatives are exclusive and a combined call like: 615 * add_quadratic_constraint(x * x + y <= 2.0, lb=1.0), or 616 * add_quadratic_constraint(x * x+ y <= 2.0, ub=math.inf) 617 will raise a ValueError. A ValueError is also raised if expr's offset is 618 infinite. 619 620 Args: 621 bounded_expr: a quadratic inequality describing the constraint. Cannot be 622 specified together with lb, ub, or expr. 623 lb: The constraint's lower bound if bounded_expr is omitted (if both 624 bounder_expr and lb are omitted, the lower bound is -math.inf). 625 ub: The constraint's upper bound if bounded_expr is omitted (if both 626 bounder_expr and ub are omitted, the upper bound is math.inf). 627 expr: The constraint's quadratic expression if bounded_expr is omitted. 628 name: For debugging purposes only, but nonempty names must be distinct. 629 630 Returns: 631 A reference to the new quadratic constraint. 632 """ 633 norm_quad = normalized_inequality.as_normalized_quadratic_inequality( 634 bounded_expr, lb=lb, ub=ub, expr=expr 635 ) 636 quad_con_id = self._elemental.add_element( 637 enums.ElementType.QUADRATIC_CONSTRAINT, name 638 ) 639 for var, coef in norm_quad.linear_coefficients.items(): 640 self._elemental.set_attr( 641 enums.DoubleAttr2.QUADRATIC_CONSTRAINT_LINEAR_COEFFICIENT, 642 (quad_con_id, var.id), 643 coef, 644 ) 645 for key, coef in norm_quad.quadratic_coefficients.items(): 646 self._elemental.set_attr( 647 enums.SymmetricDoubleAttr3.QUADRATIC_CONSTRAINT_QUADRATIC_COEFFICIENT, 648 (quad_con_id, key.first_var.id, key.second_var.id), 649 coef, 650 ) 651 if norm_quad.lb > -math.inf: 652 self._elemental.set_attr( 653 enums.DoubleAttr1.QUADRATIC_CONSTRAINT_LOWER_BOUND, 654 (quad_con_id,), 655 norm_quad.lb, 656 ) 657 if norm_quad.ub < math.inf: 658 self._elemental.set_attr( 659 enums.DoubleAttr1.QUADRATIC_CONSTRAINT_UPPER_BOUND, 660 (quad_con_id,), 661 norm_quad.ub, 662 ) 663 return quadratic_constraints.QuadraticConstraint(self._elemental, quad_con_id)
Adds a quadratic constraint to the optimization model.
The simplest way to specify the constraint is by passing a one-sided or two-sided quadratic inequality as in:
- add_quadratic_constraint(x * x + y + 1.0 <= 2.0),
- add_quadratic_constraint(x * x + y >= 2.0), or
- add_quadratic_constraint((1.0 <= x * x + y) <= 2.0).
Note the extra parenthesis for two-sided linear inequalities, which is required due to some language limitations (see add_linear_constraint for details).
The second way to specify the constraint is by setting lb, ub, and/or expr as in:
- add_quadratic_constraint(expr=x * x + y + 1.0, ub=2.0),
- add_quadratic_constraint(expr=x * x + y, lb=2.0),
- add_quadratic_constraint(expr=x * x + y, lb=1.0, ub=2.0), or
- add_quadratic_constraint(lb=1.0). Omitting lb is equivalent to setting it to -math.inf and omiting ub is equivalent to setting it to math.inf.
These two alternatives are exclusive and a combined call like:
- add_quadratic_constraint(x * x + y <= 2.0, lb=1.0), or
- add_quadratic_constraint(x * x+ y <= 2.0, ub=math.inf)
will raise a ValueError. A ValueError is also raised if expr's offset is infinite.
Arguments:
- bounded_expr: a quadratic inequality describing the constraint. Cannot be specified together with lb, ub, or expr.
- lb: The constraint's lower bound if bounded_expr is omitted (if both bounder_expr and lb are omitted, the lower bound is -math.inf).
- ub: The constraint's upper bound if bounded_expr is omitted (if both bounder_expr and ub are omitted, the upper bound is math.inf).
- expr: The constraint's quadratic expression if bounded_expr is omitted.
- name: For debugging purposes only, but nonempty names must be distinct.
Returns:
A reference to the new quadratic constraint.
665 def has_quadratic_constraint(self, con_id: int) -> bool: 666 """Returns true if a quadratic constraint with this id is in the model.""" 667 return self._elemental.element_exists( 668 enums.ElementType.QUADRATIC_CONSTRAINT, con_id 669 )
Returns true if a quadratic constraint with this id is in the model.
671 def get_num_quadratic_constraints(self) -> int: 672 """Returns the number of quadratic constraints in the model.""" 673 return self._elemental.get_num_elements(enums.ElementType.QUADRATIC_CONSTRAINT)
Returns the number of quadratic constraints in the model.
675 def get_next_quadratic_constraint_id(self) -> int: 676 """Returns the id of the next quadratic constraint created in the model.""" 677 return self._elemental.get_next_element_id( 678 enums.ElementType.QUADRATIC_CONSTRAINT 679 )
Returns the id of the next quadratic constraint created in the model.
681 def ensure_next_quadratic_constraint_id_at_least(self, con_id: int) -> None: 682 """If the next quadratic constraint id would be less than `con_id`, sets it to `con_id`.""" 683 self._elemental.ensure_next_element_id_at_least( 684 enums.ElementType.QUADRATIC_CONSTRAINT, con_id 685 )
If the next quadratic constraint id would be less than con_id, sets it to con_id.
687 def get_quadratic_constraint( 688 self, con_id: int, *, validate: bool = True 689 ) -> quadratic_constraints.QuadraticConstraint: 690 """Returns the constraint for the id, or raises KeyError if not in model.""" 691 if validate and not self._elemental.element_exists( 692 enums.ElementType.QUADRATIC_CONSTRAINT, con_id 693 ): 694 raise KeyError(f"Quadratic constraint does not exist with id {con_id}.") 695 return quadratic_constraints.QuadraticConstraint(self._elemental, con_id)
Returns the constraint for the id, or raises KeyError if not in model.
697 def delete_quadratic_constraint( 698 self, quad_con: quadratic_constraints.QuadraticConstraint 699 ) -> None: 700 """Deletes the constraint with id, or raises ValueError if not in model.""" 701 self.check_compatible(quad_con) 702 if not self._elemental.delete_element( 703 enums.ElementType.QUADRATIC_CONSTRAINT, quad_con.id 704 ): 705 raise ValueError( 706 f"Quadratic constraint with id {quad_con.id} was not in the model." 707 )
Deletes the constraint with id, or raises ValueError if not in model.
709 def get_quadratic_constraints( 710 self, 711 ) -> Iterator[quadratic_constraints.QuadraticConstraint]: 712 """Yields the quadratic constraints in the order of creation.""" 713 quad_con_ids = self._elemental.get_elements( 714 enums.ElementType.QUADRATIC_CONSTRAINT 715 ) 716 quad_con_ids.sort() 717 for quad_con_id in quad_con_ids: 718 yield quadratic_constraints.QuadraticConstraint( 719 self._elemental, int(quad_con_id) 720 )
Yields the quadratic constraints in the order of creation.
722 def quadratic_constraint_linear_nonzeros( 723 self, 724 ) -> Iterator[ 725 Tuple[ 726 quadratic_constraints.QuadraticConstraint, 727 variables_mod.Variable, 728 float, 729 ] 730 ]: 731 """Yields the linear coefficients for all quadratic constraints in the model.""" 732 keys = self._elemental.get_attr_non_defaults( 733 enums.DoubleAttr2.QUADRATIC_CONSTRAINT_LINEAR_COEFFICIENT 734 ) 735 coefs = self._elemental.get_attrs( 736 enums.DoubleAttr2.QUADRATIC_CONSTRAINT_LINEAR_COEFFICIENT, keys 737 ) 738 for i in range(len(keys)): 739 yield ( 740 quadratic_constraints.QuadraticConstraint( 741 self._elemental, int(keys[i, 0]) 742 ), 743 variables_mod.Variable(self._elemental, int(keys[i, 1])), 744 float(coefs[i]), 745 )
Yields the linear coefficients for all quadratic constraints in the model.
747 def quadratic_constraint_quadratic_nonzeros( 748 self, 749 ) -> Iterator[ 750 Tuple[ 751 quadratic_constraints.QuadraticConstraint, 752 variables_mod.Variable, 753 variables_mod.Variable, 754 float, 755 ] 756 ]: 757 """Yields the quadratic coefficients for all quadratic constraints in the model.""" 758 keys = self._elemental.get_attr_non_defaults( 759 enums.SymmetricDoubleAttr3.QUADRATIC_CONSTRAINT_QUADRATIC_COEFFICIENT 760 ) 761 coefs = self._elemental.get_attrs( 762 enums.SymmetricDoubleAttr3.QUADRATIC_CONSTRAINT_QUADRATIC_COEFFICIENT, 763 keys, 764 ) 765 for i in range(len(keys)): 766 yield ( 767 quadratic_constraints.QuadraticConstraint( 768 self._elemental, int(keys[i, 0]) 769 ), 770 variables_mod.Variable(self._elemental, int(keys[i, 1])), 771 variables_mod.Variable(self._elemental, int(keys[i, 2])), 772 float(coefs[i]), 773 )
Yields the quadratic coefficients for all quadratic constraints in the model.
779 def add_indicator_constraint( 780 self, 781 *, 782 indicator: Optional[variables_mod.Variable] = None, 783 activate_on_zero: bool = False, 784 implied_constraint: Optional[ 785 Union[bool, variables_mod.BoundedLinearTypes] 786 ] = None, 787 implied_lb: Optional[float] = None, 788 implied_ub: Optional[float] = None, 789 implied_expr: Optional[variables_mod.LinearTypes] = None, 790 name: str = "", 791 ) -> indicator_constraints.IndicatorConstraint: 792 """Adds an indicator constraint to the model. 793 794 If indicator is None or the variable equal to indicator is deleted from 795 the model, the model will be considered invalid at solve time (unless this 796 constraint is also deleted before solving). Likewise, the variable indicator 797 must be binary at solve time for the model to be valid. 798 799 If implied_constraint is set, you may not set implied_lb, implied_ub, or 800 implied_expr. 801 802 Args: 803 indicator: The variable whose value determines if implied_constraint must 804 be enforced. 805 activate_on_zero: If true, implied_constraint must hold when indicator is 806 zero, otherwise, the implied_constraint must hold when indicator is one. 807 implied_constraint: A linear constraint to conditionally enforce, if set. 808 If None, that information is instead passed via implied_lb, implied_ub, 809 and implied_expr. 810 implied_lb: The lower bound of the condtionally enforced linear constraint 811 (or -inf if None), used only when implied_constraint is None. 812 implied_ub: The upper bound of the condtionally enforced linear constraint 813 (or +inf if None), used only when implied_constraint is None. 814 implied_expr: The linear part of the condtionally enforced linear 815 constraint (or 0 if None), used only when implied_constraint is None. If 816 expr has a nonzero offset, it is subtracted from lb and ub. 817 name: For debugging purposes only, but nonempty names must be distinct. 818 819 Returns: 820 A reference to the new indicator constraint. 821 """ 822 ind_con_id = self._elemental.add_element( 823 enums.ElementType.INDICATOR_CONSTRAINT, name 824 ) 825 if indicator is not None: 826 self._elemental.set_attr( 827 enums.VariableAttr1.INDICATOR_CONSTRAINT_INDICATOR, 828 (ind_con_id,), 829 indicator.id, 830 ) 831 self._elemental.set_attr( 832 enums.BoolAttr1.INDICATOR_CONSTRAINT_ACTIVATE_ON_ZERO, 833 (ind_con_id,), 834 activate_on_zero, 835 ) 836 implied_inequality = normalized_inequality.as_normalized_linear_inequality( 837 implied_constraint, lb=implied_lb, ub=implied_ub, expr=implied_expr 838 ) 839 self._elemental.set_attr( 840 enums.DoubleAttr1.INDICATOR_CONSTRAINT_LOWER_BOUND, 841 (ind_con_id,), 842 implied_inequality.lb, 843 ) 844 self._elemental.set_attr( 845 enums.DoubleAttr1.INDICATOR_CONSTRAINT_UPPER_BOUND, 846 (ind_con_id,), 847 implied_inequality.ub, 848 ) 849 for var, coef in implied_inequality.coefficients.items(): 850 self._elemental.set_attr( 851 enums.DoubleAttr2.INDICATOR_CONSTRAINT_LINEAR_COEFFICIENT, 852 (ind_con_id, var.id), 853 coef, 854 ) 855 856 return indicator_constraints.IndicatorConstraint(self._elemental, ind_con_id)
Adds an indicator constraint to the model.
If indicator is None or the variable equal to indicator is deleted from the model, the model will be considered invalid at solve time (unless this constraint is also deleted before solving). Likewise, the variable indicator must be binary at solve time for the model to be valid.
If implied_constraint is set, you may not set implied_lb, implied_ub, or implied_expr.
Arguments:
- indicator: The variable whose value determines if implied_constraint must be enforced.
- activate_on_zero: If true, implied_constraint must hold when indicator is zero, otherwise, the implied_constraint must hold when indicator is one.
- implied_constraint: A linear constraint to conditionally enforce, if set. If None, that information is instead passed via implied_lb, implied_ub, and implied_expr.
- implied_lb: The lower bound of the condtionally enforced linear constraint (or -inf if None), used only when implied_constraint is None.
- implied_ub: The upper bound of the condtionally enforced linear constraint (or +inf if None), used only when implied_constraint is None.
- implied_expr: The linear part of the condtionally enforced linear constraint (or 0 if None), used only when implied_constraint is None. If expr has a nonzero offset, it is subtracted from lb and ub.
- name: For debugging purposes only, but nonempty names must be distinct.
Returns:
A reference to the new indicator constraint.
858 def has_indicator_constraint(self, con_id: int) -> bool: 859 """Returns true if an indicator constraint with this id is in the model.""" 860 return self._elemental.element_exists( 861 enums.ElementType.INDICATOR_CONSTRAINT, con_id 862 )
Returns true if an indicator constraint with this id is in the model.
864 def get_num_indicator_constraints(self) -> int: 865 """Returns the number of indicator constraints in the model.""" 866 return self._elemental.get_num_elements(enums.ElementType.INDICATOR_CONSTRAINT)
Returns the number of indicator constraints in the model.
868 def get_next_indicator_constraint_id(self) -> int: 869 """Returns the id of the next indicator constraint created in the model.""" 870 return self._elemental.get_next_element_id( 871 enums.ElementType.INDICATOR_CONSTRAINT 872 )
Returns the id of the next indicator constraint created in the model.
874 def ensure_next_indicator_constraint_id_at_least(self, con_id: int) -> None: 875 """If the next indicator constraint id would be less than `con_id`, sets it to `con_id`.""" 876 self._elemental.ensure_next_element_id_at_least( 877 enums.ElementType.INDICATOR_CONSTRAINT, con_id 878 )
If the next indicator constraint id would be less than con_id, sets it to con_id.
880 def get_indicator_constraint( 881 self, con_id: int, *, validate: bool = True 882 ) -> indicator_constraints.IndicatorConstraint: 883 """Returns the IndicatorConstraint for the id con_id.""" 884 if validate and not self._elemental.element_exists( 885 enums.ElementType.INDICATOR_CONSTRAINT, con_id 886 ): 887 raise KeyError(f"Indicator constraint does not exist with id {con_id}.") 888 return indicator_constraints.IndicatorConstraint(self._elemental, con_id)
Returns the IndicatorConstraint for the id con_id.
890 def delete_indicator_constraint( 891 self, ind_con: indicator_constraints.IndicatorConstraint 892 ) -> None: 893 self.check_compatible(ind_con) 894 if not self._elemental.delete_element( 895 enums.ElementType.INDICATOR_CONSTRAINT, ind_con.id 896 ): 897 raise ValueError( 898 f"Indicator constraint with id {ind_con.id} was not in the model." 899 )
901 def get_indicator_constraints( 902 self, 903 ) -> Iterator[indicator_constraints.IndicatorConstraint]: 904 """Yields the indicator constraints in the order of creation.""" 905 ind_con_ids = self._elemental.get_elements( 906 enums.ElementType.INDICATOR_CONSTRAINT 907 ) 908 ind_con_ids.sort() 909 for ind_con_id in ind_con_ids: 910 yield indicator_constraints.IndicatorConstraint( 911 self._elemental, int(ind_con_id) 912 )
Yields the indicator constraints in the order of creation.
918 @classmethod 919 def from_model_proto(cls, proto: model_pb2.ModelProto) -> Self: 920 """Returns a Model equivalent to the input model proto.""" 921 model = cls() 922 model._elemental = cpp_elemental.CppElemental.from_model_proto(proto) 923 return model
Returns a Model equivalent to the input model proto.
925 def export_model(self, *, remove_names: bool = False) -> model_pb2.ModelProto: 926 """Returns a protocol buffer equivalent to this model. 927 928 Args: 929 remove_names: When true, remove all names for the ModelProto. 930 931 Returns: 932 The model proto. 933 """ 934 return self._elemental.export_model(remove_names=remove_names)
Returns a protocol buffer equivalent to this model.
Arguments:
- remove_names: When true, remove all names for the ModelProto.
Returns:
The model proto.
936 def add_update_tracker(self) -> UpdateTracker: 937 """Creates an UpdateTracker registered on this model to view changes.""" 938 return UpdateTracker(self._elemental.add_diff(), self._elemental)
Creates an UpdateTracker registered on this model to view changes.
940 def remove_update_tracker(self, tracker: UpdateTracker): 941 """Stops tracker from getting updates on changes to this model. 942 943 An error will be raised if tracker was not created by this Model or if 944 tracker has been previously removed. 945 946 Using (via checkpoint or update) an UpdateTracker after it has been removed 947 will result in an error. 948 949 Args: 950 tracker: The UpdateTracker to unregister. 951 952 Raises: 953 KeyError: The tracker was created by another model or was already removed. 954 """ 955 self._elemental.delete_diff(tracker.diff_id)
Stops tracker from getting updates on changes to this model.
An error will be raised if tracker was not created by this Model or if tracker has been previously removed.
Using (via checkpoint or update) an UpdateTracker after it has been removed will result in an error.
Arguments:
- tracker: The UpdateTracker to unregister.
Raises:
- KeyError: The tracker was created by another model or was already removed.
957 def check_compatible(self, e: from_model.FromModel) -> None: 958 """Raises a ValueError if the model of var_or_constraint is not self.""" 959 if e.elemental is not self._elemental: 960 raise ValueError( 961 f"Expected element from model named: '{self._elemental.model_name}'," 962 f" but observed element {e} from model named:" 963 f" '{e.elemental.model_name}'." 964 )
Raises a ValueError if the model of var_or_constraint is not self.